12장. Contour Detection 과 객체 분석
실습 5 — 잡티 걸러 개수 세기와 connectedComponentsWithStats
먼지가 흩어진 가상의 작업대 사진(ch12_tray.png)에서 부품을 셉니다. 면적의 분포를 보고 기준을 정해 먼지를 거르고, 같은 일을 cv2.connectedComponentsWithStats 로 해서 견줍니다.
1단계. 이진화하고 모든 윤곽선 세기
import cv2
import numpy as np
# 밝은 작업대 위의 어두운 부품과 먼지. 연습용으로 그려 만든 가상의 사진이다
gray = cv2.imread("ch12_tray.png", cv2.IMREAD_GRAYSCALE)
# 부품이 어두우므로 THRESH_BINARY_INV 로 뒤집고, 기준값은 Otsu 가 고르게 한다(8차시)
t, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
print("Otsu 가 고른 기준값:", int(t))
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
areas = sorted(cv2.contourArea(c) for c in contours)
print("윤곽선 수:", len(contours))
print("작은 쪽 면적 5개:", areas[:5])
print("큰 쪽 면적 5개:", areas[-5:])Otsu 가 고른 기준값: 135
윤곽선 수: 51
작은 쪽 면적 5개: [2.0, 2.0, 2.0, 2.0, 2.0]
큰 쪽 면적 5개: [913.0, 1040.0, 1290.0, 1542.0, 1832.0]2단계. 면적 분포를 보고 거르기
# 면적을 구간으로 나눠 몇 개씩 있는지 센다. 두 무리 사이의 빈 구간에 기준을 둔다
bins = [0, 10, 50, 100, 200, 500, 1000, 2000]
counts, _ = np.histogram(areas, bins=bins)
for lo, hi, n in zip(bins[:-1], bins[1:], counts):
print(f"면적 {lo:4} 이상 {hi:4} 미만: {int(n)}개")
MIN_AREA = 100 # 가장 큰 먼지보다 크고, 가장 작은 부품보다 작게
parts = [c for c in contours if cv2.contourArea(c) >= MIN_AREA]
print("면적", MIN_AREA, "이상만 남기면:", len(parts), "개")면적 0 이상 10 미만: 38개
면적 10 이상 50 미만: 1개
면적 50 이상 100 미만: 0개
면적 100 이상 200 미만: 1개
면적 200 이상 500 미만: 4개
면적 500 이상 1000 미만: 3개
면적 1000 이상 2000 미만: 4개
면적 100 이상만 남기면: 12 개3단계. connectedComponentsWithStats 로 같은 일 하기
import pandas as pd
# (덩어리 수 + 바탕 1, 칸마다 번호, 덩어리마다 통계, 덩어리마다 무게중심)
n, labels, stats, centroids = cv2.connectedComponentsWithStats(binary)
print("덩어리 수(바탕 제외):", n - 1)
keep = [k for k in range(1, n) if stats[k, cv2.CC_STAT_AREA] >= MIN_AREA] # 0번은 바탕
print("면적", MIN_AREA, "이상:", len(keep), "개")
# 같은 부품끼리 짝지어 두 면적을 견준다: 윤곽선의 무게중심과 가장 가까운 덩어리
rows = []
for c in parts:
m = cv2.moments(c)
cx, cy = m["m10"] / m["m00"], m["m01"] / m["m00"]
k = min(keep, key=lambda k: (centroids[k][0] - cx) ** 2 + (centroids[k][1] - cy) ** 2)
rows.append({"cx": round(cx, 1), "cy": round(cy, 1),
"contourArea": cv2.contourArea(c),
"흰 칸 수(CC_STAT_AREA)": int(stats[k, cv2.CC_STAT_AREA])})
compare = pd.DataFrame(rows).sort_values("contourArea").reset_index(drop=True)
compare["차이"] = compare["흰 칸 수(CC_STAT_AREA)"] - compare["contourArea"]
print(compare.to_string())덩어리 수(바탕 제외): 51
면적 100 이상: 12 개
cx cy contourArea 흰 칸 수(CC_STAT_AREA) 차이
0 185.0 120.0 198.0 221 23.0
1 105.0 40.0 256.0 283 27.0
2 245.0 200.0 388.0 421 33.0
3 45.0 125.0 400.0 441 41.0
4 285.0 45.0 454.0 489 35.0
5 60.0 195.0 632.0 673 41.0
6 230.0 50.0 714.0 744 30.0
7 165.0 45.0 913.0 949 36.0
8 115.0 125.0 1040.0 1093 53.0
9 255.0 125.0 1290.0 1340 50.0
10 40.0 45.0 1542.0 1587 45.0
11 150.0 195.0 1832.0 1878 46.04단계. 거른 결과에 번호 쓰기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 위 줄부터, 같은 줄에서는 왼쪽부터 번호를 매기려고 정렬한다(줄 높이 80칸)
parts.sort(key=lambda c: (cv2.boundingRect(c)[1] // 80, cv2.boundingRect(c)[0]))
canvas = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
cv2.drawContours(canvas, contours, -1, (0, 0, 255), 1) # 모든 윤곽선(먼지 포함): 빨강
for i, c in enumerate(parts, start=1):
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(canvas, (x, y), (x + w - 1, y + h - 1), (0, 160, 0), 2) # 남긴 부품: 초록 상자
# putText(그림, 글, 왼쪽 아래 좌표, 글꼴, 크기, 색, 두께). 한글은 쓸 수 없다
cv2.putText(canvas, str(i), (x, max(y - 3, 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
plt.figure(figsize=(7, 5.2))
plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
plt.title(f"윤곽선 {len(contours)}개 가운데 면적 {MIN_AREA} 이상 {len(parts)}개")
plt.axis("off")
plt.show()
코드 설명
- Otsu 가 고른 기준값은 135 입니다. 작업대(188~212)와 부품(70) 사이라 부품과 먼지가 모두 흰색이 되어 윤곽선이 51개 잡혔습니다.
- 면적 분포를 보면 10 미만이 38개, 10~50 이 1개(먼지 둘이 붙은 16)이고 50~100 은 0개입니다. 100 이상은 12개로, 빈 구간 50~100 을 지나 기준 100 을 두면 부품만 남습니다.
cv2.connectedComponentsWithStats는 덩어리 51개(바탕 제외)를 세고,stats[k, cv2.CC_STAT_AREA] >= 100으로 거르면 12개로 findContours 와 같습니다.- 두 면적을 짝지어 보면 흰 칸 수가 contourArea 보다 늘 23~53 큽니다. 둘레가 긴 큰 부품일수록 차이가 큽니다. 가장자리 칸의 바깥 절반만큼 차이가 나기 때문입니다. 부품 안의 밝은 반사점은 흰 칸 수에서는 빠지지만, EXTERNAL 윤곽선의 면적에는 들어갑니다.
- 번호는 줄 높이 80칸으로 위 줄부터 매겼습니다. 그림에서 1번이 왼쪽 위의 큰 원, 12번이 오른쪽 아래의 원입니다.