도형 탐지
윤곽선의 다각형 근사와 원형도, 무게중심, 볼록 껍질
실습 준비
import cv2 as cv
import numpy as np
from yeom import to_pil
# 도형 이미지를 읽고 밝기 영상으로 변환
shapes_image = cv.imread("shapes.webp")
shapes_gray = cv.cvtColor(
shapes_image,
cv.COLOR_BGR2GRAY,
)
# 도형을 흰색 전경으로 이진화
_, shapes_binary = cv.threshold(
shapes_gray,
60,
255,
cv.THRESH_BINARY,
)
# 열림과 닫힘으로 작은 잡음과 도형 내부의 빈틈 정리
kernel = cv.getStructuringElement(
cv.MORPH_ELLIPSE,
(3, 3),
)
shapes_cleaned = cv.morphologyEx(
shapes_binary,
cv.MORPH_OPEN,
kernel,
)
shapes_cleaned = cv.morphologyEx(
shapes_cleaned,
cv.MORPH_CLOSE,
kernel,
)
# 정리된 이진 영상에서 바깥 윤곽선만 추출
shape_contours, _ = cv.findContours(
shapes_cleaned,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
)
len(shape_contours)
17
윤곽선 추출에서 만든 이진화·열림·닫힘·윤곽선 추출 흐름을 그대로 사용함.
다각형 근사
다각형 근사(polygonal approximation)는 많은 점으로 이루어진 윤곽선을 적은 꼭짓점의 선분으로 단순화하는 방법임. OpenCV의 cv.approxPolyDP()는 Ramer–Douglas–Peucker 알고리즘을 사용함.
# 첫 번째 윤곽선의 둘레 계산
contour = shape_contours[0]
perimeter = cv.arcLength(contour, True)
# 둘레의 1%를 허용 오차로 사용해 닫힌 다각형으로 근사
approximation = cv.approxPolyDP(
contour,
0.01 * perimeter,
True,
)
len(contour), len(approximation)
(94, 5)
윤곽선을 이루던 94개의 점이 5개의 꼭짓점으로 줄어듦.
- 첫 번째 인자: 원본 윤곽선 좌표
epsilon=0.01 * perimeter: 원본 윤곽선과 근사 선분 사이에서 허용할 최대 거리True: 닫힌 다각형으로 근사epsilon증가: 더 많은 세부 점을 제거하여 꼭짓점 수 감소epsilon감소: 원래 윤곽선의 굴곡을 더 많이 보존
근사 결과의 꼭짓점이 3개면 삼각형, 4개면 사각형, 5개면 오각형으로 분류 가능함.
원형도와 도형 분류
원은 픽셀 경계가 계단 모양이므로 다각형 근사 후에도 여러 꼭짓점이 남음. 면적 A와 둘레 P로 원형도(circularity)를 계산하여 구분함.
C=P24πA완전한 원의 원형도는 1이며 길거나 불규칙한 도형일수록 작아짐. 이 실습에서는 circularity > 0.85를 원으로 분류함.
# 도형별 검출 개수를 기록할 딕셔너리 준비
shape_counts = {
"triangle": 0,
"quadrilateral": 0,
"pentagon": 0,
"circle": 0,
}
for contour in shape_contours:
# 각 윤곽선의 면적·둘레·근사 꼭짓점 계산
area = cv.contourArea(contour)
perimeter = cv.arcLength(contour, True)
approximation = cv.approxPolyDP(
contour,
0.01 * perimeter,
True,
)
# 면적과 둘레를 이용해 원에 가까운 정도 계산
circularity = (
4 * np.pi * area / perimeter**2
)
# 원형도를 먼저 확인하고 나머지는 꼭짓점 수로 분류
if circularity > 0.85:
label = "circle"
elif len(approximation) == 3:
label = "triangle"
elif len(approximation) == 4:
label = "quadrilateral"
elif len(approximation) == 5:
label = "pentagon"
else:
continue
shape_counts[label] += 1
shape_counts
{'triangle': 2, 'quadrilateral': 7, 'pentagon': 5, 'circle': 3}
원형도를 먼저 확인해야 픽셀 경계가 여러 꼭짓점으로 근사된 원을 다각형으로 잘못 분류하는 문제를 줄일 수 있음.
무게중심 표시
각 윤곽선의 무게중심을 계산.
centroids = []
for contour in shape_contours:
# 윤곽선 좌표 분포를 나타내는 모멘트 계산
moments = cv.moments(contour)
# 면적이 0이 아닐 때 1차 모멘트를 면적으로 나눠 무게중심 계산
if moments["m00"] != 0:
center_x = int(
moments["m10"] / moments["m00"]
)
center_y = int(
moments["m01"] / moments["m00"]
)
centroids.append((center_x, center_y))
len(centroids)
17
실습 결과에서 빨간색 T는 삼각형, 초록색 Q는 사각형, 파란색 P는 오각형, 자홍색 C는 원을 의미함. 각 문자 옆의 점이 모멘트로 구한 무게중심임.

볼록 껍질
볼록 껍질(convex hull)은 윤곽선의 모든 점을 포함하는 가장 작은 볼록 다각형임. 두 점을 이은 선분이 항상 도형 내부에 있으면 볼록 도형임.
- 별과 번개처럼 안쪽으로 들어간 오목한 부분을 바깥쪽 선분으로 연결
- 원본 윤곽선의 세부 굴곡보다 전체 외곽 형태를 표현
- 손가락 사이의 오목한 영역이나 물체의 파인 부분을 분석할 때 기준으로 사용
# 볼록 껍질을 확인할 이미지를 밝기 영상으로 변환
convex_image = cv.imread("convex.webp")
convex_gray = cv.cvtColor(
convex_image,
cv.COLOR_BGR2GRAY,
)
# 흰 배경은 0, 색 도형은 255가 되도록 반전 이진화
_, convex_binary = cv.threshold(
convex_gray,
0,
255,
cv.THRESH_BINARY_INV | cv.THRESH_OTSU,
)
# 이진 영상에서 각 도형의 바깥 윤곽선 추출
convex_contours, _ = cv.findContours(
convex_binary,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
)
hull_result = convex_image.copy()
# 원본 윤곽선은 초록색, 볼록 껍질은 빨간색으로 표시
for contour in convex_contours:
hull = cv.convexHull(contour)
cv.drawContours(
hull_result,
[contour],
-1,
(0, 180, 0),
2,
)
cv.drawContours(
hull_result,
[hull],
-1,
(0, 0, 255),
3,
)
len(convex_contours)
3
to_pil(hull_result)

초록색은 원본 윤곽선, 빨간색은 볼록 껍질임. 오목한 별과 번개의 파인 부분은 빨간 선이 건너뛰며, 오각형에서는 두 선이 거의 겹침.
퀴즈
cv.approxPolyDP(contour, epsilon, True)에서 epsilon을 크게 하면 일반적으로 어떻게 되는가?
퀴즈를 풀려면 대화형 기능을 불러와야 합니다.