컨투어(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()
실용적인 활용 팁
전처리 단계
컨투어 검출 전에 다음 전처리를 권장합니다:
-
그레이스케일 변환
-
가우시안 블러 적용 (노이즈 제거)
-
적절한 임계값으로 이진화
-
필요시 모폴로지 연산 적용
성능 최적화
-
적절한 근사 방법 선택: 정확도가 중요하지 않다면
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)