12장. Contour Detection 과 객체 분석
실습 2 — 세 가지 찾는 방식과 hierarchy 읽기
구멍이 있는 물체가 든 그림(ch12_nested.png)으로 세 방식의 차이를 보고, RETR_TREE 의 hierarchy 를 표로 바꿔 읽습니다. 그다음 구멍의 면적을 빼서 도넛의 실제 넓이를 구합니다.
1단계. 세 방식으로 찾아 개수 견주기
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 를 표로 읽기
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 1583단계. 구멍 세기와 도넛의 실제 넓이
# 층이 홀수(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 / 흰 칸 수: 48444단계. 층마다 색을 달리해 그리기
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()
코드 설명
- 부모 칸(
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층(구멍 속 점)입니다.