12장. Contour Detection 과 객체 분석
개수 세기와 잡티 걸러 내기
콩을 고르는 할머니는 체에 콩을 담아 흔듭니다. 흙이나 작은 돌가루는 체 구멍으로 빠지고, 콩만 남습니다. 남은 콩을 한 줌씩 세면 몇 알인지 압니다. 이진화한 사진에도 먼지나 노이즈가 만든 작은 흰 점이 섞여 있습니다. 윤곽선의 면적을 체로 삼아 작은 것을 버리면 물체만 남습니다.
그림의 다섯 단계를 차례로 밟습니다. 어두운 부품을 Otsu INV 로 흰색으로 만들고, EXTERNAL 로 윤곽선을 찾고, 윤곽선마다 면적을 재고, 기준보다 작으면 버린 뒤, 남은 목록의 길이로 개수를 셉니다. 여기서 체의 구멍 크기가 기준 면적, 빠져나가는 흙이 잡티, 남은 콩이 물체입니다.
가상의 작업대 사진입니다. 이진화하면 부품 사이에 먼지가 흰 점으로 남아, 윤곽선이 51개나 잡힙니다(셋째 칸, 다홍). 면적 100 이상만 남기면 부품 12개가 남습니다(넷째 칸, 초록).
기준 면적 정하기
| 무엇 | 교안 PC 의 면적(contourArea) | 기준과의 관계 |
|---|---|---|
| 먼지(반지름 1) | 2 | 기준보다 훨씬 작다 |
| 먼지(반지름 2) | 8 | 기준보다 훨씬 작다 |
| 붙어 버린 먼지 두 개 | 16 | 기준보다 작다 |
| 가장 작은 부품(반지름 8) | 198 | 기준보다 크다 |
기준은 가장 큰 잡티(16)보다 크고 가장 작은 부품(198)보다 작게, 그 사이의 넉넉한 값 100 으로 골랐습니다. 두 무리의 사이가 넓을수록 기준을 고르기 쉽습니다.
connectedComponentsWithStats 와 견주기
9장에서 흰 덩어리의 수를 세던 cv2.connectedComponents 에는 통계까지 주는 형제 함수가 있습니다. 초록 상자의 findContours 는 테두리 점 목록을 주고 면적과 상자는 따로 재야 합니다. 다홍 상자의 connectedComponentsWithStats 는 칸마다 덩어리 번호를 매기고, 덩어리마다 상자, 면적(흰 칸 수), 무게중심을 한 번에 줍니다.
| 견줄 것 | findContours | connectedComponentsWithStats |
|---|---|---|
| 돌려주는 것 | 윤곽선 목록, hierarchy | 덩어리 수, 번호 그림, 통계표, 무게중심 |
| 바탕 | 세지 않는다 | 0번으로 센다(개수에서 1 을 뺀다) |
| 면적 | 점을 이은 넓이(조금 작다) | 흰 칸의 수 |
| 구멍 | TREE 로 따로 찾는다 | 면적에서 저절로 빠진다 |
| 더 할 수 있는 일 | 회전 상자, 외접원, 13장의 모양 특징 | 번호 그림으로 덩어리 칸만 골라 내기 |
통계표(stats)는 덩어리마다 한 줄이고 칸은 다섯 개입니다.
| 칸 | 상수 | 뜻 |
|---|---|---|
| 0 | cv2.CC_STAT_LEFT | 상자의 왼쪽 x |
| 1 | cv2.CC_STAT_TOP | 상자의 위쪽 y |
| 2 | cv2.CC_STAT_WIDTH | 상자의 가로 |
| 3 | cv2.CC_STAT_HEIGHT | 상자의 세로 |
| 4 | cv2.CC_STAT_AREA | 흰 칸의 수(면적) |
짧은 예시
import cv2
gray = cv2.imread("ch12_tray.png", cv2.IMREAD_GRAYSCALE)
# 부품이 어두우므로 INV 로 뒤집어 부품을 흰색으로. Otsu 가 기준값을 고른다
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
parts = [c for c in contours if cv2.contourArea(c) >= 100] # 면적 100 이상만 남긴다
print("윤곽선:", len(contours), "→ 거른 뒤:", len(parts))
# connectedComponentsWithStats: (덩어리 수 + 바탕 1, 번호 그림, 통계표, 무게중심)
n, labels, stats, centroids = cv2.connectedComponentsWithStats(binary)
big = (stats[1:, cv2.CC_STAT_AREA] >= 100).sum() # 0번(바탕)을 빼고 센다
print("덩어리:", n - 1, "→ 거른 뒤:", int(big))교안 PC 에서는 윤곽선 51개 → 12개, 덩어리 51개 → 12개로 두 길의 개수가 같습니다.
주의할 점
- 기준 면적은 사진의 해상도에 따라 다릅니다. 같은 부품도 사진을 두 배로 키우면 면적은 네 배가 됩니다. 사진 크기가 바뀌면 기준도 다시 정합니다.
- 잡티가 부품에 붙어 있으면 면적으로 거를 수 없습니다. 그때는 9장의 열기로 먼저 떼어 냅니다.
- 그림 가장자리에 걸쳐 잘린 물체도 윤곽선이 잡힙니다. 크기를 재는 검사라면 상자가 그림 끝에 닿은 물체는 따로 표시합니다.