윤곽선 추출
윤곽선의 면적·둘레·모멘트 측정
실습 준비
import cv2 as cv
from yeom import to_pil
image = cv.imread("shapes.webp")
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
_, binary = cv.threshold(
gray,
60, # 임계값
255, # 임계값보다 밝은 픽셀에 넣을 값
cv.THRESH_BINARY,
)
image.shape
(561, 600, 3)
to_pil(binary)
임계값 60보다 밝은 픽셀은 흰색 처리. 도형 17개와 배경의 작은 밝은 점이 함께 남는다.

결과 이미지는 contour.ipynb 실행 결과.
윤곽선 추출에는 모폴로지 연산으로 잡음을 정리한 cleaned 이미지를 사용.
kernel = cv.getStructuringElement(
cv.MORPH_ELLIPSE,
(3, 3),
)
opened = cv.morphologyEx(
binary,
cv.MORPH_OPEN,
kernel,
)
cleaned = cv.morphologyEx(
opened,
cv.MORPH_CLOSE,
kernel,
)
윤곽선 추출
윤곽선(contour)은 같은 객체의 경계를 따라 연결된 좌표 집합. cv.findContours()는 8비트 단일 채널 이미지에서 0이 아닌 픽셀을 전경으로 보고 윤곽선을 탐색.
contours, hierarchy = cv.findContours(
cleaned,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
)
len(contours), hierarchy.shape
(17, (1, 17, 4))
cv.RETR_EXTERNAL: 가장 바깥쪽 윤곽선만 반환cv.CHAIN_APPROX_SIMPLE: 직선 구간의 중간 좌표를 줄이고 양 끝점 위주로 저장contours[i]: i번째 윤곽선의 좌표 배열, 기본 형태는(N, 1, 2)hierarchy[0, i]:[다음, 이전, 첫 자식, 부모]인덱스, 대상이 없으면 -1
cv.RETR_EXTERNAL은 바깥 윤곽선만 반환하므로 이 실습에서는 계층 미사용. 도형 안의 구멍처럼 중첩된 윤곽선까지 필요하면 cv.RETR_TREE로 전체 계층 탐색. OpenCV 4의 cv.findContours()는 입력 이미지를 변경하지 않아 사본 불필요.
모든 윤곽선을 초록색으로 표시. contourIdx=-1은 리스트의 모든 윤곽선을 지정.
contour_image = image.copy()
cv.drawContours(
contour_image,
contours,
-1, # 모든 윤곽선
(0, 255, 0),
2, # 선 두께
)
to_pil(contour_image)

면적과 둘레
cv.contourArea()는 윤곽선이 감싸는 면적을 계산. cv.arcLength()는 윤곽선 선분의 길이를 합산해 둘레 계산.
main_contour = max(
contours,
key=cv.contourArea,
)
area = cv.contourArea(main_contour)
perimeter = cv.arcLength(
main_contour,
True, # 처음과 끝이 이어진 폐곡선
)
area, perimeter
(10145.5, 454.7422024011612)
- 면적: 10,145.5픽셀²
- 둘레: 약 454.7픽셀
True: 처음과 끝이 연결된 폐곡선으로 둘레 계산
면적이 가장 큰 윤곽선은 화면 아래쪽의 주황색 사각형. 실제 물체 선택에는 가장 큰 윤곽선 하나 또는 최소 면적을 넘는 여러 윤곽선 사용 가능.
모멘트와 무게중심
이미지 모멘트(image moment)는 픽셀 또는 윤곽선 좌표의 분포를 나타내는 요약값. 아래 수식은 이미지 전체를 대상으로 한 정의. cv.moments()에 윤곽선을 넘기면 픽셀 수 대신 윤곽선 다각형으로 계산.
m00: 윤곽선의 면적m10: x좌표를 반영한 1차 모멘트m01: y좌표를 반영한 1차 모멘트
무게중심은 1차 모멘트를 면적으로 나누어 계산.
moments = cv.moments(main_contour)
if moments["m00"] == 0:
raise ValueError("윤곽선의 면적이 0입니다")
center_x = int(
moments["m10"] / moments["m00"]
)
center_y = int(
moments["m01"] / moments["m00"]
)
(center_x, center_y) # 무게중심 (x, y)
(344, 450)
m00=0이면 나눗셈 불가. 열린 선이나 퇴화 윤곽선에 대비해 먼저 확인.
주요 물체 표시
가장 큰 윤곽선은 초록색, 모멘트로 계산한 무게중심은 빨간색 점으로 표시.
main_result = image.copy()
cv.drawContours(
main_result,
[main_contour],
-1,
(0, 255, 0),
4,
)
cv.circle(
main_result,
(center_x, center_y),
7, # 반지름
(0, 0, 255), # BGR 순서이므로 빨간색
-1, # 음수는 내부를 채움
)
to_pil(main_result)

처리 흐름: 이진화 → 열림·닫힘 → 윤곽선 추출 → 면적 기준 선택 → 모멘트 계산 → 결과 표시. 모폴로지 연산으로 잡음을 먼저 제거하면 작은 점의 오검출 감소.