귀퉁이 서재

OpenCV - 22. 컨투어(Contour) 본문

OpenCV

OpenCV - 22. 컨투어(Contour)

Baek Kyun Shin 2020. 10. 20. 11:49

이번 포스팅에서는 영상 분할 방법 중 하나인 컨투어에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/07.segmentation

컨투어(Contour)

컨투어(contour)는 등고선을 의미합니다. 등고선은 지형의 높이가 같은 영역을 하나의 선으로 표시한 것입니다. 영상에서 컨투어를 그리면 모양을 쉽게 인식할 수 있습니다. OpenCV에서 제공하는 컨투어 함수는 다음과 같습니다.

  • dst, contours, hierarchy = cv2.findContours(src, mode, method, contours, hierarchy, offset)
    src: 입력 영상, 검정과 흰색으로 구성된 바이너리 이미지
    mode: 컨투어 제공 방식 (cv2.RETR_EXTERNAL: 가장 바깥쪽 라인만 생성, cv2.RETR_LIST: 모든 라인을 계층 없이 생성, cv2.RET_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(optional): 검출한 컨투어 좌표 (list type)
    hierarchy(optional): 컨투어 계층 정보 (Next, Prev, FirstChild, Parent, -1 [해당 없음])
    offset(optional): ROI 등으로 인해 이동한 컨투어 좌표의 오프셋

위 함수로 컨투어를 찾아낸 다음 아래 함수로 컨투어를 그려줄 수 있습니다.

  • cv2.drawContours(img, contours, contourIdx, color, thickness)
    img: 입력 영상
    contours: 그림 그릴 컨투어 배열 (cv2.findContours() 함수의 반환 결과를 전달해주면 됨)
    contourIdx: 그림 그릴 컨투어 인덱스, -1: 모든 컨투어 표시
    color: 색상 값
    thickness: 선 두께, 0: 채우기

cv2.darwContours()는 실제로 컨투어 선을 그리는 함수입니다. img영상에 contours 배열에 있는 컨투어 중 contourIdx에 해당하는 컨투어를 color 색상과 thickness 두께로 선을 그립니다. 위 두 함수를 활용하여 컨투어를 그려보겠습니다.

# 컨투어 찾기와 그리기 (cntr_find.py)

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)

# 가장 바깥쪽 컨투어에 대해 모든 좌표 반환 ---③
im2, contour, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, \
                                                 cv2.CHAIN_APPROX_NONE)
# 가장 바깥쪽 컨투어에 대해 꼭지점 좌표만 반환 ---④
im2, 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.CHAIN_APPROX_SIMPLE을 사용해서 꼭짓점만 표시를 했고, 오른쪽은 cv2.CHAIN_APPROX_NONE을 사용해서 모든 좌표에 컨투어를 그렸습니다.

다음으로는 트리 계층의 컨투어를 그려보겠습니다.

# 컨투어 계층 트리 (cntr_hierachy.py)

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)

# 가장 바깥 컨투어만 수집   --- ①
im2, contour, hierarchy = cv2.findContours(imthres, cv2.RETR_EXTERNAL, \
                                                cv2.CHAIN_APPROX_NONE)
# 컨투어 갯수와 계층 트리 출력 --- ②
print(len(contour), hierarchy)

# 모든 컨투어를 트리 계층 으로 수집 ---③
im2, 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_EXTERNAL을 전달할 때는 그림의 외곽 부분에만 컨투어를 그립니다. 하지만 cv2.RETR_TREE를 전달할 때는 모든 경계에 컨투어를 그립니다. 이를 트리 계층 컨투어라고 합니다. 위 그림에서는 보기 쉽게 다양한 색깔로 컨투어를 그렸습니다.

위 코드 중

print(len(contour2), hierarchy)

의 출력 결과를 살펴보겠습니다. cv2.findContours() 함수를 호출하면 컨투어 좌표뿐만 아니라 hierarchy를 다음과 같이 출력합니다. 아래는 contour2, 즉 위 출력 그림에서 오른쪽 결과(트리 계층 컨투어)에 해당하는 hierarchy입니다.

인덱스 다음(Next) 이전(Prev) 자식(First Child) 부모(Parent)
0 2 -1 1 -1
1 -1 -1 -1 0
2 4 0 3 -1
3 -1 -1 -1 2
4 -1 2 5 -1
5 -1 -1 -1 4

요소 값 중 -1은 의미 없음을 나타냅니다. 우선, 0번째 행을 보겠습니다. 0번째 행은 첫 번째 도형의 컨투어를 의미합니다. 이는 위 두 번째 그림에서 왼쪽의 삼각형 외곽을 뜻합니다. 0번째 행의 Next, Prev, First Child, Parent는 각각 2, -1, 1, -1입니다. Prev와 Parent는 -1이므로 아무 의미가 없다는 뜻입니다. 즉, 삼각형 외곽 컨투어의 기준에는 이전 도형이 없고, 부모 도형도 없다는 뜻입니다. 그러나 Next와 First Child는 각 2와 1입니다. 이 말은 다음 도형은 2행이고, 자식은 1행이라는 것입니다. 2행인 4, 0 ,3 -1을 이루고 있는 컨투어는 맨 오른쪽 원 도형의 외곽입니다. 1행인 -1, -1, -1, 0은 왼쪽 삼각형의 내부 컨투어입니다. 자연스럽게 내부 삼각형의 부모는 0입니다. 2행인 4, 0 ,3 -1을 이루고 있는 컨투어의 다음 도형은 4행인 사각형 외곽이고, 이전은 0행인 삼각형 외곽이며, 자식은 3행인 원 내부입니다. 이런 식으로 컨투어 계층 정보(hierarchy)를 보면 외곽 요소와 자식 요소를 순회할 수 있습니다. 최외곽 컨투어만 골라내려면 부모 항목이 -1인 행만 찾으면 되고, 그것이 이 예제에서는 도형의 개수와 같습니다.

컨투어를 감싸는 도형 그리기

OpenCV를 활용하면 컨투어를 감싸는 도형을 그릴 수도 있습니다. 컨투어를 감싸는 도형을 그리는 아래 함수들에 대해 먼저 알아보겠습니다.

  • x, y, w, h = cv2.boundingRect(contour): 좌표를 감싸는 사각형 반환
    x, y: 사각형의 왼쪽 상단 좌표
    w, h: 사각형의 폭과 높이
  • rotateRect = cv2.minAreaRect(contour): 좌표를 감싸는 최소한의 사각형 계산
  • 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.fitEllipse(points): 좌표를 감싸는 최소한의 타원 계산
  • line = cv2.fitLine(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(optional): vx, vy 정규화된 단위 벡터, x0, y0: 중심점 좌표

위 함수를 활용하여 컨투어를 감싸는 다양한 도형을 그려보겠습니다.

# 컨투어를 감싸는 도형 그리기 (cntr_bound_fit.py)

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)

# 컨튜어 찾기
im, 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, 0-x*(vy/vx) + y), (cols-1, (cols-x)*(vy/vx) + y), \
                                                        (0,0,255),2)

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

까만 번개 이미지 주위를 둘러싼 여러 도형들을 그려봤습니다. 

컨투어 단순화 

지금까지 살펴본 컨투어 함수는 이미지 외곽을 따라 그림을 그려주는 기능을 제공했습니다. 하지만 실생활에서 얻는 대부분의 이미지는 약간의 노이즈가 포함되어 있습니다. 그래서 컨투어를 너무 정확히 그리는 것도 바람직하지 않습니다. 오히려 약간 단순화해 그리는 게 정확하게 그리는 것보다 더 쓸모 있는 경우가 있습니다. OpenCV는 아래와 같은 함수를 통해 근사 값으로 컨투어를 계산해줍니다.

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

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)

# 컨투어 찾기 ---①
temp, 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.approxPolyDP() 함수를 활용하면 오른쪽과 같이 요철이 있는 부분은 무시하고 컨투어를 계산해줍니다. 

컨투어를 단순화하는 또 다른 형태는 볼록 선체(convex hull)를 만드는 것입니다. 볼록 선체란 어느 한 부분도 오목하지 않은 도형을 의미합니다. 따라서 볼록 선체는 대상을 완전히 포함하는 외곽 영역을 찾는데 유용합니다.

  • hull = cv2.convexHull(points, hull, clockwise, returnPoints): 볼록 선체 계산
    points: 입력 컨투어
    hull(optional): 볼록 선체 결과
    clockwise(optional): 방향 지정 (True: 시계 방향)
    returnPoints(optional): 결과 좌표 형식 선택 (True: 볼록 선체 좌표 변환, False: 입력 컨투어 중에 볼록 선체에 해당하는 인덱스 반환)
  • retval = cv2.isContourConvex(contour): 볼록 선체 만족 여부 확인
    retval: True인 경우 볼록 선체임
  • defects = cv2.convexityDefects(contour, convexhull): 볼록 선체 결함 찾기
    contour: 입력 컨투어
    convexhull: 볼록 선체에 해당하는 컨투어의 인덱스
    defects: 볼록 선체 결함이 있는 컨투어의 배열 인덱스, N x 1 x 4 배열, [starts, end, farthest, distance]
      start: 오목한 각이 시작되는 컨투어의 인덱스
      end: 오목한 각이 끝나는 컨투어의 인덱스
      farthest: 볼록 선체에서 가장 먼 오목한 지점의 컨투어 인덱스
      distance: farthest와 볼록 선체와의 거리
# 볼록 선체 (cntr_convexhull.py)

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

왼쪽은 손 모양 이미지에 대한 컨투어입니다. 오른쪽은 볼록 선체를 그려주었고, 또한 볼록 선체의 결점을 빨간 점으로 표시해주었습니다. 

컨투어와 도형 매칭

서로 다른 물체의 컨투어를 비교하면 두 물체가 얼마나 비슷한지 알 수 있습니다. 이는 아래 함수로 간단히 구현할 수 있습니다.

  • retval = cv2.matchShapes(contour1, contour2, method, parameter): 두 개의 컨투어로 도형 매칭
    contour1, contour2: 비교할 두 개의 컨투어
    method: 휴 모멘트 비교 알고리즘 선택 플래그 (cv2.CONTOURS_MATCH_I1, cv2.CONTOURS_MATCH_I2, cv2.CONTOURS_MATCH_I3)
    parameter: 알고리즘에 전달을 위한 예비 인수로 0으로 고정
    retval: 두 도형의 닮은 정도 (0=동일, 숫자가 클수록 다름)
# 도형 매칭으로 비슷한 도형 찾기 (contr_matchShape.py)

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

왼쪽의 별 모양 도형과 가장 유사한 도형을 오른쪽 세 도형 중 찾는 코드입니다. 매칭 정도를 숫자로 표현했고 (숫자가 작을수록 서로 닮은 도형임) 가장 닮은 도형 주위에 초록색 컨투어를 그려주었습니다.

지금까지 컨투어를 그리는 다양한 방법에 대해 알아봤습니다.

Comments