12장. Contour Detection 과 객체 분석
실습 4 — 곧은 상자, 회전 상자, 최소 외접원
도형 판의 여섯 도형에 세 가지 틀을 씌우고, 틀마다 넓이를 견줍니다. 기운 네모에서 곧은 상자와 회전 상자의 차이가 가장 크게 나는지 확인합니다.
1단계. 곧은 상자
import cv2
import numpy as np
import pandas as pd
shapes = cv2.imread("ch12_shapes.png", cv2.IMREAD_GRAYSCALE)
contours, _ = cv2.findContours(shapes, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 작은 점을 빼고, 왼쪽 위 도형부터 차례로 놓는다(위 줄 y < 100, 아래 줄)
big = [c for c in contours if cv2.contourArea(c) >= 50]
big.sort(key=lambda c: (cv2.boundingRect(c)[1] >= 100, cv2.boundingRect(c)[0]))
names = ["네모", "원", "기운 네모", "세모", "도넛", "타원"]
rows = []
for name, c in zip(names, big):
x, y, w, h = cv2.boundingRect(c) # 곧은 상자: 왼쪽 위 x, y, 가로 w, 세로 h
rows.append({"도형": name, "면적": cv2.contourArea(c), "곧은 상자": (x, y, w, h), "곧은 상자 넓이": w * h})
table = pd.DataFrame(rows).set_index("도형")
print(table) 면적 곧은 상자 곧은 상자 넓이
도형
네모 3871.0 (20, 20, 80, 50) 4000
원 3754.0 (135, 20, 71, 71) 5041
기운 네모 2596.0 (221, 24, 83, 69) 5727
세모 2800.0 (25, 145, 81, 71) 5751
도넛 5406.0 (128, 128, 85, 85) 7225
타원 2770.5 (227, 155, 77, 51) 39272단계. 회전 상자
rot_area, boxes = [], []
for c in big:
rect = cv2.minAreaRect(c) # ((중심 x, y), (가로, 세로), 각도)
(cx, cy), (rw, rh), angle = rect
rot_area.append(round(rw * rh, 1))
# boxPoints: 회전 상자의 꼭짓점 넷(실수) → 그리려고 반올림해 int32 로
boxes.append(np.round(cv2.boxPoints(rect)).astype(np.int32))
print(f"중심 ({round(cx, 1)}, {round(cy, 1)}), 크기 {round(max(rw, rh), 1)} × {round(min(rw, rh), 1)}")
table["회전 상자 넓이"] = rot_area중심 (59.5, 44.5), 크기 79.0 × 49.0
중심 (170.0, 55.0), 크기 69.3 × 69.3
중심 (262.0, 58.0), 크기 76.4 × 35.3
중심 (65.0, 180.0), 크기 80.0 × 70.0
중심 (170.0, 170.0), 크기 83.2 × 83.2
중심 (265.0, 180.0), 크기 80.8 × 44.93단계. 최소 외접원과 넓이 견주기
circles, circle_area = [], []
for c in big:
(ox, oy), r = cv2.minEnclosingCircle(c) # (중심 x, y), 반지름
circles.append(((int(round(ox)), int(round(oy))), int(round(r))))
circle_area.append(round(3.14 * r * r, 1))
table["외접원 반지름"] = [r for _, r in circles]
table["외접원 넓이"] = circle_area
# 곧은 상자가 회전 상자보다 몇 배 넓은가
table["곧은÷회전"] = (table["곧은 상자 넓이"] / table["회전 상자 넓이"]).round(2)
print(table[["면적", "곧은 상자 넓이", "회전 상자 넓이", "외접원 넓이", "곧은÷회전"]]) 면적 곧은 상자 넓이 회전 상자 넓이 외접원 넓이 곧은÷회전
도형
네모 3871.0 4000 3871.0 6784.0 1.03
원 3754.0 5041 4802.0 3846.5 1.05
기운 네모 2596.0 5727 2696.3 5438.5 2.12
세모 2800.0 5751 5600.0 6768.7 1.03
도넛 5406.0 7225 6919.2 5539.0 1.04
타원 2770.5 3927 3631.5 5149.6 1.084단계. 세 가지 틀을 함께 그리기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
canvas = cv2.cvtColor(shapes // 4, cv2.COLOR_GRAY2BGR)
for c, box, (center, r) in zip(big, boxes, circles):
x, y, w, h = cv2.boundingRect(c)
# 곧은 상자(초록): 오른쪽 아래는 x + w - 1, y + h - 1
cv2.rectangle(canvas, (x, y), (x + w - 1, y + h - 1), (0, 200, 0), 2)
# 회전 상자(빨강): 꼭짓점 넷을 윤곽선 하나로 보고 그린다
cv2.drawContours(canvas, [box], 0, (0, 0, 255), 2)
# 외접원(파랑)
cv2.circle(canvas, center, r, (255, 0, 0), 1)
plt.figure(figsize=(7, 5.2))
plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
plt.title("곧은 상자(초록), 회전 상자(빨강), 최소 외접원(파랑)")
plt.axis("off")
plt.show()
코드 설명
cv2.boundingRect는 (x, y, w, h) 를 돌려줍니다. 곧은 네모는 (20, 20, 80, 50) 으로 칠한 칸과 같고, 곧은 상자 넓이도 흰 칸 수와 같은 4,000 입니다.cv2.minAreaRect의 크기는 큰 쪽, 작은 쪽으로 읽었습니다. 기운 네모는 76.4 × 35.3 으로 칠한 76 × 34 에 가깝습니다. 곧은 상자 넓이 5,727 은 회전 상자 넓이 2,696.3 의 2.12배로, 여섯 도형 가운데 차이가 가장 큽니다. 나머지 다섯 도형은 1.03~1.08배였습니다.- 회전 상자가 늘 물체에 꼭 맞는 것은 아닙니다. 세모의 회전 상자는 80 × 70 = 5,600 으로 세모 넓이(2,800)의 꼭 두 배입니다. 세모는 어떤 네모에 담아도 절반이 빕니다.
cv2.minEnclosingCircle은 원에서 반지름 35 로 원 자체와 같았고(넓이 3,846.5), 길쭉한 기운 네모에서는 외접원 넓이(5,438.5)가 물체의 두 배를 넘었습니다.cv2.boxPoints의 실수 꼭짓점은np.round(...).astype(np.int32)로 바꾼 뒤cv2.drawContours(canvas, [box], 0, ...)처럼 목록에 넣어 그립니다.