OpenCV 비전 실무12장 · Contour Detection 과 객체 분석18 / 21← 이전목차다음 →MetaContents

12장. Contour Detection 과 객체 분석

실습 5 — 잡티 걸러 개수 세기와 connectedComponentsWithStats

먼지가 흩어진 가상의 작업대 사진(ch12_tray.png)에서 부품을 셉니다. 면적의 분포를 보고 기준을 정해 먼지를 거르고, 같은 일을 cv2.connectedComponentsWithStats 로 해서 견줍니다.

1단계. 이진화하고 모든 윤곽선 세기

Python
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단계. 면적 분포를 보고 거르기

Python
# 면적을 구간으로 나눠 몇 개씩 있는지 센다. 두 무리 사이의 빈 구간에 기준을 둔다
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 로 같은 일 하기

Python
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.0

4단계. 거른 결과에 번호 쓰기

Python
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()
작업대 사진의 모든 윤곽선을 빨간 선으로, 면적 100 이상인 부품에 초록 상자와 파란 번호를 그린 그림

코드 설명

  • 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번이 오른쪽 아래의 원입니다.
← 이전 절12장 · 실습 4 — 곧은 상자, 회전 상자, 최소 외접원다음 절 →12장 · 프로젝트 1 — 컨베이어 부품 개수와 크기 검사