--- 본 포스팅은 데이콘 서포터즈 "데이크루 2기" 활동의 일환입니다 ---
- 안녕하세요 데이콘 서포터즈 데이크루 2기 포스(POS)팀의 Rahites입니다 :)
- POS팀은 Python OpenCV Study의 약자로 활동 기간동안 저희 팀은 '파이썬으로 만드는 OpenCV 프로젝트' 책을 가지고 OpenCV를 공부해보고 프로젝트를 진행할 것입니다.
- 자세한 스터디 계획과 운영 방안은 아래의 포스팅에서 확인하실 수 있습니다.
https://dacon.io/codeshare/4759?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
https://dacon.io/codeshare/5072?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
지금부터 8주차 활동 시작하겠습니다 ( •̀ ω •́ ) ✧✧
7. 영상 분할¶
영상에서 객체를 인식하려면 배경과 전경을 분할해야 합니다. 이번 장에서는 검출한 경계들에서 의미 있는 선들을 찾는 방법과 경계 안쪽과 바깥쪽을 구분하는 등 영상을 분할하는 방법에 대해서 알아보겠습니다.
7.1. 컨투어¶
컨투어(contour)는 우리말로 등고선, 윤곽선, 외곽선 등으로 번역합니다. 영상에서는 같은 색상이나 밝기의 연속된 점을 찾아 잇는 곡선을 찾아내면 모양 분석과 객체 인식에 사옹할 수 있는 데, 이것을 컨투어라고 합니다.
contours, hierarchy = cv2.findContours(src, mode, method [, contours, hierarchy, offset])[-2:]
- src : 입력 이미지, 바이너리 스케일, 검은색 배경 흰색 전경
- mode : 컨투어 제공 방식 선택
- cv2.RETR_EXTERNAL : 가장 바깥쪽 라인만 제공
- cv2.RETR_LIST : 모든 라인을 계층 없이 제공
- cv2.RETR_CCOMP : 모든 라인을 2계층으로 제공
- cv2.RETR_TREE : 모든 라인의 모든 계층 정보를 트리 구조로 제공
- method : 근사 값 방식 선택
- cv2.CHAIN_APPROX_NONE : 근사 계산하지 않고 모든 좌표 제공
- cv2.CHAIN_APPROX_SIMPLE : 컨투어 꼭짓점 좌표만 제공
- cv2.CHAIN_APPROX_TC89_L1 : Teh-Chin 알고리즘으로 좌표 개수 축소
- cv2.CHAIN_APPROX_TC89_KCOS : Teh-Chin 알고리즘으로 좌표 개수 축소
- contours : 검출한 컨투어 좌표, 파이썬 리스트
- hierarchy : 컨투어 계층 정보
- Next, Prev, FirstChild, Parent
- -1 : 해당 사항 없음
- offset : ROI 등으로 인해 이동한 컨투어 좌표의 오프셋
cv2.drawContours(img, contours, contourIdx, color, thickness)
- img : 입력 영상
- contours : 그림 그릴 컨투어 배열
- contourIdx : 그림 그릴 컨투어 인덱스
- color : 색상 값
- thickness : 선 두께
cv2.findContours() 함수는 src 인자에 검은색 배경에 흰색 전경으로 표현된 바이너리 이미지를 전달하면 컨투어에 해당하는 좌표를 갖는 NumPy 배열을 파이썬 리스트로 반환합니다. cv2.RETR_EXTERNAL을 지정하면 컨투어 중에 가장 바깥쪽 컨투어만 담아서 반환하고, 그 나머지는 여러 컨투어를 담아서 반환합니다. method는 컨투어 좌표를 전부 받을지 여부를 결정합니다. cv2.CHAIN_APPROX_NONE는 컨투어를 표시하기 위한 모든 좌표를 담아서 반환하고 나머지는 선분의 꼭짓점만 담거나 Teh-Chin 체인 근사 알고리즘으로 좌표 개수를 줄여서 반환합니다. 결과 값인 hierarchy는 컨투어들 간 계층구조에 대한 정보를 갖습니다.
cv2.findContours() 함수로 얻은 컨투어 좌표를 선으로 표시하고 싶을 때는 cv2.lines()나 cv2.polylines()와 같은 함수로 직접 그려도 되지만, cv2.drawContours() 함수를 통해 쉽게 컨투어 연결선을 그릴 수 있습니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('../img/shape.jpg')
img2 = img.copy()
# 그레이 스케일로 변환 ---①
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 스레시홀드로 바이너리 이미지로 만들어서 검은배경에 흰색전경으로 반전 ---②
ret, imthres = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV)
# 가장 바깥쪽 컨투어에 대해 모든 좌표 반환 ---③
contour, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2:]
# 가장 바깥쪽 컨투어에 대해 꼭지점 좌표만 반환 ---④
contour2, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
# 각각의 컨투의 갯수 출력 ---⑤
print('도형의 갯수: %d(%d)'% (len(contour), len(contour2)))
# 모든 좌표를 갖는 컨투어 그리기, 초록색 ---⑥)
cv2.drawContours(img, contour, -1, (0,255,0), 4)
# 꼭지점 좌표만을 갖는 컨투어 그리기, 초록색 ---⑦
cv2.drawContours(img2, contour2, -1, (0,255,0), 4)
# 컨투어 모든 좌표를 작은 파랑색 점(원)으로 표시 ---⑧
for i in contour:
for j in i:
cv2.circle(img, tuple(j[0]), 1, (255,0,0), -1)
# 컨투어 꼭지점 좌표를 작은 파랑색 점(원)으로 표시 ---⑨
for i in contour2:
for j in i:
cv2.circle(img2, tuple(j[0]), 1, (255,0,0), -1)
# 결과 출력 ---⑩
plt.figure(figsize = (10,6))
imgs = {'CHAIN_APPROX_NONE':img, 'CHAIN_APPROX_SIMPLE':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
도형의 갯수: 3(3)
- 원본 이미지를 그레이 스케일로 변환한 후에 스레시홀드로 바이너리 이미지로 만들면서 배경은 검은색, 전경은 흰색이 되도록 반전합니다.
- 각각 가장 바깥의 컨투어만 구하는데, 모든 좌표를 구하게도 하고 꼭짓점 좌표만을 구하게도 합니다.
- 컨투어가 모든 좌표를 갖든 꼭짓점 좌표만 갖든 간에 도형의 외곽을 완전히 그려내는 것을 알 수 있습니다. ( cv2.drawContours() )
import cv2
import numpy as np
# 영상 읽기
img = cv2.imread('../img/shape_donut.jpg')
img2 = img.copy()
# 바이너리 이미지로 변환
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, imthres = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV)
# 가장 바깥 컨투어만 수집 --- ①
contour, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_NONE)
# 컨투어 갯수와 계층 트리 출력 --- ②
print(len(contour), hierarchy)
# 모든 컨투어를 트리 계층 으로 수집 ---③
contour2, hierarchy = cv2.findContours(imthres, cv2.RETR_TREE, \
cv2.CHAIN_APPROX_SIMPLE)
# 컨투어 갯수와 계층 트리 출력 ---④
print(len(contour2), hierarchy)
# 가장 바깥 컨투어만 그리기 ---⑤
cv2.drawContours(img, contour, -1, (0,255,0), 3)
# 모든 컨투어 그리기 ---⑥
for idx, cont in enumerate(contour2):
# 랜덤한 컬러 추출 ---⑦
color = [int(i) for i in np.random.randint(0,255, 3)]
# 컨투어 인덱스 마다 랜덤한 색상으로 그리기 ---⑧
cv2.drawContours(img2, contour2, idx, color, 3)
# 컨투어 첫 좌표에 인덱스 숫자 표시 ---⑨
cv2.putText(img2, str(idx), tuple(cont[0][0]), cv2.FONT_HERSHEY_PLAIN, \
1, (0,0,255))
# 화면 출력
plt.figure(figsize = (10,6))
imgs = {'RETR_EXTERNAL':img, 'RETR_TREE':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
3 [[[ 1 -1 -1 -1] [ 2 0 -1 -1] [-1 1 -1 -1]]] 6 [[[ 2 -1 1 -1] [-1 -1 -1 0] [ 4 0 3 -1] [-1 -1 -1 2] [-1 2 5 -1] [-1 -1 -1 4]]]
위에서 사용한 영상은 도형안에 도형과 같은 모양으로 뚫려 있습니다. 이 때 컨투어를 찾는 과정은 다음과 같습니다.
- cv2.RETR_EXTERNAL을 이용해서 가장 바깥 컨투어만 수집
- cv2.RETR_TREE 를 써서 모든 컨투어를 계층구조로 수집
위에서 출력된 결과는 컨투어 인덱스 계층 구조를 설명하기 위한 값들을 가진 것을 알 수 있습니다. 위의 len이 6인 출력 결과에서 0번째 행은 첫번 째 삼각형의 외곽선을 의미하고 다음 항목으로는 2번째, 자식 항목으로는 1번째를 가리킵니다. 따라서 1번째 행은 삼각형 내부의 컨투어이고, 1번째 행의 부모는 0번째 행, 나머지 항목의 값은 모두 -1을 가지므로 의미 없음을 나타냅니다. 이와 같이 각 컨투어의 외곽 요소와 자식 요소들을 순회하면서 컨투어 계층 정보를 활용할 수 있습니다. 계층적인 컨투어 정보에서 최외곽 컨투어만 골라내려면 부모 항목이 -1인 행만 찾으면 되고, 그것이 이 예제에서는 도형의 개수와 같습니다.
7.1.1. 이미지 모멘트와 컨투어 속성¶
모멘트(moment)는 물리학에서 힘의 양을 기술할 때 사용하는 용어인데, 영상에서 대상 물체의 양적인 속성을 표현할 때 모멘트라는 용어를 사용합니다.
$$m_{p}{q} = \sum_{x}\sum _{y}f(x,y)x^py^q$$
위 모멘트 계산 공식은 컨투어가 둘러싸는 영역의 x, y 좌표의 픽셀 값과 좌표 인덱스의 p, q차수를 곱한 것의 합을 구합니다. 이 때 각 픽셀의 값은 바이너리 이미지이므로 0이 아닌 모든 값은 1로 계산하고 p, q의 차수는 0 ~ 3까지로 합니다. 이 때 0 ~ 3 차수 모멘트는 공간 모멘트라고 하며, 위치나 크기가 달라지면 그 값도 달라집니다. 위치가 변해도 값이 동일한 모멘트는 중심 모멘트라고 합니다.
moment = cv2.moments(contour)
- contour : 모멘트 계산 대상 컨투어 좌표
- moment : 결과 모멘트, 파이썬 딕셔너리 ( 공간 모멘트, 중심 모멘트, 정규화 중심 모멘트 )
retval = cv2.contourArea(contour[, oriented=False]) : 컨투어로 넓이 계산
- contour : 넓이를 계산할 컨투어
- oriented : 컨투어 방향 클래그
- True : 컨투어 방향에 따라 음수 반환
- False : 절대 값 반환
- retval : 컨투어 영역의 넓이 값
- retval = cv2.arcLength(curve, closed) : 컨투어로 둘레의 길이 계산
- curve : 둘레 길이를 계산할 컨투어
- closed : 닫힌 호인지 여부 플래그
- retval : 컨투어의 둘레 길이 값
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("../img/shape.jpg")
# 그레이 스케일 변환
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 바이너리 스케일 변환
ret, th = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV)
# 컨투어 찾기
contours, hierachy = cv2.findContours(th, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)
# 각 도형의 컨투어에 대한 루프
for c in contours:
# 모멘트 계산
mmt = cv2.moments(c)
# m10/m00, m01/m00 중심점 계산
cx = int(mmt['m10']/mmt['m00'])
cy = int(mmt['m01']/mmt['m00'])
# 영역 넓이
a = mmt['m00']
# 영역 외곽선 길이
l = cv2.arcLength(c, True)
# 중심점에 노란색 점 그리기
cv2.circle(img, (cx, cy), 5, (0, 255, 255), -1)
# 중심점 근처에 넓이 그리기
cv2.putText(img, "A:%.0f"%a, (cx, cy+20) , cv2.FONT_HERSHEY_PLAIN, \
4, (0,0,255),3)
# 컨투어 시작점에 길이 그리기
cv2.putText(img, "L:%.2f"%l, tuple(c[0][0]), cv2.FONT_HERSHEY_PLAIN, \
4, (255,0,0),3)
# 함수로 컨투어 넓이 계산해서 출력
print("area:%.2f"%cv2.contourArea(c, False))
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'center':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
area:159637.00 area:169739.00 area:147855.00
cv2.contourArea() 함수로 구한 것을 각각 결과 영상에 표시해 출력해 주었습니다.
컨투어를 이용해서 해당 영역을 감싸는 여러가지 도형 좌표를 계산하는 함수
- x,y,w,h = cv2.boundingRect(contour) : 좌표를 감싸는 사각형 구하기
- rotateRect = cv2.minAreaRect(contour) : 좌표를 감싸는 최소한의 사각형 계산
- rotateRect = 회전한 사각형 좌표
- vertex = cv2.boxPoints(rotateRect) : rotateRect로부터 꼭짓점 좌표 계산
- vertex = 4개의 꼭짓점 좌표, 소수점 포함, 정수 변환 필요
- center, radius = cv2.minEnclosingCircle(contour) : 좌표를 감싸는 최소한의 동그라미 계산
- area, triangle = cv2.minEnclosingTriangle(points) : 좌표를 감싸는 최소한의 삼각형 계산
- ellipse = cv2.fitEllipse(points) : 좌표를 감싸는 최소한의 타원 계산
- line = cv2.fitLine(points, distType, param, reps, aeps [, line]) : 중심점을 통과하는 직선 계산
- distType : 거리 계산 방식
- param : distType에 전달할 인자, 0 = 최적값 선택
- reps : 반지름 정확도, 선과 원본 좌표의 거리, 0.01 권장
- aeps : 각도 정확도, 0.01 권장
import cv2
import numpy as np
# 이미지 읽어서 그레이스케일 변환, 바이너리 스케일 변환
img = cv2.imread("../img/human.jpg")
origin = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(imgray, 127,255,cv2.THRESH_BINARY_INV)
# 컨튜어 찾기
contours, hr = cv2.findContours(th, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)[-2:]
contr = contours[0]
# 감싸는 사각형 표시(검정색)
x,y,w,h = cv2.boundingRect(contr)
cv2.rectangle(img, (x,y), (x+w, y+h), (0,0,0), 3)
# 최소한의 사각형 표시(초록색)
rect = cv2.minAreaRect(contr)
box = cv2.boxPoints(rect) # 중심점과 각도를 4개의 꼭지점 좌표로 변환
box = np.int0(box) # 정수로 변환
cv2.drawContours(img, [box], -1, (0,255,0), 3)
# 최소한의 원 표시(파랑색)
(x,y), radius = cv2.minEnclosingCircle(contr)
cv2.circle(img, (int(x), int(y)), int(radius), (255,0,0), 2)
# 최소한의 삼각형 표시(분홍색)
ret, tri = cv2.minEnclosingTriangle(contr)
cv2.polylines(img, [np.int32(tri)], True, (255,0,255), 2)
# 최소한의 타원 표시(노랑색)
ellipse = cv2.fitEllipse(contr)
cv2.ellipse(img, ellipse, (0,255,255), 3)
# 중심점 통과하는 직선 표시(빨강색)
[vx,vy,x,y] = cv2.fitLine(contr, cv2.DIST_L2,0,0.01,0.01)
cols,rows = img.shape[:2]
cv2.line(img,(0, int(0-x*(vy/vx) + y)), (cols-1, int((cols-x)*(vy/vx) + y)),(0,0,255),2)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'origin':origin, 'Bound Fit shapes':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
7.1.2. 컨투어 단순화¶
실생활에서 얻는 영상은 대부분 물체가 정확히 표현되는 경우보다 노이즈와 침식이 일어나는 경우가 더 많습니다. 따라서 컨투어 또한 정확한 컨투어보다는 오히려 부정확하게 단순화한 컨투어가 쓸모있는 경우가 더 많습니다.
- approx = cv2.approxPolyDP(contour, epsilon, closed)
- contour : 대상 컨투어 좌표
- epsilon : 근사 값 정확도, 오차 범위
- closed : 컨투어의 닫힘 여부
- approx : 근사 계산한 컨투어 좌표
# 상처투성이 사각형에서 컨투어를 찾는 예제
import cv2
import numpy as np
img = cv2.imread('../img_1/bad_rect.png')
img2 = img.copy()
# 그레이스케일과 바이너리 스케일 변환
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY)
# 컨투어 찾기 ---①
contours, hierachy = cv2.findContours(th, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)[-2:]
contour = contours[0]
# 전체 둘레의 0.05로 오차 범위 지정 ---②
epsilon = 0.05 * cv2.arcLength(contour, True)
# 근사 컨투어 계산 ---③
approx = cv2.approxPolyDP(contour, epsilon, True)
# 각각 컨투어 선 그리기 ---④
cv2.drawContours(img, [contour], -1, (0,255,0), 3)
cv2.drawContours(img2, [approx], -1, (0,255,0), 3)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'contour':img, 'approx':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
cv2.arcLength() 함수로 원래의 컨투어의 둘레 길이를 구해서 그의 0.05만큼의 값으로 epsilon을 설정해서 근사 컨투어를 계산합니다. 왼쪽 그림은 원래의 contour를 표시한 것이고 오른쪽은 근사 컨투어를 표시한 것인데, 들쭉날쭉한 상처를 무시한 사각형을 잘 찾아내고 있습니다.
컨투어를 단순화하는 또 다른 방법은 볼록 선체(convex hull)를 만드는 것입니다. 볼록 선체는 어느 한 부분도 오목하지 않은 상태를 말하는 것으로 대상 객체를 완전히 포함하므로 객체의 외곽 영역을 찾는 데 좋습니다.
- hull = cv2.convexHull(points[, hull, clockwise, returnPoints]): 볼록 선체 찾기
- points : 입력 컨투어
- hull : 볼록 선체 결과
- clockwise : 방향 지정
- returnPoints : 결과 좌표 형식 선택
- True : 볼록 선체 좌표 반환
- False : 입력 컨투어 중에 볼록 선체에 해당하는 인덱스 반환
- retval = cv2.isContourConvex(contour) : 볼록 선체 만족 여부 확인
- retval : True인 경우 볼록 선체 만족
- defects = cv2.convexityDefects(contour, convexhull) :볼록 선체 결함 찾기
- contour : 입력 컨투어
- convexhull : 볼록 선체에 해당하는 컨투어의 인덱스
- defects : 볼록 선체 결함이 있는 컨투어의 배열 인덱스
import cv2
import numpy as np
img = cv2.imread('../img/human.jpg')
img2 = img.copy()
# 그레이 스케일 및 바이너리 스케일 변환 ---①
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
# 컨투어 찾기와 그리기 ---②
contours, heiarchy = cv2.findContours(th, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)
cntr = contours[0]
cv2.drawContours(img, [cntr], -1, (0, 255,0), 1)
# 볼록 선체 찾기(좌표 기준)와 그리기 ---③
hull = cv2.convexHull(cntr)
cv2.drawContours(img2, [hull], -1, (0,255,0), 1)
# 볼록 선체 만족 여부 확인 ---④
print(cv2.isContourConvex(cntr), cv2.isContourConvex(hull))
# 볼록 선체 찾기(인덱스 기준) ---⑤
hull2 = cv2.convexHull(cntr, returnPoints=False)
# 볼록 선체 결함 찾기 ---⑥
defects = cv2.convexityDefects(cntr, hull2)
# 볼록 선체 결함 순회
for i in range(defects.shape[0]):
# 시작, 종료, 가장 먼 지점, 거리 ---⑦
startP, endP, farthestP, distance = defects[i, 0]
# 가장 먼 지점의 좌표 구하기 ---⑧
farthest = tuple(cntr[farthestP][0])
# 거리를 부동 소수점으로 변환 ---⑨
dist = distance/256.0
# 거리가 1보다 큰 경우 ---⑩
if dist > 1 :
# 빨강색 점 표시
cv2.circle(img2, farthest, 3, (0,0,255), -1)
# 결과 이미지 표시
plt.figure(figsize = (10,6))
imgs = {'contour':img, 'convex hull':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
False True
손 모양 그림의 컨투어와 볼록 선체를 각각 구해서 표시하였습니다.
- 손 그림의 컨투어를 찾아서 표시하기
- 손 그림의 컨투어로 볼록선체를 구해서 표시
- 각각의 결과에 대해서 볼록 선체 여부를 확인해서 출력
- 볼록 선체의 결점을 찾기
- 찾은 결점에 빨간색 점을 표시
- 결과 값을 다시 컨투어의 인덱스로 넣어 실제 좌표를 얻기
7.1.3. 컨투어와 도형 매칭¶
서로 다른 물체의 컨투어를 비교하면 두 물체가 얼마나 비슷한 모양인지를 알 수 있습니다. 이를 하려면 앞서 설명한 위치, 크기 그리고 방향에 불변하는 휴 모멘트들을 이용한 복잡한 연산이 필요합니다.
- retval = cv2.matchShapes(contour1, contour2, method, parameter) : 두개의 컨투어로 도형 매칭
- contour1, contour2 : 비교할 2개의 컨투어
- method : 휴 모멘트 비교 알고리즘 선택 플래그
- parameter : 알고리즘에 전달을 위한 예비 인수, 현재 지원 안됨
- retval : 닮음 정도
cv2.matchShapes() 함수는 2개의 컨투어를 인자로 받아서 휴 모멘트 비교 알고리즘에 따라 비교한 결과를 소수점 있는 숫자로 반환합니다. 두 컨투어가 완전히 같으면 0을 반환하고 그 닮음 정도가 다를수록 큰 수를 반환합니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 매칭을 위한 이미지 읽기
target = cv2.imread('../img/spade.png') # 매칭 대상
shapes = cv2.imread('../img/trump_card.png') # 여러 도형
# 그레이 스케일 변환
targetGray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
shapesGray = cv2.cvtColor(shapes, cv2.COLOR_BGR2GRAY)
# 바이너리 스케일 변환
ret, targetTh = cv2.threshold(targetGray, 127, 255, cv2.THRESH_BINARY_INV)
ret, shapesTh = cv2.threshold(shapesGray, 127, 255, cv2.THRESH_BINARY_INV)
# 컨투어 찾기
cntrs_target, _ = cv2.findContours(targetTh, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)
cntrs_shapes, _ = cv2.findContours(shapesTh, cv2.RETR_EXTERNAL, \
cv2.CHAIN_APPROX_SIMPLE)
# 각 도형과 매칭을 위한 반복문
matchs = [] # 컨투어와 매칭 점수를 보관할 리스트
for contr in cntrs_shapes:
# 대상 도형과 여러 도형 중 하나와 매칭 실행 ---①
match = cv2.matchShapes(cntrs_target[0], contr, cv2.CONTOURS_MATCH_I2, 0.0)
# 해당 도형의 매칭 점수와 컨투어를 쌍으로 저장 ---②
matchs.append( (match, contr) )
# 해당 도형의 컨투어 시작지점에 매칭 점수 표시 ---③
cv2.putText(shapes, '%.2f'%match, tuple(contr[0][0]),\
cv2.FONT_HERSHEY_PLAIN, 7,(255,0,0),3 )
# 매칭 점수로 정렬 ---④
matchs.sort(key=lambda x : x[0])
# 가장 적은 매칭 점수를 얻는 도형의 컨투어에 선 그리기 ---⑤
cv2.drawContours(shapes, [matchs[0][1]], -1, (0,255,0), 3)
plt.figure(figsize = (10,6))
imgs = {'target':target, 'Match Shape':shapes}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
왼쪽에 있는 별 모양의 도형과 오른쪽에 있는 3개의 도형을 매칭해서 가장 비슷한 도형을 찾는 코드입니다. target 도형과 3개의 도형의 컨투어를 반복 매칭하여 매칭 결과와 각 도형의 컨투어를 쌍으로 matchs에 저장합니다. 이후 각 매칭 점수를 3개의 도형에 표시하고 3개의 매칭 결과를 정렬한 후 첫번째 요소만 꺼내서 컨투어를 표시하였습니다.
7.2. 허프 변환¶
허프 변환(Hough transform)은 영상에서 직선과 원 같은 간단한 모양을 식별하는 방법입니다.
7.2.1. 허프 선 변환¶
영상은 수많은 픽셀로 구성되는데 수많은 픽셀 속에서 직선 관계를 갖는 픽셀들만 골라내는 것이 허프 선 변환의 핵심입니다. 각 점마다 여러개의 가상의 선을 그어서 그 선들 중 평면 원점과 직각을 이루는 선을 찾아 각도와 거리를 구해서 모든 점에게 동일하게 나타나는 선이 있다면, 그 점들은 그 선을 따라 직선의 형태를 띠는 것이라고 볼 수 있습니다.
- lines = cv2.HoughLines(img, rho, theta, threshold[, lines, srn=0, stn=0, min_theta, max_theta])
- img : 입력 영상
- rho : 거리 측정 해상도
- theta : 각도 측정 해상도
- threshold : 직선으로 판단할 최소한의 동일 개수
- lines : 검출 결과
- srn, stn : 멀티 스케일 허프 변환에 사용
- min_theta, max_theta : 검출을 위해 사용할 최대, 최소 각도
위 함수는 바이너리 스케일 영상을 입력으로 전달하면 r, $\theta$를 값으로 하는 N x 1 배열을 반환합니다.
import cv2
import numpy as np
img = cv2.imread('../img_1/sudoku.jpg')
img2 = img.copy()
h, w = img.shape[:2]
# 그레이 스케일 변환 및 엣지 검출 ---①
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(imgray, 100, 200 ) # 캐니 엣지로 경계 검출
# 허프 선 검출 ---②
lines = cv2.HoughLines(edges, 1, np.pi/180, 130)
for line in lines: # 검출된 모든 선 순회
r,theta = line[0] # 거리와 각도wh
tx, ty = np.cos(theta), np.sin(theta) # x, y축에 대한 삼각비
x0, y0 = tx*r, ty*r #x, y 기준(절편) 좌표
# 기준 좌표에 빨강색 점 그리기
cv2.circle(img2, (int(abs(x0)), int(abs(y0))), 3, (0,0,255), -1)
# 직선 방정식으로 그리기 위한 시작점, 끝점 계산
x1, y1 = int(x0 + w*(-ty)), int(y0 + h * tx)
x2, y2 = int(x0 - w*(-ty)), int(y0 - h * tx)
# 선그리기
cv2.line(img2, (x1, y1), (x2, y2), (0,255,0), 1)
#결과 출력
plt.figure(figsize = (10,6))
imgs = {'target':img, 'hough line':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
7.2.2. 확률적 허프 선 변환¶
허프선 검출은 모든 점에 대해 선을 그어 직선을 찾기 때문에 매우 많은 연산이 필요합니다. 따라서 이를 극복하기 위해 무작위로 선정한 픽셀로 허프 변환을 수행해서 점점 그 수를 증가시켜 가는 방법입니다.
이 방법은 cv2.HoughLines() 함수에 비해 선 검출이 적게 되기 때문에 엣지를 강하게 하고 threshold 값을 낮게 지정해야 합니다.
import cv2
import numpy as np
img = cv2.imread('../img_1/sudoku.jpg')
img2 = img.copy()
# 그레이 스케일로 변환 및 엣지 검출 ---①
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(imgray, 50, 200 )
# 확율 허프 변환 적용 ---②
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 10, None, 20, 2)
for line in lines:
# 검출된 선 그리기 ---③
x1, y1, x2, y2 = line[0]
cv2.line(img2, (x1,y1), (x2, y2), (0,255,0), 1)
plt.figure(figsize = (10,6))
imgs = {'target':img, 'Probability hough line':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
7.2.3. 허프 원 변환¶
직교좌표를 극좌표로 바꾸면 (x,y) 좌표를 (r,$\theta$) 좌표로 변환할 수 있으므로 허프 직선 변환의 알고리즘을 그대로 적용하여 원을 검출할 수 있습니다. 하지만 메모리와 연산속도를 이유로 OpenCV에서는 이 방법을 구현하지 않고 캐니 엣지를 수행하고 나서 소벨 필터를 적용해서 엣지의 경사도를 누적하는 방법으로 구현하였습니다.
- circles = cv2.HoughCircles(img, method, dp, minDist[, circles, param1, param2, minRadius, maxRadius])
- img : 입력 영상
- method : 검출 방식 선택
- cv2.HOUGH_STANDARD
- cv2.HOUGH_PROBABILISTIC
- cv2.HOUGH_MULTI_SCALE
- cv2.HOUGH_GRADIENT
- dp : 입력 영상과 경사 누적의 해상도 반비례율
- minDist : 원들 중심 간의 최소 거리
- circles : 검출 원 결과
- param1 : 캐니 엣지에 전달할 스레시홀드 최대 값
- param2 : 경사도 누적 경계 값
- minRadius, maxRadius : 원의 최소 반지름, 최대 반지름
import cv2
import numpy as np
img = cv2.imread('../img/circles.png')
# 그레이 스케일 변환 ---①
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 노이즈 제거를 위한 가우시안 블러 ---②
blur = cv2.GaussianBlur(gray, (3,3), 0)
# 허프 원 변환 적용( dp=1.5, minDist=30, cany_max=200 ) ---③
circles = cv2.HoughCircles(blur, cv2.HOUGH_GRADIENT, 1.5, 30, None, 200)
if circles is not None:
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# 원 둘레에 초록색 원 그리기
cv2.circle(img,(i[0], i[1]), i[2], (0, 255, 0), 2)
# 원 중심점에 빨강색 원 그리기
cv2.circle(img, (i[0], i[1]), 2, (0,0,255), 5)
# 결과 출력
plt.figure(figsize = (10,6))
imgs = {'hough circle':img}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,1,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
- 원본이미지에서 중앙의 작은 원을 제외하고 나머지 원을 검출할 수 있었습니다.
- 바깥쪽 원의 중심(x, y) 좌표가 안쪽 원과 같으므로 안쪽 원이 생략되기 때문입니다.
cv2.HoughCircles() 함수는 내부적으로 캐니 엣지를 사용하기 때문에 노이즈를 제거하기 위해 가우시안 블러만 처리하고 엣지 검출은 따로 하지 않습니다. 대신 캐니 엣지에 사용할 최대 스레시홀드 값을 전달해야 합니다 (200). 함수를 호출할 때 dp는 원본 영상과 경사도 누적도의 해상도를 조정하는데 1로 했을 때 해상도가 동일하기 때문에 가장 정확하고, 값이 커질수록 부정확한 원을 검출할 수 있습니다. 이 값은 1부터 시작해서 조금씩 늘려가며 경험적으로 찾아야 합니다. minDist 값은 중심점 간의 최소 거리를 의미하는 것으로 0을 넣을 수 없습니다.
이 전까지는 외곽 경계를 이용하여 객체 영역을 분할하는 방법에 대하여 알아보았습니다. 하지만 실제 이미지에는 노이즈도 많고 경계선도 뚜렷하지 않아 객체를 정확하게 분할하기 힘든 경우도 있습니다. 연속된 영역을 찾아 분할하는 방법도 필요하기 때문에 다음 장에서는 '연속 영역 분할'에 관한 내용으로 알아보겠습니다.
7.3. 연속 영역 분할¶
7.3.1 거리 변환¶
이미지에서 객체 영역을 정확하게 파악하기 위해 객체 영역의 뼈대를 찾아야 합니다. 뼈대를 검출하는 방법 중 하나로 외곽 경계로부터 가장 멀리 떨어진 곳을 찾는 방법인 거리 변환을 이야기 할 수 있습니다.
거리 변환 : 바이너리 스케일 이미지를 대상으로 원본 이미지와 같은 크기의 배열에 픽셀값이 0인 위치에 0으로 시작해서 멀어질 때마다 1씩 증가하는 방식으로 경계로부터 가장 먼 픽셀이 가장 큰 값을 갖게 하는 변환.
cv2.distanceTransform(src, distanceType, maskSize)
- src: 입력 영상, 바이너리 스케일
- distanceType: 거리 계산 방식
- cv2.DIST_L2
- cv2.DIST_L1
- cv2.DIST_L12
- cv2.DIST_FAIR
- cv2.DIST_WELSCH
- cv2.DIST_HUBER
- maskSize: 거리 변환 커널 크기
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 이미지를 읽어서 바이너리 스케일로 변환
img = cv2.imread('./img/arrow.jpg', cv2.IMREAD_GRAYSCALE)
print(img.shape)
_, biimg = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
# 거리 변환 함수를 이용하여 거리 변환
dst = cv2.distanceTransform(biimg, cv2.DIST_L2, 5)
# 거리를 0 ~ 255로 정규화
dst = (dst/(dst.max()-dst.min()) * 255).astype(np.uint8)
# 거리 값에 쓰레시홀드로 완전한 뼈대 찾기
skeleton = cv2.adaptiveThreshold(dst, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, \
cv2.THRESH_BINARY, 7, -3)
print(skeleton.shape)
# 결과 출력
# cv2.imshow('origin', img)
# cv2.imshow('dist', dst)
# cv2.imshow('skel', skeleton)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
biimg = cv2.cvtColor(biimg, cv2.COLOR_GRAY2RGB)
dst = cv2.cvtColor(dst, cv2.COLOR_GRAY2RGB)
skeleton = cv2.cvtColor(skeleton, cv2.COLOR_GRAY2RGB)
plt.figure(figsize = (10,10))
imgs = {'img':img, 'biimg':biimg, 'dist':dst, 'skel':skeleton}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v[:,::-1])
plt.xticks([]),plt.yticks([])
(261, 411) (261, 411)
위 결과가 차례로 나타내는 바는 다음과 같습니다.
- img : 원본 이미지입니다.
- biimg : 원본 이미지를 바이너리 스케일로 변환한 이미지입니다.
- dist : 거리 변환 함수를 이용하여 거리 변환을 한 후 0 ~ 255로 정규화를 한 이미지입니다.
- skeleton : 경계로부터 가장 멀리 떨어져 있는 부분만 추출한 이미지입니다.
이렇게 위 과정을 통해 객체 영역의 뼈대를 추출하였습니다.
7.3.2 연결 요소 레이블링¶
연결된 요소끼리 분리하는 방법 중 하나는 레이블링 방법이 있습니다. 이는 이미지에서 픽셀 값이 0으로 끊어지지 않는 부분끼리 같은 값을 부여해 분리를 할 수 있습니다.
연결 요소 레이블링과 개수 반환
retval, labels = cv2.connectedComponents(src, labels, connectivity=8, ltype)
- src: 입력 이미지, 바이너리 스케일
- labels(optional): 레이블링된 입력 이미지와 같은 크기의 배열
- connectivity(optional): 연결성을 검사할 방향 개수(4, 8 중 선택)
- ltype(optional): 결과 레이블 배열 dtype
- retval(optional): 레이블 개수
레이블링된 각종 상태 정보 반환
- retval, labels, stats, centroids = cv2.connectedComponentsWithStats(src, labels, stats, centroids, connectivity, ltype)
- stats: N x 5 행렬 (N: 레이블 개수) [x좌표, y좌표, 폭, 높이, 너비]
- centroids: 각 레이블의 중심점 좌표, N x 2 행렬 (N: 레이블 개수)
cv2.connectedComponents() 함수와 cv2.connectedComponentsWithStats() 함수 중 하나를 이용하여 레이블링을 하면 됩니다. 두 함수는 각종 추가 정보를 제공하는 것 말고는 동일합니다.
import cv2
import numpy as np
# 이미지 읽기
img = cv2.imread('./img/fish.jpg')
# 결과 이미지 생성
img2 = np.zeros_like(img)
# 그레이 스케일과 바이너리 스케일 변환
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, th = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 연결된 요소 레이블링 적용
cnt, labels = cv2.connectedComponents(th)
#retval, labels, stats, cent = cv2.connectedComponentsWithStats(th)
print(cnt)
# 레이블 갯수 만큼 순회
for i in range(cnt):
# 레이블이 같은 영역에 랜덤한 색상 적용
img2[labels==i] = [int(j) for j in np.random.randint(0,255, 3)]
# 결과 출력
# cv2.imshow('origin', img)
# cv2.imshow('labeled', img2)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
print(img2.shape)
plt.figure(figsize = (10,10))
imgs = {'img':img, 'labeled':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v)
plt.xticks([]),plt.yticks([])
9 (256, 256, 3)
- 각 빈 부분에 9가지의 색으로 채워진 것을 확인할 수 있습니다.
7.3.3 색 채우기¶
위 단원에서는 '채우기' 기능을 이용하여 색상을 칠하는 작업을 해볼 것입니다.
- retval, img, mask, rect = cv2.floodFill(img, mask, seed, newVal, loDiff, upDiff, flags)
- img: 입력 이미지, 1 또는 3채널
- mask: 입력 이미지보다 2 x 2 픽셀이 더 큰 배열, 0이 아닌 영역을 만나면 채우기 중지
- seed: 채우기 시작할 좌표
- newVal: 채우기에 사용할 색상 값
- loDiff, upDiff(optional): 채우기 진행을 결정할 최소/최대 차이 값
- flags(optional): 채우기 방식 선택
- cv2.FLOODFILL_MASK_ONLY: img가 아닌 mask에만 채우기 적용
- cv2.FLOODFILL_FIXED_RANGE: 이웃 픽셀이 아닌 seed 픽셀과 비교)
- retval: 채우기 한 픽셀의 개수
- rect: 채우기가 이루어진 영역을 감싸는 사각형
이 함수는 img의 seed 좌표에서 시작하여 newVal으 값으로 채우기를 시작합니다. 이때, 이웃하는 픽셀에 채우기를 계속 하려면 현재 픽셀이 이웃 픽셀의 IoDifff를 뺀 값보다 크건 같고 upDiff를 더한 값보다는 작거나 같아야 합니다.(생략시 SEED와 같은 값을 가지는 이웃 픽셀만 채우기 진행)
import cv2
import numpy as np
img = cv2.imread('./img/woman.jpg')
img2 = img.copy()
rows, cols = img2.shape[:2]
# 마스크 생성, 원래 이미지 보다 2픽셀 크게
mask = np.zeros((rows+2, cols+2), np.uint8)
# 채우기에 사용할 색
newVal = (255,255,255)
# 최소 최대 차이 값
loDiff, upDiff = (10,10,10), (10,10,10)
# 마우스 이벤트 처리 함수
def onMouse(event, x, y, flags, param):
global mask, img2
if event == cv2.EVENT_LBUTTONDOWN:
seed = (x,y)
# 색 채우기 적용
retval = cv2.floodFill(img2, mask, seed, newVal, loDiff, upDiff)
# 채우기 변경 결과 표시
cv2.imshow('img', img2)
# 화면 출력
cv2.imshow('img', img2)
cv2.setMouseCallback('img', onMouse)
cv2.waitKey(0)
cv2.destroyAllWindows()
plt.figure(figsize = (10,10))
imgs = {'img':img, 'labeled':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,::-1])
plt.xticks([]),plt.yticks([])
- 예시 이미지는 선이 뚜렷하지 않아서 분리가 잘 되지는 않은 모습을 볼 수 있습니다.
7.3.4 워터셰드¶
워터셰드(Watershed)는 강물이 한 줄기로 흐르다가 갈라지는 경계인 분수령을 의미합니다. 영상처리에서는 경계를 찾는 방법 중 하나로 픽셀 값의 크기를 산과 골짜기 같은 높고 낮은 지형으로 보고 물을 채워서 그 물이 만나는 곳을 경계로 찾는 방식입니다. 앞서 살펴본 색 채우기와 비슷한 방식으로 연속된 영역을 찾는것이라고 볼 수 있지만, seed가 하나가 아닌 여러개를 지정할 수 있고 이런 seed를 marker라고 합니다.
- markers = cv2.watershed(img, markers)
- img: 입력 이미지
- markers: 마커, 입력 이미지와 크기가 같은 1차원 배열(int32)
이때 markers는 입력 이미지와 행과 열 크기가 같은 1차원 배열로 전달해야 합니다. 또한 markers의 값은 경계를 찾고자 하는 픽셀 영역은 0을 갖게 하고 연결된 영역이 확실한 픽셀에 대해선 동일한 양의 정수를 값으로 갖게 합니다.
진행 순서 :
1) 0으로 채워진 marker 생성
2) 마우스가 움직이면 움직인 영상의 좌표에 해당하는 marker 좌표에 현재의 marker 아이디를 채움(선 그린 곳 -> 1 또는 2)
3) 하나의 선 그리기를 끝냈으면 다음 marker를 위해 아이디 증가
4) 다 끝나고 오른쪽 마우스를 누르면 워터셰드 실행
- marker는 1로 채워진 marker 공간이 워터셰드로 분리될 때까지 1로 채우는 식으로 연결된 공간의 marker값을 같게 하고 경계에 해당하는 영역만 -1로 채움
5) -1로 채워진 marker와 같은 좌표의 영상 픽셀을 초록색으로 변환
6) 미리 저장해둔 marker 아이디와 marker를 선택할 때 픽셀값을 이용하여 같은 marker 아이디 값을 가진 영역을 같은 색으로 채움
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('./img/taekwonv1.jpg')
rows, cols = img.shape[:2]
img_draw = img.copy()
# 마커 생성, 모든 요소는 0으로 초기화
marker = np.zeros((rows, cols), np.int32)
markerId = 1 # 마커 아이디, 1에서 시작
colors = [] # 마커 선택한 영역 색상 저장할 공간
isDragging = False # 드래그 여부 확인 변수
# 마우스 이벤트 처리 함수
def onMouse(event, x, y, flags, param):
global img_draw, marker, markerId, isDragging
if event == cv2.EVENT_LBUTTONDOWN: # 왼쪽 마우스 버튼 다운, 드래그 시작
isDragging = True
# 각 마커의 아이디와 현 위치의 색상 값을 쌍으로 매핑해서 저장
colors.append((markerId, img[y,x]))
elif event == cv2.EVENT_MOUSEMOVE: # 마우스 움직임
if isDragging: # 드래그 진행 중
# 마우스 좌표에 해당하는 마커의 좌표에 동일한 마커 아이디로 채워 넣기
marker[y,x] = markerId
# 마커 표시한 곳을 빨강색점으로 표시해서 출력
cv2.circle(img_draw, (x,y), 3, (0,0,255), -1)
cv2.imshow('watershed', img_draw)
elif event == cv2.EVENT_LBUTTONUP: # 왼쪽 마우스 버튼 업
if isDragging:
isDragging = False # 드래그 중지
# 다음 마커 선택을 위해 마커 아이디 증가
markerId +=1
elif event == cv2.EVENT_RBUTTONDOWN: # 오른쪽 마우스 버튼 누름
# 모아 놓은 마커를 이용해서 워터 쉐드 적용
cv2.watershed(img, marker)
# 마커에 -1로 표시된 경계를 초록색으로 표시
img_draw[marker == -1] = (0,255,0)
for mid, color in colors: # 선택한 마커 아이디 갯수 만큼 반복
# 같은 마커 아이디 값을 갖는 영역을 마커 선택한 색상으로 채우기
img_draw[marker==mid] = color
cv2.imshow('watershed', img_draw) # 표시한 결과 출력
# 화면 출력
cv2.imshow('watershed', img)
cv2.setMouseCallback('watershed', onMouse)
cv2.waitKey(0)
cv2.destroyAllWindows()
plt.figure(figsize = (10,10))
imgs = {'img':img, 'img_draw':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,::-1])
plt.xticks([]),plt.yticks([])
- 클릭하는 마우스에 따라 객체와 배경이 잘 분리된 것을 확인할 수 있습니다.
7.3.5 그랩컷¶
그랩컷(Graph Cut)은 그래프 컷을 기반으로 하는 알고리즘을 확장한 것입니다. 사용자가 객체로 분리할 부분에 사각형 표시를 해주면 객체와 배경의 색상 분포를 추정하여 동일한 레이블을 가진 연결된 영역에서 객체와 배경을 분리합니다.
- mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode)
- img: 입력 이미지
- mask: 입력 이미지와 크기가 같은 1 채널 배열, 배경과 전경을 구분하는 값을 저장
- cv2.GC_BGD: 확실한 배경(0)
- cv2.GC_FGD: 확실한 전경(1)
- cv2.GC_PR_BGD: 아마도 배경(2)
- cv2.GC_PR_FGD: 아마도 전경(3)
- rect: 전경이 있을 것으로 추측되는 영역의 사각형 좌표, 튜플 (x1, y1, x2, y2)
- bgdModel, fgdModel: 함수 내에서 사용할 임시 배열 버퍼 (재사용할 경우 수정하지 말 것)
- iterCount: 반복 횟수
- mode(optional): 동작 방법
- cv2.GC_INIT_WITH_RECT: rect에 지정한 좌표를 기준으로 그랩컷 수행
- cv2.GC_INIT_WITH_MASK: mask에 지정한 값을 기준으로 그랩컷 수행
- cv2.GC_EVAL: 재시도
mode에 cv2.GC_INIT_WITH_RECT를 전달하면 세 번째 파라미터인 rect에 전달한 사각형 좌표를 이용해 객체와 배경을 분리합니다. 그 결과를 mask에 할당하여 반환합니다. mask에 할당받은 값이 0과 1이면 확실한 배경, 객체를 의미하고 2와 3이면 아마도 배경, 전경을 의미합니다. 1차적으로 구분한 후 mode에 cv2.GC_INIT_WITH_MASK를 지정하여 다시 호출하면 조금 더 정확한 mask를 얻을 수 있습니다.
import cv2
import numpy as np
img = cv2.imread('./img/taekwonv1.jpg')
img_draw = img.copy()
mask = np.zeros(img.shape[:2], dtype=np.uint8) # 마스크 생성
rect = [0,0,0,0] # 사각형 영역 좌표 초기화
mode = cv2.GC_EVAL # 그랩컷 초기 모드
# 배경 및 전경 모델 버퍼
bgdmodel = np.zeros((1,65),np.float64)
fgdmodel = np.zeros((1,65),np.float64)
# 마우스 이벤트 처리 함수
def onMouse(event, x, y, flags, param):
global mouse_mode, rect, mask, mode
if event == cv2.EVENT_LBUTTONDOWN : # 왼쪽 마우스 누름
if flags <= 1: # 아무 키도 안 눌렀으면
mode = cv2.GC_INIT_WITH_RECT # 드래그 시작, 사각형 모드
rect[:2] = x, y # 시작 좌표 저장
# 마우스가 움직이고 왼쪽 버튼이 눌러진 상태
elif event == cv2.EVENT_MOUSEMOVE and flags & cv2.EVENT_FLAG_LBUTTON :
if mode == cv2.GC_INIT_WITH_RECT: # 드래그 진행 중
img_temp = img.copy()
# 드래그 사각형 화면에 표시
cv2.rectangle(img_temp, (rect[0], rect[1]), (x, y), (0,255,0), 2)
cv2.imshow('img', img_temp)
elif flags > 1: # 키가 눌러진 상태
mode = cv2.GC_INIT_WITH_MASK # 마스크 모드
if flags & cv2.EVENT_FLAG_CTRLKEY :# 컨트롤 키, 분명한 전경
# 흰색 점 화면에 표시
cv2.circle(img_draw,(x,y),3, (255,255,255),-1)
# 마스크에 GC_FGD로 채우기
cv2.circle(mask,(x,y),3, cv2.GC_FGD,-1)
if flags & cv2.EVENT_FLAG_SHIFTKEY : # 쉬프트키, 분명한 배경
# 검정색 점 화면에 표시
cv2.circle(img_draw,(x,y),3, (0,0,0),-1)
# 마스크에 GC_BGD로 채우기
cv2.circle(mask,(x,y),3, cv2.GC_BGD,-1)
cv2.imshow('img', img_draw) # 그려진 모습 화면에 출력
elif event == cv2.EVENT_LBUTTONUP: # 마우스 왼쪽 버튼 뗀 상태
if mode == cv2.GC_INIT_WITH_RECT : # 사각형 그리기 종료
rect[2:] =x, y # 사각형 마지막 좌표 수집
# 사각형 그려서 화면에 출력
cv2.rectangle(img_draw, (rect[0], rect[1]), (x, y), (255,0,0), 2)
cv2.imshow('img', img_draw)
# 그랩컷 적용
cv2.grabCut(img, mask, tuple(rect), bgdmodel, fgdmodel, 1, mode)
img2 = img.copy()
# 마스크에 확실한 배경, 아마도 배경으로 표시된 영역을 0으로 채우기
img2[(mask==cv2.GC_BGD) | (mask==cv2.GC_PR_BGD)] = 0
cv2.imshow('grabcut', img2) # 최종 결과 출력
mode = cv2.GC_EVAL # 그랩컷 모드 리셋
# 초기 화면 출력 및 마우스 이벤트 등록
cv2.imshow('img', img)
cv2.setMouseCallback('img', onMouse)
while True:
if cv2.waitKey(0) & 0xFF == 27 : # esc
break
cv2.destroyAllWindows()
plt.figure(figsize = (10,10))
imgs = {'img':img, 'img_draw':img_draw}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,::-1])
plt.xticks([]),plt.yticks([])
마우스 사용
- 객체 영역을 드래그 해서 사각형 그리기
- 미처 지우지 못한 배경에 지우고 싶은 부분에 검은색 선으로 그리기
- 잘못 지워진 부분은 흰색 선으로 그리기
결과값인 img2를 살펴보면 잘 제거된 것을 확인할 수 있습니다.
[마무리]¶
- 여러 영상 분할 방법에 대하여 공부를 해보았습니다.
- 보통 영상을 분할한다고 하면 딥러닝 방법을 많이 이용하여 진행을 하는데 데이터가 적거나 뚜렷한 선이 보인다면 OpenCV를 이용해서 하는 방법도 좋은 방법이 될 수 있을 것 같습니다 :)
감사합니다 :)
'대외활동 > DACrew 2기' 카테고리의 다른 글
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 8장. 영상매칭과 추적 (0) | 2022.06.14 |
---|---|
[🔥팀 포스🔥] 첫번째 프로젝트, Multi-Hand Gesture Recognition-2 (0) | 2022.05.27 |
[🔥팀 포스🔥] 첫번째 프로젝트, Multi-Hand Gesture Recognition (0) | 2022.05.21 |
댓글