12장. Contour Detection 과 객체 분석
실습 1 — findContours 와 drawContours
가상의 도형 판(ch12_shapes.png)에서 윤곽선을 찾아 개수와 점 수를 보고, 컬러 그림 위에 그려 봅니다. 마지막에는 11장의 도형 그림(ch11_set/shapes.png)에서 이진화로 찾은 윤곽선과 Canny 결과로 찾은 윤곽선을 견줍니다.
1단계. 그림 읽고 윤곽선 찾기
import cv2
import numpy as np
# 흰 도형, 검은 바탕의 가상 도형 판. 연습용으로 그려 만든 그림이다
shapes = cv2.imread("ch12_shapes.png", cv2.IMREAD_GRAYSCALE)
print("모양:", shapes.shape, "/ 값의 종류:", [int(v) for v in np.unique(shapes)])
# findContours(그림, 찾는 방식, 점을 담는 방식) → (윤곽선 목록, 계층 구조)
contours, hierarchy = cv2.findContours(shapes, # image: 흰 물체, 검은 바탕
cv2.RETR_EXTERNAL, # mode: 가장 바깥 테두리만
cv2.CHAIN_APPROX_SIMPLE) # method: 곧은 줄은 양 끝만
# 결과: 윤곽선 8개(도형 6 + 작은 점 2)
print("윤곽선 수:", len(contours))
print("hierarchy 모양:", hierarchy.shape)모양: (240, 320) / 값의 종류: [0, 255]
윤곽선 수: 8
hierarchy 모양: (1, 8, 4)2단계. 윤곽선마다 점 수와 첫 점 보기
# 윤곽선마다 배열 모양과 첫 점의 좌표를 찍는다. 번호는 findContours 가 준 차례 그대로다
for i, c in enumerate(contours):
first = [int(v) for v in c[0, 0]] # c[0, 0]: 첫 점의 [x, y]
print(f"{i}번: 모양 {c.shape}, 첫 점 {first}")
# 점을 모두 담는 NONE 으로 다시 찾아 점 수를 견준다
full, _ = cv2.findContours(shapes, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
print("점 수 NONE :", [len(c) for c in full])
print("점 수 SIMPLE:", [len(c) for c in contours])0번: 모양 (4, 1, 2), 첫 점 [300, 227]
1번: 모양 (89, 1, 2), 첫 점 [274, 155]
2번: 모양 (123, 1, 2), 첫 점 [65, 145]
3번: 모양 (132, 1, 2), 첫 점 [170, 128]
4번: 모양 (4, 1, 2), 첫 점 [128, 110]
5번: 모양 (164, 1, 2), 첫 점 [238, 24]
6번: 모양 (100, 1, 2), 첫 점 [170, 20]
7번: 모양 (4, 1, 2), 첫 점 [20, 20]
점 수 NONE : [4, 181, 220, 236, 8, 190, 196, 256]
점 수 SIMPLE: [4, 89, 123, 132, 4, 164, 100, 4]3단계. 컬러 그림에 그리고 번호 쓰기
import matplotlib.pyplot as plt
# 그래프의 한글이 깨지지 않게 맑은 고딕을 쓰고, 빼기 기호가 네모로 나오지 않게 한다
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 흰 도형을 조금 어둡게(// 3) 해서 색 선이 잘 보이게 하고, 컬러(3채널)로 바꾼다
canvas = cv2.cvtColor(shapes // 3, cv2.COLOR_GRAY2BGR)
# drawContours(그림, 윤곽선 목록, 번호, 색, 두께)
cv2.drawContours(canvas, # 이 그림이 바로 바뀐다
contours, # 윤곽선 목록
-1, # -1: 모두 그린다
(0, 0, 255), # 빨강(B, G, R)
2) # 선 두께 2칸
# 윤곽선마다 첫 점 옆에 findContours 가 준 번호를 노란 글자로 쓴다
for i, c in enumerate(contours):
x, y = [int(v) for v in c[0, 0]]
cv2.putText(canvas, str(i), (x + 3, max(y - 3, 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
# 하나만 골라 안을 채운 그림: 7번(곧은 네모)을 초록으로
one = cv2.cvtColor(shapes // 3, cv2.COLOR_GRAY2BGR)
cv2.drawContours(one, contours, 7, (0, 200, 0), -1)
fig, axes = plt.subplots(1, 2, figsize=(10, 3.8))
axes[0].imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
axes[0].set_title("모든 윤곽선과 찾은 차례")
axes[1].imshow(cv2.cvtColor(one, cv2.COLOR_BGR2RGB))
axes[1].set_title("7번 하나만 채우기")
for ax in axes:
ax.axis("off")
plt.tight_layout()
plt.show()
4단계. 11차시의 그림에서 이진화와 Canny 로 찾기
# 11차시의 도형 그림: 바탕(밝기 170~190)보다 어두운 도형 셋, 밝은 타원 하나, 차이가 작은 네모 하나
gray = cv2.imread("ch11_set/shapes.png", cv2.IMREAD_GRAYSCALE)
# 길 1: Otsu INV 로 어두운 도형을 흰색으로 만든 뒤 찾는다
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
by_binary, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 길 2: 11차시처럼 블러 뒤 Canny 로 경계를 찾고, 그 선의 윤곽선을 찾는다
edges = cv2.Canny(cv2.GaussianBlur(gray, (5, 5), 0), 50, 150)
by_canny, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Canny 선은 한 칸 두께라 LIST 로 찾으면 선의 바깥쪽과 안쪽이 따로 잡힌다
by_canny_list, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
print("이진화 → 윤곽선:", len(by_binary))
print("Canny → 윤곽선(EXTERNAL):", len(by_canny))
print("Canny → 윤곽선(LIST):", len(by_canny_list))
panels = []
for found in (by_binary, by_canny):
out = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
cv2.drawContours(out, found, -1, (0, 0, 255), 2)
panels.append(out)
fig, axes = plt.subplots(1, 2, figsize=(10, 3.8))
axes[0].imshow(cv2.cvtColor(panels[0], cv2.COLOR_BGR2RGB))
axes[0].set_title(f"Otsu INV 로 찾은 윤곽선 {len(by_binary)}개")
axes[1].imshow(cv2.cvtColor(panels[1], cv2.COLOR_BGR2RGB))
axes[1].set_title(f"Canny 로 찾은 윤곽선 {len(by_canny)}개")
for ax in axes:
ax.axis("off")
plt.tight_layout()
plt.show()이진화 → 윤곽선: 3
Canny → 윤곽선(EXTERNAL): 4
Canny → 윤곽선(LIST): 8
코드 설명
cv2.findContours(shapes, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)는 (윤곽선 목록, hierarchy) 두 값을 돌려줍니다. 도형 판에서는 윤곽선 8개(도형 6, 작은 점 2), hierarchy 모양 (1, 8, 4) 였습니다.- 찾은 차례는 위치 순서가 아닙니다. 0번이 오른쪽 아래의 작은 점, 7번이 왼쪽 위의 네모로, 아래쪽 물체가 앞 번호를 받았습니다. 3단계 그림의 노란 숫자가 이 차례입니다.
- 점 수는 NONE 이 [4, 181, 220, 236, 8, 190, 196, 256], SIMPLE 이 [4, 89, 123, 132, 4, 164, 100, 4] 입니다. 곧은 네모(7번)는 256점이 꼭짓점 4점으로 줄었고, 비스듬한 기운 네모(5번)는 190점에서 164점으로 조금만 줄었습니다.
cv2.drawContours(canvas, contours, 7, (0, 200, 0), -1)처럼 번호와 두께 -1 을 주면 그 윤곽선 하나의 안을 채웁니다.- 11장의 그림에서 Otsu INV 는 바탕보다 어두운 원, 네모, 세모 3개만 찾고, 바탕보다 밝은 타원은 놓쳤습니다. Canny 는 밝기 차이의 방향을 가리지 않아 타원까지 4개를 찾았지만, 11장에서 블러 뒤에 놓쳤던 오른쪽 아래의 옅은 네모는 여기서도 빠졌습니다. Canny 선을 LIST 로 찾으면 한 칸 두께 선의 바깥쪽과 안쪽이 따로 잡혀 8개(4 × 2)가 되므로 EXTERNAL 로 찾습니다.