본문 바로가기

Deep Learning(강의 및 책)/OpenCV

[OpenCV] 영상 분할

728x90

이 글은 '파이썬으로 만드는 OpenCV 프로젝트'를 보고 작성했습니다.

 

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: 그림 그릴 컨투어 인덱스, -1: 모든 컨투어 표시
    • color: 색상 값
    • thickness: 선 두께, 0: 채우기

cv2.findContours() 함수는 src 인자에 검은색 배경에 흰색 전경으로 표현된 바이너리 이미지를 전달하면 컨투어에 해당하는 좌표를 갖는 Numpy 배열을 파이썬 리스트로 반환합니다. mode는 반환 값으로 받을 contours에 영상의 모든 컨투어가 다 들어 있을지 아닐지를 선택하는 것입니다. cv2.RETR_EXTERNAL을 지정하면 컨투어 중에 가장 바깥쪽 컨투어만 담아서 반환하고, 그 나머지는 여러 컨투어를 담아서 반환합니다.

cv2.findContours() 함수의 반환 값은 OpenCV의 버전에 따라 달라서 버전 간에 호환되는 코드를 작성하기 위해서는 함수의 반환 값 중 마지막 2개만 사용하도록 하는 [-2:]를 추가하는 것이 좋습니다. cv2.lines()나 cv2.polylines()와 같은 함수로 직접 선을 그려도 되지만 OpenCV는 컨투어 연결선을 손쉽게 그릴 수 있게 cv2.drawContours() 함수를 제공하며, 이 함수는  img 영상에 contours 배열에 있는 컨투어 중에 contourIdx에 해당하는 컨투어를 color 색상과 thickness 두께로 선을 그립니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/shapes.png')
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)
# 가장 바깥쪽 컨투어에 대해 꼭지점 좌표만 반환 ---④
contour2, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, \
                                                cv2.CHAIN_APPROX_SIMPLE)
# 각각의 컨투의 갯수 출력 ---⑤
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) 

# 결과 출력 ---⑩
cv2.imshow('CHAIN_APPROX_NONE', img)
cv2.imshow('CHAIN_APPROX_SIMPLE', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

세 도형의 컨투어를 구해서 그려주는 코드입니다. 원본 이미지를 그레이 스케일로 변환한 후 스레스홀드로 바이너리 이미지로 만들면서 배경은 검은색, 전경은 흰색이 되게 반전합니다. 컨투어의 개수는 도형의 개수와 같고 초록색 선으로 그렸습니다. cv2.drawContours()를 이용하면 컨투어의 모든 좌표를 갖든 꼭짓점 좌표만 갖든 간에 도형의 외곽을 완전히 그려내는 것을 볼 수 있습니다.

 

import cv2
import numpy as np

# 영상 읽기
img = cv2.imread('../img/shapes_donut.png')
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))

# 화면 출력
cv2.imshow('RETR_EXTERNAL', img)
cv2.imshow('RETR_TREE', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

이번에는 cv2.RETR_TREE를 사용해 모든 컨투어를 계층 구조로 수집하는 모습입니다. hierarchy의 출력 결과를 보면 각 컨투어에서 tree구조를 따라 부모, 자식을 의미하고 컨투어 순서를 의미하는 것을 알 수 있습니다.

 

■ 이미지 모멘트와 컨투어 속성

모멘트(moment)는 물리학에서 힘의 양을 기술할 때 사용하는 용어인데, 영상에서도 대상 물체의 양적인 속성을 표현할 때 모멘트라는 용어를 사용합니다. 이미지 모멘트는 m_(p, q) = Σ_xΣ_y (f(x, y)x^p*y^q) 로 표현 가능합니다. 컨투어가 둘러싸는 영역의 x, y 좌표의 픽셀 값과 좌표 인덱스의 p, q 차수를 곱한 것의 합입니다. 각 픽셀의 값은 바이너리 이미지이므로 0이 아닌 모든 값은 1이고 p, q의 차수는 0 ~ 3까지로 합니다. 이를 이용해 m_(0, 1), m_(1, 0)을 각각 m_(0, 0)으로 나누면 평균 x, y 값을 구할 수 있고 이것은 컨투어 영역의 중심 좌표입니다.

  • moment = cv2.moments(contour)
    • contour: 모멘트 계싼 대상 컨투어 좌표
    • moment: 결과 모멘트, 파이썬 딕셔너리
      • m00, m01, m10, m11, m02, m12, m20, m21, m03, m30: 공간 모멘트
      • mu20, mu11, mu02, mu30, mu21, mu12, mu03: 중심 모멘트
      • nu20, nu11, nu30, nu21, nu03: 정규화 중심 모멘트

OpenCV에서 제공하는 모멘트 계산을 위한 함수입니다. OpenCV는 컨투어를 통해 얻는 정보 중에 넓이와 둘레 길이를 위해서 별도로 함수를 제공합니다.

  • 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

img = cv2.imread("../img/shapes.png")
# 그레이 스케일 변환
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 바이너리 스케일 변환
ret, th = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV)
# 컨투어 찾기
img2, 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, \
                                                            1, (0,0,255))
    # 컨투어 시작점에 길이 그리기
    cv2.putText(img, "L:%.2f"%l, tuple(c[0][0]), cv2.FONT_HERSHEY_PLAIN, \
                                                            1, (255,0,0))
    # 함수로 컨투어 넓이 계산해서 출력
    print("area:%.2f"%cv2.contourArea(c, False))

# 결과 출력
cv2.imshow('center', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

컨투어의 이미지 모멘트로 각 도형의 중심점, 넓이, 둘레 길이를 구해서 표시하는 코드입니다.

OpenCV는 컨투어를 이용해 해당 영역을 감싸는 여러 가지 도형 좌표를 계산하는 함수도 제공합니다.

  • x, y, w, h = cv2.boundingRect(contour): 좌표를 감싸는 사각형 구하기
    • x, y: 사각형 왼쪽 상단 좌표
    • w, h: 폭, 높이
  • rotateRect = cv2.minAreaRect(contour): 좌표를 감싸는 최소한의 사각형 계산
    • rotateRect: 회전한 사각형 좌표
      • center: 중심점(x, y)
      • size: 크기(w, h)
      • angle: 회전 각(양수: 시계 방향, 음수: 반시계 방향)
  • vertex = cv2.boxPoints(rotateRect): rotateRect로부터 꼭짓점 좌표 계산
    • vertex: 4개의 꼭짓점 좌표, 소수점 포함, 정수 변환 필요
  • center, radius = cv2.minEnclosingCircle(contour): 좌표를 감싸는 최소한의 동그라미 계산
    • center: 원점 좌표(x, y), 튜플
    • radius: 반지름
  • area, triangle = cv2.minEnclosingTriangle(points): 좌표를 감싸는 최소한의 삼각형 계산
    • area: 넓이
    • triangle: 3개의 꼭짓점 좌표
  • ellipse = cv2.filtEllipse(points): 좌표를 감싸는 초소한의 타원 계산
  • ellipse
    • center: 원점 좌표(x, y), 튜플
    • axes: 축의 길이(x축, y축), 튜플
    • angle: 회전 각도
  • line = cv2.fitLines(points, distType, param, reps, aeps [, Line] ): 중심점을 통과하는 직선 계산
    • distType: 거리 계산 방식
      • cv2.DIST_L2, cv2.DIST_L1, cv2.DIST_L12, cv2.DIST_FAIR, cv2.DIST_WELSCH, cv2.DIST_HUBER
    • param: distType에 전달할 인자, 0 = 최적 값 선택
    • reps: 반지름 정확도, 선과 원본 좌표의 거리, 0,01 권장
    • aeps: 각도 정확도, 0.01 권장
    • line
      • vx, vy: 정규화된 단위 벡터, vy/vx: 직선의 기울기, 튜플
      • x0, y0: 중심점 좌표, 튜플
import cv2
import numpy as np

# 이미지 읽어서 그레이스케일 변환, 바이너리 스케일 변환
img = cv2.imread("../img/lightning.png")
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)
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)

# 결과 출력
cv2.imshow('Bound Fit shapes', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

■ 컨투어 단순화

실생활에서 얻는 영상은 대부분 물체가 정확히 표현되는 경우보다는 노이즈와 침식이 일어나는 경우가 더 많습니다. 그래서 컨투어도 정화한 컨투어보다는 오히려 부정확하게 단순화한 컨투어가 쓸모 있는 경우가 더 많습니다.

  • approx = cv2.approxPolyDP(contour, epsilon, closed)
    • contour: 대상 컨투어 좌표
    • epsilon: 근사 값 정확도, 오차 범위
    • closed: 컨투어의 닫힘 여부
    • approx: 근사 계산한 컨투어 좌표
import cv2
import numpy as np

img = cv2.imread('../img/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)
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)

# 결과 출력
cv2.imshow('contour', img)
cv2.imshow('approx', img2)
cv2.waitKey()
cv2.destroyAllWindows()

 

cv2.arcLength() 함수로 원래의 컨투어의 둘레 길이를 구해서 그의 0.05만큼의 값으로 epsilon을 설정해 근사 컨투어를 계산해 그린 코드입니다.

컨투어를 단순화하는 또 다른 방법은 볼록 선체(convex hull)를 만드는 것입니다. 볼록 선체는 어느 한 부분도 오목하지 않은 상태를 말하는 것으로 대상 객체를 완전히 포함하므로 객체의 외곽 영역을 찾는 데 좋습니다.

  • hull = cv2.convexHull(points [, hull, clockwise, returnPoints]): 볼록 선체 찾기
    • points: 입력 컨투어
    • hull: 볼록 선체 결과
    • clockwise: 방향 지정(True: 시계 방향)
    • returnPoints: 결과 좌표 형식 선택
      • True: 볼록 선체 좌표 반환
      • False: 입력 컨투어 중에 볼록 선체에 해당하는 인덱스 반환
  • retval = cv2.isConvexityDefects(contour, convexhull): 볼록 선체 결함 찾기
    • contour: 입력 컨투어
    • convexhull: 볼록 선체에 해당하는 컨투어의 인덱스
    • deects: 볼록 선체 결함이 있는 컨투어의 배열 인덱스, N x 1 x 4 배열
      • [start, end, farthest, distance]
        • start: 오목한 각이 시작되는 컨투어의 인덱스
        • end: 오목한 각이 끝나는 컨투어의 인덱스
        • farthest: 볼록 선체에서 가장 먼 오목한 지점의 컨투어 인덱스
        • distance: farthest와 볼록 선체와의 거리, 8비트 고정 소수점(distance / 256.0)
import cv2
import numpy as np

img = cv2.imread('../img/hand.jpg')
img2 = img.copy()
# 그레이 스케일 및 바이너리 스케일 변환 ---①
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

# 컨투어 찾기와 그리기 ---②
temp, 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)
# 결과 이미지 표시
cv2.imshow('contour', img)
cv2.imshow('convex hull', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

손 모양의 그림의 컨투어와 볼록 선체를 각각 구해서 표시하는 코드입니다. 볼록 선체 결함점을 찾아 빨간색 점으로 표시했습니다.

 

■ 컨투어와 도형 매칭

서로 다른 물체의 컨투어를 비교하면 두 물체가 얼마나 비슷한 모양인지를 알아낼 수 있습니다. 이를 위해 OpenCV는 함수를 제공합니다.

  • retval = cv2.matchShapes(contour1, contour2, method, parameter): 두 개의 컨투어로 도형 매칭
    • contour1, contour2: 비교할 2개의 컨투어
    • method: 휴 모멘트 비교 알고리즘 선택 플래그
      • cv2.CONTOURS_MATCH_I1
      • cv2.CONTOURS_MATCH_I2
      • cv2.CONTOURS_MATCH_I3
    • parameter: 알고리즘에 전달을 위한 예비 인수, 현재 지원 안 됨(0으로 고정)
    • retval: 닮음 정도, 0 = 동일, 클수록 다름

cv2.matchShapes() 함수는 2개의 컨투어를 인자로 받아서 휴 모멘트 비교 알고리즘에 따라 비교한 결과를 소수점 있는 숫자로 반환합니다. 두 컨투어가 완전히 같으면 0(영)을 반환하고 그 닮음 정도가 다를수록 큰 수를 반환합니다.

 

import cv2
import numpy as np

# 매칭을 위한 이미지 읽기
target = cv2.imread('../img/4star.jpg') # 매칭 대상
shapes = cv2.imread('../img/shapestomatch.jpg') # 여러 도형
# 그레이 스케일 변환
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, 1,(0,0,255),1 )
# 매칭 점수로 정렬 ---④
matchs.sort(key=lambda x : x[0])
# 가장 적은 매칭 점수를 얻는 도형의 컨투어에 선 그리기 ---⑤
cv2.drawContours(shapes, [matchs[0][1]], -1, (0,255,0), 3)
cv2.imshow('target', target)
cv2.imshow('Match Shape', shapes)
cv2.waitKey()
cv2.destroyAllWindows()

 

가장 비슷한 도형만 표시하는 코드입니다.

 

7.2 허프 변환

허프 변환(Hough transform)은 영상에서 직선과 원 같은 간단한 모양을 식별합니다.

 

■ 허프 선 변환

영상은 수많은 픽셀로 구성되는데, 수많은 픽셀 속에서 직선 관계를 갖는 픽셀들만 골라내는 것이 허프 선 변환의 핵심입니다. 바이너리 스케일로 경계를 검출한 영상에는 선으로 보이는 여러 점이 있는데, 각 점마다 여러 개의 가상의 선을 그어서 그 선들 중 평면 원점과 직각을 이루는 선을 찾아 각도와 거리를 구해서 모든 점에게 도일하게 나타나는 선이 있다면, 그 점들은 그 선을 따라 직선의 형태를 띠는 것이라고 볼 수 있습니다.

  • lines = cv2.HoughLines(img, rho, theta, threshold [, lines, srn = 0, stn = 0, min_theta, max_theta])
    • img: 입력 영상, 1채널 바이너리 스케일
    • rho: 거리 측정 해상도, 0 ~ 1
    • theta: 각도 측정 해상도, 라디안 단위(np.pi / 0 ~ 180)
    • threshold: 직선으로 판단할 최소한의 동일 개수
      • 작은 값: 정확도 감소, 검출 개수 증가
      • 큰 값: 정확도 증가, 검출 개수 감소
    • lines: 검출 결과, N x 1 x 2 배열(r, θ)
    • src, stn: 멀티 스케일 허프 변환에 사용, 선 검출에서는 사용 안 함
    • min_theta, max_theta: 검출을 위해 사용할 최대, 최소 각도
import cv2
import numpy as np

img = cv2.imread('../img/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, (abs(int(x0)), abs(int(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)

#결과 출력
merged = np.hstack((img, img2))
cv2.imshow('hough line', merged)
cv2.waitKey()
cv2.destroyAllWindows()

 

캐니 엣지로 경계를 검출한 후에 허프 선 검출을 적용한 코드입니다.

 

■ 확률적 허프 선 변환

허프 선 검출은 모든 점에 대해서 수많은 선을 그어서 직선을 찾기 때문에 무척 많은 연산이 필요합니다. 이를 개선한 것이 점진적 확률(progressive probabilistic) 허프 변환입니다.

  • lines = c2.HoughLinesP(img, rho, theta, threshold [, lines, minLineLength, maxLineGap])
    • minLineLength: 선으로 인정할 최소 길이
    • maxLineGap: 선으로 판단한 최대 간격
    • lines: 검출된 선 좌표, N x 1 x 4 배열(x1, y1, x2, y2)
    • 이외의 인자는 cv2.HoughLines()와 동일

cv2.HoughLines()와 거의 비슷하지만, 검출한 선의 결과 값이 선의 시작과 끝 좌표라는 점과 선 검출 제약 조건으로 minLineLength, maxLineGap을 지정할 수 있는 점이 다릅니다. cv2.HoughLinesP()는 선 검출이 적게 되므로 엣지를 강하게 하고 threshold를 낮게 지정해야 합니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/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)

merged = np.hstack((img, img2))
cv2.imshow('Probability hough line', merged)
cv2.waitKey()
cv2.destroyAllWindows()

 

확률 허프 변환을 적용하고 그 결과를 출력하는 코드인데 선의 시작과 끝 좌표가 함께 제공되어 선으로 표시하기가 편리합니다.

 

■ 허프 원 변환

직교좌표를 극좌표로 바꾸면 (x, y) 좌표를 (r, θ) 좌표로 변환할 수 있으므로 허프 직선 변환의 알고리즘을 그대로 적용해서 원을 검출할 수 있습니다. 하지만 OpenCV는 메모리와 연산 속도를 이유로 이 방법으로 구현하지 않고 캐니 엣지를 수행하고 나서 소벨 필터를 적용해서 엣지의 경사도(gradient)를 누적하는 방법으로 구현했습니다.

  • circles = cv2.HoughCircles(img, method, dp, minDist [, circles, param1, param2, minRadius, maxRadius])
    • img: 입력 영상, 1채널 배열
    • method: 검출 방식 선택, 현재 cv2.HOUGH_GRADIENT만 가능
      • cv2.HOUGH_STANDARD
      • cv2.HOUGH_PROBABILISTIC
      • cv2.HOUGH_MULTI_SCALE
      • cv2.HOUGH_GRADIENT
    • dp: 입력 영상과 경사 누적의 해상도 반비례율, 1: 입력과 동일, 값이 커질수록 부정확
    • minDist: 원들 중심 간의 최소 거리, 0: 에러(동심원 검출 불가)
    • circles: 검출 원 결과, N x 1 x 3 부동 소수점 배열(x, y, 반지름)
    • param1: 캐니 엣지에 전달할 스레시홀드 최대 값(최소 값은 최대 값의 2배 작은 값 전달)
    • param2: 경사도 누적 경계 값(값이 작을수록 잘못된 원 검출)
    • minRadius, maxRadius: 원의 최소 반지름, 최대 반지름(0이면 영상의 크기)
import cv2
import numpy as np

img = cv2.imread('../img/coins_spread1.jpg')
# 그레이 스케일 변환 ---①
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)

# 결과 출력
cv2.imshow('hough circle', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
    

동전 사진에서 원을 검출하는 코드입니다.

 

7.3 연속 영역 분할

외곽 경계를 이용해서 객체 영역을 분할하는 방법은 실생활에서 카메라로 찍은 영상에서는 경계선을 분명하지 않아 영역이 닫히지 않거나 그 반대로 겹치는 경우가 많아 문제를 해결하기 어려운 경우가 있습니다.

 

■ 거리 변환

영상에서 물체의 영역을 정확히 파악하기 위한 방법으로 물체의 최 중심점을 찾는 것이 중요한데, 그것을 사람이나 동물로 비유하면 뼈대와 같은 것으로 흔히 스켈레톤(skeleton)이라고 합니다. 스켈레톤을 검출하는 방법 중 하나가 주변 경계로부터 가장 멀리 떨어진 곳을 찾는 거리 변환입니다.

거리 변환(distance trasnform)은 바이너리 스케일 이미지를 대상으로 원본 이미지와 같은 크기의 배열에 픽셀 값이 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

# 이미지를 읽어서 바이너리 스케일로 변환
img = cv2.imread('../img/full_body.jpg', cv2.IMREAD_GRAYSCALE)
_, 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)
# 결과 출력
cv2.imshow('origin', img)
cv2.imshow('dist', dst)
cv2.imshow('skel', skeleton)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

거리 변환한 결과는 입력 영상의 픽셀 수가 아무리 크고 가장 먼 거리 값이라 하더라도 그 수는 적을 수밖에 없어서 영상에 표시하면 거의 표시가 되지 않습니다.

 

■ 연결 요소 레이블링

연결된 요소들끼리 분리하는 방법으로 레이블링(labeling)이라는 방법이 있습니다. 바이너리 스케일 이미지에서 픽셀 값이 0으로 끊어지지 않는 영역끼리 같은 값을 부여해서 분리하는 방법입니다.

  • retval, labels = cv2.connectedComponents(src [, labels, connectivity = 8, ltype]): 연결 요소 레이블링과 개수 반환
    • src: 입력 영상, 바이너리 스케일 이미지
    • labels: 레이블링된 입력 영상과 같은 크기의 배열
    • connectivity: 연결성을 검사할 방향 개수(4, 8 중 선택)
    • ltype: 결과 레이블 배열 dtype
    • retval: 레이블 개수
  • retval, labels, stats, cetroids = cv2.connectedComponentsWithStats(src [, labels, stats, centroids, connectivity, ltype]): 레이블링과 각종 상태 정보 반환
    • stats: N x 5 행렬(N: 레이블 개수)
    • [x 좌표, y 좌표, 폭, 높이, 넓이]
  • centroids: 각 레이블의 중심점 좌표, N x 2 행렬(N: 레이블 개수)
import cv2
import numpy as np

# 이미지 읽기
img = cv2.imread('../img/shapes_donut.png')
# 결과 이미지 생성
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)

# 레이블 갯수 만큼 순회
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()

 

cv2.connectedComponent() 함수로 연결된 요소의 레이블링을 적용합니다. 레이블의 개수만큼 순회하면서 같은 레이블을 갖는 픽셀을 랜덤하게 구한 동일한 색상으로 표시합니다.

 

■ 색 채우기

연속되는 영역에 같은 색상을 채워 넣을 수 있는데, OpenCV는 cv2.floodFill()이라는 함수로 기능을 제공합니다.

  • 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: 채우기 진행을 결정할 최소/최대 차이 값
    • flags: 채우기 방식 선택 플래그,
      • 4또는 8방향 채우기
        • cv2.FLOODFILL_MASK_ONLY: img 가 아닌 mask에만 채우기 적용
        • 채우기에 사용할 값을 8 ~ 16 비트에 포함시켜야 함
      • cv2.FLOODFILL_FIXED_RANGE: 이웃 픽셀이 아닌 seed 픽셀과 비교
    • retval: 채우기 한 픽셀의 개수
    • rect: 채우기가 이뤄진 영역을 감싸는 사각형

img 영상의 seed 좌표에서부터 시작해서 newVal의 값으로 채우기를 시작합니다. 이때 이웃하는 픽셀에 채우기를 계속하려면 현재 픽셀이 이웃 픽셀의 loDiff를 뺀 값보다 크거나 같고 upDiff를 더한 값보다 작거나 같아야 합니다. 만약 이 값을 생략한다면 seed와 같은 값을 갖는 이웃 픽셀만 채우기를 진행합니다.

마지막 인자인 flags에 cv2.FLOODFILL_FIXED_RANGE가 포함되어 있으면 이웃한 픽셀이 아니라 seed 픽셀과 비교합니다. 또 flags에 cv2.FLOODFILL_MASK_ONLY가 포함되어 있으면 img에 채우기를 하지 않고 mask에만 채우기를 하는데, 이때는 채우기에 사용할 값은 newVal이 아니라 flags의 8 ~ 16번째 비트의 값입니다. 만약 8방향 채우기, 고정된 영역, 마스크만 255로 채우기를 설정한다면 코드는 flags = 8 | cv2.FLOODFILL_MASK_ONLY | cv2.FLOODFILL_FIXED_RANGE | (255 < 8)로 작성하면 됩니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/taekwonv1.jpg')
rows, cols = img.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, img
    if event == cv2.EVENT_LBUTTONDOWN:
        seed = (x,y)
        # 색 채우기 적용 ---④
        retval = cv2.floodFill(img, mask, seed, newVal, loDiff, upDiff)
        # 채우기 변경 결과 표시 ---⑤
        cv2.imshow('img', img)

# 화면 출력
cv2.imshow('img', img)
cv2.setMouseCallback('img', onMouse)
cv2.waitKey(0)
cv2.destroyAllWindows()

마우스로 영상의 특정 영역을 흰색으로 채우는 코드입니다.

 

■ 워터셰드

워터셰드(watershed)는 우리말로 분수령 혹은 분수계라고 번역할 수 있는데, 공원 같은 곳에서 시원하게 물을 뿜는 분수가 아니라 강물이 한 줄기로 흐르다가 갈라지는 경계를 말합니다. 영상 처리에서 워터셰드는 경계를 찾는 방법 중 하나로 픽셀 값의 크기를 산과 골짜기 같은 높고 낮은 지형으로 보고 물을 채워서 그 물이 만나는 곳을 경계로 찾는 방식입니다. flood fill과 비슷한 방식으로 영역을 찾는 것으로 볼 수 있는데, seed를 하나가 아닌 여러 곳을 사용합니다. 그리고 이 seed들을 마커라고 합니다.

  • markers = cv2.watershed(img, markers)
    • img: 입력 영상
    • markers: 마커, 입력 영상과 크기가 같은 1차원 배열(int32)

markers는 입력 영상의 행과 열 크기가 같은 1차원 배열로 dtype = np.int32로 생성해야 합니다. markers의 값은 경계를 찾고자 하는 픽셀 영역은 0을 갖게 하고 연결된 영역이 확실한 픽셀에 대해서는 동일한 양의 정수를 값으로 갖게 합니다. 예를 들어 배경은 1로, 전경은 2로 채우는 식입니다. cv2.watershed() 함수는 markers에 0이 아닌 값들을 이용해서 같은 영역 모두를 같은 값으로 채우고 그 경계는 -1로 채워서 반환합니다.

 

import cv2
import numpy as np

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()

배경과 전경을 2번에 걸쳐서 마커를 선택해 분리하는 모습입니다. flood fill의 seed의 값이 여러 개인 모습입니다.

 

■ 그랩컷

그랩컷(grabcut)은 그래프 컷(graph cut)을 기반으로 하는 알고리즘을 확장한 것으로 사용자가 전경으로 분리할 대상 객체가 있는 사각형 좌표를 주면 대상 객체와 배경의 색상 분포를 추정해서 동일한 레이블을 가진 연결된 영역에서 배경과 전경을 분리합니다.

  • mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount [, model])
    • 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: 동작 방법
      • cv2.GC_INIT_WITH_RECT: rect에 지정한 좌표를 기준으로 그랩컷 수행
      • cv2.GC_INIT_WITH_MASK: mask에 지정한 값을 기준으로 그랩컷 수행
      • cv2.GC_EVAL: 재시도

마지막 인자인 mode에 cv2.GC_INIT_WITH_RECT를 지정해서 호출하면 세 번째 인자인 rect에 있는 사각형 좌표를 가지고 전경과 배경을 분리해서 두 번째 인자인 mask에 0 ~ 1에 해당하는 값을 할당해서 반환합니다. 1차적으로 배경과 전경이 구분되고 나면 사용자는 mask에 확실한 배경과 확실한 전경 값을 추가적으로 교정한 후에 mode에 cv2.GC_INIT_WITH_MASK를 지정해서 다시 호출하면 좀 더 정확한 mask를 얻을 수 있습니다. bgdModel과 fgdModel은 함수가 내부적으로 연산에 사용하는 임시 배열로 다음번 호출에 이전 연산을 반영하기 위해 재사용해야 하므로 그 내용을 수정해서는 안됩니다.

 

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()

 

마우스 사용은 크게 세 가지 경우로 객체 영역을 드래그해서 사각형을 그리는 것, 미처 지우지 못한 배경에서 지우고 싶은 부분을 검은색 선으로 그리는 것, 전경인데 잘못 지워진 부분을 흰색 선으로 그리는 것입니다. cv2.grabCut()을 호출할 때 mode를 cv2.GC_INIT_WITH_RECT로 설정해서 호출해 사각형 영역으로 배경과 전경을 구분했습니다. 전경과 배경을 교정하기 위해선 mask에 좌표를 반영했다가 마우스 버튼을 떼는 시점에 cv2.grabCut()을 호출할 때 mode를 cv2.GC_INIT_WITH_MASK로 설정해서 호출한 모습입니다.

 

 

 

'Deep Learning(강의 및 책) > OpenCV' 카테고리의 다른 글

[OpenCV] 머신러닝  (0) 2022.08.31
[OpenCV] 영상 매칭과 추적  (0) 2022.08.20
[OpenCV] 영상 필터  (0) 2022.08.01
[OpenCV] 기하학적 변환  (0) 2022.07.30
[OpenCV] 이미지 프로세싱 기초  (0) 2022.07.23