13장. Shape 분석과 ROI 처리
근사 다각형
지도에서 구불구불한 해안선을 손으로 따라 그린다고 해 봅시다. 작은 굴곡까지 다 그리려면 끝이 없습니다. 그래서 "1cm 안쪽의 굴곡은 무시하고 곧은 선으로 긋자"고 정하면, 해안선이 꼭짓점 몇 개짜리 다각형이 됩니다. 무시하는 폭을 넓히면 꼭짓점이 더 줄어듭니다. cv2.approxPolyDP 가 이 일을 합니다.
그림의 네 단계를 따라 꼭짓점을 셉니다. 윤곽선의 둘레를 재고, 둘레에 비율(예: 0.01)을 곱해 epsilon 을 정하고, approxPolyDP 가 돌려준 점의 개수를 셉니다. 여기서 해안선이 윤곽선이고, "1cm 안쪽은 무시"의 1cm 가 epsilon 입니다.
위 줄은 원, 아래 줄은 별입니다. 다홍 선이 근사 다각형이고, 점이 꼭짓점입니다. 비율 0.005 에서 원은 꼭짓점 16개로 거의 원 그대로이고, 0.02 에서는 8개, 0.08 에서는 4개로 네모가 됩니다. 별은 0.005 와 0.02 에서 모두 10개(뾰족한 끝 5개와 안쪽 꺾임 5개)를 지키다가 0.08 에서 5개가 됩니다. 곧은 변으로 된 모양은 꼭짓점 수가 잘 바뀌지 않고, 둥근 모양은 epsilon 에 따라 크게 바뀝니다.
epsilon 을 둘레의 비율로 주는 까닭
epsilon 은 "곧은 선에서 이만큼까지 비뚤어져도 곧은 선으로 본다"는 칸 수입니다. 그런데 큰 물체와 작은 물체에 같은 칸 수를 주면 결과가 달라집니다. 둘레 400 인 큰 원에서 2칸은 작은 굴곡이지만, 둘레 40 인 작은 원에서 2칸은 큰 굴곡입니다. 그래서 둘레에 비율을 곱해 정합니다.
| 비율 | 원(둘레 178.5)의 epsilon | 원의 꼭짓점 | 별의 꼭짓점 | 쓰임 |
|---|---|---|---|---|
| 0.005 | 178.5 × 0.005 ≈ 0.9칸 | 16 | 10 | 작은 굴곡까지 남긴다 |
| 0.01 | 178.5 × 0.01 ≈ 1.8칸 | 12 | 10 | 이 장의 기본값 |
| 0.02 | 178.5 × 0.02 ≈ 3.6칸 | 8 | 10 | 인터넷 예제에 흔한 값 |
| 0.08 | 178.5 × 0.08 ≈ 14.3칸 | 4 | 5 | 아주 거칠게 어림한다 |
짧은 예시
import cv2
img = cv2.imread("ch13_shapes.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 넓이가 2,704 인 윤곽선이 파란 네모다
square = max(contours, key=cv2.contourArea)
peri = cv2.arcLength(square, True) # True: 닫힌 선(처음과 끝이 이어짐)
print(round(peri, 1)) # 결과: 208.0
# approxPolyDP(윤곽선, epsilon, 닫힌 선인가)
approx = cv2.approxPolyDP(square, # curve: 어림할 윤곽선
0.01 * peri, # epsilon: 둘레의 1% 까지 비뚤어져도 곧은 선으로
True) # closed: 닫힌 다각형으로
print(len(approx)) # 결과: 4 → 꼭짓점 4개
print(approx.reshape(-1, 2).tolist()) # 꼭짓점 네 개의 (x, y)approx 는 (꼭짓점 수, 1, 2) 모양이라 reshape(-1, 2) 로 (꼭짓점 수, 2) 로 폈습니다. 네모의 꼭짓점은 (174, 34), (174, 86), (226, 86), (226, 34) 네 개입니다.
주의할 점
- 원은 꼭짓점 수가 epsilon 에 따라 크게 바뀝니다. 비율 0.02 에서 원이 꼭짓점 8개가 되어 팔각형과 똑같아집니다. 이 장의 실습은 둘을 가르려고 0.01 을 씁니다.
cv2.CHAIN_APPROX_SIMPLE로 찾은 윤곽선도 곧은 변의 가운데 점을 이미 줄여 둔 것입니다. 그래도 둥근 곳의 점은 많이 남아 있어서 approxPolyDP 가 필요합니다.