13장. Shape 분석과 ROI 처리
실습 3 — 규칙으로 모양 이름 붙이기
실습 2의 특징 숫자로 "모양 분류 규칙 만들기" 절의 규칙을 함수로 만들고, 도형마다 이름을 붙여 그림 위에 적습니다. 결과 그림은 out_ch13_named.png 로 저장합니다. 마지막으로 epsilon 을 바꾸거나 그림을 작게 줄였을 때 규칙이 어디서 틀리는지 봅니다.
1단계. 특징 재기 준비
import math
import cv2
import numpy as np
import pandas as pd
img = cv2.imread("ch13_shapes.png")
def find_shapes(color_img):
"""그림에서 바깥 윤곽선을 찾아 위 줄 → 아래 줄, 왼쪽 → 오른쪽 순서로 돌려준다."""
gray = cv2.cvtColor(color_img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
found, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
row_h = color_img.shape[0] // 2 # 위 줄과 아래 줄을 가르는 높이
return sorted(found, key=lambda c: (cv2.boundingRect(c)[1] // row_h, cv2.boundingRect(c)[0]))
def features(c, ratio=0.01):
"""규칙에 쓰는 숫자 네 개: 꼭짓점, 긴 변 ÷ 짧은 변, 채움 비율, 볼록 비율."""
area = cv2.contourArea(c)
peri = cv2.arcLength(c, True)
x, y, w, h = cv2.boundingRect(c)
return {
"꼭짓점": len(cv2.approxPolyDP(c, ratio * peri, True)),
"길쭉함": round(max(w, h) / min(w, h), 2), # 방향과 관계없이 긴 변 ÷ 짧은 변
"채움비율": round(area / (w * h), 3),
"볼록비율": round(area / cv2.contourArea(cv2.convexHull(c)), 3),
}
contours = find_shapes(img)
truth = ["원", "세모", "네모", "막대", "별", "초승달", "팔각형", "기운 네모"]
print("윤곽선:", len(contours))윤곽선: 82단계. 규칙 함수로 이름 붙이기
def name_shape(f):
"""위에서부터 차례로 묻고 처음 맞는 곳에서 멈춘다."""
if f["볼록비율"] < 0.9: # 1. 움푹한 모양
return "별" if f["꼭짓점"] == 10 else "초승달"
if f["꼭짓점"] == 3: # 2. 꼭짓점 셋
return "세모"
if f["꼭짓점"] == 4:
if f["길쭉함"] >= 2: # 3. 길쭉한 네모
return "막대"
return "네모" if f["채움비율"] >= 0.9 else "기운 네모" # 4. 상자를 꽉 채웠나
if f["꼭짓점"] == 8: # 5. 꼭짓점 여덟
return "팔각형"
if f["꼭짓점"] > 8:
return "원"
return "모름" # 6. 어디에도 안 맞음
rows = []
for answer, c in zip(truth, contours):
f = features(c)
rows.append({"정답": answer, **f, "규칙의 답": name_shape(f)})
result = pd.DataFrame(rows)
print(result.to_string(index=False))
print("맞힌 개수:", int((result["정답"] == result["규칙의 답"]).sum()), "/", len(result)) 정답 꼭짓점 길쭉함 채움비율 볼록비율 규칙의 답
원 12 1.00 0.731 0.977 원
세모 3 1.16 0.481 0.971 세모
네모 4 1.00 0.963 1.000 네모
막대 4 3.10 0.938 1.000 막대
별 10 1.03 0.342 0.507 별
초승달 12 1.11 0.379 0.583 초승달
팔각형 8 1.00 0.787 1.000 팔각형
기운 네모 4 1.00 0.596 0.966 기운 네모
맞힌 개수: 8 / 83단계. 그림 위에 이름 적고 저장하기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# putText 는 한글을 그리지 못하므로, 그림에 적을 이름은 영어로 바꾼다
english = {"원": "circle", "세모": "triangle", "네모": "square", "막대": "bar", "별": "star",
"초승달": "crescent", "팔각형": "octagon", "기운 네모": "tilted", "모름": "?"}
named = img.copy()
for c, label in zip(contours, result["규칙의 답"]):
x, y, w, h = cv2.boundingRect(c)
# 윤곽선을 흰 선으로 두르고, 상자 아래쪽에 이름을 적는다
cv2.drawContours(named, [c], -1, (255, 255, 255), 1)
cv2.putText(named, english[label], (x, min(y + h + 14, 236)),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA)
print("저장:", cv2.imwrite("out_ch13_named.png", named))
plt.figure(figsize=(6.4, 4.8))
plt.imshow(cv2.cvtColor(named, cv2.COLOR_BGR2RGB))
plt.title("규칙으로 붙인 이름")
plt.axis("off")
plt.show()저장: True
4단계. 규칙이 틀리는 경우 보기
# (가) epsilon 비율을 0.02 로 바꾼다
by_eps = [name_shape(features(c, ratio=0.02)) for c in contours]
# (나) 그림을 가로세로 0.3 배로 줄인다. INTER_AREA 는 줄일 때 쓰는 보간법이다
small = cv2.resize(img, None, fx=0.3, fy=0.3, interpolation=cv2.INTER_AREA)
small_contours = find_shapes(small)
by_small = [name_shape(features(c)) for c in small_contours]
print("줄인 그림의 크기:", small.shape[:2], "/ 찾은 윤곽선:", len(small_contours))
check = pd.DataFrame({"정답": truth, "비율 0.01": result["규칙의 답"],
"비율 0.02": by_eps, "0.3배 그림": by_small})
print(check.to_string(index=False))
for col in ["비율 0.01", "비율 0.02", "0.3배 그림"]:
print(f"{col}: 맞힌 개수 {int((check['정답'] == check[col]).sum())} / 8")줄인 그림의 크기: (72, 96) / 찾은 윤곽선: 8
정답 비율 0.01 비율 0.02 0.3배 그림
원 원 팔각형 원
세모 세모 세모 원
네모 네모 네모 기운 네모
막대 막대 막대 모름
별 별 별 초승달
초승달 초승달 초승달 초승달
팔각형 팔각형 팔각형 팔각형
기운 네모 기운 네모 기운 네모 원
비율 0.01: 맞힌 개수 8 / 8
비율 0.02: 맞힌 개수 7 / 8
0.3배 그림: 맞힌 개수 3 / 8코드 설명
features()의 "길쭉함"은max(w, h) / min(w, h)입니다. 가로 ÷ 세로로 재면 세로로 선 막대가 1 보다 작게 나오므로, 긴 변 ÷ 짧은 변으로 방향과 관계없이 잽니다.cv2.putText는 한글 글꼴이 없어 한글을 물음표로 그립니다. 그래서 그림에 적을 이름은english사전으로 영어로 바꿨고, 표와 제목은 Matplotlib 이 한글로 적습니다.- 규칙은 도형 판의 8개를 모두 맞혔습니다. 경계값을 이 그림의 숫자를 보고 정했으니 맞는 것이 당연하고, 규칙이 쓸 만한지는 조건이 바뀐 그림에서 봐야 합니다.
- epsilon 비율 0.02 에서는 원이 꼭짓점 8개가 되어 팔각형으로 틀렸습니다(8개 가운데 7개). 0.3배로 줄인 그림(가로 96, 세로 72)에서는 3개만 맞혔습니다. 세모와 기운 네모가 원으로, 네모가 기운 네모로, 막대가 모름으로, 별이 초승달로 바뀌었습니다. 도형이 작아지면 칸의 계단이 차지하는 몫이 커져 꼭짓점 수와 채움 비율이 흔들립니다.