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

12장. Contour Detection 과 객체 분석

실습 4 — 곧은 상자, 회전 상자, 최소 외접원

도형 판의 여섯 도형에 세 가지 틀을 씌우고, 틀마다 넓이를 견줍니다. 기운 네모에서 곧은 상자와 회전 상자의 차이가 가장 크게 나는지 확인합니다.

1단계. 곧은 상자

Python
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)      3927

2단계. 회전 상자

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

3단계. 최소 외접원과 넓이 견주기

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

4단계. 세 가지 틀을 함께 그리기

Python
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, ...) 처럼 목록에 넣어 그립니다.
← 이전 절12장 · 실습 3 — 면적, 둘레, 무게중심 재기다음 절 →12장 · 실습 5 — 잡티 걸러 개수 세기와 connectedComponentsWithStats