1"""
209. 윤곽선 검출
3- findContours, drawContours
4- 윤곽선 계층 구조
5- 윤곽선 근사 (approxPolyDP)
6"""
7
8import cv2
9import numpy as np
10
11
12def create_shapes_image():
13 """도형이 있는 이진 이미지"""
14 img = np.zeros((400, 500), dtype=np.uint8)
15
16 # 사각형
17 cv2.rectangle(img, (50, 50), (150, 150), 255, -1)
18
19 # 원
20 cv2.circle(img, (300, 100), 50, 255, -1)
21
22 # 삼각형
23 pts = np.array([[400, 50], [350, 150], [450, 150]], np.int32)
24 cv2.fillPoly(img, [pts], 255)
25
26 # 중첩된 사각형 (계층 구조)
27 cv2.rectangle(img, (50, 200), (200, 350), 255, -1)
28 cv2.rectangle(img, (80, 230), (170, 320), 0, -1) # 내부 구멍
29 cv2.rectangle(img, (100, 250), (150, 290), 255, -1) # 구멍 안의 객체
30
31 # 별 모양
32 pts_star = np.array([
33 [350, 200], [365, 250], [420, 250], [375, 280],
34 [390, 330], [350, 300], [310, 330], [325, 280],
35 [280, 250], [335, 250]
36 ], np.int32)
37 cv2.fillPoly(img, [pts_star], 255)
38
39 return img
40
41
42def find_contours_demo():
43 """윤곽선 찾기 데모"""
44 print("=" * 50)
45 print("윤곽선 찾기 (findContours)")
46 print("=" * 50)
47
48 img = create_shapes_image()
49
50 # 윤곽선 찾기
51 # RETR_EXTERNAL: 외부 윤곽선만
52 # RETR_LIST: 모든 윤곽선 (계층 무시)
53 # RETR_TREE: 모든 윤곽선 (계층 포함)
54 # RETR_CCOMP: 2단계 계층
55
56 contours, hierarchy = cv2.findContours(
57 img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
58 )
59
60 print(f"검출된 윤곽선 수: {len(contours)}")
61 print(f"계층 구조 shape: {hierarchy.shape if hierarchy is not None else None}")
62
63 # 검색 모드 설명
64 print("\n검색 모드 (Retrieval Mode):")
65 print(" RETR_EXTERNAL: 가장 바깥 윤곽선만")
66 print(" RETR_LIST: 모든 윤곽선 (평평하게)")
67 print(" RETR_TREE: 전체 계층 구조")
68 print(" RETR_CCOMP: 2단계 계층")
69
70 return img, contours, hierarchy
71
72
73def draw_contours_demo():
74 """윤곽선 그리기 데모"""
75 print("\n" + "=" * 50)
76 print("윤곽선 그리기 (drawContours)")
77 print("=" * 50)
78
79 img = create_shapes_image()
80 contours, _ = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
81
82 # 컬러 이미지로 변환 (그리기용)
83 color_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
84
85 # 모든 윤곽선 그리기
86 all_contours = color_img.copy()
87 cv2.drawContours(all_contours, contours, -1, (0, 255, 0), 2)
88
89 # 특정 윤곽선만 그리기
90 specific = color_img.copy()
91 cv2.drawContours(specific, contours, 0, (255, 0, 0), 2) # 첫 번째 윤곽선
92 cv2.drawContours(specific, contours, 1, (0, 255, 0), 2) # 두 번째 윤곽선
93
94 # 채우기
95 filled = color_img.copy()
96 cv2.drawContours(filled, contours, 0, (0, 0, 255), -1) # thickness=-1 → 채우기
97
98 print("drawContours 파라미터:")
99 print(" contourIdx=-1: 모든 윤곽선")
100 print(" contourIdx=n: n번째 윤곽선만")
101 print(" thickness=-1: 내부 채우기")
102
103 cv2.imwrite('contours_original.jpg', img)
104 cv2.imwrite('contours_all.jpg', all_contours)
105 cv2.imwrite('contours_specific.jpg', specific)
106 cv2.imwrite('contours_filled.jpg', filled)
107
108
109def contour_hierarchy_demo():
110 """윤곽선 계층 구조 데모"""
111 print("\n" + "=" * 50)
112 print("윤곽선 계층 구조")
113 print("=" * 50)
114
115 img = create_shapes_image()
116 contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
117
118 # hierarchy[i] = [Next, Previous, First_Child, Parent]
119 print("계층 구조 형식: [Next, Previous, First_Child, Parent]")
120 print("(-1은 해당 관계 없음)")
121
122 for i, h in enumerate(hierarchy[0]):
123 print(f"윤곽선 {i}: Next={h[0]}, Prev={h[1]}, Child={h[2]}, Parent={h[3]}")
124
125 # 외부 윤곽선만 (부모가 없는 것)
126 color_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
127 external_only = color_img.copy()
128
129 for i, h in enumerate(hierarchy[0]):
130 if h[3] == -1: # 부모가 없음 = 외부 윤곽선
131 cv2.drawContours(external_only, contours, i, (0, 255, 0), 2)
132
133 cv2.imwrite('contours_external.jpg', external_only)
134
135
136def contour_approximation_demo():
137 """윤곽선 근사 데모"""
138 print("\n" + "=" * 50)
139 print("윤곽선 근사 (approxPolyDP)")
140 print("=" * 50)
141
142 # 복잡한 곡선 이미지
143 img = np.zeros((300, 400), dtype=np.uint8)
144 pts = []
145 for angle in range(0, 360, 5):
146 r = 80 + 20 * np.sin(5 * np.radians(angle))
147 x = int(200 + r * np.cos(np.radians(angle)))
148 y = int(150 + r * np.sin(np.radians(angle)))
149 pts.append([x, y])
150 pts = np.array(pts, np.int32)
151 cv2.fillPoly(img, [pts], 255)
152
153 contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
154
155 # 원본 윤곽선
156 color_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
157
158 # 다양한 epsilon 값으로 근사
159 epsilons = [0.01, 0.02, 0.05, 0.1]
160
161 for eps in epsilons:
162 approx_img = color_img.copy()
163 for cnt in contours:
164 # epsilon = 둘레의 비율
165 epsilon = eps * cv2.arcLength(cnt, True)
166 approx = cv2.approxPolyDP(cnt, epsilon, True)
167
168 print(f"epsilon={eps}: {len(cnt)} 점 → {len(approx)} 점")
169
170 cv2.drawContours(approx_img, [approx], -1, (0, 255, 0), 2)
171 # 꼭짓점 표시
172 for pt in approx:
173 cv2.circle(approx_img, tuple(pt[0]), 3, (0, 0, 255), -1)
174
175 cv2.imwrite(f'approx_eps_{eps}.jpg', approx_img)
176
177
178def contour_properties_demo():
179 """윤곽선 속성 데모"""
180 print("\n" + "=" * 50)
181 print("윤곽선 속성")
182 print("=" * 50)
183
184 img = create_shapes_image()
185 contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
186
187 for i, cnt in enumerate(contours):
188 # 면적
189 area = cv2.contourArea(cnt)
190
191 # 둘레 (호 길이)
192 perimeter = cv2.arcLength(cnt, True)
193
194 # 경계 사각형
195 x, y, w, h = cv2.boundingRect(cnt)
196
197 # 중심점 (모멘트)
198 M = cv2.moments(cnt)
199 if M['m00'] != 0:
200 cx = int(M['m10'] / M['m00'])
201 cy = int(M['m01'] / M['m00'])
202 else:
203 cx, cy = 0, 0
204
205 print(f"\n윤곽선 {i}:")
206 print(f" 면적: {area:.1f}")
207 print(f" 둘레: {perimeter:.1f}")
208 print(f" 경계 사각형: ({x}, {y}, {w}, {h})")
209 print(f" 중심: ({cx}, {cy})")
210
211
212def detect_shapes():
213 """도형 인식"""
214 print("\n" + "=" * 50)
215 print("도형 인식")
216 print("=" * 50)
217
218 img = create_shapes_image()
219 contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
220
221 color_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
222
223 for cnt in contours:
224 # 윤곽선 근사
225 epsilon = 0.04 * cv2.arcLength(cnt, True)
226 approx = cv2.approxPolyDP(cnt, epsilon, True)
227
228 # 중심점 계산
229 M = cv2.moments(cnt)
230 if M['m00'] != 0:
231 cx = int(M['m10'] / M['m00'])
232 cy = int(M['m01'] / M['m00'])
233 else:
234 continue
235
236 # 꼭짓점 수로 도형 판별
237 vertices = len(approx)
238
239 if vertices == 3:
240 shape = "Triangle"
241 elif vertices == 4:
242 # 정사각형 vs 직사각형
243 x, y, w, h = cv2.boundingRect(approx)
244 ar = w / float(h)
245 shape = "Square" if 0.95 <= ar <= 1.05 else "Rectangle"
246 elif vertices == 5:
247 shape = "Pentagon"
248 elif vertices > 5:
249 # 원 판별 (면적/둘레 비율)
250 area = cv2.contourArea(cnt)
251 perimeter = cv2.arcLength(cnt, True)
252 circularity = 4 * np.pi * area / (perimeter ** 2)
253 shape = "Circle" if circularity > 0.8 else f"Polygon({vertices})"
254 else:
255 shape = f"Polygon({vertices})"
256
257 # 결과 표시
258 cv2.drawContours(color_img, [approx], -1, (0, 255, 0), 2)
259 cv2.putText(color_img, shape, (cx - 30, cy),
260 cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
261
262 print(f" {shape}: 꼭짓점 {vertices}개")
263
264 cv2.imwrite('shapes_detected.jpg', color_img)
265
266
267def main():
268 """메인 함수"""
269 # 윤곽선 찾기
270 find_contours_demo()
271
272 # 윤곽선 그리기
273 draw_contours_demo()
274
275 # 계층 구조
276 contour_hierarchy_demo()
277
278 # 윤곽선 근사
279 contour_approximation_demo()
280
281 # 윤곽선 속성
282 contour_properties_demo()
283
284 # 도형 인식
285 detect_shapes()
286
287 print("\n윤곽선 검출 데모 완료!")
288
289
290if __name__ == '__main__':
291 main()