OpenCV - 외곽선(Contour) 검출
외곽선 검출
레이블링과 더불어 이진 영상에서 객체의 위치 및 크기 정보를 추출하는 방법으로 외곽선 검출이 있다. 객체의 외곽선(contour)은 객체 영역 픽셀 중에서 배경 영역과 인접한 일련의 픽셀을 의미한다.
보통 검은색 배경 안에 있는 흰색 객체영역의 가장 외곽에 있는 픽셀들을 외곽선으로 정의하는데, 만약 흰색 객체 영역 안에 검은색 배경 영역인 홀(hole)이 존재한다면 홀을 둘러싸고 있는 객체 픽셀들도 외곽선으로 검출할 수 있다. 그리고 이를 통해 외곽선 계층 구조를 표현할 수 있다.
외곽선 검출
cv2.findContours(src, mode, method, offset) → contours, hierarchy
- 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 L1 근사화를 적용, 외곽선 점의 개수를 줄여주지만 외곽선 모양에 변화가 생길 수 있다.
- cv2.CHAIN_APPROX_TC89_KCOS: Teh & Chin k cos 근사화 적용, 외곽선 점의 개수를 줄여주지만 외곽선 모양에 변화가 생길 수 있다.
- offset: 외곽선 점 좌표의 오프셋(이동 변위). 기본값 (0, 0)
- contours: 검출된 외곽선들의 좌표 행렬. len(contours)는 외곽선 좌표의 개수.
- hierarchy: 외곽선 계층 정보 행렬.
- hierarchy.shape = (1, N, 4) (0부터 카운트 X)
- type = numpy.int32
- hierarchy[0, i, 0] ~ hierarchy[0, i, 3] 순서대로 “다음 외곽선 번호 (next), 이전 외곽선 번호(prev), 자식 외곽선 번호(child), 부모 외곽선 번호(parent)” 외곽선 인덱스를 가리킴
- 해당하는 외곽선이 존재하지 않으면 -1 반환.
위 이미지에서 외곽선 검출 방식에 따라 계층구조는 다음과 같다:
- RETR_EXTERNAL: 0 → 4
- RETR_LIST: 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
위 둘은 계층 정보 없음
- RETR_CCOMP: 0(1→2→3) → 4(5→8) → 6(7)
- RETR_TREE: 0(1→2→3) → 4((5→6→7) → 8)
위 둘은 외곽선 간 부모-자식 관계가 존재
외곽선 그리기
findContours() 함수로 검출한 외곽선 정보를 이용해서 drawContours() 함수로 영상위에 외관선을 그릴 수 있다.
cv2.drawContours(src, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offest=None)
- contours: findContours() 함수로 구한 외곽선 좌표 정보
- contourIdx: 그릴 외곽선 번호. 음수(-1)로 지정하면 모든 외곽선을 그림
- color: 외곽선 색상
- thickness: 외곽선 두께. 음수(-1)로 지정하면 내부를 채움
- lineType: LINE_4, LINE_8, LINE_AA 중 하나 지정
- hierarchy: 외곽선 계층 정보
- maxLevel: 그리기를 수행할 최대 외곽선 레벨. 0이면 contourIdx로 지정된 외곽선만 그림
- offset: 오프셋(이동 변위). 지정한 좌표 크기만큼 외곽선 좌표를 이동하여 그림
import cv2, random, sys
src = cv2.imread('img/contours.bmp', cv2.IMREAD_GRAYSCALE)
if src is None:
print('Image load failed!')
sys.exit()
mode = [cv2.RETR_EXTERNAL, cv2.RETR_LIST, cv2.RETR_CCOMP, cv2.RETR_TREE]
name = ['RETR_EXTERNAL', 'RETR_LIST', 'RETR_CCOMP', 'RETR_TREE']
for m in mode:
contours, hier = cv2.findContours(src, m, cv2.CHAIN_APPROX_NONE)
dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR)
idx = 0
while idx >= 0:
c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) # 랜덤 BGR값 생성
cv2.drawContours(dst, contours, idx, c, 2, cv2.LINE_8, hier)
idx = hier[0, idx, 0] # 다음 외곽선이 없으면 -1 반환
cv2.imshow(name[m], dst)
cv2.waitKey()
cv2.destroyAllWindows()
예2)
src = cv2.imread('img/milkdrop.bmp', cv2.IMREAD_GRAYSCALE)
_, src_bin = cv2.threshold(src, 0, 255, cv2.THRESH_OTSU)
contours, _ = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
h, w = src.shape[:2]
dst = np.zeros((h, w, 3), np.uint8)
for i in range(len(contours)):
c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
cv2.drawContours(dst, contours, i, c, 1, cv2.LINE_AA)
cv2.imshow('src', src)
cv2.imshow('src_bin', src_bin)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
외곽선 응용
모멘트 (Moments)
모멘트는 이미지 객체의 무게중심, 면적 등과 같은 특성을 계산할 때 유용한 기술자(descriptor)다. 기술자가 무엇인지는 나중에 더 알아보고, 간단하게 모멘트에는 이미지 특징값이 담겨있다고만 우선 이해하자.
cv2.moments(contour) 함수를 사용하면 Contour에 대한 특징값을 담은 모멘트 딕셔너리를 반환하는데, 반환된 딕셔너리를 사용하면 다음과 같은 특성값들을 얻을 수 있다.
- M = cv2.moments(contour) : 모멘트 딕셔너리
- M[‘m00’] : ‘contour’ 객체 영역의 넓이
- M[‘m10’]/M[‘m00’] : 객체 중심점의 x좌표
- M[‘m01’]/M[‘m00’] : 객체 중심점의 y좌표
외곽선 영역의 넓이 / 둘레 길이 활용
cv2.contourArea(contour)
- 넓이 반환
- cv2.moments(contour)[‘m00’] 과 같은 값
cv2.arcLength(contour, closed)
- 곡선의 길이 반환
- closed: True - 막힌 폐곡선, False - 뚫려있는 선
cv2.approxPolyDP(contour, 근사치, closed)
-
contour의 점들을 근사치로 줄인 행렬 반환
-
사용 예
#곱하는 숫자가 클수록 점들의 수는 작아짐 p1 = 0.01 * cv2.arcLength(cont1, True) p2 = 0.1 * cv2.arcLength(cont1, True) ap = cv2.approxPolyDP(cont1, p1, True) cont_img = cv2.drawContours(img, [ap], 0, (0,0,255), 2)
종합 활용 결과
import random, cv2
import numpy as np
src = cv2.imread('img/milkdrop.bmp', cv2.IMREAD_GRAYSCALE)
_, src_bin = cv2.threshold(src, 0, 255, cv2.THRESH_OTSU)
contours, _ = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
h, w = src.shape[:2]
dst = np.zeros((h, w, 3), np.uint8)
for i in range(len(contours)):
points = contours[i] # 외곽선을 그릴 객체의 포인트 행렬
area = cv2.contourArea(points) # 객체의 넓이 계산
if area > 600:
#외곽선 그리기
c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
cv2.drawContours(dst, contours, i, c, 1, cv2.LINE_AA)
#외곽선으로 모멘트 계산
m = cv2.moments(points)
#외곽선의 중심점 좌표
x = m['m10']/m['m00']
y = m['m01']/m['m00']
cv2.circle(dst, (int(x),int(y)), 3, c, -1)
#외곽선 둘레 * 0.01
p1 = 0.01 * cv2.arcLength(points, True)
#외곽선 근사화(점의 수를 줄임)
ap = cv2.approxPolyDP(points, p1, True)
c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
#계산된 근사치 좌표로 외곽선 그림
cv2.drawContours(dst, [ap], 0, c, 1, cv2.LINE_AA)
cv2.imshow('src', src)
cv2.imshow('src_bin', src_bin)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
블록 껍질 (Convex Hull)
외곽선의 볼록하게 튀어나온 점들을 연결하여 객체의 경계면을 둘러싸는 다각형을 구하는 알고리즘.
cv2.convexHull(contour)
contours, hierachy = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
hull = cv2.convexHull(cnt) #convex hull 추출
cont_img = cv2.drawContours(img, [hull], 0, (0,0,255), 2)
바운딩 박스
외곽선 점들을 모두 감싸는 가장 작은 사각형, 바운딩 박스의 좌측 상단 좌표와 가로, 세로 길이를 반환하는 함수
cv2.boundingRect(contour) → x, y, w, h
contours, hierachy = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cont1 = contours[0]
x,y,w,h = cv2.boundingRect(cont1)
img = cv2.rectangle(img, (x,y), (x+w, y+h), (0,0,255),2)
바운딩박스의 가로/세로 비율을 구하고 싶을 경우, ratio = w/h
외곽선 객체의 면적과 바운딩 박스 면적의 비율을 구하고 싶을 경우, ratio = cv2.contourArea(contour) / w*h
최소 영역 사각형
외곽선 점들을 모두 감싸는 회전된 가장 작은 사각형을 구하고 싶은 경우 사용하는 함수
cv2.minAreaRect(conour) → RotatedRect 클래스 객체 반환
사각형을 화면에 그리고 싶은 경우 별도로 아래 함수를 사용해서 사각형의 네 꼭지점 좌표를 구해야 한다.
cv2.boxPoints(rect)
contours, hierachy = cv2.findContours(src_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cont1 = contours[0]
rect = cv2.minAreaRect(cont1)
box = cv2.boxPoints(rect)
cont_img = cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
최소 크기의 원
외곽선 점들을 감싸는 최소 크기의 원을 구하고 싶은 경우 사용하는 함수.
cv2.minEnclosingCircle(contour) → (x,y), r
- 원의 중심점 좌표와 원의 반지름 반환
최소 크기의 타원을 구하고 싶은 경우,
cv2.fitEllipse(contour) → minAreaRect처럼 RotatedRect 클래스 객체 반환
Extream Point
외곽선 객체의 동서남북 최동단, 최서단, 최남단, 최북단 꼭지점을 다음과 같이 찾을 수 있다.
- leftmost = tuple(cont[cont[:,:,0].argmin()][0])
- rightmost = tuple(cont[cont[:,:,0].argmax()][0])
- topmost = tuple(cont[cont[:,:,1].argmin()][0])
- bottommost = tuple(cont[cont[:,:,1].argmax()][0])
댓글남기기