12장. Contour Detection 과 객체 분석
실습 3 — 면적, 둘레, 무게중심 재기
도형 판의 여섯 도형마다 면적, 둘레, 무게중심을 재서 표로 만들고, 네모와 원은 손계산과 견줍니다. 마지막에 무게중심에 점을 찍어 봅니다.
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)
rows = []
for c in contours:
area = cv2.contourArea(c) # 면적: 점을 이은 도형의 넓이
if area < 50: # 작은 점 둘은 이 실습에서 뺀다
continue
m = cv2.moments(c) # 모멘트: m00(넓이), m10, m01(좌표의 합)
rows.append({"면적": area,
"둘레": round(cv2.arcLength(c, True), 1), # True: 닫힌 둘레
"cx": round(m["m10"] / m["m00"], 1), # x 좌표 평균
"cy": round(m["m01"] / m["m00"], 1)}) # y 좌표 평균
table = pd.DataFrame(rows)
# 위쪽 줄(cy 100 미만)부터, 같은 줄에서는 왼쪽부터 번호를 매긴다
table = table.sort_values(["cy", "cx"], key=lambda s: s // 100 if s.name == "cy" else s)
table.index = ["네모", "원", "기운 네모", "세모", "도넛", "타원"]
print(table) 면적 둘레 cx cy
네모 3871.0 256.0 59.5 44.5
원 3754.0 230.8 170.0 55.0
기운 네모 2596.0 235.6 262.0 58.0
세모 2800.0 253.1 65.0 191.7
도넛 5406.0 277.4 170.0 170.0
타원 2770.5 210.4 265.0 180.02단계. 네모와 원을 손계산과 견주기
# 네모는 가로 80칸, 세로 50칸으로, 원은 반지름 35 로 칠했다
checks = [
("네모 흰 칸 수", int((shapes[20:70, 20:100] > 0).sum()), 80 * 50),
("네모 면적", table.loc["네모", "면적"], 79 * 49),
("네모 둘레", table.loc["네모", "둘레"], 2 * (79 + 49)),
("원 흰 칸 수", int((shapes[15:96, 130:211] > 0).sum()), round(3.14 * 35 * 35, 1)),
("원 면적", table.loc["원", "면적"], round(3.14 * 35 * 35, 1)),
("원 둘레", table.loc["원", "둘레"], round(2 * 3.14 * 35, 1)),
]
for name, measured, by_hand in checks:
print(f"{name:8}: 잰 값 {measured:7} / 손계산 {by_hand:7}")네모 흰 칸 수: 잰 값 4000 / 손계산 4000
네모 면적 : 잰 값 3871.0 / 손계산 3871
네모 둘레 : 잰 값 256.0 / 손계산 256
원 흰 칸 수 : 잰 값 3853 / 손계산 3846.5
원 면적 : 잰 값 3754.0 / 손계산 3846.5
원 둘레 : 잰 값 230.8 / 손계산 219.83단계. 아주 작은 점의 면적과 흰 칸 수
# 작은 점 둘(반지름 2, 1)은 contourArea 와 흰 칸 수가 크게 다르다
for c in contours:
area = cv2.contourArea(c)
if area >= 50:
continue
x, y, w, h = cv2.boundingRect(c)
pixels = int((shapes[y:y + h, x:x + w] > 0).sum()) # 상자 안의 흰 칸 수
# moments(그림, binaryImage=True): 그림의 흰 칸으로 잰 모멘트. m00 이 흰 칸 수다
m = cv2.moments(shapes[y:y + h, x:x + w], binaryImage=True)
print(f"상자 {(x, y, w, h)}: contourArea {area}, 흰 칸 {pixels}, 그림 모멘트 m00 {m['m00']}")상자 (299, 227, 3, 3): contourArea 2.0, 흰 칸 5, 그림 모멘트 m00 5.0
상자 (126, 110, 5, 5): contourArea 8.0, 흰 칸 13, 그림 모멘트 m00 13.04단계. 무게중심에 점 찍기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
canvas = cv2.cvtColor(shapes // 3, cv2.COLOR_GRAY2BGR)
cv2.drawContours(canvas, contours, -1, (0, 0, 255), 1)
for name, row in table.iterrows():
center = (int(round(row["cx"])), int(round(row["cy"])))
# circle(그림, 중심, 반지름, 색, 두께 -1 = 채우기)
cv2.circle(canvas, center, 4, (0, 255, 255), -1)
plt.figure(figsize=(6, 4.5))
plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB))
plt.title("도형마다 무게중심(노란 점)")
plt.axis("off")
plt.show()
코드 설명
- 여섯 도형의 면적은 네모 3871.0, 원 3754.0, 기운 네모 2596.0, 세모 2800.0, 도넛 5406.0, 타원 2770.5 입니다. 도넛은 EXTERNAL 로 찾아 구멍이 면적에 들어 있습니다.
- 네모는 흰 칸 4,000 개, contourArea 3,871(79 × 49), 둘레 256(2 × (79 + 49))으로 손계산과 꼭 맞았습니다. 원은 흰 칸 3,853 개가 πr²(3,846.5)와 거의 같고, contourArea 는 3,754 로 조금 작으며, 둘레는 230.8 로 손계산(219.8)보다 깁니다. 테두리가 계단 모양이기 때문입니다.
- 반지름 2 짜리 점은 contourArea 8.0, 흰 칸 13개이고, 반지름 1 짜리 점은 2.0 과 5개입니다.
cv2.moments(그림, binaryImage=True)의 m00 은 흰 칸 수와 같습니다. 작은 덩어리일수록 두 값의 차이가 큽니다. - 무게중심은 네모 (59.5, 44.5), 원 (170.0, 55.0) 처럼 칠할 때 정한 가운데와 같습니다. 세모는 (65.0, 191.7) 로, 꼭짓점 (65, 145) 와 밑변 y 215 사이에서 넓은 밑변 쪽으로 치우쳤습니다.