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

12장. Contour Detection 과 객체 분석

실습 2 — 세 가지 찾는 방식과 hierarchy 읽기

구멍이 있는 물체가 든 그림(ch12_nested.png)으로 세 방식의 차이를 보고, RETR_TREE 의 hierarchy 를 표로 바꿔 읽습니다. 그다음 구멍의 면적을 빼서 도넛의 실제 넓이를 구합니다.

1단계. 세 방식으로 찾아 개수 견주기

Python
import cv2
import numpy as np

# 도넛, 구멍 속에 점이 든 액자, 꽉 찬 원이 있는 가상의 이진 그림
nested = cv2.imread("ch12_nested.png", cv2.IMREAD_GRAYSCALE)

found = {}
for name in ["RETR_EXTERNAL", "RETR_LIST", "RETR_TREE"]:
    # getattr(cv2, "RETR_LIST") 는 cv2.RETR_LIST 와 같다. 이름을 글자로 돌리려고 쓴다
    contours, hierarchy = cv2.findContours(nested, getattr(cv2, name), cv2.CHAIN_APPROX_SIMPLE)
    found[name] = (contours, hierarchy)
    # 부모 칸(넷째)이 -1 인 윤곽선 = 가장 바깥의 윤곽선
    outer = int((hierarchy[0][:, 3] == -1).sum())
    print(f"{name:13}: 윤곽선 {len(contours)}개, 부모가 없는 것 {outer}개")
출력
RETR_EXTERNAL: 윤곽선 3개, 부모가 없는 것 3개
RETR_LIST    : 윤곽선 6개, 부모가 없는 것 6개
RETR_TREE    : 윤곽선 6개, 부모가 없는 것 3개

2단계. TREE 의 hierarchy 를 표로 읽기

Python
import pandas as pd

contours, hierarchy = found["RETR_TREE"]
rows = []
for i, (nxt, prev, child, parent) in enumerate(hierarchy[0]):
    # 층: 부모를 따라 올라가며 몇 번 올라가야 -1 이 되는지 센다
    depth, p = 0, parent
    while p != -1:
        depth, p = depth + 1, hierarchy[0][p][3]
    x, y, w, h = cv2.boundingRect(contours[i])
    rows.append({"번호": i, "다음": int(nxt), "이전": int(prev), "첫 자식": int(child),
                 "부모": int(parent), "층": depth, "면적": cv2.contourArea(contours[i]),
                 "상자 왼쪽 x": x})
tree = pd.DataFrame(rows)
print(tree.to_string(index=False))
출력
 번호  다음  이전  첫 자식  부모  층     면적  상자 왼쪽 x
  0   1  -1    -1  -1  0 3754.0      235
  1   3   0     2  -1  0 6234.0       15
  2  -1  -1    -1   1  1 1580.0       37
  3  -1   1     4  -1  0 9900.0      125
  4  -1  -1     5   3  1 3742.0      144
  5  -1  -1    -1   4  2  408.0      158

3단계. 구멍 세기와 도넛의 실제 넓이

Python
# 층이 홀수(1, 3, ...)인 윤곽선 = 구멍의 테두리
holes = tree[tree["층"] % 2 == 1]
print("구멍의 수:", len(holes))

# 도넛: 상자 왼쪽 x 가 가장 작은 바깥 윤곽선. 그 첫 자식이 구멍이다
donut = tree[tree["층"] == 0].sort_values("상자 왼쪽 x").iloc[0]
hole_area = tree.loc[tree["번호"] == donut["첫 자식"], "면적"].iloc[0]
print("도넛 바깥 면적:", donut["면적"])
print("구멍 면적:", hole_area)
print("구멍을 뺀 넓이:", donut["면적"] - hole_area)
# 손계산: 3.14 × (45 × 45 - 22 × 22)
print("손계산:", round(3.14 * (45 * 45 - 22 * 22), 1), "/ 흰 칸 수:", int((nested[:, :110] > 0).sum()))
출력
구멍의 수: 2
도넛 바깥 면적: 6234.0
구멍 면적: 1580.0
구멍을 뺀 넓이: 4654.0
손계산: 4838.7 / 흰 칸 수: 4844

4단계. 층마다 색을 달리해 그리기

Python
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

# 층 0 은 초록, 1 은 빨강, 2 는 파랑(B, G, R)
colors = {0: (0, 160, 0), 1: (0, 0, 255), 2: (255, 0, 0)}
canvas = cv2.cvtColor(nested // 3, cv2.COLOR_GRAY2BGR)
for i, c in enumerate(contours):
    cv2.drawContours(canvas, contours, i, colors[int(tree.loc[i, "층"])], 3)

ext = cv2.cvtColor(nested // 3, cv2.COLOR_GRAY2BGR)
cv2.drawContours(ext, found["RETR_EXTERNAL"][0], -1, (0, 160, 0), 3)

fig, axes = plt.subplots(1, 2, figsize=(10, 2.9))
axes[0].imshow(cv2.cvtColor(ext, cv2.COLOR_BGR2RGB))
axes[0].set_title("RETR_EXTERNAL: 바깥만")
axes[1].imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
axes[1].set_title("RETR_TREE: 층 0 초록, 1 빨강, 2 파랑")
for ax in axes:
    ax.axis("off")
plt.tight_layout()
plt.show()
EXTERNAL 로 찾은 바깥 윤곽선 세 개와, TREE 로 찾은 윤곽선 여섯 개를 층마다 초록, 빨강, 파랑으로 그린 두 그림

코드 설명

  • 부모 칸(hierarchy[0][:, 3])이 -1 인 윤곽선을 세면 가장 바깥의 물체 수입니다. TREE 에서는 6개 가운데 3개였고, LIST 는 부모를 적지 않아 6개 모두 -1 이었습니다.
  • 층은 부모 칸을 따라 -1 이 나올 때까지 올라간 횟수입니다. 표에서 도넛 구멍(2번)과 액자 구멍(4번)이 1층, 구멍 속 점(5번)이 2층입니다. 층이 홀수면 구멍의 테두리라서 구멍이 2개로 세어졌습니다.
  • 도넛의 바깥 면적 6234.0 에서 구멍 면적 1580.0 을 빼면 4654.0 입니다. 손계산 4838.7 과 흰 칸 수 4,844 보다 작은데, 바깥 윤곽선은 가장자리 칸의 가운데를 지나 바깥 절반을 덜 세고, 구멍의 윤곽선도 구멍을 둘러싼 흰 칸의 가운데를 지나 그 절반을 구멍 쪽에 넣기 때문입니다. 테두리가 두 겹이라 차이가 두 번 쌓입니다.
  • cv2.drawContours(canvas, contours, i, 색, 3) 처럼 번호를 하나씩 주면 윤곽선마다 다른 색을 칠할 수 있습니다. 4단계 그림에서 초록이 0층, 빨강이 1층(구멍), 파랑이 2층(구멍 속 점)입니다.
← 이전 절12장 · 실습 1 — findContours 와 drawContours다음 절 →12장 · 실습 3 — 면적, 둘레, 무게중심 재기