😏컨투어

컨투어(Contour)

컨투어(Contour)는 영상에서 모양을 인식하고 분석하는 핵심 기술입니다.

컨투어 기본 개념

컨투어는 등고선을 의미합니다. 지형의 높이가 같은 영역을 하나의 선으로 표시한 것처럼, 영상에서 컨투어를 그리면 물체의 모양을 쉽게 인식할 수 있습니다.

컨투어 찾기와 그리기

컨투어 찾기 함수

dst, contours, hierarchy = cv2.findContours(src, mode, method, contours, hierarchy, offset)

매개변수 설명:

  • 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: 검출한 컨투어 좌표 (list type)

  • hierarchy: 컨투어 계층 정보 (Next, Prev, FirstChild, Parent, -1은 해당 없음)

  • offset: ROI 등으로 인해 이동한 컨투어 좌표의 오프셋

컨투어 그리기 함수

cv2.drawContours(img, contours, contourIdx, color, thickness)

매개변수 설명:

  • img: 입력 영상

  • contours: 그릴 컨투어 배열 (cv2.findContours() 함수의 반환 결과)

  • contourIdx: 그릴 컨투어 인덱스 (-1: 모든 컨투어 표시)

  • color: 색상 값

  • thickness: 선 두께 (0: 채우기)

기본 컨투어 예제

# 컨투어 찾기와 그리기
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()

CHAIN_APPROX_NONE은 모든 좌표에 컨투어를 그리고, CHAIN_APPROX_SIMPLE은 꼭짓점만 표시합니다.


컨투어 계층 구조

트리 계층 컨투어

컨투어는 계층 구조를 가질 수 있습니다. 예를 들어, 도넛 모양의 도형에서는 외곽 컨투어와 내부 홀의 컨투어가 부모-자식 관계를 형성합니다.

# 컨투어 계층 트리
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()

계층 정보 해석

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

값 의미:

  • Next: 같은 레벨의 다음 컨투어 인덱스

  • Prev: 같은 레벨의 이전 컨투어 인덱스

  • First Child: 첫 번째 자식 컨투어 인덱스

  • Parent: 부모 컨투어 인덱스

  • -1: 해당 관계가 없음을 의미


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

경계 사각형과 최소 사각형

# 좌표를 감싸는 사각형 반환
x, y, w, h = cv2.boundingRect(contour)

# 좌표를 감싸는 최소한의 사각형 계산
rotateRect = cv2.minAreaRect(contour)

# rotateRect로부터 꼭짓점 좌표 계산
vertex = cv2.boxPoints(rotateRect)

최소 원과 타원

# 좌표를 감싸는 최소한의 동그라미 계산
center, radius = cv2.minEnclosingCircle(contour)

# 좌표를 감싸는 최소한의 타원 계산
ellipse = cv2.fitEllipse(points)

최소 삼각형과 중심선

# 좌표를 감싸는 최소한의 삼각형 계산
area, triangle = cv2.minEnclosingTriangle(points)

# 중심점을 통과하는 직선 계산
line = cv2.fitLine(points, distType, param, reps, aeps, line)

cv2.fitLine() 매개변수:

  • distType: 거리 계산 방식 (cv2.DIST_L2, cv2.DIST_L1 등)

  • param: distType에 전달할 인자 (0 = 최적 값 선택)

  • reps: 반지름 정확도 (0.01 권장)

  • aeps: 각도 정확도 (0.01 권장)

종합 예제

# 컨투어를 감싸는 도형 그리기
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)
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(-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: 컨투어의 닫힌 여부

# 근사 컨투어 예제
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, hierarchy = cv2.findContours(th, cv2.RETR_EXTERNAL, 
                                             cv2.CHAIN_APPROX_SIMPLE)
contour = contours[0]

# 전체 둘레의 5%로 오차 범위 지정
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()

볼록 선체 (Convex Hull)

볼록 선체는 어느 한 부분도 오목하지 않은 도형을 의미합니다. 대상을 완전히 포함하는 외곽 영역을 찾는데 유용합니다.

# 볼록 선체 계산
hull = cv2.convexHull(points, hull, clockwise, returnPoints)

# 볼록 선체 만족 여부 확인
retval = cv2.isContourConvex(contour)

# 볼록 선체 결함 찾기
defects = cv2.convexityDefects(contour, convexhull)

cv2.convexityDefects() 반환값:

  • start: 오목한 각이 시작되는 컨투어의 인덱스

  • end: 오목한 각이 끝나는 컨투어의 인덱스

  • farthest: 볼록 선체에서 가장 먼 오목한 지점의 컨투어 인덱스

  • distance: farthest와 볼록 선체와의 거리

# 볼록 선체 예제
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, hierarchy = 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=동일, 숫자가 클수록 다름)

# 도형 매칭으로 비슷한 도형 찾기
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()

실용적인 활용 팁

전처리 단계

컨투어 검출 전에 다음 전처리를 권장합니다:

  1. 그레이스케일 변환

  2. 가우시안 블러 적용 (노이즈 제거)

  3. 적절한 임계값으로 이진화

  4. 필요시 모폴로지 연산 적용

성능 최적화

  • 적절한 근사 방법 선택: 정확도가 중요하지 않다면 CHAIN_APPROX_SIMPLE 사용

  • 계층 모드 선택: 필요에 따라 RETR_EXTERNAL 또는 RETR_TREE 선택

  • 컨투어 필터링: 면적이나 둘레를 기준으로 불필요한 컨투어 제거

일반적인 문제 해결

작은 노이즈 컨투어 제거:

# 면적 기준으로 컨투어 필터링
filtered_contours = [c for c in contours if cv2.contourArea(c) > min_area]

컨투어 정렬:

# 면적 기준으로 내림차순 정렬
contours = sorted(contours, key=cv2.contourArea, reverse=True)

참고 자료