Be2Jay commited on
Commit
1e0b89b
ยท
1 Parent(s): 9e3ae69

Add testing framework, example images, and improved filters

Browse files
.gitignore CHANGED
@@ -89,6 +89,8 @@ data/**/*.bmp
89
  # ํ•˜์ง€๋งŒ ์ƒ˜ํ”Œ/ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๋Š” ํฌํ•จ (์˜ˆ์™ธ)
90
  !data/samples/
91
  !data/test/
 
 
92
  !test_*.jpg
93
  !test_*.png
94
  !sample_*.jpg
 
89
  # ํ•˜์ง€๋งŒ ์ƒ˜ํ”Œ/ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๋Š” ํฌํ•จ (์˜ˆ์™ธ)
90
  !data/samples/
91
  !data/test/
92
+ !data/251015/*.jpg
93
+ !data/251015/*.png
94
  !test_*.jpg
95
  !test_*.png
96
  !sample_*.jpg
data/251015/251015_01-1.jpg ADDED

Git LFS Details

  • SHA256: 4043af8a840b0bb8f2fe3d8c20f2a6b567137eca11e9de51406e655887420091
  • Pointer size: 131 Bytes
  • Size of remote file: 339 kB
data/251015/251015_01.jpg ADDED

Git LFS Details

  • SHA256: 1be51320fea95314f275bc9d0cdc7db6fe21b984c2c5d0c81495cccc5b9ebc32
  • Pointer size: 131 Bytes
  • Size of remote file: 452 kB
data/251015/251015_02-1.jpg ADDED

Git LFS Details

  • SHA256: 7295e82bde1ec552745efa8bf8a696abcc2a0664697c9aa53afff576358f6616
  • Pointer size: 131 Bytes
  • Size of remote file: 339 kB
data/251015/251015_02.jpg ADDED

Git LFS Details

  • SHA256: 410b8c5d5e3b75b2a59ebcef75e358563667c2c650e894bd98d1fd00e1c6e1a4
  • Pointer size: 131 Bytes
  • Size of remote file: 433 kB
data/251015/251015_03-1.jpg ADDED

Git LFS Details

  • SHA256: eeded79a3891d66aaa460452f982981f9cce141512056c46a6fa64716bec13eb
  • Pointer size: 131 Bytes
  • Size of remote file: 345 kB
data/251015/251015_03.jpg ADDED

Git LFS Details

  • SHA256: 63c1135387c5df434aa8a5d1d09c8daceb217e63774fb742f692ec6d1a02161c
  • Pointer size: 131 Bytes
  • Size of remote file: 478 kB
data/251015/251015_04-1.jpg ADDED

Git LFS Details

  • SHA256: 88c8b81c63b0cabec0884267412bd7dd34e8c5aad69b4a9c622a572365db7860
  • Pointer size: 131 Bytes
  • Size of remote file: 342 kB
data/251015/251015_04.jpg ADDED

Git LFS Details

  • SHA256: f4837317775f330e8d3fec3b424ab98df709cf5ac44a85760c45dea296aa1fd4
  • Pointer size: 131 Bytes
  • Size of remote file: 430 kB
data/251015/251015_05-1.jpg ADDED

Git LFS Details

  • SHA256: 2e1648bb2fadcaabb2e3b738a318cd4f6c7c4afe650609874799b6d48cac6ee9
  • Pointer size: 131 Bytes
  • Size of remote file: 342 kB
data/251015/251015_05.jpg ADDED

Git LFS Details

  • SHA256: c883e1e454ad0eb625b1d04c74cfc3e2fa11ede78dde9583da6b08cdb6effb88
  • Pointer size: 131 Bytes
  • Size of remote file: 451 kB
data/251015/251015_06-1.jpg ADDED

Git LFS Details

  • SHA256: 326d02801b9dd4bd5af4d940bb24d1ba8727e153795adc4475580fb204d9cc70
  • Pointer size: 131 Bytes
  • Size of remote file: 328 kB
data/251015/251015_06.jpg ADDED

Git LFS Details

  • SHA256: 401c819c47d78df5f7fee8dd95fc7ca5fb18ce1bc9d453d86cb7eb0a3427ad9f
  • Pointer size: 131 Bytes
  • Size of remote file: 422 kB
data/251015/251015_07-1.jpg ADDED

Git LFS Details

  • SHA256: ef2e1330d5ffcd72384d77996ccdb93c90b5b5ee202719c36367fefc271e6cc6
  • Pointer size: 131 Bytes
  • Size of remote file: 335 kB
data/251015/251015_07.jpg ADDED

Git LFS Details

  • SHA256: 14b80381c739da99f5da53b9a4542b380c68b2b1370d64e3d3677d28fe67cadc
  • Pointer size: 131 Bytes
  • Size of remote file: 501 kB
data/251015/251015_08-1.jpg ADDED

Git LFS Details

  • SHA256: cddc764065efa0d4b00558c3f80ffbcc63513cbb955973780226f86b979cc36c
  • Pointer size: 131 Bytes
  • Size of remote file: 365 kB
data/251015/251015_08.jpg ADDED

Git LFS Details

  • SHA256: e19a105b5c368bd319e75d12877057359ee9fb0d5416dc54812ec5d44516f9eb
  • Pointer size: 131 Bytes
  • Size of remote file: 546 kB
data/251015/251015_09-1.jpg ADDED

Git LFS Details

  • SHA256: ff428a159ba5a8234356466019c6ab45dfd4fecf19a9c4236d5329cbc2903719
  • Pointer size: 131 Bytes
  • Size of remote file: 343 kB
data/251015/251015_09.jpg ADDED

Git LFS Details

  • SHA256: cdd2533eb437954f9218e1a147328e052d120f21053f61b76894de71fedb4a9c
  • Pointer size: 131 Bytes
  • Size of remote file: 420 kB
data/251015/251015_10-1.jpg ADDED

Git LFS Details

  • SHA256: ffdb3c6676befc3765018d153d1f3725c5458b50caf7db7a37239e269db240bd
  • Pointer size: 131 Bytes
  • Size of remote file: 319 kB
data/251015/251015_10.jpg ADDED

Git LFS Details

  • SHA256: ade4056ad3dbc85f2065d6597d54d1b8a86021eff867a0622e60cab8f58ee168
  • Pointer size: 131 Bytes
  • Size of remote file: 481 kB
docs/detection_testing_and_validation.md ADDED
@@ -0,0 +1,931 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ๐Ÿงช ์ƒˆ์šฐ ๊ฒ€์ถœ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ˆ˜ ๊ฐ€์ด๋“œ
2
+
3
+ ## ๐Ÿ“‹ ๋ชฉ์ฐจ
4
+ 1. [ํ…Œ์ŠคํŠธ ์ค€๋น„](#ํ…Œ์ŠคํŠธ-์ค€๋น„)
5
+ 2. [ํ…Œ์ŠคํŠธ ์‹คํ–‰](#ํ…Œ์ŠคํŠธ-์‹คํ–‰)
6
+ 3. [๊ฒฐ๊ณผ ๊ฒ€์ˆ˜ ๋ฐฉ๋ฒ•](#๊ฒฐ๊ณผ-๊ฒ€์ˆ˜-๋ฐฉ๋ฒ•)
7
+ 4. [์„ฑ๋Šฅ ํ‰๊ฐ€](#์„ฑ๋Šฅ-ํ‰๊ฐ€)
8
+ 5. [๊ฐœ์„  ๋ฐฉํ–ฅ](#๊ฐœ์„ -๋ฐฉํ–ฅ)
9
+
10
+ ---
11
+
12
+ ## ๐ŸŽฏ ํ…Œ์ŠคํŠธ ์ค€๋น„
13
+
14
+ ### 1. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์„ฑ
15
+
16
+ #### ํด๋” ๊ตฌ์กฐ
17
+ ```
18
+ test_dataset/
19
+ โ”œโ”€โ”€ positive/ # ์ƒˆ์šฐ ์žˆ๋Š” ์ด๋ฏธ์ง€
20
+ โ”‚ โ”œโ”€โ”€ clean/ # ๋ฐฐ๊ฒฝ ๊นจ๋— (10์žฅ)
21
+ โ”‚ โ”‚ โ”œโ”€โ”€ shrimp_001.jpg
22
+ โ”‚ โ”‚ โ””โ”€โ”€ ...
23
+ โ”‚ โ”œโ”€โ”€ with_ruler/ # ์ž ํฌํ•จ (10์žฅ)
24
+ โ”‚ โ”œโ”€โ”€ with_hand/ # ์† ํฌํ•จ (10์žฅ)
25
+ โ”‚ โ”œโ”€โ”€ complex_background/ # ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ (10์žฅ)
26
+ โ”‚ โ””โ”€โ”€ various_positions/ # ๋‹ค์–‘ํ•œ ์œ„์น˜ (10์žฅ)
27
+ โ”‚
28
+ โ”œโ”€โ”€ negative/ # ์ƒˆ์šฐ ์—†๋Š” ์ด๋ฏธ์ง€ (10์žฅ)
29
+ โ”‚ โ”œโ”€โ”€ only_ruler.jpg
30
+ โ”‚ โ”œโ”€โ”€ only_hand.jpg
31
+ โ”‚ โ””โ”€โ”€ ...
32
+ โ”‚
33
+ โ””โ”€โ”€ ground_truth/ # ์ •๋‹ต ๋ผ๋ฒจ
34
+ โ”œโ”€โ”€ annotations.json # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒํ‘œ
35
+ โ””โ”€โ”€ metadata.csv # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
36
+ ```
37
+
38
+ #### annotations.json ํ˜•์‹
39
+ ```json
40
+ {
41
+ "images": [
42
+ {
43
+ "file_name": "shrimp_001.jpg",
44
+ "width": 1920,
45
+ "height": 1080,
46
+ "annotations": [
47
+ {
48
+ "bbox": [100, 200, 500, 280],
49
+ "category": "shrimp",
50
+ "difficult": false
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ## ๐Ÿ”ฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰
61
+
62
+ ### ๋ฐฉ๋ฒ• 1: ์‹œ๊ฐ์  ๊ฒ€์ˆ˜์šฉ ํ…Œ์ŠคํŠธ (์ถ”์ฒœ)
63
+
64
+ **๋ชฉ์ **: ๊ฐ ํ•„ํ„ฐ ๋‹จ๊ณ„๋ณ„๋กœ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธ
65
+
66
+ ```python
67
+ # test_visual_validation.py
68
+ """
69
+ ์‹œ๊ฐ์  ๊ฒ€์ˆ˜์šฉ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ
70
+ ๊ฐ ํ•„ํ„ฐ ๋‹จ๊ณ„๋ณ„๋กœ ์ค‘๊ฐ„ ๊ฒฐ๊ณผ ์ €์žฅ
71
+ """
72
+
73
+ import cv2
74
+ import os
75
+ import json
76
+ from pathlib import Path
77
+ from universal_shrimp_filter import UniversalShrimpFilter
78
+
79
+ def run_visual_test(test_image_dir, output_dir):
80
+ """
81
+ ์‹œ๊ฐ์  ๊ฒ€์ˆ˜์šฉ ํ…Œ์ŠคํŠธ
82
+
83
+ Args:
84
+ test_image_dir: ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ํด๋”
85
+ output_dir: ๊ฒฐ๊ณผ ์ €์žฅ ํด๋”
86
+ """
87
+
88
+ # ์ถœ๋ ฅ ํด๋” ์ƒ์„ฑ
89
+ os.makedirs(output_dir, exist_ok=True)
90
+
91
+ # ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”
92
+ shrimp_filter = UniversalShrimpFilter()
93
+
94
+ # ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
95
+ test_images = list(Path(test_image_dir).glob("*.jpg")) + \
96
+ list(Path(test_image_dir).glob("*.png"))
97
+
98
+ results = []
99
+
100
+ for img_path in test_images:
101
+ print(f"\n{'='*60}")
102
+ print(f"Testing: {img_path.name}")
103
+ print(f"{'='*60}")
104
+
105
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ
106
+ image = cv2.imread(str(img_path))
107
+ if image is None:
108
+ print(f"โš ๏ธ Failed to load image: {img_path}")
109
+ continue
110
+
111
+ # RT-DETR ๊ฒ€์ถœ (์‹ค์ œ๋กœ๋Š” ๋ชจ๋ธ ์‹คํ–‰, ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ)
112
+ rtdetr_detections = run_rtdetr(image) # ๋ณ„๋„ ํ•จ์ˆ˜
113
+
114
+ print(f"RT-DETR detected: {len(rtdetr_detections)} objects")
115
+
116
+ # ํ•„ํ„ฐ๋ง ์ ์šฉ (๋‹จ๊ณ„๋ณ„ ์ €์žฅ)
117
+ filtered_detections = shrimp_filter.filter_with_stages(
118
+ image, rtdetr_detections, save_stages=True
119
+ )
120
+
121
+ # ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
122
+ result_image = visualize_detections(image, filtered_detections)
123
+
124
+ # ๊ฒฐ๊ณผ ์ €์žฅ
125
+ output_path = os.path.join(output_dir, f"result_{img_path.name}")
126
+ cv2.imwrite(output_path, result_image)
127
+
128
+ # ๊ฒฐ๊ณผ ๊ธฐ๋ก
129
+ results.append({
130
+ 'image': img_path.name,
131
+ 'rtdetr_count': len(rtdetr_detections),
132
+ 'filtered_count': len(filtered_detections),
133
+ 'detections': filtered_detections
134
+ })
135
+
136
+ print(f"โœ… Final detections: {len(filtered_detections)}")
137
+ for i, det in enumerate(filtered_detections, 1):
138
+ print(f" #{i}: score={det.get('total_score', 0):.1f}/100, "
139
+ f"confidence={det['confidence']:.2f}")
140
+
141
+ # ๊ฒฐ๊ณผ ์š”์•ฝ ์ €์žฅ
142
+ with open(os.path.join(output_dir, 'test_results.json'), 'w') as f:
143
+ json.dump(results, f, indent=2)
144
+
145
+ print(f"\n{'='*60}")
146
+ print(f"โœ… Test completed! Results saved to: {output_dir}")
147
+ print(f"{'='*60}")
148
+
149
+ return results
150
+
151
+
152
+ def visualize_detections(image, detections):
153
+ """
154
+ ๊ฒ€์ถœ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
155
+
156
+ Args:
157
+ image: ์›๋ณธ ์ด๋ฏธ์ง€
158
+ detections: ๊ฒ€์ถœ ๊ฒฐ๊ณผ
159
+
160
+ Returns:
161
+ ์‹œ๊ฐํ™”๋œ ์ด๋ฏธ์ง€
162
+ """
163
+ import cv2
164
+ import numpy as np
165
+
166
+ result = image.copy()
167
+
168
+ for i, det in enumerate(detections, 1):
169
+ bbox = det['bbox']
170
+ x1, y1, x2, y2 = map(int, bbox)
171
+
172
+ # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค
173
+ color = (0, 255, 0) # ๋…น์ƒ‰
174
+ cv2.rectangle(result, (x1, y1), (x2, y2), color, 3)
175
+
176
+ # ๋ผ๋ฒจ
177
+ score = det.get('total_score', 0)
178
+ confidence = det['confidence']
179
+ label = f"#{i} Score:{score:.1f} Conf:{confidence:.2f}"
180
+
181
+ # ๋ผ๋ฒจ ๋ฐฐ๊ฒฝ
182
+ (label_w, label_h), _ = cv2.getTextSize(
183
+ label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2
184
+ )
185
+ cv2.rectangle(result, (x1, y1 - label_h - 10),
186
+ (x1 + label_w, y1), color, -1)
187
+
188
+ # ๋ผ๋ฒจ ํ…์ŠคํŠธ
189
+ cv2.putText(result, label, (x1, y1 - 5),
190
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
191
+
192
+ # ์ „์ฒด ์š”์•ฝ (์ƒ๋‹จ)
193
+ summary = f"Detections: {len(detections)}"
194
+ cv2.putText(result, summary, (10, 30),
195
+ cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 3)
196
+
197
+ return result
198
+
199
+
200
+ def run_rtdetr(image):
201
+ """
202
+ RT-DETR ๊ฒ€์ถœ ์‹คํ–‰
203
+
204
+ Args:
205
+ image: ์ž…๋ ฅ ์ด๋ฏธ์ง€
206
+
207
+ Returns:
208
+ ๊ฒ€์ถœ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ
209
+ """
210
+ from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
211
+ import torch
212
+ from PIL import Image
213
+
214
+ # ๋ชจ๋ธ ๋กœ๋“œ (์บ์‹œ)
215
+ if not hasattr(run_rtdetr, 'model'):
216
+ processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
217
+ model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
218
+ model.eval()
219
+ run_rtdetr.processor = processor
220
+ run_rtdetr.model = model
221
+
222
+ processor = run_rtdetr.processor
223
+ model = run_rtdetr.model
224
+
225
+ # ์ด๋ฏธ์ง€ ๋ณ€ํ™˜
226
+ pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
227
+
228
+ # ์ถ”๋ก 
229
+ inputs = processor(images=pil_image, return_tensors="pt")
230
+ with torch.no_grad():
231
+ outputs = model(**inputs)
232
+
233
+ # ํ›„์ฒ˜๋ฆฌ
234
+ target_sizes = torch.tensor([pil_image.size[::-1]])
235
+ results = processor.post_process_object_detection(
236
+ outputs,
237
+ target_sizes=target_sizes,
238
+ threshold=0.3
239
+ )[0]
240
+
241
+ # ๊ฒฐ๊ณผ ๋ณ€ํ™˜
242
+ detections = []
243
+ for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
244
+ detections.append({
245
+ 'bbox': box.tolist(),
246
+ 'confidence': score.item(),
247
+ 'class_id': label.item()
248
+ })
249
+
250
+ return detections
251
+
252
+
253
+ # ์‹คํ–‰
254
+ if __name__ == "__main__":
255
+ run_visual_test(
256
+ test_image_dir="test_dataset/positive/clean",
257
+ output_dir="test_results/visual"
258
+ )
259
+ ```
260
+
261
+ ---
262
+
263
+ ### ๋ฐฉ๋ฒ• 2: ์ •๋Ÿ‰์  ํ‰๊ฐ€์šฉ ํ…Œ์ŠคํŠธ
264
+
265
+ **๋ชฉ์ **: Precision, Recall, F1 ๊ณ„์‚ฐ
266
+
267
+ ```python
268
+ # test_quantitative_evaluation.py
269
+ """
270
+ ์ •๋Ÿ‰์  ํ‰๊ฐ€์šฉ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ
271
+ Ground Truth์™€ ๋น„๊ตํ•˜์—ฌ ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ
272
+ """
273
+
274
+ import json
275
+ import numpy as np
276
+ from pathlib import Path
277
+ import cv2
278
+
279
+ def calculate_iou(bbox1, bbox2):
280
+ """IoU ๊ณ„์‚ฐ"""
281
+ x1_min, y1_min, x1_max, y1_max = bbox1
282
+ x2_min, y2_min, x2_max, y2_max = bbox2
283
+
284
+ # ๊ต์ง‘ํ•ฉ
285
+ inter_x_min = max(x1_min, x2_min)
286
+ inter_y_min = max(y1_min, y2_min)
287
+ inter_x_max = min(x1_max, x2_max)
288
+ inter_y_max = min(y1_max, y2_max)
289
+
290
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
291
+ return 0.0
292
+
293
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
294
+
295
+ # ํ•ฉ์ง‘ํ•ฉ
296
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
297
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
298
+ union_area = area1 + area2 - inter_area
299
+
300
+ return inter_area / union_area if union_area > 0 else 0.0
301
+
302
+
303
+ def evaluate_detection(predictions, ground_truths, iou_threshold=0.5):
304
+ """
305
+ ๊ฒ€์ถœ ์„ฑ๋Šฅ ํ‰๊ฐ€
306
+
307
+ Args:
308
+ predictions: ์˜ˆ์ธก ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ฆฌ์ŠคํŠธ
309
+ ground_truths: ์ •๋‹ต ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ฆฌ์ŠคํŠธ
310
+ iou_threshold: IoU ์ž„๊ณ„๊ฐ’ (๊ธฐ๋ณธ 0.5)
311
+
312
+ Returns:
313
+ dict: TP, FP, FN, Precision, Recall, F1
314
+ """
315
+
316
+ TP = 0 # True Positive
317
+ FP = 0 # False Positive
318
+ FN = 0 # False Negative
319
+
320
+ matched_gts = set()
321
+
322
+ # ๊ฐ ์˜ˆ์ธก์— ๋Œ€ํ•ด
323
+ for pred in predictions:
324
+ pred_bbox = pred['bbox']
325
+
326
+ # Ground Truth์™€ ๋งค์นญ
327
+ max_iou = 0
328
+ max_iou_gt_idx = -1
329
+
330
+ for gt_idx, gt in enumerate(ground_truths):
331
+ if gt_idx in matched_gts:
332
+ continue
333
+
334
+ gt_bbox = gt['bbox']
335
+ iou = calculate_iou(pred_bbox, gt_bbox)
336
+
337
+ if iou > max_iou:
338
+ max_iou = iou
339
+ max_iou_gt_idx = gt_idx
340
+
341
+ # IoU ์ž„๊ณ„๊ฐ’ ์ด์ƒ์ด๋ฉด ๋งค์นญ ์„ฑ๊ณต
342
+ if max_iou >= iou_threshold:
343
+ TP += 1
344
+ matched_gts.add(max_iou_gt_idx)
345
+ else:
346
+ FP += 1 # ์ž˜๋ชป๋œ ๊ฒ€์ถœ
347
+
348
+ # ๋งค์นญ ์•ˆ ๋œ GT = ๋†“์นœ ๊ฒƒ
349
+ FN = len(ground_truths) - len(matched_gts)
350
+
351
+ # ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ
352
+ precision = TP / (TP + FP) if (TP + FP) > 0 else 0
353
+ recall = TP / (TP + FN) if (TP + FN) > 0 else 0
354
+ f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
355
+
356
+ return {
357
+ 'TP': TP,
358
+ 'FP': FP,
359
+ 'FN': FN,
360
+ 'precision': precision,
361
+ 'recall': recall,
362
+ 'f1_score': f1_score
363
+ }
364
+
365
+
366
+ def run_quantitative_test(test_image_dir, ground_truth_file, output_file):
367
+ """
368
+ ์ •๋Ÿ‰์  ํ‰๊ฐ€ ์‹คํ–‰
369
+
370
+ Args:
371
+ test_image_dir: ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ํด๋”
372
+ ground_truth_file: Ground Truth JSON ํŒŒ์ผ
373
+ output_file: ๊ฒฐ๊ณผ ์ €์žฅ ํŒŒ์ผ
374
+ """
375
+
376
+ # Ground Truth ๋กœ๋“œ
377
+ with open(ground_truth_file, 'r') as f:
378
+ ground_truth_data = json.load(f)
379
+
380
+ # ์ด๋ฏธ์ง€๋ณ„ GT ๋งคํ•‘
381
+ gt_map = {}
382
+ for img_data in ground_truth_data['images']:
383
+ gt_map[img_data['file_name']] = img_data['annotations']
384
+
385
+ # ํ•„ํ„ฐ ์ดˆ๊ธฐํ™”
386
+ from universal_shrimp_filter import UniversalShrimpFilter
387
+ shrimp_filter = UniversalShrimpFilter()
388
+
389
+ # ์ „์ฒด ๊ฒฐ๊ณผ
390
+ all_results = []
391
+ total_metrics = {
392
+ 'TP': 0,
393
+ 'FP': 0,
394
+ 'FN': 0
395
+ }
396
+
397
+ # ๊ฐ ์ด๋ฏธ์ง€ ํ…Œ์ŠคํŠธ
398
+ for img_name, gt_annotations in gt_map.items():
399
+ img_path = Path(test_image_dir) / img_name
400
+ if not img_path.exists():
401
+ print(f"โš ๏ธ Image not found: {img_path}")
402
+ continue
403
+
404
+ print(f"\nTesting: {img_name}")
405
+
406
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ
407
+ image = cv2.imread(str(img_path))
408
+
409
+ # RT-DETR ๊ฒ€์ถœ
410
+ rtdetr_detections = run_rtdetr(image)
411
+
412
+ # ํ•„ํ„ฐ๋ง
413
+ predictions = shrimp_filter.filter(image, rtdetr_detections)
414
+
415
+ # Ground Truth
416
+ ground_truths = gt_annotations
417
+
418
+ # ํ‰๊ฐ€
419
+ metrics = evaluate_detection(predictions, ground_truths, iou_threshold=0.5)
420
+
421
+ # ๊ฒฐ๊ณผ ์ €์žฅ
422
+ result = {
423
+ 'image': img_name,
424
+ 'num_predictions': len(predictions),
425
+ 'num_ground_truths': len(ground_truths),
426
+ 'metrics': metrics
427
+ }
428
+ all_results.append(result)
429
+
430
+ # ์ „์ฒด ์ง‘๊ณ„
431
+ total_metrics['TP'] += metrics['TP']
432
+ total_metrics['FP'] += metrics['FP']
433
+ total_metrics['FN'] += metrics['FN']
434
+
435
+ print(f" GT: {len(ground_truths)}, Pred: {len(predictions)}")
436
+ print(f" TP={metrics['TP']}, FP={metrics['FP']}, FN={metrics['FN']}")
437
+ print(f" Precision={metrics['precision']:.2%}, Recall={metrics['recall']:.2%}, F1={metrics['f1_score']:.2%}")
438
+
439
+ # ์ „์ฒด ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ
440
+ total_precision = total_metrics['TP'] / (total_metrics['TP'] + total_metrics['FP']) \
441
+ if (total_metrics['TP'] + total_metrics['FP']) > 0 else 0
442
+ total_recall = total_metrics['TP'] / (total_metrics['TP'] + total_metrics['FN']) \
443
+ if (total_metrics['TP'] + total_metrics['FN']) > 0 else 0
444
+ total_f1 = 2 * (total_precision * total_recall) / (total_precision + total_recall) \
445
+ if (total_precision + total_recall) > 0 else 0
446
+
447
+ # ์š”์•ฝ
448
+ summary = {
449
+ 'total_images': len(all_results),
450
+ 'total_metrics': {
451
+ 'TP': total_metrics['TP'],
452
+ 'FP': total_metrics['FP'],
453
+ 'FN': total_metrics['FN'],
454
+ 'precision': total_precision,
455
+ 'recall': total_recall,
456
+ 'f1_score': total_f1
457
+ },
458
+ 'per_image_results': all_results
459
+ }
460
+
461
+ # ๊ฒฐ๊ณผ ์ €์žฅ
462
+ with open(output_file, 'w') as f:
463
+ json.dump(summary, f, indent=2)
464
+
465
+ # ์ถœ๋ ฅ
466
+ print(f"\n{'='*60}")
467
+ print("๐Ÿ“Š Overall Performance")
468
+ print(f"{'='*60}")
469
+ print(f"Total Images: {len(all_results)}")
470
+ print(f"TP: {total_metrics['TP']}, FP: {total_metrics['FP']}, FN: {total_metrics['FN']}")
471
+ print(f"Precision: {total_precision:.2%}")
472
+ print(f"Recall: {total_recall:.2%}")
473
+ print(f"F1 Score: {total_f1:.2%}")
474
+ print(f"{'='*60}")
475
+ print(f"Results saved to: {output_file}")
476
+
477
+ return summary
478
+
479
+
480
+ # ์‹คํ–‰
481
+ if __name__ == "__main__":
482
+ run_quantitative_test(
483
+ test_image_dir="test_dataset/positive/clean",
484
+ ground_truth_file="test_dataset/ground_truth/annotations.json",
485
+ output_file="test_results/quantitative_results.json"
486
+ )
487
+ ```
488
+
489
+ ---
490
+
491
+ ## ๐Ÿ“Š ๊ฒฐ๊ณผ ๊ฒ€์ˆ˜ ๋ฐฉ๋ฒ•
492
+
493
+ ### ๋ฐฉ๋ฒ• 1: ์‹œ๊ฐ์  ๊ฒ€์ˆ˜ (๋น ๋ฅธ ํ™•์ธ)
494
+
495
+ #### ์ ˆ์ฐจ
496
+ 1. **ํ…Œ์ŠคํŠธ ์‹คํ–‰**
497
+ ```bash
498
+ python test_visual_validation.py
499
+ ```
500
+
501
+ 2. **๊ฒฐ๊ณผ ์ด๋ฏธ์ง€ ํ™•์ธ**
502
+ ```
503
+ test_results/visual/
504
+ โ”œโ”€โ”€ result_shrimp_001.jpg โœ… ์ƒˆ์šฐ ๊ฒ€์ถœ๋จ (๋…น์ƒ‰ ๋ฐ•์Šค)
505
+ โ”œโ”€โ”€ result_shrimp_002.jpg โŒ ๊ฒ€์ถœ ์‹คํŒจ
506
+ โ””โ”€โ”€ ...
507
+ ```
508
+
509
+ 3. **์œก์•ˆ ๊ฒ€์ˆ˜**
510
+ - โœ… **์ •์ƒ**: ์ƒˆ์šฐ์—๋งŒ ๋ฐ•์Šค, ๋‹ค๋ฅธ ๊ฐ์ฒด ์ œ์™ธ
511
+ - โš ๏ธ **๊ณผ์†Œ ๊ฒ€์ถœ**: ์ƒˆ์šฐ ๋†“์นจ โ†’ Recall ๋‚ฎ์Œ
512
+ - โš ๏ธ **๊ณผ๋‹ค ๊ฒ€์ถœ**: ์ž/์† ๋“ฑ์—๋„ ๋ฐ•์Šค โ†’ Precision ๋‚ฎ์Œ
513
+
514
+ 4. **๊ฒ€์ˆ˜ ์‹œํŠธ ์ž‘์„ฑ**
515
+ ```csv
516
+ ์ด๋ฏธ์ง€๋ช…,GT๊ฐœ์ˆ˜,๊ฒ€์ถœ๊ฐœ์ˆ˜,TP,FP,FN,๋น„๊ณ 
517
+ shrimp_001.jpg,1,1,1,0,0,์ •์ƒ
518
+ shrimp_002.jpg,1,0,0,0,1,๊ฒ€์ถœ์‹คํŒจ
519
+ shrimp_003.jpg,1,2,1,1,0,์ž๋„๊ฒ€์ถœ๋จ
520
+ ```
521
+
522
+ ---
523
+
524
+ ### ๋ฐฉ๋ฒ• 2: ์ •๋Ÿ‰์  ๊ฒ€์ˆ˜ (์ •ํ™•ํ•œ ํ‰๊ฐ€)
525
+
526
+ #### ์ ˆ์ฐจ
527
+ 1. **Ground Truth ์ค€๋น„**
528
+ - LabelImg, CVAT ๋“ฑ์œผ๋กœ ์ˆ˜๋™ ๋ผ๋ฒจ๋ง
529
+ - JSON ํ˜•์‹์œผ๋กœ ์ €์žฅ
530
+
531
+ 2. **ํ…Œ์ŠคํŠธ ์‹คํ–‰**
532
+ ```bash
533
+ python test_quantitative_evaluation.py
534
+ ```
535
+
536
+ 3. **๊ฒฐ๊ณผ ๋ถ„์„**
537
+ ```json
538
+ {
539
+ "total_metrics": {
540
+ "TP": 42,
541
+ "FP": 8,
542
+ "FN": 6,
543
+ "precision": 0.84,
544
+ "recall": 0.88,
545
+ "f1_score": 0.86
546
+ }
547
+ }
548
+ ```
549
+
550
+ 4. **ํ•ด์„**
551
+ - **Precision 84%**: ๊ฒ€์ถœํ•œ ๊ฒƒ ์ค‘ 84%๊ฐ€ ์‹ค์ œ ์ƒˆ์šฐ
552
+ - 16%๋Š” ์˜ค๊ฒ€์ถœ (์ž, ์† ๋“ฑ)
553
+ - **Recall 88%**: ์ „์ฒด ์ƒˆ์šฐ ์ค‘ 88% ๊ฒ€์ถœ
554
+ - 12%๋Š” ๋†“์นจ
555
+ - **F1 86%**: ์ข…ํ•ฉ ์„ฑ๋Šฅ
556
+
557
+ ---
558
+
559
+ ### ๋ฐฉ๋ฒ• 3: ๋Œ€ํ™”ํ˜• ๊ฒ€์ˆ˜ (Gradio ํ™œ์šฉ)
560
+
561
+ ```python
562
+ # interactive_validation.py
563
+ """
564
+ ๋Œ€ํ™”ํ˜• ๊ฒ€์ˆ˜ ๋„๊ตฌ
565
+ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œํ•˜๊ณ  ๊ฒฐ๊ณผ ํ™•์ธ
566
+ """
567
+
568
+ import gradio as gr
569
+ import cv2
570
+ import numpy as np
571
+ from PIL import Image
572
+
573
+ def detect_and_validate(image, confidence_threshold):
574
+ """
575
+ ๊ฒ€์ถœ ๋ฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
576
+
577
+ Args:
578
+ image: PIL Image
579
+ confidence_threshold: ์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’
580
+
581
+ Returns:
582
+ (๊ฒฐ๊ณผ ์ด๋ฏธ์ง€, ์ƒ์„ธ ์ •๋ณด)
583
+ """
584
+ # numpy๋กœ ๋ณ€ํ™˜
585
+ img_array = np.array(image)
586
+ img_bgr = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR)
587
+
588
+ # RT-DETR ๊ฒ€์ถœ
589
+ rtdetr_detections = run_rtdetr(img_bgr)
590
+
591
+ # ํ•„ํ„ฐ๋ง
592
+ from universal_shrimp_filter import UniversalShrimpFilter
593
+ shrimp_filter = UniversalShrimpFilter()
594
+ filtered_detections = shrimp_filter.filter(img_bgr, rtdetr_detections)
595
+
596
+ # ์‹œ๊ฐํ™”
597
+ result_img = visualize_detections(img_bgr, filtered_detections)
598
+ result_img_rgb = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)
599
+
600
+ # ์ƒ์„ธ ์ •๋ณด
601
+ info = f"""
602
+ ### ๐Ÿ“Š ๊ฒ€์ถœ ๊ฒฐ๊ณผ
603
+
604
+ - **RT-DETR ๊ฒ€์ถœ**: {len(rtdetr_detections)}๊ฐœ
605
+ - **ํ•„ํ„ฐ๋ง ํ›„**: {len(filtered_detections)}๊ฐœ (์ƒˆ์šฐ๋งŒ)
606
+
607
+ #### ์ƒ์„ธ ์ •๋ณด:
608
+ """
609
+
610
+ for i, det in enumerate(filtered_detections, 1):
611
+ score = det.get('total_score', 0)
612
+ conf = det['confidence']
613
+ bbox = det['bbox']
614
+
615
+ info += f"""
616
+ **์ƒˆ์šฐ #{i}**
617
+ - ์ข…ํ•ฉ ์ ์ˆ˜: {score:.1f}/100
618
+ - RT-DETR ์‹ ๋ขฐ๋„: {conf:.2%}
619
+ - ์œ„์น˜: ({bbox[0]:.0f}, {bbox[1]:.0f}) - ({bbox[2]:.0f}, {bbox[3]:.0f})
620
+ """
621
+
622
+ if len(filtered_detections) == 0:
623
+ info += "\nโš ๏ธ **์ƒˆ์šฐ๊ฐ€ ๊ฒ€์ถœ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.**"
624
+
625
+ return Image.fromarray(result_img_rgb), info
626
+
627
+
628
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค
629
+ with gr.Blocks(title="์ƒˆ์šฐ ๊ฒ€์ถœ ๊ฒ€์ˆ˜ ๋„๊ตฌ") as demo:
630
+ gr.Markdown("""
631
+ # ๐Ÿฆ ์ƒˆ์šฐ ๊ฒ€์ถœ ๊ฒ€์ˆ˜ ๋„๊ตฌ
632
+
633
+ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ƒˆ์šฐ๋ฅผ ๊ฒ€์ถœํ•ฉ๋‹ˆ๋‹ค.
634
+ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ฒ€์ถœ ํ’ˆ์งˆ์„ ํ‰๊ฐ€ํ•˜์„ธ์š”.
635
+ """)
636
+
637
+ with gr.Row():
638
+ with gr.Column():
639
+ input_image = gr.Image(label="ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€", type="pil")
640
+ confidence_slider = gr.Slider(
641
+ 0.1, 0.9, 0.3,
642
+ label="RT-DETR ์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’"
643
+ )
644
+ detect_btn = gr.Button("๐Ÿš€ ๊ฒ€์ถœ ์‹คํ–‰", variant="primary")
645
+
646
+ with gr.Column():
647
+ output_image = gr.Image(label="๊ฒ€์ถœ ๊ฒฐ๊ณผ")
648
+ output_info = gr.Markdown()
649
+
650
+ # ์ด๋ฒคํŠธ
651
+ detect_btn.click(
652
+ detect_and_validate,
653
+ [input_image, confidence_slider],
654
+ [output_image, output_info]
655
+ )
656
+
657
+ gr.Markdown("""
658
+ ---
659
+ ### โœ… ๊ฒ€์ˆ˜ ๊ธฐ์ค€
660
+
661
+ **์ •์ƒ ๊ฒ€์ถœ**:
662
+ - ์ƒˆ์šฐ์—๋งŒ ๋…น์ƒ‰ ๋ฐ•์Šค
663
+ - ์ž, ์†, ๋ฐฐ๊ฒฝ ๋“ฑ์€ ์ œ์™ธ
664
+
665
+ **๋ฌธ์ œ ์žˆ์Œ**:
666
+ - ์ƒˆ์šฐ ๋†“์นจ โ†’ Recall ๋‚ฎ์Œ (ํŒŒ๋ผ๋ฏธํ„ฐ ์™„ํ™” ํ•„์š”)
667
+ - ์ž/์†๋„ ๊ฒ€์ถœ โ†’ Precision ๋‚ฎ์Œ (ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ•ํ™” ํ•„์š”)
668
+ """)
669
+
670
+
671
+ if __name__ == "__main__":
672
+ demo.launch(server_port=7862)
673
+ ```
674
+
675
+ ---
676
+
677
+ ## ๐Ÿ“ˆ ์„ฑ๋Šฅ ๋ถ„์„ ๋„๊ตฌ
678
+
679
+ ### ํ˜ผ๋™ ํ–‰๋ ฌ (Confusion Matrix) ์ƒ์„ฑ
680
+
681
+ ```python
682
+ def generate_confusion_matrix(test_results):
683
+ """
684
+ ํ˜ผ๋™ ํ–‰๋ ฌ ์ƒ์„ฑ ๋ฐ ์‹œ๊ฐํ™”
685
+
686
+ Args:
687
+ test_results: ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ
688
+ """
689
+ import matplotlib.pyplot as plt
690
+ import seaborn as sns
691
+
692
+ # ์ง‘๊ณ„
693
+ TP = sum(r['metrics']['TP'] for r in test_results)
694
+ FP = sum(r['metrics']['FP'] for r in test_results)
695
+ TN = 0 # ์ƒˆ์šฐ ์—†๋Š” ์ด๋ฏธ์ง€์—์„œ ๊ฒ€์ถœ ์•ˆ ํ•จ
696
+ FN = sum(r['metrics']['FN'] for r in test_results)
697
+
698
+ # ํ˜ผ๋™ ํ–‰๋ ฌ
699
+ cm = np.array([[TP, FP], [FN, TN]])
700
+
701
+ # ์‹œ๊ฐํ™”
702
+ plt.figure(figsize=(8, 6))
703
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
704
+ xticklabels=['Predicted Positive', 'Predicted Negative'],
705
+ yticklabels=['Actual Positive', 'Actual Negative'])
706
+ plt.title('Confusion Matrix')
707
+ plt.ylabel('Actual')
708
+ plt.xlabel('Predicted')
709
+ plt.tight_layout()
710
+ plt.savefig('confusion_matrix.png')
711
+ print("โœ… Confusion matrix saved to confusion_matrix.png")
712
+ ```
713
+
714
+ ### PR ๊ณก์„  (Precision-Recall Curve)
715
+
716
+ ```python
717
+ def generate_pr_curve(detections_by_threshold):
718
+ """
719
+ Precision-Recall ๊ณก์„  ์ƒ์„ฑ
720
+
721
+ Args:
722
+ detections_by_threshold: ์ž„๊ณ„๊ฐ’๋ณ„ ๊ฒ€์ถœ ๊ฒฐ๊ณผ
723
+ """
724
+ import matplotlib.pyplot as plt
725
+
726
+ thresholds = []
727
+ precisions = []
728
+ recalls = []
729
+
730
+ for threshold, results in sorted(detections_by_threshold.items()):
731
+ metrics = results['metrics']
732
+ thresholds.append(threshold)
733
+ precisions.append(metrics['precision'])
734
+ recalls.append(metrics['recall'])
735
+
736
+ # ๊ทธ๋ž˜ํ”„
737
+ plt.figure(figsize=(10, 6))
738
+ plt.plot(recalls, precisions, 'b-', linewidth=2)
739
+ plt.xlabel('Recall')
740
+ plt.ylabel('Precision')
741
+ plt.title('Precision-Recall Curve')
742
+ plt.grid(True)
743
+
744
+ # AP (Average Precision) ๊ณ„์‚ฐ
745
+ ap = np.trapz(precisions, recalls)
746
+ plt.text(0.1, 0.1, f'AP = {ap:.2%}', fontsize=12,
747
+ bbox=dict(boxstyle='round', facecolor='wheat'))
748
+
749
+ plt.tight_layout()
750
+ plt.savefig('pr_curve.png')
751
+ print(f"โœ… PR curve saved to pr_curve.png (AP = {ap:.2%})")
752
+ ```
753
+
754
+ ---
755
+
756
+ ## ๐ŸŽฏ ๊ฒ€์ˆ˜ ๊ฒฐ๊ณผ ๊ธฐ๋ฐ˜ ๊ฐœ์„ 
757
+
758
+ ### ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ๋Œ€์‘
759
+
760
+ #### 1. Recall ๋‚ฎ์Œ (์ƒˆ์šฐ ๋†“์นจ)
761
+
762
+ **์ฆ์ƒ**: ์‹ค์ œ ์ƒˆ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ ๊ฒ€์ถœ ์•ˆ ๋จ
763
+
764
+ **์›์ธ ๋ถ„์„**:
765
+ ```python
766
+ # ์–ด๋А ํ•„ํ„ฐ์—์„œ ํƒˆ๋ฝํ–ˆ๋Š”์ง€ ํ™•์ธ
767
+ def analyze_filter_stages(image, detections):
768
+ """๊ฐ ํ•„ํ„ฐ ๋‹จ๊ณ„๋ณ„ ํ†ต๊ณผ์œจ ๋ถ„์„"""
769
+
770
+ results = {}
771
+
772
+ # Step 1: ํ˜•ํƒœ ํ•„ํ„ฐ
773
+ after_aspect = filter_by_aspect_ratio(detections)
774
+ results['aspect_ratio'] = {
775
+ 'input': len(detections),
776
+ 'output': len(after_aspect),
777
+ 'pass_rate': len(after_aspect) / len(detections) if detections else 0
778
+ }
779
+
780
+ # Step 2: ์ƒ‰์ƒ ํ•„ํ„ฐ
781
+ after_color = filter_by_saturation(image, after_aspect)
782
+ results['saturation'] = {
783
+ 'input': len(after_aspect),
784
+ 'output': len(after_color),
785
+ 'pass_rate': len(after_color) / len(after_aspect) if after_aspect else 0
786
+ }
787
+
788
+ # ... ๊ฐ ํ•„ํ„ฐ๋ณ„๋กœ ๋ฐ˜๋ณต
789
+
790
+ return results
791
+ ```
792
+
793
+ **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**:
794
+ ```python
795
+ # ํ†ต๊ณผ์œจ์ด ๋‚ฎ์€ ํ•„ํ„ฐ์˜ ์ž„๊ณ„๊ฐ’ ์™„ํ™”
796
+ if results['aspect_ratio']['pass_rate'] < 0.5:
797
+ # ์ข…ํšก๋น„ ๋ฒ”์œ„ ํ™•๋Œ€
798
+ params['min_aspect_ratio'] = 1.5 # 2.0 โ†’ 1.5
799
+ params['max_aspect_ratio'] = 12.0 # 10.0 โ†’ 12.0
800
+ ```
801
+
802
+ ---
803
+
804
+ #### 2. Precision ๋‚ฎ์Œ (์˜ค๊ฒ€์ถœ)
805
+
806
+ **์ฆ์ƒ**: ์ž, ์† ๋“ฑ๋„ ์ƒˆ์šฐ๋กœ ๊ฒ€์ถœ
807
+
808
+ **์›์ธ ๋ถ„์„**:
809
+ ```python
810
+ # ์˜ค๊ฒ€์ถœ๋œ ๊ฐ์ฒด ๋ถ„์„
811
+ def analyze_false_positives(false_positive_detections, images):
812
+ """์˜ค๊ฒ€์ถœ ํŒจํ„ด ๋ถ„์„"""
813
+
814
+ # ์˜ค๊ฒ€์ถœ ๊ฐ์ฒด์˜ ํŠน์ง• ์ˆ˜์ง‘
815
+ fp_features = []
816
+ for det in false_positive_detections:
817
+ features = {
818
+ 'aspect_ratio': calculate_aspect_ratio(det['bbox']),
819
+ 'area_ratio': calculate_area_ratio(det['bbox'], image_shape),
820
+ 'color': extract_color_features(image, det['bbox']),
821
+ 'texture': extract_texture_features(image, det['bbox'])
822
+ }
823
+ fp_features.append(features)
824
+
825
+ # ๊ณตํ†ต ํŒจํ„ด ์ฐพ๊ธฐ
826
+ avg_aspect = np.mean([f['aspect_ratio'] for f in fp_features])
827
+ print(f"FP ํ‰๊ท  ์ข…ํšก๋น„: {avg_aspect:.2f}")
828
+
829
+ return fp_features
830
+ ```
831
+
832
+ **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**:
833
+ ```python
834
+ # ์ž„๊ณ„๊ฐ’ ๊ฐ•ํ™”
835
+ if avg_fp_aspect > 15:
836
+ # ์ž(ruler) ๊ฐ™์€ ๋งค์šฐ ๊ธด ๊ฐ์ฒด
837
+ params['max_aspect_ratio'] = 10.0 # ์ค„์ด๊ธฐ
838
+
839
+ if avg_fp_saturation > 100:
840
+ # ์ƒ‰์ƒ์ด ๊ฐ•ํ•œ ๊ฐ์ฒด (์ƒˆ์šฐ๋Š” ๋‚ฎ์€ ์ฑ„๋„)
841
+ params['max_saturation'] = 100 # 120 โ†’ 100
842
+ ```
843
+
844
+ ---
845
+
846
+ #### 3. F1 ๋‚ฎ์Œ (์ „๋ฐ˜์  ์„ฑ๋Šฅ ๋ถ€์กฑ)
847
+
848
+ **์ฆ์ƒ**: Precision, Recall ๋ชจ๋‘ ๋‚ฎ์Œ
849
+
850
+ **ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•**:
851
+ 1. **RT-DETR ์ž„๊ณ„๊ฐ’ ์กฐ์ •**
852
+ ```python
853
+ # ๋” ๋งŽ์€ ํ›„๋ณด ๊ฒ€์ถœ
854
+ rtdetr_confidence = 0.2 # 0.3 โ†’ 0.2
855
+ ```
856
+
857
+ 2. **ํ•„ํ„ฐ ์ˆœ์„œ ์ตœ์ ํ™”**
858
+ ```python
859
+ # ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ํ•„ํ„ฐ๋ฅผ ๋จผ์ €
860
+ # (๋น ๋ฅด๊ฒŒ ํ™•์‹คํ•œ ๊ฒƒ๋ถ€ํ„ฐ ์ œ๊ฑฐ)
861
+ ```
862
+
863
+ 3. **๊ทผ๋ณธ์  ํ•ด๊ฒฐ**: Roboflow ํŒŒ์ธํŠœ๋‹
864
+ ```
865
+ โ†’ docs/measurement_shrimp_detection_strategy.md ์ฐธ๊ณ 
866
+ ```
867
+
868
+ ---
869
+
870
+ ## ๐Ÿ“‹ ๊ฒ€์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
871
+
872
+ ### ํ…Œ์ŠคํŠธ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
873
+
874
+ - [ ] ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์…‹ ์ค€๋น„ (์ตœ์†Œ 50์žฅ)
875
+ - [ ] Ground Truth ๋ผ๋ฒจ๋ง ์™„๋ฃŒ
876
+ - [ ] ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌํ•จ (๋ฐฐ๊ฒฝ, ์œ„์น˜, ํฌ๊ธฐ)
877
+ - [ ] Negative ์ƒ˜ํ”Œ ํฌํ•จ (์ƒˆ์šฐ ์—†๋Š” ์ด๋ฏธ์ง€)
878
+
879
+ ### ๊ฒ€์ˆ˜ ์ค‘ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
880
+
881
+ - [ ] ์‹œ๊ฐ์  ๊ฒ€์ˆ˜ ์™„๋ฃŒ (๋ชจ๋“  ์ด๋ฏธ์ง€ ์œก์•ˆ ํ™•์ธ)
882
+ - [ ] ์ •๋Ÿ‰์  ํ‰๊ฐ€ ์™„๋ฃŒ (Precision, Recall, F1 ๊ณ„์‚ฐ)
883
+ - [ ] ์‹คํŒจ ์‚ฌ๋ก€ ๋ถ„์„ ์™„๋ฃŒ
884
+ - [ ] ํ•„ํ„ฐ๋ณ„ ํ†ต๊ณผ์œจ ๋ถ„์„ ์™„๋ฃŒ
885
+
886
+ ### ๊ฒ€์ˆ˜ ํ›„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
887
+
888
+ - [ ] ๋ชฉํ‘œ ์„ฑ๋Šฅ ๋‹ฌ์„ฑ ์—ฌ๋ถ€ ํ™•์ธ (F1 > 70%)
889
+ - [ ] ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ํ•„์š” ์—ฌ๋ถ€ ํŒ๋‹จ
890
+ - [ ] ๋ฌธ์„œํ™” (์‹คํŒจ ์‚ฌ๋ก€, ๊ฐœ์„  ๋ฐฉํ–ฅ)
891
+ - [ ] ๋‹ค์Œ iteration ๊ณ„ํš ์ˆ˜๋ฆฝ
892
+
893
+ ---
894
+
895
+ ## ๐ŸŽ“ ์š”์•ฝ
896
+
897
+ ### ๋น ๋ฅธ ๊ฒ€์ˆ˜ (1์‹œ๊ฐ„)
898
+ ```bash
899
+ # 1. ์‹œ๊ฐ์  ํ…Œ์ŠคํŠธ
900
+ python test_visual_validation.py
901
+
902
+ # 2. ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€ ํ™•์ธ
903
+ open test_results/visual/
904
+
905
+ # 3. ์œก์•ˆ ๊ฒ€์ˆ˜ โ†’ ๋ฌธ์ œ ํŒŒ์•…
906
+ ```
907
+
908
+ ### ์ •ํ™•ํ•œ ๊ฒ€์ˆ˜ (๋ฐ˜๋‚˜์ ˆ)
909
+ ```bash
910
+ # 1. Ground Truth ์ค€๋น„
911
+ # 2. ์ •๋Ÿ‰์  ํ…Œ์ŠคํŠธ
912
+ python test_quantitative_evaluation.py
913
+
914
+ # 3. ๋ฉ”ํŠธ๋ฆญ ๋ถ„์„
915
+ # 4. ํ•„ํ„ฐ๋ณ„ ๋ถ„์„ โ†’ ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹
916
+ ```
917
+
918
+ ### ๋Œ€ํ™”ํ˜• ๊ฒ€์ˆ˜ (์‹ค์‹œ๊ฐ„)
919
+ ```bash
920
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค ์‹คํ–‰
921
+ python interactive_validation.py
922
+
923
+ # ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹ค์‹œ๊ฐ„ ํ…Œ์ŠคํŠธ
924
+ # โ†’ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ
925
+ ```
926
+
927
+ ---
928
+
929
+ **์ž‘์„ฑ์ผ**: 2025-11-07
930
+ **๋ฒ„์ „**: 1.0
931
+ **์ž‘์„ฑ์ž**: VIDraft Team
docs/measurement_shrimp_detection_strategy.md ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ๐Ÿ“ ์ธก์ •์šฉ ์ƒˆ์šฐ ๊ฒ€์ถœ ์ „๋žต
2
+
3
+ ## ๐ŸŽฏ ๋ชฉํ‘œ
4
+ ์ธก์ • ๋งคํŠธ ์œ„์˜ ์ฃฝ์€ ์ƒˆ์šฐ๋งŒ ์ •ํ™•ํ•˜๊ฒŒ ๊ฒ€์ถœํ•˜๊ณ , ์ž(ruler), ์†, ๋ฐฐ๊ฒฝ ๊ฐ์ฒด๋Š” ์ œ์™ธ
5
+
6
+ ---
7
+
8
+ ## ๐Ÿ“Š ํ˜„์žฌ ์ƒํ™ฉ
9
+
10
+ ### ์ด๋ฏธ์ง€ ํŠน์ง•
11
+ - **ํ™˜๊ฒฝ**: ํŒŒ๋ž€์ƒ‰ ์ธก์ • ๋งคํŠธ
12
+ - **๊ฐ์ฒด**: ์ฃฝ์€ ์ƒˆ์šฐ (ํˆฌ๋ช…/ํฐ์ƒ‰)
13
+ - **๋ฐฐ๊ฒฝ**: ์ธก์ • ์ž, ์‚ฌ๋žŒ ์†/์‹ ๋ฐœ, ๊ธฐํƒ€ ๋ฌผ์ฒด
14
+ - **์šฉ๋„**: ์ฒด์žฅ/์ฒด์ค‘ ์ธก์ •์„ ์œ„ํ•œ ์‹ค์ธก ์ด๋ฏธ์ง€
15
+
16
+ ### ํ˜„์žฌ ๋ชจ๋ธ ์„ฑ๋Šฅ
17
+
18
+ | ๋ชจ๋ธ | ๊ฒ€์ถœ ๊ฒฐ๊ณผ | ๋ฌธ์ œ์  |
19
+ |------|----------|--------|
20
+ | **VIDraft/Shrimp (Roboflow)** | โŒ ๊ฒ€์ถœ ์‹คํŒจ | ์ˆ˜์กฐ ํ™˜๊ฒฝ์˜ ์‚ด์•„์žˆ๋Š” ์ƒˆ์šฐ๋กœ๋งŒ ํ•™์Šต๋จ |
21
+ | **RT-DETR (๋ฒ”์šฉ)** | โš ๏ธ ๊ณผ๋‹ค ๊ฒ€์ถœ | ์ƒˆ์šฐ + ์ž + ์† + ๋งคํŠธ ๋“ฑ ๋ชจ๋‘ ๊ฒ€์ถœ |
22
+
23
+ ---
24
+
25
+ ## ๐Ÿ’ก ์Šค๋งˆํŠธํ•œ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ
26
+
27
+ ### ๋ฐฉ์•ˆ 1: Roboflow ๋ชจ๋ธ ํŒŒ์ธํŠœ๋‹ โญ (์ถ”์ฒœ)
28
+
29
+ **๊ฐœ์š”**: ๊ธฐ์กด VIDraft/Shrimp ๋ชจ๋ธ์— ์ธก์ • ํ™˜๊ฒฝ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ํ•™์Šต
30
+
31
+ **์žฅ์ :**
32
+ - โœ… ๊ธฐ์กด ์ˆ˜์กฐ ๊ฒ€์ถœ ์„ฑ๋Šฅ ์œ ์ง€
33
+ - โœ… ์ธก์ • ํ™˜๊ฒฝ ๊ฒ€์ถœ ๋Šฅ๋ ฅ ์ถ”๊ฐ€
34
+ - โœ… ๋‹จ์ผ ๋ชจ๋ธ๋กœ ํ†ตํ•ฉ ๊ด€๋ฆฌ
35
+ - โœ… Roboflow ํ”Œ๋žซํผ์—์„œ ์‰ฝ๊ฒŒ ํ•™์Šต ๊ฐ€๋Šฅ
36
+
37
+ **๋‹จ์ :**
38
+ - โš ๏ธ ์ธก์ • ํ™˜๊ฒฝ ์ด๋ฏธ์ง€ ๋ผ๋ฒจ๋ง ํ•„์š” (์ตœ์†Œ 50-100์žฅ)
39
+ - โš ๏ธ ์žฌํ•™์Šต ์‹œ๊ฐ„ ์†Œ์š”
40
+
41
+ **๊ตฌํ˜„ ์ ˆ์ฐจ:**
42
+ 1. **๋ฐ์ดํ„ฐ ์ˆ˜์ง‘** (50-100์žฅ)
43
+ - ์ธก์ • ๋งคํŠธ ์œ„ ์ฃฝ์€ ์ƒˆ์šฐ ์ด๋ฏธ์ง€
44
+ - ๋‹ค์–‘ํ•œ ํฌ๊ธฐ, ๊ฐ๋„, ์กฐ๋ช… ์กฐ๊ฑด
45
+
46
+ 2. **Roboflow์—์„œ ๋ผ๋ฒจ๋ง**
47
+ - https://app.roboflow.com
48
+ - ์ƒˆ์šฐ ์˜์—ญ๋งŒ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ํ‘œ์‹œ
49
+ - ์ž, ์†, ๋ฐฐ๊ฒฝ์€ ์ œ์™ธ
50
+
51
+ 3. **๋ชจ๋ธ ์žฌํ•™์Šต**
52
+ - ๊ธฐ์กด ์ˆ˜์กฐ ๋ฐ์ดํ„ฐ + ์ƒˆ๋กœ์šด ์ธก์ • ๋ฐ์ดํ„ฐ
53
+ - Transfer Learning์œผ๋กœ ๋น ๋ฅธ ํ•™์Šต
54
+ - Validation์œผ๋กœ ์„ฑ๋Šฅ ๊ฒ€์ฆ
55
+
56
+ 4. **๋ฐฐํฌ**
57
+ - ๋™์ผํ•œ API ํ‚ค๋กœ ์—…๋ฐ์ดํŠธ๋œ ๋ชจ๋ธ ์‚ฌ์šฉ
58
+ - ์ฝ”๋“œ ๋ณ€๊ฒฝ ์—†์ด ์ž๋™ ์ ์šฉ
59
+
60
+ **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 2-3์ผ
61
+ **์˜ˆ์ƒ ์ •ํ™•๋„**: 90%+ (๊ธฐ์กด ์ˆ˜์กฐ: 90%, ์ธก์ •: 85%+)
62
+
63
+ ---
64
+
65
+ ### ๋ฐฉ์•ˆ 2: SAM (Segment Anything Model) + ํ›„์ฒ˜๋ฆฌ ํ•„ํ„ฐ
66
+
67
+ **๊ฐœ์š”**: SAM์œผ๋กœ ๋ชจ๋“  ๊ฐ์ฒด ์„ธ๊ทธ๋จผํŠธ ํ›„, ์ƒˆ์šฐ ํŠน์ง•์œผ๋กœ ํ•„ํ„ฐ๋ง
68
+
69
+ **์žฅ์ :**
70
+ - โœ… ์ถ”๊ฐ€ ํ•™์Šต ๋ถˆํ•„์š”
71
+ - โœ… Zero-shot ๊ฒ€์ถœ ๊ฐ€๋Šฅ
72
+ - โœ… ์ •๋ฐ€ํ•œ ์„ธ๊ทธ๋จผํ…Œ์ด์…˜
73
+
74
+ **๋‹จ์ :**
75
+ - โš ๏ธ ๋А๋ฆฐ ์ถ”๋ก  ์†๋„ (CPU: 10-30์ดˆ/์ด๋ฏธ์ง€)
76
+ - โš ๏ธ ์ƒˆ์šฐ ํŠน์ง• ์ •์˜ ์–ด๋ ค์›€ (์ƒ‰์ƒ, ํฌ๊ธฐ, ํ˜•ํƒœ ๋“ฑ)
77
+ - โš ๏ธ ๋ณต์žกํ•œ ํ›„์ฒ˜๋ฆฌ ๋กœ์ง ํ•„์š”
78
+
79
+ **๊ตฌํ˜„ ์˜ˆ์‹œ:**
80
+ ```python
81
+ from segment_anything import SamPredictor, sam_model_registry
82
+
83
+ # SAM ๋ชจ๋ธ ๋กœ๋“œ
84
+ sam = sam_model_registry["vit_h"](checkpoint="sam_vit_h.pth")
85
+ predictor = SamPredictor(sam)
86
+
87
+ # ์ž๋™ ์„ธ๊ทธ๋จผํŠธ
88
+ masks = predictor.generate(image)
89
+
90
+ # ์ƒˆ์šฐ ํ•„ํ„ฐ๋ง (ํœด๋ฆฌ์Šคํ‹ฑ)
91
+ for mask in masks:
92
+ # 1. ํฌ๊ธฐ ํ•„ํ„ฐ: ๋„ˆ๋ฌด ํฌ๊ฑฐ๋‚˜ ์ž‘์œผ๋ฉด ์ œ์™ธ
93
+ area = mask.sum()
94
+ if area < 5000 or area > 50000:
95
+ continue
96
+
97
+ # 2. ์œ„์น˜ ํ•„ํ„ฐ: ๋งคํŠธ ์ค‘์•™๋ถ€๋งŒ
98
+ bbox = get_bbox(mask)
99
+ if not is_in_center_region(bbox):
100
+ continue
101
+
102
+ # 3. ํ˜•ํƒœ ํ•„ํ„ฐ: ๊ฐ€๋กœ๋กœ ๊ธด ํ˜•ํƒœ
103
+ aspect_ratio = bbox_width / bbox_height
104
+ if aspect_ratio < 2.0 or aspect_ratio > 8.0:
105
+ continue
106
+
107
+ # ์ƒˆ์šฐ๋กœ ํŒ์ •
108
+ shrimp_masks.append(mask)
109
+ ```
110
+
111
+ **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 3-5์ผ (๋กœ์ง ๊ฐœ๋ฐœ + ํŠœ๋‹)
112
+ **์˜ˆ์ƒ ์ •ํ™•๋„**: 70-80% (ํœด๋ฆฌ์Šคํ‹ฑ ์˜์กด)
113
+
114
+ ---
115
+
116
+ ### ๋ฐฉ์•ˆ 3: YOLOv8 ์ปค์Šคํ…€ ํ•™์Šต
117
+
118
+ **๊ฐœ์š”**: YOLOv8 ๋ชจ๋ธ์„ ์ธก์ • ํ™˜๊ฒฝ ๋ฐ์ดํ„ฐ๋กœ ์ฒ˜์Œ๋ถ€ํ„ฐ ํ•™์Šต
119
+
120
+ **์žฅ์ :**
121
+ - โœ… ๋น ๋ฅธ ์ถ”๋ก  ์†๋„ (CPU: 0.5-1์ดˆ/์ด๋ฏธ์ง€)
122
+ - โœ… ์˜คํ”ˆ์†Œ์Šค๋กœ ์™„์ „ ์ œ์–ด ๊ฐ€๋Šฅ
123
+ - โœ… ๋‹ค์–‘ํ•œ ๋‚ด๋ณด๋‚ด๊ธฐ ํ˜•์‹ (ONNX, TensorRT ๋“ฑ)
124
+
125
+ **๋‹จ์ :**
126
+ - โš ๏ธ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ํ•„์š” (์ตœ์†Œ 200-500์žฅ)
127
+ - โš ๏ธ ํ•™์Šต ํ™˜๊ฒฝ ๊ตฌ์ถ• ํ•„์š” (GPU)
128
+ - โš ๏ธ ๊ด€๋ฆฌ ๋ถ€๋‹ด ์ฆ๊ฐ€
129
+
130
+ **๊ตฌํ˜„ ์ ˆ์ฐจ:**
131
+ ```bash
132
+ # ์„ค์น˜
133
+ pip install ultralytics
134
+
135
+ # ๋ฐ์ดํ„ฐ ์ค€๋น„ (YOLO ํ˜•์‹)
136
+ # data.yaml:
137
+ # path: ./dataset
138
+ # train: images/train
139
+ # val: images/val
140
+ # nc: 1
141
+ # names: ['shrimp']
142
+
143
+ # ํ•™์Šต
144
+ yolo detect train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640
145
+
146
+ # ์ถ”๋ก 
147
+ yolo detect predict model=best.pt source=test.jpg
148
+ ```
149
+
150
+ **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1-2์ฃผ
151
+ **์˜ˆ์ƒ ์ •ํ™•๋„**: 85%+ (์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ ์‹œ)
152
+
153
+ ---
154
+
155
+ ### ๋ฐฉ์•ˆ 4: ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ (ํ›„์ฒ˜๋ฆฌ ํ•„ํ„ฐ ์ถ”๊ฐ€) ๐Ÿš€ (์ฆ‰์‹œ ๊ฐ€๋Šฅ)
156
+
157
+ **๊ฐœ์š”**: RT-DETR ๊ฒฐ๊ณผ์— ์Šค๋งˆํŠธ ํ•„ํ„ฐ๋ง ์ ์šฉ
158
+
159
+ **์žฅ์ :**
160
+ - โœ… ์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅ (์ฝ”๋“œ๋งŒ ์ˆ˜์ •)
161
+ - โœ… ์ถ”๊ฐ€ ํ•™์Šต ๋ถˆํ•„์š”
162
+ - โœ… ๊ธฐ์กด ์ธํ”„๋ผ ํ™œ์šฉ
163
+
164
+ **๊ตฌํ˜„ ์ „๋žต:**
165
+
166
+ #### A. ํด๋ž˜์Šค ํ•„ํ„ฐ๋ง
167
+ ```python
168
+ # RT-DETR๋Š” COCO ํด๋ž˜์Šค ์‚ฌ์šฉ
169
+ # ์ƒˆ์šฐ์™€ ์œ ์‚ฌํ•œ ํด๋ž˜์Šค๋งŒ ํ—ˆ์šฉ
170
+ ALLOWED_CLASSES = [
171
+ # COCO dataset class IDs
172
+ # ์ƒˆ์šฐ์™€ ๋น„์Šทํ•œ ๊ฒƒ๋“ค๋งŒ
173
+ ]
174
+
175
+ def filter_by_class(detections):
176
+ return [d for d in detections if d['class_id'] in ALLOWED_CLASSES]
177
+ ```
178
+
179
+ #### B. ์œ„์น˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
180
+ ```python
181
+ def filter_by_position(detections, image_shape):
182
+ """๋งคํŠธ ์ค‘์•™ ์˜์—ญ๋งŒ ํ—ˆ์šฉ"""
183
+ h, w = image_shape[:2]
184
+ center_region = {
185
+ 'x_min': w * 0.2,
186
+ 'x_max': w * 0.8,
187
+ 'y_min': h * 0.3,
188
+ 'y_max': h * 0.7
189
+ }
190
+
191
+ filtered = []
192
+ for det in detections:
193
+ bbox_center_x = (det['bbox'][0] + det['bbox'][2]) / 2
194
+ bbox_center_y = (det['bbox'][1] + det['bbox'][3]) / 2
195
+
196
+ if (center_region['x_min'] < bbox_center_x < center_region['x_max'] and
197
+ center_region['y_min'] < bbox_center_y < center_region['y_max']):
198
+ filtered.append(det)
199
+
200
+ return filtered
201
+ ```
202
+
203
+ #### C. ํฌ๊ธฐ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
204
+ ```python
205
+ def filter_by_size(detections, image_shape):
206
+ """์ ์ ˆํ•œ ํฌ๊ธฐ์˜ ๊ฐ์ฒด๋งŒ ํ—ˆ์šฉ"""
207
+ h, w = image_shape[:2]
208
+ image_area = h * w
209
+
210
+ filtered = []
211
+ for det in detections:
212
+ bbox = det['bbox']
213
+ bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
214
+ area_ratio = bbox_area / image_area
215
+
216
+ # ์ด๋ฏธ์ง€์˜ 5%~50% ํฌ๊ธฐ๋งŒ ํ—ˆ์šฉ
217
+ if 0.05 < area_ratio < 0.50:
218
+ filtered.append(det)
219
+
220
+ return filtered
221
+ ```
222
+
223
+ #### D. ํ˜•ํƒœ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
224
+ ```python
225
+ def filter_by_shape(detections):
226
+ """๊ฐ€๋กœ๋กœ ๊ธด ํ˜•ํƒœ๋งŒ ํ—ˆ์šฉ (์ƒˆ์šฐ ํŠน์ง•)"""
227
+ filtered = []
228
+ for det in detections:
229
+ bbox = det['bbox']
230
+ width = bbox[2] - bbox[0]
231
+ height = bbox[3] - bbox[1]
232
+ aspect_ratio = width / height
233
+
234
+ # ๊ฐ€๋กœ:์„ธ๋กœ ๋น„์œจ 2:1 ~ 8:1
235
+ if 2.0 < aspect_ratio < 8.0:
236
+ filtered.append(det)
237
+
238
+ return filtered
239
+ ```
240
+
241
+ #### E. ์ƒ‰์ƒ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
242
+ ```python
243
+ def filter_by_color(image, detections):
244
+ """ํ‰๊ท  ์ƒ‰์ƒ์ด ์ƒˆ์šฐ์™€ ์œ ์‚ฌํ•œ ๊ฐ์ฒด๋งŒ ํ—ˆ์šฉ"""
245
+ filtered = []
246
+ for det in detections:
247
+ bbox = det['bbox']
248
+ roi = image[int(bbox[1]):int(bbox[3]), int(bbox[0]):int(bbox[2])]
249
+
250
+ # ํ‰๊ท  BGR ๊ฐ’
251
+ mean_color = roi.mean(axis=(0, 1))
252
+
253
+ # ํˆฌ๋ช…/ํฐ์ƒ‰/ํšŒ์ƒ‰ ๋ฒ”์œ„ (์ฃฝ์€ ์ƒˆ์šฐ)
254
+ # B, G, R ๊ฐ’์ด ๋ชจ๋‘ ๋น„์Šทํ•˜๊ณ  ์ค‘๊ฐ„ ์ด์ƒ
255
+ if (abs(mean_color[0] - mean_color[1]) < 30 and
256
+ abs(mean_color[1] - mean_color[2]) < 30 and
257
+ mean_color.mean() > 100):
258
+ filtered.append(det)
259
+
260
+ return filtered
261
+ ```
262
+
263
+ #### ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ
264
+ ```python
265
+ def detect_measurement_shrimp(image, confidence=0.3):
266
+ """์ธก์ •์šฉ ์ƒˆ์šฐ ๊ฒ€์ถœ (์Šค๋งˆํŠธ ํ•„ํ„ฐ๋ง)"""
267
+
268
+ # 1. RT-DETR๋กœ ๊ธฐ๋ณธ ๊ฒ€์ถœ
269
+ raw_detections = rtdetr_detect(image, confidence)
270
+
271
+ # 2. ๋‹ค๋‹จ๊ณ„ ํ•„ํ„ฐ๋ง
272
+ detections = raw_detections
273
+ detections = filter_by_position(detections, image.shape)
274
+ detections = filter_by_size(detections, image.shape)
275
+ detections = filter_by_shape(detections)
276
+ detections = filter_by_color(image, detections)
277
+
278
+ # 3. ์‹ ๋ขฐ๋„ ์ˆœ ์ •๋ ฌ ํ›„ ์ƒ์œ„ 1๊ฐœ๋งŒ (์ธก์ •์€ ๋ณดํ†ต 1๋งˆ๋ฆฌ)
279
+ detections = sorted(detections, key=lambda x: x['confidence'], reverse=True)
280
+
281
+ return detections[:1] if detections else []
282
+ ```
283
+
284
+ **์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„**: 1์ผ
285
+ **์˜ˆ์ƒ ์ •ํ™•๋„**: 60-75% (ํŠœ๋‹์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง)
286
+
287
+ ---
288
+
289
+ ## ๐Ÿ“ˆ ๋ฐฉ์•ˆ ๋น„๊ต
290
+
291
+ | ๋ฐฉ์•ˆ | ์ •ํ™•๋„ | ์†๋„ | ๊ตฌํ˜„ ๋‚œ์ด๋„ | ์†Œ์š” ์‹œ๊ฐ„ | ๋น„์šฉ | ์ถ”์ฒœ๋„ |
292
+ |------|--------|------|------------|----------|------|--------|
293
+ | **1. Roboflow ํŒŒ์ธํŠœ๋‹** | โญโญโญโญโญ 90%+ | โญโญโญโญ 1-2์ดˆ | โญโญโญ ์ค‘๊ฐ„ | 2-3์ผ | $ | โญโญโญโญโญ |
294
+ | **2. SAM + ํ›„์ฒ˜๋ฆฌ** | โญโญโญ 70-80% | โญโญ 10-30์ดˆ | โญโญโญโญ ์–ด๋ ค์›€ | 3-5์ผ | ๋ฌด๋ฃŒ | โญโญ |
295
+ | **3. YOLOv8 ์ปค์Šคํ…€** | โญโญโญโญ 85%+ | โญโญโญโญโญ 0.5์ดˆ | โญโญโญโญโญ ๋งค์šฐ ์–ด๋ ค์›€ | 1-2์ฃผ | ๋ฌด๋ฃŒ | โญโญโญ |
296
+ | **4. RT-DETR + ํ•„ํ„ฐ** | โญโญโญ 60-75% | โญโญโญโญ 1-2์ดˆ | โญโญ ์‰ฌ์›€ | 1์ผ | ๋ฌด๋ฃŒ | โญโญโญโญ |
297
+
298
+ ---
299
+
300
+ ## ๐ŸŽฏ ์ตœ์ข… ์ถ”์ฒœ ์ „๋žต
301
+
302
+ ### ๋‹จ๊ธฐ (์ฆ‰์‹œ~1์ผ): ๋ฐฉ์•ˆ 4 ๊ตฌํ˜„
303
+ **RT-DETR + ์Šค๋งˆํŠธ ํ•„ํ„ฐ๋ง**์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๋ฌธ์ œ ํ•ด๊ฒฐ
304
+ - ์œ„์น˜, ํฌ๊ธฐ, ํ˜•ํƒœ, ์ƒ‰์ƒ ํ•„ํ„ฐ ์กฐํ•ฉ
305
+ - ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹
306
+ - 60-75% ์ •ํ™•๋„๋กœ ์‹ค์šฉ์  ์‚ฌ์šฉ ๊ฐ€๋Šฅ
307
+
308
+ ### ์ค‘๊ธฐ (1์ฃผ~2์ฃผ): ๋ฐฉ์•ˆ 1 ๊ตฌํ˜„
309
+ **Roboflow ๋ชจ๋ธ ํŒŒ์ธํŠœ๋‹**์œผ๋กœ ๊ทผ๋ณธ์  ํ•ด๊ฒฐ
310
+ - ์ธก์ • ํ™˜๊ฒฝ ์ด๋ฏธ์ง€ 50-100์žฅ ์ˆ˜์ง‘ ๋ฐ ๋ผ๋ฒจ๋ง
311
+ - ๊ธฐ์กด ๋ชจ๋ธ์— ์ถ”๊ฐ€ ํ•™์Šต
312
+ - 90%+ ์ •ํ™•๋„ ๋‹ฌ์„ฑ
313
+ - ์ˆ˜์กฐ + ์ธก์ • ํ†ตํ•ฉ ๋ชจ๋ธ ์™„์„ฑ
314
+
315
+ ### ์žฅ๊ธฐ (์„ ํƒ): ๋ฐฉ์•ˆ 3 ๊ณ ๋ ค
316
+ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ํ™•๋ณด ์‹œ **YOLOv8**์œผ๋กœ ์™„์ „ ์ž์ฒด ๋ชจ๋ธ ๊ตฌ์ถ•
317
+
318
+ ---
319
+
320
+ ## ๐Ÿ“ ์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์•ก์…˜ ํ”Œ๋žœ
321
+
322
+ ### Phase 1: ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ž… (1์ผ)
323
+ 1. **RT-DETR + ํ•„ํ„ฐ ๊ตฌํ˜„**
324
+ - `app.py`์— `detect_measurement_shrimp()` ํ•จ์ˆ˜ ์ถ”๊ฐ€
325
+ - 5๊ฐ€์ง€ ํ•„ํ„ฐ ์ˆœ์ฐจ ์ ์šฉ
326
+ - ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ฆ
327
+
328
+ 2. **UI ์—…๋ฐ์ดํŠธ**
329
+ - "์ธก์ •์šฉ ๋ชจ๋“œ" ํ† ๊ธ€ ์ถ”๊ฐ€
330
+ - ํ™œ์„ฑํ™” ์‹œ ์Šค๋งˆํŠธ ํ•„ํ„ฐ ์ ์šฉ
331
+
332
+ ### Phase 2: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค€๋น„ (๋ณ‘๋ ฌ ์ง„ํ–‰)
333
+ 1. **์ด๋ฏธ์ง€ ์ˆ˜์ง‘**
334
+ - ๊ธฐ์กด `data/` ํด๋”์˜ ์ธก์ • ์ด๋ฏธ์ง€ ํ™œ์šฉ
335
+ - ์ถ”๊ฐ€๋กœ 50-100์žฅ ์ดฌ์˜
336
+ - ๋‹ค์–‘ํ•œ ๊ฐ๋„, ํฌ๊ธฐ, ์กฐ๋ช…
337
+
338
+ 2. **Roboflow ํ”„๋กœ์ ํŠธ ์„ค์ •**
339
+ - ๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ์ƒˆ ๋ฒ„์ „ ์ƒ์„ฑ
340
+ - ๋ผ๋ฒจ๋ง ์ง„ํ–‰
341
+
342
+ ### Phase 3: ๋ชจ๋ธ ์žฌํ•™์Šต (2-3์ผ)
343
+ 1. **Roboflow์—์„œ ํ•™์Šต**
344
+ - ๊ธฐ์กด + ์ƒˆ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ
345
+ - Auto-Orient, Auto-Adjust ๋“ฑ ์ฆ๊ฐ• ์ ์šฉ
346
+ - Validation ์„ฑ๋Šฅ ํ™•์ธ
347
+
348
+ 2. **๋ฐฐํฌ ๋ฐ ํ…Œ์ŠคํŠธ**
349
+ - ์—…๋ฐ์ดํŠธ๋œ ๋ชจ๋ธ API ์—ฐ๋™
350
+ - A/B ํ…Œ์ŠคํŠธ๋กœ ์„ฑ๋Šฅ ๋น„๊ต
351
+
352
+ ---
353
+
354
+ ## ๐Ÿ’ฐ ๋น„์šฉ ๋ถ„์„
355
+
356
+ ### Roboflow ํŒŒ์ธํŠœ๋‹
357
+ - **๋ฌด๋ฃŒ ํ‹ฐ์–ด**: 1,000 predictions/month
358
+ - **Starter ํ”Œ๋žœ**: $49/month - 10,000 predictions
359
+ - **์˜ˆ์ƒ ๋น„์šฉ**: ์›” $0-49 (์‚ฌ์šฉ๋Ÿ‰์— ๋”ฐ๋ผ)
360
+
361
+ ### ๋Œ€์•ˆ (์™„์ „ ๋ฌด๋ฃŒ)
362
+ - **YOLOv8 ์ž์ฒด ํ•™์Šต**: ๋ฌด๋ฃŒ (GPU ํ™˜๊ฒฝ ํ•„์š”)
363
+ - **RT-DETR + ํ•„ํ„ฐ**: ๋ฌด๋ฃŒ (๋กœ์ปฌ ์‹คํ–‰)
364
+
365
+ ---
366
+
367
+ ## ๐Ÿ”ฌ ์‹คํ—˜ ๋ฐ ๊ฒ€์ฆ
368
+
369
+ ### ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์…‹ ์ค€๋น„
370
+ ```
371
+ test_images/
372
+ โ”œโ”€โ”€ measurement/
373
+ โ”‚ โ”œโ”€โ”€ single_shrimp/ # ์ƒˆ์šฐ 1๋งˆ๋ฆฌ๋งŒ
374
+ โ”‚ โ”œโ”€โ”€ with_ruler/ # ์ž ํฌํ•จ
375
+ โ”‚ โ”œโ”€โ”€ with_hand/ # ์† ํฌํ•จ
376
+ โ”‚ โ””โ”€โ”€ complex/ # ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ
377
+ โ””โ”€โ”€ tank/
378
+ โ””โ”€โ”€ live_shrimp/ # ์ˆ˜์กฐ ์ƒˆ์šฐ
379
+ ```
380
+
381
+ ### ํ‰๊ฐ€ ์ง€ํ‘œ
382
+ - **Precision**: ๊ฒ€์ถœ๋œ ๊ฒƒ ์ค‘ ์‹ค์ œ ์ƒˆ์šฐ ๋น„์œจ
383
+ - **Recall**: ์‹ค์ œ ์ƒˆ์šฐ ์ค‘ ๊ฒ€์ถœ๋œ ๋น„์œจ
384
+ - **F1 Score**: Precision๊ณผ Recall์˜ ์กฐํ™”ํ‰๊ท 
385
+ - **์ถ”๋ก  ์‹œ๊ฐ„**: CPU ๊ธฐ์ค€ ms/์ด๋ฏธ์ง€
386
+
387
+ ---
388
+
389
+ ## ๐Ÿ“š ์ฐธ๊ณ  ์ž๋ฃŒ
390
+
391
+ - [Roboflow Training Tutorial](https://docs.roboflow.com/train)
392
+ - [YOLOv8 Documentation](https://docs.ultralytics.com/)
393
+ - [Segment Anything (SAM)](https://segment-anything.com/)
394
+ - [RT-DETR Paper](https://arxiv.org/abs/2304.08069)
395
+
396
+ ---
397
+
398
+ **์ž‘์„ฑ์ผ**: 2025-11-07
399
+ **์ž‘์„ฑ์ž**: VIDraft Team
400
+ **๋ฒ„์ „**: 1.0
docs/rt_detr_smart_filtering_strategy.md ADDED
@@ -0,0 +1,1252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ๐ŸŽฏ RT-DETR + ์Šค๋งˆํŠธ ํ•„ํ„ฐ๋ง ์ „๋žต (MECE)
2
+
3
+ ## ๐Ÿ“‹ ๋ชฉ์ฐจ
4
+ 1. [๋ฌธ์ œ ์ •์˜](#๋ฌธ์ œ-์ •์˜)
5
+ 2. [ํ•„ํ„ฐ๋ง ์ „๋žต ์ฒด๊ณ„](#ํ•„ํ„ฐ๋ง-์ „๋žต-์ฒด๊ณ„)
6
+ 3. [์ƒ์„ธ ๊ตฌํ˜„ ๋ฐฉ์•ˆ](#์ƒ์„ธ-๊ตฌํ˜„-๋ฐฉ์•ˆ)
7
+ 4. [์˜์‚ฌ๊ฒฐ์ • ํŠธ๋ฆฌ](#์˜์‚ฌ๊ฒฐ์ •-ํŠธ๋ฆฌ)
8
+ 5. [๊ตฌํ˜„ ์ฝ”๋“œ](#๊ตฌํ˜„-์ฝ”๋“œ)
9
+ 6. [ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ](#ํ…Œ์ŠคํŠธ-๋ฐ-๊ฒ€์ฆ)
10
+
11
+ ---
12
+
13
+ ## ๐ŸŽฏ ๋ฌธ์ œ ์ •์˜
14
+
15
+ ### ์ž…๋ ฅ ์กฐ๊ฑด
16
+ - **์ด๋ฏธ์ง€**: ์ธก์ • ๋งคํŠธ ์œ„์˜ ์ฃฝ์€ ์ƒˆ์šฐ
17
+ - **์ œ์•ฝ์‚ฌํ•ญ**:
18
+ - ์ƒˆ์šฐ ์œ„์น˜๋Š” ๋žœ๋ค (๋งคํŠธ ์–ด๋””๋“  ๊ฐ€๋Šฅ)
19
+ - ๋ฐฐ๊ฒฝ์— ์‚ฌ๋žŒ ์†, ์ž, ์‹ ๋ฐœ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ฐ์ฒด
20
+ - ์ƒˆ์šฐ๋Š” ํˆฌ๋ช…/ํฐ์ƒ‰/ํšŒ์ƒ‰ (์ฃฝ์€ ์ƒํƒœ)
21
+
22
+ ### ๋ชฉํ‘œ
23
+ - **๊ฒ€์ถœ ๋Œ€์ƒ**: ์ƒˆ์šฐ๋งŒ ๊ฒ€์ถœ
24
+ - **์ œ์™ธ ๋Œ€์ƒ**: ์ž(ruler), ์†, ์‹ ๋ฐœ, ๋งคํŠธ, ๊ธฐํƒ€ ๋ฐฐ๊ฒฝ ๊ฐ์ฒด
25
+ - **์ •ํ™•๋„**: 60-75% (์ฆ‰์‹œ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€)
26
+
27
+ ### RT-DETR์˜ ํ•œ๊ณ„
28
+ - COCO ๋ฐ์ดํ„ฐ์…‹ ํ•™์Šต (80๊ฐœ ๋ฒ”์šฉ ํด๋ž˜์Šค)
29
+ - ์ƒˆ์šฐ ์ „์šฉ ํ•™์Šต ์—†์Œ
30
+ - ๋ชจ๋“  ๊ฐ์ฒด๋ฅผ ๊ฒ€์ถœํ•˜์—ฌ ๊ณผ๋‹ค ๊ฒ€์ถœ ๋ฐœ์ƒ
31
+
32
+ ---
33
+
34
+ ## ๐Ÿ—๏ธ ํ•„ํ„ฐ๋ง ์ „๋žต ์ฒด๊ณ„ (MECE)
35
+
36
+ ### Level 1: ๊ฐ์ฒด ์†์„ฑ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜
37
+
38
+ ๋ชจ๋“  ๊ฒ€์ถœ๋œ ๊ฐ์ฒด๋Š” ๋‹ค์Œ **4๊ฐ€์ง€ ๋…๋ฆฝ์  ์†์„ฑ**์œผ๋กœ ๋ถ„๋ฅ˜๋ฉ๋‹ˆ๋‹ค:
39
+
40
+ ```
41
+ ๊ฒ€์ถœ ๊ฐ์ฒด
42
+ โ”œโ”€โ”€ 1. ๋ฌผ๋ฆฌ์  ์†์„ฑ (Physical Attributes)
43
+ โ”‚ โ”œโ”€โ”€ ํฌ๊ธฐ (Size)
44
+ โ”‚ โ”œโ”€โ”€ ํ˜•ํƒœ (Shape/Aspect Ratio)
45
+ โ”‚ โ””โ”€โ”€ ๋ฐฉํ–ฅ (Orientation)
46
+ โ”‚
47
+ โ”œโ”€โ”€ 2. ์‹œ๊ฐ์  ์†์„ฑ (Visual Attributes)
48
+ โ”‚ โ”œโ”€โ”€ ์ƒ‰์ƒ (Color)
49
+ โ”‚ โ”œโ”€โ”€ ํ…์Šค์ฒ˜ (Texture)
50
+ โ”‚ โ””โ”€โ”€ ํˆฌ๋ช…๋„ (Transparency)
51
+ โ”‚
52
+ โ”œโ”€โ”€ 3. ์˜๋ฏธ์  ์†์„ฑ (Semantic Attributes)
53
+ โ”‚ โ”œโ”€โ”€ RT-DETR ํด๋ž˜์Šค (COCO Class)
54
+ โ”‚ โ””โ”€โ”€ ์‹ ๋ขฐ๋„ (Confidence Score)
55
+ โ”‚
56
+ โ””โ”€โ”€ 4. ์ปจํ…์ŠคํŠธ ์†์„ฑ (Contextual Attributes)
57
+ โ”œโ”€โ”€ ๊ณต๊ฐ„์  ๊ด€๊ณ„ (Spatial Relations)
58
+ โ””โ”€โ”€ ์ฃผ๋ณ€ ๊ฐ์ฒด (Neighboring Objects)
59
+ ```
60
+
61
+ ### Level 2: ํ•„ํ„ฐ๋ง ๋‹จ๊ณ„ (์ˆœ์ฐจ ์ ์šฉ)
62
+
63
+ ```
64
+ Step 1: ์‚ฌ์ „ ํ•„ํ„ฐ (Pre-Filter)
65
+ โ†’ RT-DETR ๊ฒฐ๊ณผ์—์„œ ๋ช…๋ฐฑํžˆ ์ƒˆ์šฐ๊ฐ€ ์•„๋‹Œ ๊ฒƒ ์ œ๊ฑฐ
66
+ โ†’ COCO ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ 1์ฐจ ํ•„ํ„ฐ๋ง
67
+
68
+ Step 2: ๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ (Physical Filter)
69
+ โ†’ ํฌ๊ธฐ, ํ˜•ํƒœ, ๋ฐฉํ–ฅ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
70
+ โ†’ ์œ„์น˜ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅ
71
+
72
+ Step 3: ์‹œ๊ฐ์  ํ•„ํ„ฐ (Visual Filter)
73
+ โ†’ ์ƒ‰์ƒ, ํ…์Šค์ฒ˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง
74
+ โ†’ ์ƒˆ์šฐ์˜ ์‹œ๊ฐ์  ํŠน์ง• ํ™œ์šฉ
75
+
76
+ Step 4: ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ (Context Filter)
77
+ โ†’ ์ฃผ๋ณ€ ๊ฐ์ฒด์™€์˜ ๊ด€๊ณ„ ๋ถ„์„
78
+ โ†’ ์ž(ruler)์™€์˜ ์ƒ๋Œ€์  ์œ„์น˜
79
+
80
+ Step 5: ํ›„์ฒ˜๋ฆฌ (Post-Processing)
81
+ โ†’ ์‹ ๋ขฐ๋„ ๊ธฐ๋ฐ˜ ์ตœ์ข… ์„ ํƒ
82
+ โ†’ ์ค‘๋ณต ์ œ๊ฑฐ (NMS)
83
+ ```
84
+
85
+ ---
86
+
87
+ ## ๐Ÿ“ ์ƒ์„ธ ๊ตฌํ˜„ ๋ฐฉ์•ˆ
88
+
89
+ ### Step 1: ์‚ฌ์ „ ํ•„ํ„ฐ (Pre-Filter)
90
+
91
+ #### 1.1 COCO ํด๋ž˜์Šค ํ•„ํ„ฐ๋ง
92
+
93
+ **์ „๋žต**: RT-DETR์ด ๊ฒ€์ถœํ•œ ํด๋ž˜์Šค ์ค‘ ์ƒˆ์šฐ์™€ ์œ ์‚ฌํ•œ ํด๋ž˜์Šค๋งŒ ํ—ˆ์šฉ
94
+
95
+ **COCO 80๊ฐœ ํด๋ž˜์Šค ๋ถ„๋ฅ˜**:
96
+
97
+ ```python
98
+ # COCO Classes ์™„์ „ ๋ถ„๋ฅ˜ (MECE)
99
+ COCO_CLASSES = {
100
+ # A. ์ƒˆ์šฐ ๊ฐ€๋Šฅ์„ฑ ์žˆ๋Š” ํด๋ž˜์Šค (ํ—ˆ์šฉ) โœ…
101
+ "POSSIBLY_SHRIMP": [
102
+ # ์œ ์‚ฌ ํ˜•ํƒœ ํ•ด์–‘ ์ƒ๋ฌผ
103
+ # (COCO์—๋Š” ์ƒˆ์šฐ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์œ ์‚ฌํ•œ ๊ฒƒ๋“ค)
104
+ ],
105
+
106
+ # B. ์ƒˆ์šฐ์™€ ๋ฌด๊ด€ํ•œ ํด๋ž˜์Šค (์ œ๊ฑฐ) โŒ
107
+ "DEFINITELY_NOT_SHRIMP": {
108
+ # B1. ์‚ฌ๋žŒ ๊ด€๋ จ
109
+ "PERSON_RELATED": [0], # person
110
+
111
+ # B2. ์˜๋ฅ˜/์•ก์„ธ์„œ๋ฆฌ
112
+ "CLOTHING": [24, 25, 26, 27, 31, 32, 33],
113
+ # backpack, umbrella, handbag, tie, suitcase, frisbee, skis
114
+
115
+ # B3. ์Šคํฌ์ธ  ์šฉํ’ˆ
116
+ "SPORTS": [32, 33, 34, 35, 36, 37, 38, 39, 40, 41],
117
+ # sports ball, baseball bat, etc.
118
+
119
+ # B4. ์‹ค๋‚ด ๊ฐ€๊ตฌ
120
+ "FURNITURE": [56, 57, 58, 59, 60, 61, 62, 63],
121
+ # chair, couch, bed, etc.
122
+
123
+ # B5. ์ „์ž๊ธฐ๊ธฐ
124
+ "ELECTRONICS": [63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73],
125
+ # laptop, mouse, keyboard, etc.
126
+
127
+ # B6. ์ฃผ๋ฐฉ ์šฉํ’ˆ
128
+ "KITCHEN": [42, 43, 44, 45, 46, 47, 48, 49, 50, 51],
129
+ # wine glass, cup, fork, knife, etc.
130
+
131
+ # B7. ๊ตํ†ต์ˆ˜๋‹จ
132
+ "VEHICLES": [1, 2, 3, 4, 5, 6, 7, 8],
133
+ # bicycle, car, motorcycle, etc.
134
+ },
135
+
136
+ # C. ์• ๋งคํ•œ ํด๋ž˜์Šค (์กฐ๊ฑด๋ถ€ ํ—ˆ์šฉ) โš ๏ธ
137
+ "AMBIGUOUS": [
138
+ # ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง ํ•„์š”
139
+ # ์˜ˆ: ์ผ๋ถ€ ๊ณผ์ผ, ๋ฌผ์ฒด ๋“ฑ
140
+ ]
141
+ }
142
+ ```
143
+
144
+ **๊ตฌํ˜„**:
145
+ ```python
146
+ def filter_by_coco_class(detections):
147
+ """COCO ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง"""
148
+
149
+ # ๋ช…๋ฐฑํžˆ ์ œ์™ธํ•  ํด๋ž˜์Šค ID
150
+ EXCLUDE_CLASSES = set()
151
+ for category in COCO_CLASSES["DEFINITELY_NOT_SHRIMP"].values():
152
+ EXCLUDE_CLASSES.update(category)
153
+
154
+ filtered = []
155
+ for det in detections:
156
+ class_id = det.get('class_id', -1)
157
+ if class_id not in EXCLUDE_CLASSES:
158
+ filtered.append(det)
159
+
160
+ return filtered
161
+ ```
162
+
163
+ #### 1.2 ์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’
164
+
165
+ **์ „๋žต**: ๋„ˆ๋ฌด ๋‚ฎ์€ ์‹ ๋ขฐ๋„๋Š” ๋…ธ์ด์ฆˆ์ผ ๊ฐ€๋Šฅ์„ฑ
166
+
167
+ ```python
168
+ def filter_by_confidence(detections, min_confidence=0.15):
169
+ """์ตœ์†Œ ์‹ ๋ขฐ๋„ ํ•„ํ„ฐ"""
170
+ return [d for d in detections if d['confidence'] >= min_confidence]
171
+ ```
172
+
173
+ ---
174
+
175
+ ### Step 2: ๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ (Physical Filter)
176
+
177
+ **ํ•ต์‹ฌ**: ์œ„์น˜ ๋ฌด๊ด€ํ•˜๊ฒŒ ์ ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฌผ๏ฟฝ๏ฟฝ์  ํŠน์„ฑ
178
+
179
+ #### 2.1 ์ ˆ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ
180
+
181
+ **๊ฐ€์ •**: ์ธก์ • ์ด๋ฏธ์ง€๋Š” ๋Œ€๋ถ€๋ถ„ ๋น„์Šทํ•œ ํ•ด์ƒ๋„ (1000x1000 ~ 2000x2000)
182
+
183
+ ```python
184
+ def filter_by_absolute_size(detections, image_shape):
185
+ """์ ˆ๋Œ€ ํ”ฝ์…€ ํฌ๊ธฐ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ"""
186
+
187
+ h, w = image_shape[:2]
188
+
189
+ filtered = []
190
+ for det in detections:
191
+ bbox = det['bbox']
192
+ bbox_width = bbox[2] - bbox[0]
193
+ bbox_height = bbox[3] - bbox[1]
194
+ bbox_area = bbox_width * bbox_height
195
+
196
+ # ์ƒˆ์šฐ ํฌ๊ธฐ ๋ฒ”์œ„ (๊ฒฝํ—˜์ )
197
+ # ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด: ๋…ธ์ด์ฆˆ
198
+ # ๋„ˆ๋ฌด ํฌ๋ฉด: ๋งคํŠธ, ๋ฐฐ๊ฒฝ
199
+ if bbox_width < 50: # ์ตœ์†Œ 50px
200
+ continue
201
+ if bbox_height < 20: # ์ตœ์†Œ 20px
202
+ continue
203
+ if bbox_area < 2000: # ์ตœ์†Œ ๋ฉด์ 
204
+ continue
205
+ if bbox_area > (w * h * 0.6): # ์ด๋ฏธ์ง€์˜ 60% ์ด์ƒ์ด๋ฉด ๋ฐฐ๊ฒฝ
206
+ continue
207
+
208
+ filtered.append(det)
209
+
210
+ return filtered
211
+ ```
212
+
213
+ #### 2.2 ์ƒ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ
214
+
215
+ **์ „๋žต**: ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋Œ€๋น„ ๋น„์œจ๋กœ ํ•„ํ„ฐ๋ง
216
+
217
+ ```python
218
+ def filter_by_relative_size(detections, image_shape):
219
+ """์ด๋ฏธ์ง€ ๋Œ€๋น„ ์ƒ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ"""
220
+
221
+ h, w = image_shape[:2]
222
+ image_area = h * w
223
+
224
+ filtered = []
225
+ for det in detections:
226
+ bbox = det['bbox']
227
+ bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
228
+ area_ratio = bbox_area / image_area
229
+
230
+ # ์ด๋ฏธ์ง€์˜ 3%~50% ํฌ๊ธฐ๋งŒ ํ—ˆ์šฉ
231
+ if 0.03 < area_ratio < 0.50:
232
+ filtered.append(det)
233
+
234
+ return filtered
235
+ ```
236
+
237
+ #### 2.3 ์ข…ํšก๋น„ ํ•„ํ„ฐ (Aspect Ratio)
238
+
239
+ **ํŠน์ง•**: ์ƒˆ์šฐ๋Š” ๊ฐ€๋กœ๋กœ ๊ธด ํ˜•ํƒœ
240
+
241
+ ```python
242
+ def filter_by_aspect_ratio(detections):
243
+ """์ข…ํšก๋น„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ (๊ฐ€๋กœ/์„ธ๋กœ)"""
244
+
245
+ filtered = []
246
+ for det in detections:
247
+ bbox = det['bbox']
248
+ width = bbox[2] - bbox[0]
249
+ height = bbox[3] - bbox[1]
250
+
251
+ # ๊ฐ€๋กœ๊ฐ€ 0์ด๋ฉด ์ œ์™ธ
252
+ if height == 0:
253
+ continue
254
+
255
+ aspect_ratio = width / height
256
+
257
+ # ์ƒˆ์šฐ๋Š” ๊ฐ€๋กœ๋กœ ๊ธธ๋‹ค
258
+ # 1.5:1 ~ 10:1 ๋ฒ”์œ„
259
+ if 1.5 < aspect_ratio < 10.0:
260
+ filtered.append(det)
261
+
262
+ return filtered
263
+ ```
264
+
265
+ #### 2.4 ์ถฉ๋งŒ๋„ ํ•„ํ„ฐ (Solidity)
266
+
267
+ **์ •์˜**: Solidity = ๊ฐ์ฒด ๋ฉด์  / ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ฉด์ 
268
+
269
+ **๊ฐ€์ •**: ์ƒˆ์šฐ๋Š” ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋ฅผ ์–ด๋А ์ •๋„ ์ฑ„์›€ (0.3~0.8)
270
+
271
+ ```python
272
+ def filter_by_solidity(image, detections):
273
+ """์ถฉ๋งŒ๋„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ (์„ธ๊ทธ๋จผํŠธ ํ•„์š” ์‹œ)"""
274
+ import cv2
275
+ import numpy as np
276
+
277
+ filtered = []
278
+ for det in detections:
279
+ bbox = det['bbox']
280
+ x1, y1, x2, y2 = map(int, bbox)
281
+
282
+ # ROI ์ถ”์ถœ
283
+ roi = image[y1:y2, x1:x2]
284
+ if roi.size == 0:
285
+ continue
286
+
287
+ # ๊ฐ„๋‹จํ•œ thresholding์œผ๋กœ ๊ฐ์ฒด ์˜์—ญ ์ถ”์ •
288
+ gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
289
+ _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
290
+
291
+ # ๊ฐ์ฒด ๋ฉด์ 
292
+ object_area = np.sum(binary > 0)
293
+ bbox_area = (x2 - x1) * (y2 - y1)
294
+
295
+ if bbox_area == 0:
296
+ continue
297
+
298
+ solidity = object_area / bbox_area
299
+
300
+ # ์ƒˆ์šฐ๋Š” 0.3~0.8 ๋ฒ”์œ„
301
+ if 0.25 < solidity < 0.85:
302
+ filtered.append(det)
303
+
304
+ return filtered
305
+ ```
306
+
307
+ ---
308
+
309
+ ### Step 3: ์‹œ๊ฐ์  ํ•„ํ„ฐ (Visual Filter)
310
+
311
+ **ํ•ต์‹ฌ**: ์ƒ‰์ƒ๊ณผ ํ…์Šค์ฒ˜๋กœ ์ƒˆ์šฐ ์‹๋ณ„ (์œ„์น˜ ๋ฌด๊ด€)
312
+
313
+ #### 3.1 ์ƒ‰์ƒ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ
314
+
315
+ **ํŠน์ง•**: ์ฃฝ์€ ์ƒˆ์šฐ = ํˆฌ๋ช…/ํฐ์ƒ‰/ํšŒ์ƒ‰ (๋†’์€ ๋ช…๋„, ๋‚ฎ์€ ์ฑ„๋„)
316
+
317
+ ```python
318
+ def filter_by_color(image, detections):
319
+ """์ƒ‰์ƒ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ (HSV ์ƒ‰๊ณต๊ฐ„)"""
320
+ import cv2
321
+ import numpy as np
322
+
323
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
324
+
325
+ filtered = []
326
+ for det in detections:
327
+ bbox = det['bbox']
328
+ x1, y1, x2, y2 = map(int, bbox)
329
+
330
+ # ROI ์ถ”์ถœ
331
+ roi_hsv = hsv_image[y1:y2, x1:x2]
332
+ if roi_hsv.size == 0:
333
+ continue
334
+
335
+ # ํ‰๊ท  HSV ๊ฐ’
336
+ mean_h, mean_s, mean_v = roi_hsv.mean(axis=(0, 1))
337
+
338
+ # ์ƒˆ์šฐ ํŠน์ง•:
339
+ # - ๋‚ฎ์€ ์ฑ„๋„ (Saturation < 100)
340
+ # - ๋†’์€ ๋ช…๋„ (Value > 100)
341
+ # - ์ƒ‰์ƒ ๋ฌด๊ด€ (ํˆฌ๋ช…ํ•˜๋ฏ€๋กœ)
342
+
343
+ if mean_s < 120 and mean_v > 80: # ์ฑ„๋„ ๋‚ฎ๊ณ  ๋ช…๋„ ๋†’์Œ
344
+ filtered.append(det)
345
+
346
+ return filtered
347
+ ```
348
+
349
+ #### 3.2 ํ…์Šค์ฒ˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ
350
+
351
+ **ํŠน์ง•**: ์ƒˆ์šฐ๋Š” ๋ถ€๋“œ๋Ÿฌ์šด ํ…์Šค์ฒ˜ (์ž๋‚˜ ๋งคํŠธ๋Š” ํŒจํ„ด์ด ๊ฐ•ํ•จ)
352
+
353
+ ```python
354
+ def filter_by_texture(image, detections):
355
+ """ํ…์Šค์ฒ˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ (ํ‘œ์ค€ํŽธ์ฐจ ํ™œ์šฉ)"""
356
+ import cv2
357
+ import numpy as np
358
+
359
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
360
+
361
+ filtered = []
362
+ for det in detections:
363
+ bbox = det['bbox']
364
+ x1, y1, x2, y2 = map(int, bbox)
365
+
366
+ # ROI ์ถ”์ถœ
367
+ roi = gray_image[y1:y2, x1:x2]
368
+ if roi.size == 0:
369
+ continue
370
+
371
+ # ํ…์Šค์ฒ˜ ๋ณต์žก๋„ (ํ‘œ์ค€ํŽธ์ฐจ)
372
+ texture_std = np.std(roi)
373
+
374
+ # ์ƒˆ์šฐ: ๋ถ€๋“œ๋Ÿฌ์šด ํ…์Šค์ฒ˜ (std ๋‚ฎ์Œ)
375
+ # ์ž, ๋งคํŠธ: ๊ฐ•๏ฟฝ๏ฟฝ ํŒจํ„ด (std ๋†’์Œ)
376
+ if texture_std < 50: # ๋ถ€๋“œ๋Ÿฌ์šด ํ…์Šค์ฒ˜
377
+ filtered.append(det)
378
+
379
+ return filtered
380
+ ```
381
+
382
+ #### 3.3 ์—์ง€ ๋ฐ€๋„ ํ•„ํ„ฐ
383
+
384
+ **ํŠน์ง•**: ์ƒˆ์šฐ๋Š” ์—์ง€๊ฐ€ ๋ถ€๋“œ๋Ÿฌ์›€ (์ž๋Š” ๊ฐ•ํ•œ ์ง์„  ์—์ง€)
385
+
386
+ ```python
387
+ def filter_by_edge_density(image, detections):
388
+ """์—์ง€ ๋ฐ€๋„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ"""
389
+ import cv2
390
+ import numpy as np
391
+
392
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
393
+ edges = cv2.Canny(gray_image, 50, 150)
394
+
395
+ filtered = []
396
+ for det in detections:
397
+ bbox = det['bbox']
398
+ x1, y1, x2, y2 = map(int, bbox)
399
+
400
+ # ROI ์—์ง€
401
+ roi_edges = edges[y1:y2, x1:x2]
402
+ if roi_edges.size == 0:
403
+ continue
404
+
405
+ # ์—์ง€ ํ”ฝ์…€ ๋น„์œจ
406
+ edge_density = np.sum(roi_edges > 0) / roi_edges.size
407
+
408
+ # ์ƒˆ์šฐ: ์ค‘๊ฐ„ ์—์ง€ ๋ฐ€๋„ (0.05~0.25)
409
+ # ์ž/๋งคํŠธ: ๋†’์€ ์—์ง€ ๋ฐ€๋„
410
+ if 0.03 < edge_density < 0.30:
411
+ filtered.append(det)
412
+
413
+ return filtered
414
+ ```
415
+
416
+ ---
417
+
418
+ ### Step 4: ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ (Context Filter)
419
+
420
+ **ํ•ต์‹ฌ**: ์ฃผ๋ณ€ ๊ฐ์ฒด์™€์˜ ๊ด€๊ณ„ (์œ„์น˜๋Š” ๋žœ๋ค์ด์ง€๋งŒ ์ƒ๋Œ€์  ๊ด€๊ณ„๋Š” ํ™œ์šฉ ๊ฐ€๋Šฅ)
421
+
422
+ #### 4.1 ์ž(Ruler) ๊ฒ€์ถœ ๋ฐ ํ™œ์šฉ
423
+
424
+ **์ „๋žต**: ์ž๊ฐ€ ๊ฒ€์ถœ๋˜๋ฉด ์ƒˆ์šฐ๋Š” ์ž ๊ทผ์ฒ˜์— ์žˆ์„ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ
425
+
426
+ ```python
427
+ def detect_ruler(detections):
428
+ """์ž(ruler) ๊ฒ€์ถœ"""
429
+
430
+ rulers = []
431
+ for det in detections:
432
+ bbox = det['bbox']
433
+ width = bbox[2] - bbox[0]
434
+ height = bbox[3] - bbox[1]
435
+
436
+ # ์ž ํŠน์ง•:
437
+ # - ๋งค์šฐ ๊ฐ€๋Š˜๊ณ  ๊ธด ํ˜•ํƒœ (aspect ratio > 10)
438
+ # - ์ง์„  ํŒจํ„ด
439
+
440
+ if height > 0:
441
+ aspect_ratio = width / height
442
+ if aspect_ratio > 10 or aspect_ratio < 0.1: # ๊ฐ€๋กœ ๋˜๋Š” ์„ธ๋กœ๋กœ ๋งค์šฐ ๊ธด
443
+ rulers.append(det)
444
+
445
+ return rulers
446
+
447
+ def filter_by_ruler_proximity(detections, rulers):
448
+ """์ž์™€์˜ ๊ทผ์ ‘๋„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ"""
449
+ if not rulers:
450
+ # ์ž๊ฐ€ ์—†์œผ๋ฉด ์ด ํ•„ํ„ฐ ์Šคํ‚ต
451
+ return detections
452
+
453
+ import numpy as np
454
+
455
+ filtered = []
456
+ for det in detections:
457
+ det_center = np.array([
458
+ (det['bbox'][0] + det['bbox'][2]) / 2,
459
+ (det['bbox'][1] + det['bbox'][3]) / 2
460
+ ])
461
+
462
+ # ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์ž์™€์˜ ๊ฑฐ๋ฆฌ
463
+ min_distance = float('inf')
464
+ for ruler in rulers:
465
+ ruler_center = np.array([
466
+ (ruler['bbox'][0] + ruler['bbox'][2]) / 2,
467
+ (ruler['bbox'][1] + ruler['bbox'][3]) / 2
468
+ ])
469
+ distance = np.linalg.norm(det_center - ruler_center)
470
+ min_distance = min(min_distance, distance)
471
+
472
+ # ์ž์™€ ์ ๋‹นํžˆ ๊ฐ€๊นŒ์šฐ๋ฉด ์ƒˆ์šฐ์ผ ๊ฐ€๋Šฅ์„ฑ ๋†’์Œ
473
+ # (๋„ˆ๋ฌด ๊ฐ€๊นŒ์šฐ๋ฉด ์ž ์ž์ฒด์ผ ์ˆ˜ ์žˆ์Œ)
474
+ if 50 < min_distance < 500:
475
+ filtered.append(det)
476
+
477
+ return filtered
478
+ ```
479
+
480
+ #### 4.2 ์†(Hand) ์ œ๊ฑฐ
481
+
482
+ **์ „๋žต**: ์†์ด ๊ฒ€์ถœ๋˜๋ฉด ์ œ์™ธ
483
+
484
+ ```python
485
+ def filter_out_hands(detections):
486
+ """์† ์ œ๊ฑฐ (COCO class_id = 0 ๋˜๋Š” ํŠน์ • ์กฐ๊ฑด)"""
487
+
488
+ filtered = []
489
+ for det in detections:
490
+ # COCO์—์„œ person = 0
491
+ # ์†์€ ๋ณดํ†ต person์œผ๋กœ ๊ฒ€์ถœ๋˜๊ฑฐ๋‚˜
492
+ # ํ”ผ๋ถ€์ƒ‰ + ํŠน์ • ํ˜•ํƒœ
493
+
494
+ # ๊ฐ„๋‹จํžˆ class_id๋กœ ์ œ๊ฑฐ
495
+ if det.get('class_id') == 0: # person
496
+ continue
497
+
498
+ filtered.append(det)
499
+
500
+ return filtered
501
+ ```
502
+
503
+ #### 4.3 ์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ (Centrality Score)
504
+
505
+ **์ „๋žต**: ์™„์ „ํžˆ ๋žœ๋ค์€ ์•„๋‹˜. ๋Œ€๋ถ€๋ถ„ ์ด๋ฏธ์ง€ ์ค‘์•™~์ค‘๊ฐ„ ์˜์—ญ์— ๋ฐฐ์น˜
506
+
507
+ ```python
508
+ def calculate_centrality_score(bbox, image_shape):
509
+ """์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ (0~1, ๋†’์„์ˆ˜๋ก ์ค‘์•™)"""
510
+ h, w = image_shape[:2]
511
+ image_center = np.array([w / 2, h / 2])
512
+
513
+ bbox_center = np.array([
514
+ (bbox[0] + bbox[2]) / 2,
515
+ (bbox[1] + bbox[3]) / 2
516
+ ])
517
+
518
+ # ์ค‘์‹ฌ์œผ๋กœ๋ถ€ํ„ฐ ๊ฑฐ๋ฆฌ
519
+ distance = np.linalg.norm(bbox_center - image_center)
520
+ max_distance = np.linalg.norm(np.array([w / 2, h / 2])) # ๋Œ€๊ฐ์„  ์ ˆ๋ฐ˜
521
+
522
+ # ๊ฑฐ๋ฆฌ๊ฐ€ 0์ด๋ฉด ์ ์ˆ˜ 1, ๋ฉ€์ˆ˜๋ก ์ ์ˆ˜ ๊ฐ์†Œ
523
+ centrality = 1 - (distance / max_distance)
524
+
525
+ return centrality
526
+
527
+ def add_centrality_scores(detections, image_shape):
528
+ """๊ฐ ๊ฒ€์ถœ์— ์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ ์ถ”๊ฐ€"""
529
+ for det in detections:
530
+ det['centrality_score'] = calculate_centrality_score(det['bbox'], image_shape)
531
+
532
+ return detections
533
+ ```
534
+
535
+ ---
536
+
537
+ ### Step 5: ํ›„์ฒ˜๋ฆฌ (Post-Processing)
538
+
539
+ #### 5.1 ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ
540
+
541
+ **์ „๋žต**: ์—ฌ๋Ÿฌ ํŠน์ง•์„ ์ข…ํ•ฉํ•˜์—ฌ ์ตœ์ข… ์ ์ˆ˜ ์‚ฐ์ถœ
542
+
543
+ ```python
544
+ def calculate_composite_score(detection, image_shape):
545
+ """์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ (0~100)"""
546
+
547
+ # ๊ฐ€์ค‘์น˜ (์กฐ์ • ๊ฐ€๋Šฅ)
548
+ WEIGHTS = {
549
+ 'confidence': 0.30, # RT-DETR ์‹ ๋ขฐ๋„
550
+ 'centrality': 0.15, # ์ค‘์‹ฌ์„ฑ
551
+ 'aspect_ratio': 0.20, # ์ข…ํšก๋น„ ์ ํ•ฉ๋„
552
+ 'size': 0.20, # ํฌ๊ธฐ ์ ํ•ฉ๋„
553
+ 'color': 0.15 # ์ƒ‰์ƒ ์ ํ•ฉ๋„
554
+ }
555
+
556
+ score = 0.0
557
+
558
+ # 1. ์‹ ๋ขฐ๋„
559
+ score += detection['confidence'] * WEIGHTS['confidence']
560
+
561
+ # 2. ์ค‘์‹ฌ์„ฑ
562
+ if 'centrality_score' in detection:
563
+ score += detection['centrality_score'] * WEIGHTS['centrality']
564
+
565
+ # 3. ์ข…ํšก๋น„ ์ ํ•ฉ๋„
566
+ bbox = detection['bbox']
567
+ width = bbox[2] - bbox[0]
568
+ height = bbox[3] - bbox[1]
569
+ if height > 0:
570
+ aspect_ratio = width / height
571
+ # ์ด์ƒ์  ์ข…ํšก๋น„: 3~6
572
+ ideal_aspect = 4.5
573
+ aspect_fitness = 1 - abs(aspect_ratio - ideal_aspect) / ideal_aspect
574
+ aspect_fitness = max(0, min(1, aspect_fitness)) # 0~1 clipping
575
+ score += aspect_fitness * WEIGHTS['aspect_ratio']
576
+
577
+ # 4. ํฌ๊ธฐ ์ ํ•ฉ๋„
578
+ h, w = image_shape[:2]
579
+ bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
580
+ area_ratio = bbox_area / (h * w)
581
+ # ์ด์ƒ์  ํฌ๊ธฐ: 10~30%
582
+ if 0.10 < area_ratio < 0.30:
583
+ size_fitness = 1.0
584
+ else:
585
+ size_fitness = 0.5
586
+ score += size_fitness * WEIGHTS['size']
587
+
588
+ # 5. ์ƒ‰์ƒ ์ ํ•ฉ๋„ (์ด๋ฏธ ํ•„ํ„ฐ๋ง ํ†ต๊ณผํ–ˆ์œผ๋ฉด 1.0)
589
+ if 'color_fitness' in detection:
590
+ score += detection['color_fitness'] * WEIGHTS['color']
591
+ else:
592
+ score += 0.5 * WEIGHTS['color'] # ๊ธฐ๋ณธ๊ฐ’
593
+
594
+ return score * 100 # 0~100 ์Šค์ผ€์ผ
595
+ ```
596
+
597
+ #### 5.2 ์ตœ์ข… ์„ ํƒ
598
+
599
+ **์ „๋žต**: ์ข…ํ•ฉ ์ ์ˆ˜ ์ƒ์œ„ N๊ฐœ ์„ ํƒ (์ธก์ •์€ ๋ณดํ†ต 1๋งˆ๋ฆฌ)
600
+
601
+ ```python
602
+ def select_top_detections(detections, image_shape, top_n=1):
603
+ """์ข…ํ•ฉ ์ ์ˆ˜ ๊ธฐ๋ฐ˜ ์ตœ์ข… ์„ ํƒ"""
604
+
605
+ # ์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ ์ถ”๊ฐ€
606
+ detections = add_centrality_scores(detections, image_shape)
607
+
608
+ # ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ
609
+ for det in detections:
610
+ det['composite_score'] = calculate_composite_score(det, image_shape)
611
+
612
+ # ์ ์ˆ˜์ˆœ ์ •๋ ฌ
613
+ detections = sorted(detections, key=lambda x: x['composite_score'], reverse=True)
614
+
615
+ # ์ƒ์œ„ N๊ฐœ ์„ ํƒ
616
+ return detections[:top_n]
617
+ ```
618
+
619
+ #### 5.3 NMS (Non-Maximum Suppression)
620
+
621
+ **์ „๋žต**: ์ค‘๋ณต ๊ฒ€์ถœ ์ œ๊ฑฐ
622
+
623
+ ```python
624
+ def apply_nms(detections, iou_threshold=0.5):
625
+ """NMS ์ ์šฉ"""
626
+ if len(detections) <= 1:
627
+ return detections
628
+
629
+ import numpy as np
630
+
631
+ # ์‹ ๋ขฐ๋„์ˆœ ์ •๋ ฌ
632
+ detections = sorted(detections, key=lambda x: x['confidence'], reverse=True)
633
+
634
+ keep = []
635
+ while detections:
636
+ best = detections.pop(0)
637
+ keep.append(best)
638
+
639
+ # ๋‚จ์€ ๊ฒƒ ์ค‘ IoU ๋†’์€ ๊ฒƒ ์ œ๊ฑฐ
640
+ filtered = []
641
+ for det in detections:
642
+ iou = calculate_iou(best['bbox'], det['bbox'])
643
+ if iou < iou_threshold:
644
+ filtered.append(det)
645
+
646
+ detections = filtered
647
+
648
+ return keep
649
+
650
+ def calculate_iou(bbox1, bbox2):
651
+ """IoU ๊ณ„์‚ฐ"""
652
+ x1_min, y1_min, x1_max, y1_max = bbox1
653
+ x2_min, y2_min, x2_max, y2_max = bbox2
654
+
655
+ # ๊ต์ง‘ํ•ฉ
656
+ inter_x_min = max(x1_min, x2_min)
657
+ inter_y_min = max(y1_min, y2_min)
658
+ inter_x_max = min(x1_max, x2_max)
659
+ inter_y_max = min(y1_max, y2_max)
660
+
661
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
662
+ return 0.0
663
+
664
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
665
+
666
+ # ํ•ฉ์ง‘ํ•ฉ
667
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
668
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
669
+ union_area = area1 + area2 - inter_area
670
+
671
+ return inter_area / union_area if union_area > 0 else 0.0
672
+ ```
673
+
674
+ ---
675
+
676
+ ## ๐ŸŒณ ์˜์‚ฌ๊ฒฐ์ • ํŠธ๋ฆฌ
677
+
678
+ ```
679
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
680
+ โ”‚ RT-DETR ๊ฒ€์ถœ ๊ฒฐ๊ณผ (N๊ฐœ ๊ฐ์ฒด) โ”‚
681
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
682
+ โ”‚
683
+ โ–ผ
684
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
685
+ โ”‚ Step 1: ์‚ฌ์ „ ํ•„ํ„ฐ โ”‚
686
+ โ”‚ โ€ข COCO ํด๋ž˜์Šค ํ•„ํ„ฐ (๋ช…๋ฐฑํžˆ ์ œ์™ธ) โ”‚
687
+ โ”‚ โ€ข ์‹ ๋ขฐ๋„ ํ•„ํ„ฐ (< 0.15 ์ œ๊ฑฐ) โ”‚
688
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
689
+ โ”‚
690
+ โ–ผ
691
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
692
+ โ”‚ ํ•„ํ„ฐ ํ†ต๊ณผ? โ”‚
693
+ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
694
+ NO YES
695
+ โ”‚ โ”‚
696
+ [์ œ๊ฑฐ] โ–ผ
697
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
698
+ โ”‚ Step 2: ๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ โ”‚
699
+ โ”‚ โ€ข ์ ˆ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ โ”‚
700
+ โ”‚ โ€ข ์ƒ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ โ”‚
701
+ โ”‚ โ€ข ์ข…ํšก๋น„ ํ•„ํ„ฐ (1.5 ~ 10) โ”‚
702
+ โ”‚ โ€ข ์ถฉ๋งŒ๋„ ํ•„ํ„ฐ (0.25 ~ 0.85) โ”‚
703
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
704
+ โ”‚
705
+ โ–ผ
706
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
707
+ โ”‚ ๋ชจ๋‘ ํ†ต๊ณผ? โ”‚
708
+ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
709
+ NO YES
710
+ โ”‚ โ”‚
711
+ [์ œ๊ฑฐ] โ–ผ
712
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
713
+ โ”‚ Step 3: ์‹œ๊ฐ์  ํ•„ํ„ฐ โ”‚
714
+ โ”‚ โ€ข ์ƒ‰์ƒ ํ•„ํ„ฐ (๋‚ฎ์€ ์ฑ„๋„, ๋†’์€ ๋ช…๋„) โ”‚
715
+ โ”‚ โ€ข ํ…์Šค์ฒ˜ ํ•„ํ„ฐ (๋ถ€๋“œ๋Ÿฌ์›€) โ”‚
716
+ โ”‚ โ€ข ์—์ง€ ๋ฐ€๋„ ํ•„ํ„ฐ (์ค‘๊ฐ„) โ”‚
717
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
718
+ โ”‚
719
+ โ–ผ
720
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
721
+ โ”‚ ํ†ต๊ณผ? โ”‚
722
+ โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
723
+ NO YES
724
+ โ”‚ โ”‚
725
+ [์ œ๊ฑฐ] โ–ผ
726
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
727
+ โ”‚ Step 4: ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ โ”‚
728
+ โ”‚ โ€ข ์ž(ruler) ๊ฒ€์ถœ ๋ฐ ๊ทผ์ ‘๋„ โ”‚
729
+ โ”‚ โ€ข ์† ์ œ๊ฑฐ โ”‚
730
+ โ”‚ โ€ข ์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ โ”‚
731
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
732
+ โ”‚
733
+ โ–ผ
734
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
735
+ โ”‚ Step 5: ํ›„์ฒ˜๋ฆฌ โ”‚
736
+ โ”‚ โ€ข ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ โ”‚
737
+ โ”‚ โ€ข ์ƒ์œ„ N๊ฐœ ์„ ํƒ (๋ณดํ†ต 1๊ฐœ) โ”‚
738
+ โ”‚ โ€ข NMS ์ ์šฉ โ”‚
739
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
740
+ โ”‚
741
+ โ–ผ
742
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
743
+ โ”‚ ์ตœ์ข… ๊ฒ€์ถœ ๊ฒฐ๊ณผ โ”‚
744
+ โ”‚ (์ƒˆ์šฐ๋งŒ) โ”‚
745
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
746
+ ```
747
+
748
+ ---
749
+
750
+ ## ๐Ÿ’ป ํ†ตํ•ฉ ๊ตฌํ˜„ ์ฝ”๋“œ
751
+
752
+ ```python
753
+ # -*- coding: utf-8 -*-
754
+ """
755
+ RT-DETR + ์Šค๋งˆํŠธ ํ•„ํ„ฐ๋ง ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ
756
+ """
757
+
758
+ import cv2
759
+ import numpy as np
760
+ from typing import List, Dict, Tuple
761
+
762
+ class SmartShrimpFilter:
763
+ """์ธก์ •์šฉ ์ƒˆ์šฐ ๊ฒ€์ถœ ์Šค๋งˆํŠธ ํ•„ํ„ฐ"""
764
+
765
+ def __init__(self):
766
+ # ํŒŒ๋ผ๋ฏธํ„ฐ (์กฐ์ • ๊ฐ€๋Šฅ)
767
+ self.params = {
768
+ # Step 1: ์‚ฌ์ „ ํ•„ํ„ฐ
769
+ 'min_confidence': 0.15,
770
+
771
+ # Step 2: ๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ
772
+ 'min_width': 50,
773
+ 'min_height': 20,
774
+ 'min_area': 2000,
775
+ 'max_area_ratio': 0.6,
776
+ 'min_area_ratio': 0.03,
777
+ 'max_area_ratio': 0.50,
778
+ 'min_aspect_ratio': 1.5,
779
+ 'max_aspect_ratio': 10.0,
780
+ 'min_solidity': 0.25,
781
+ 'max_solidity': 0.85,
782
+
783
+ # Step 3: ์‹œ๊ฐ์  ํ•„ํ„ฐ
784
+ 'max_saturation': 120,
785
+ 'min_value': 80,
786
+ 'max_texture_std': 50,
787
+ 'min_edge_density': 0.03,
788
+ 'max_edge_density': 0.30,
789
+
790
+ # Step 4: ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ
791
+ 'ruler_min_aspect': 10.0,
792
+ 'ruler_max_aspect': 0.1,
793
+ 'min_ruler_distance': 50,
794
+ 'max_ruler_distance': 500,
795
+
796
+ # Step 5: ํ›„์ฒ˜๋ฆฌ
797
+ 'top_n': 1,
798
+ 'nms_iou_threshold': 0.5,
799
+ }
800
+
801
+ def filter(self, image: np.ndarray, detections: List[Dict]) -> List[Dict]:
802
+ """
803
+ ์ „์ฒด ํ•„ํ„ฐ๋ง ํŒŒ์ดํ”„๋ผ์ธ
804
+
805
+ Args:
806
+ image: ์ž…๋ ฅ ์ด๋ฏธ์ง€ (BGR)
807
+ detections: RT-DETR ๊ฒ€์ถœ ๊ฒฐ๊ณผ
808
+ [{'bbox': [x1,y1,x2,y2], 'confidence': 0.9, 'class_id': 0}, ...]
809
+
810
+ Returns:
811
+ ํ•„ํ„ฐ๋ง๋œ ๊ฒ€์ถœ ๊ฒฐ๊ณผ (์ƒˆ์šฐ๋งŒ)
812
+ """
813
+
814
+ print(f"[INFO] Initial detections: {len(detections)}")
815
+
816
+ # Step 1: ์‚ฌ์ „ ํ•„ํ„ฐ
817
+ detections = self._pre_filter(detections)
818
+ print(f"[STEP 1] After pre-filter: {len(detections)}")
819
+
820
+ if not detections:
821
+ return []
822
+
823
+ # Step 2: ๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ
824
+ detections = self._physical_filter(detections, image.shape)
825
+ print(f"[STEP 2] After physical filter: {len(detections)}")
826
+
827
+ if not detections:
828
+ return []
829
+
830
+ # Step 3: ์‹œ๊ฐ์  ํ•„ํ„ฐ
831
+ detections = self._visual_filter(image, detections)
832
+ print(f"[STEP 3] After visual filter: {len(detections)}")
833
+
834
+ if not detections:
835
+ return []
836
+
837
+ # Step 4: ์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ
838
+ detections = self._context_filter(detections, image.shape)
839
+ print(f"[STEP 4] After context filter: {len(detections)}")
840
+
841
+ if not detections:
842
+ return []
843
+
844
+ # Step 5: ํ›„์ฒ˜๋ฆฌ
845
+ detections = self._post_process(detections, image.shape)
846
+ print(f"[STEP 5] Final detections: {len(detections)}")
847
+
848
+ return detections
849
+
850
+ def _pre_filter(self, detections: List[Dict]) -> List[Dict]:
851
+ """์‚ฌ์ „ ํ•„ํ„ฐ: COCO ํด๋ž˜์Šค + ์‹ ๋ขฐ๋„"""
852
+
853
+ # ์ œ์™ธํ•  COCO ํด๋ž˜์Šค
854
+ EXCLUDE_CLASSES = {0} # person (์† ๋“ฑ)
855
+
856
+ filtered = []
857
+ for det in detections:
858
+ # ํด๋ž˜์Šค ํ•„ํ„ฐ
859
+ if det.get('class_id', -1) in EXCLUDE_CLASSES:
860
+ continue
861
+
862
+ # ์‹ ๋ขฐ๋„ ํ•„ํ„ฐ
863
+ if det['confidence'] < self.params['min_confidence']:
864
+ continue
865
+
866
+ filtered.append(det)
867
+
868
+ return filtered
869
+
870
+ def _physical_filter(self, detections: List[Dict], image_shape: Tuple) -> List[Dict]:
871
+ """๋ฌผ๋ฆฌ์  ํ•„ํ„ฐ: ํฌ๊ธฐ, ํ˜•ํƒœ"""
872
+
873
+ h, w = image_shape[:2]
874
+ image_area = h * w
875
+
876
+ filtered = []
877
+ for det in detections:
878
+ bbox = det['bbox']
879
+ x1, y1, x2, y2 = bbox
880
+
881
+ bbox_width = x2 - x1
882
+ bbox_height = y2 - y1
883
+ bbox_area = bbox_width * bbox_height
884
+
885
+ # ์ ˆ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ
886
+ if bbox_width < self.params['min_width']:
887
+ continue
888
+ if bbox_height < self.params['min_height']:
889
+ continue
890
+ if bbox_area < self.params['min_area']:
891
+ continue
892
+
893
+ # ์ƒ๋Œ€ ํฌ๊ธฐ ํ•„ํ„ฐ
894
+ area_ratio = bbox_area / image_area
895
+ if not (self.params['min_area_ratio'] < area_ratio < self.params['max_area_ratio']):
896
+ continue
897
+
898
+ # ์ข…ํšก๋น„ ํ•„ํ„ฐ
899
+ if bbox_height == 0:
900
+ continue
901
+ aspect_ratio = bbox_width / bbox_height
902
+ if not (self.params['min_aspect_ratio'] < aspect_ratio < self.params['max_aspect_ratio']):
903
+ continue
904
+
905
+ filtered.append(det)
906
+
907
+ return filtered
908
+
909
+ def _visual_filter(self, image: np.ndarray, detections: List[Dict]) -> List[Dict]:
910
+ """์‹œ๊ฐ์  ํ•„ํ„ฐ: ์ƒ‰์ƒ, ํ…์Šค์ฒ˜"""
911
+
912
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
913
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
914
+
915
+ filtered = []
916
+ for det in detections:
917
+ bbox = det['bbox']
918
+ x1, y1, x2, y2 = map(int, bbox)
919
+
920
+ # ROI ์ถ”์ถœ
921
+ roi_hsv = hsv_image[y1:y2, x1:x2]
922
+ roi_gray = gray_image[y1:y2, x1:x2]
923
+
924
+ if roi_hsv.size == 0 or roi_gray.size == 0:
925
+ continue
926
+
927
+ # ์ƒ‰์ƒ ํ•„ํ„ฐ
928
+ mean_h, mean_s, mean_v = roi_hsv.mean(axis=(0, 1))
929
+ if not (mean_s < self.params['max_saturation'] and mean_v > self.params['min_value']):
930
+ continue
931
+
932
+ # ํ…์Šค์ฒ˜ ํ•„ํ„ฐ
933
+ texture_std = np.std(roi_gray)
934
+ if texture_std > self.params['max_texture_std']:
935
+ continue
936
+
937
+ filtered.append(det)
938
+
939
+ return filtered
940
+
941
+ def _context_filter(self, detections: List[Dict], image_shape: Tuple) -> List[Dict]:
942
+ """์ปจํ…์ŠคํŠธ ํ•„ํ„ฐ: ์ž ๊ฒ€์ถœ, ์ค‘์‹ฌ์„ฑ"""
943
+
944
+ # ์ž ๊ฒ€์ถœ
945
+ rulers = self._detect_rulers(detections)
946
+
947
+ # ์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ ์ถ”๊ฐ€
948
+ for det in detections:
949
+ det['centrality_score'] = self._calculate_centrality(det['bbox'], image_shape)
950
+
951
+ # ์ž ๊ทผ์ ‘๋„ ํ•„ํ„ฐ (์ž๊ฐ€ ์žˆ์œผ๋ฉด)
952
+ if rulers:
953
+ filtered = []
954
+ for det in detections:
955
+ if self._is_near_ruler(det, rulers):
956
+ filtered.append(det)
957
+ return filtered if filtered else detections
958
+ else:
959
+ return detections
960
+
961
+ def _detect_rulers(self, detections: List[Dict]) -> List[Dict]:
962
+ """์ž ๊ฒ€์ถœ"""
963
+ rulers = []
964
+ for det in detections:
965
+ bbox = det['bbox']
966
+ width = bbox[2] - bbox[0]
967
+ height = bbox[3] - bbox[1]
968
+
969
+ if height > 0:
970
+ aspect_ratio = width / height
971
+ if aspect_ratio > self.params['ruler_min_aspect'] or aspect_ratio < self.params['ruler_max_aspect']:
972
+ rulers.append(det)
973
+
974
+ return rulers
975
+
976
+ def _is_near_ruler(self, det: Dict, rulers: List[Dict]) -> bool:
977
+ """์ž ๊ทผ์ฒ˜์— ์žˆ๋Š”์ง€ ํ™•์ธ"""
978
+ det_center = np.array([
979
+ (det['bbox'][0] + det['bbox'][2]) / 2,
980
+ (det['bbox'][1] + det['bbox'][3]) / 2
981
+ ])
982
+
983
+ for ruler in rulers:
984
+ ruler_center = np.array([
985
+ (ruler['bbox'][0] + ruler['bbox'][2]) / 2,
986
+ (ruler['bbox'][1] + ruler['bbox'][3]) / 2
987
+ ])
988
+ distance = np.linalg.norm(det_center - ruler_center)
989
+
990
+ if self.params['min_ruler_distance'] < distance < self.params['max_ruler_distance']:
991
+ return True
992
+
993
+ return False
994
+
995
+ def _calculate_centrality(self, bbox: List[float], image_shape: Tuple) -> float:
996
+ """์ค‘์‹ฌ์„ฑ ์ ์ˆ˜ ๊ณ„์‚ฐ"""
997
+ h, w = image_shape[:2]
998
+ image_center = np.array([w / 2, h / 2])
999
+
1000
+ bbox_center = np.array([
1001
+ (bbox[0] + bbox[2]) / 2,
1002
+ (bbox[1] + bbox[3]) / 2
1003
+ ])
1004
+
1005
+ distance = np.linalg.norm(bbox_center - image_center)
1006
+ max_distance = np.linalg.norm(np.array([w / 2, h / 2]))
1007
+
1008
+ return 1 - (distance / max_distance)
1009
+
1010
+ def _post_process(self, detections: List[Dict], image_shape: Tuple) -> List[Dict]:
1011
+ """ํ›„์ฒ˜๋ฆฌ: ์ข…ํ•ฉ ์ ์ˆ˜, NMS"""
1012
+
1013
+ # ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ
1014
+ for det in detections:
1015
+ det['composite_score'] = self._calculate_composite_score(det, image_shape)
1016
+
1017
+ # ์ ์ˆ˜์ˆœ ์ •๋ ฌ
1018
+ detections = sorted(detections, key=lambda x: x['composite_score'], reverse=True)
1019
+
1020
+ # NMS
1021
+ detections = self._apply_nms(detections)
1022
+
1023
+ # ์ƒ์œ„ N๊ฐœ
1024
+ return detections[:self.params['top_n']]
1025
+
1026
+ def _calculate_composite_score(self, det: Dict, image_shape: Tuple) -> float:
1027
+ """์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ (0~100)"""
1028
+ score = 0.0
1029
+
1030
+ # ์‹ ๋ขฐ๋„ (30%)
1031
+ score += det['confidence'] * 0.30
1032
+
1033
+ # ์ค‘์‹ฌ์„ฑ (15%)
1034
+ if 'centrality_score' in det:
1035
+ score += det['centrality_score'] * 0.15
1036
+
1037
+ # ์ข…ํšก๋น„ ์ ํ•ฉ๋„ (20%)
1038
+ bbox = det['bbox']
1039
+ width = bbox[2] - bbox[0]
1040
+ height = bbox[3] - bbox[1]
1041
+ if height > 0:
1042
+ aspect_ratio = width / height
1043
+ ideal_aspect = 4.5
1044
+ aspect_fitness = max(0, 1 - abs(aspect_ratio - ideal_aspect) / ideal_aspect)
1045
+ score += aspect_fitness * 0.20
1046
+
1047
+ # ํฌ๊ธฐ ์ ํ•ฉ๋„ (20%)
1048
+ h, w = image_shape[:2]
1049
+ bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
1050
+ area_ratio = bbox_area / (h * w)
1051
+ size_fitness = 1.0 if 0.10 < area_ratio < 0.30 else 0.5
1052
+ score += size_fitness * 0.20
1053
+
1054
+ # ์ƒ‰์ƒ ์ ํ•ฉ๋„ (15%)
1055
+ score += 0.15
1056
+
1057
+ return score * 100
1058
+
1059
+ def _apply_nms(self, detections: List[Dict]) -> List[Dict]:
1060
+ """NMS ์ ์šฉ"""
1061
+ if len(detections) <= 1:
1062
+ return detections
1063
+
1064
+ keep = []
1065
+ detections = sorted(detections, key=lambda x: x['confidence'], reverse=True)
1066
+
1067
+ while detections:
1068
+ best = detections.pop(0)
1069
+ keep.append(best)
1070
+
1071
+ filtered = []
1072
+ for det in detections:
1073
+ iou = self._calculate_iou(best['bbox'], det['bbox'])
1074
+ if iou < self.params['nms_iou_threshold']:
1075
+ filtered.append(det)
1076
+
1077
+ detections = filtered
1078
+
1079
+ return keep
1080
+
1081
+ @staticmethod
1082
+ def _calculate_iou(bbox1: List[float], bbox2: List[float]) -> float:
1083
+ """IoU ๊ณ„์‚ฐ"""
1084
+ x1_min, y1_min, x1_max, y1_max = bbox1
1085
+ x2_min, y2_min, x2_max, y2_max = bbox2
1086
+
1087
+ inter_x_min = max(x1_min, x2_min)
1088
+ inter_y_min = max(y1_min, y2_min)
1089
+ inter_x_max = min(x1_max, x2_max)
1090
+ inter_y_max = min(y1_max, y2_max)
1091
+
1092
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
1093
+ return 0.0
1094
+
1095
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
1096
+
1097
+ area1 = (x1_max - x1_min) * (y1_max - y1_min)
1098
+ area2 = (x2_max - x2_min) * (y2_max - y2_min)
1099
+ union_area = area1 + area2 - inter_area
1100
+
1101
+ return inter_area / union_area if union_area > 0 else 0.0
1102
+
1103
+
1104
+ # ์‚ฌ์šฉ ์˜ˆ์‹œ
1105
+ if __name__ == "__main__":
1106
+ # RT-DETR ๊ฒ€์ถœ ๊ฒฐ๊ณผ (์˜ˆ์‹œ)
1107
+ rtdetr_detections = [
1108
+ {'bbox': [100, 200, 400, 250], 'confidence': 0.85, 'class_id': 1},
1109
+ {'bbox': [50, 50, 100, 500], 'confidence': 0.92, 'class_id': 2}, # ์ž
1110
+ {'bbox': [200, 300, 350, 380], 'confidence': 0.45, 'class_id': 0}, # ์†
1111
+ ]
1112
+
1113
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ
1114
+ image = cv2.imread("test_image.jpg")
1115
+
1116
+ # ํ•„ํ„ฐ ์ ์šฉ
1117
+ smart_filter = SmartShrimpFilter()
1118
+ shrimp_detections = smart_filter.filter(image, rtdetr_detections)
1119
+
1120
+ print(f"Final shrimp detections: {len(shrimp_detections)}")
1121
+ for i, det in enumerate(shrimp_detections, 1):
1122
+ print(f" Shrimp #{i}: confidence={det['confidence']:.2f}, score={det['composite_score']:.2f}")
1123
+ ```
1124
+
1125
+ ---
1126
+
1127
+ ## ๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ
1128
+
1129
+ ### ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค (MECE)
1130
+
1131
+ ```
1132
+ A. ์ด๋ฏธ์ง€ ์œ ํ˜•๋ณ„
1133
+ โ”œโ”€โ”€ A1. ์ƒˆ์šฐ๋งŒ (๋ฐฐ๊ฒฝ ๊นจ๋—)
1134
+ โ”œโ”€โ”€ A2. ์ƒˆ์šฐ + ์ž
1135
+ โ”œ๏ฟฝ๏ฟฝโ”€ A3. ์ƒˆ์šฐ + ์ž + ์†
1136
+ โ”œโ”€โ”€ A4. ์ƒˆ์šฐ + ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ
1137
+ โ””โ”€โ”€ A5. ์ƒˆ์šฐ ์—†์Œ (Negative)
1138
+
1139
+ B. ์ƒˆ์šฐ ์œ„์น˜๋ณ„
1140
+ โ”œโ”€โ”€ B1. ์ค‘์•™
1141
+ โ”œโ”€โ”€ B2. ์ขŒ์ธก
1142
+ โ”œโ”€โ”€ B3. ์šฐ์ธก
1143
+ โ”œโ”€โ”€ B4. ์ƒ๋‹จ
1144
+ โ””โ”€โ”€ B5. ํ•˜๋‹จ
1145
+
1146
+ C. ์ƒˆ์šฐ ํฌ๊ธฐ๋ณ„
1147
+ โ”œโ”€โ”€ C1. ์ž‘์€ ์ƒˆ์šฐ (< 8cm)
1148
+ โ”œโ”€โ”€ C2. ์ค‘๊ฐ„ ์ƒˆ์šฐ (8-12cm)
1149
+ โ””โ”€โ”€ C3. ํฐ ์ƒˆ์šฐ (> 12cm)
1150
+
1151
+ D. ์กฐ๋ช… ์กฐ๊ฑด๋ณ„
1152
+ โ”œโ”€โ”€ D1. ๋ฐ์€ ์กฐ๋ช…
1153
+ โ”œโ”€โ”€ D2. ์–ด๋‘์šด ์กฐ๋ช…
1154
+ โ””โ”€โ”€ D3. ๊ทธ๋ฆผ์ž ํฌํ•จ
1155
+ ```
1156
+
1157
+ ### ํ‰๊ฐ€ ์ง€ํ‘œ
1158
+
1159
+ ```python
1160
+ def evaluate_performance(predictions, ground_truths):
1161
+ """์„ฑ๋Šฅ ํ‰๊ฐ€"""
1162
+
1163
+ TP = 0 # True Positive: ์ƒˆ์šฐ๋ฅผ ์ƒˆ์šฐ๋กœ ๊ฒ€์ถœ
1164
+ FP = 0 # False Positive: ์ƒˆ์šฐ ์•„๋‹Œ ๊ฒƒ์„ ์ƒˆ์šฐ๋กœ ๊ฒ€์ถœ
1165
+ FN = 0 # False Negative: ์ƒˆ์šฐ๋ฅผ ๊ฒ€์ถœ ๋ชปํ•จ
1166
+
1167
+ # ... IoU ๊ธฐ๋ฐ˜ ๋งค์นญ ๋กœ์ง ...
1168
+
1169
+ precision = TP / (TP + FP) if (TP + FP) > 0 else 0
1170
+ recall = TP / (TP + FN) if (TP + FN) > 0 else 0
1171
+ f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
1172
+
1173
+ return {
1174
+ 'precision': precision,
1175
+ 'recall': recall,
1176
+ 'f1_score': f1_score
1177
+ }
1178
+ ```
1179
+
1180
+ ### ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹
1181
+
1182
+ ```python
1183
+ # ํŒŒ๋ผ๋ฏธํ„ฐ ๊ทธ๋ฆฌ๋“œ ์„œ์น˜
1184
+ param_grid = {
1185
+ 'min_confidence': [0.10, 0.15, 0.20],
1186
+ 'min_aspect_ratio': [1.0, 1.5, 2.0],
1187
+ 'max_aspect_ratio': [8.0, 10.0, 12.0],
1188
+ 'max_saturation': [100, 120, 150],
1189
+ }
1190
+
1191
+ best_f1 = 0
1192
+ best_params = None
1193
+
1194
+ for params in itertools.product(*param_grid.values()):
1195
+ # ํ…Œ์ŠคํŠธ...
1196
+ f1 = evaluate(params)
1197
+ if f1 > best_f1:
1198
+ best_f1 = f1
1199
+ best_params = params
1200
+ ```
1201
+
1202
+ ---
1203
+
1204
+ ## ๐Ÿ“ˆ ์˜ˆ์ƒ ์„ฑ๋Šฅ
1205
+
1206
+ ### ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์ •ํ™•๋„
1207
+
1208
+ | ์‹œ๋‚˜๋ฆฌ์˜ค | ์˜ˆ์ƒ Precision | ์˜ˆ์ƒ Recall | ์˜ˆ์ƒ F1 |
1209
+ |---------|---------------|-------------|---------|
1210
+ | ์ƒˆ์šฐ๋งŒ (๊นจ๋—) | 85% | 90% | 87% |
1211
+ | ์ƒˆ์šฐ + ์ž | 75% | 85% | 80% |
1212
+ | ์ƒˆ์šฐ + ์ž + ์† | 65% | 75% | 70% |
1213
+ | ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ | 55% | 65% | 60% |
1214
+ | **ํ‰๊ท ** | **70%** | **79%** | **74%** |
1215
+
1216
+ ### ์†๋„ (CPU ๊ธฐ์ค€)
1217
+
1218
+ - **RT-DETR ์ถ”๋ก **: 1.0์ดˆ
1219
+ - **ํ•„ํ„ฐ๋ง ํŒŒ์ดํ”„๋ผ์ธ**: 0.3์ดˆ
1220
+ - **์ด ์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ~1.3์ดˆ/์ด๋ฏธ์ง€
1221
+
1222
+ ---
1223
+
1224
+ ## ๐Ÿ”ง ์ถ”๊ฐ€ ๊ฐœ์„  ๋ฐฉ์•ˆ
1225
+
1226
+ ### ๋‹จ๊ธฐ (1์ฃผ)
1227
+ 1. **ํŒŒ๋ผ๋ฏธํ„ฐ ์ž๋™ ํŠœ๋‹**: Grid Search๋กœ ์ตœ์  ํŒŒ๋ผ๋ฏธํ„ฐ ์ฐพ๊ธฐ
1228
+ 2. **๋กœ๊น… ์‹œ์Šคํ…œ**: ๊ฐ ํ•„ํ„ฐ ๋‹จ๊ณ„๋ณ„ ํ†ต๊ณ„ ์ˆ˜์ง‘
1229
+ 3. **์‹คํŒจ ์‚ฌ๋ก€ ๋ถ„์„**: ๊ฒ€์ถœ ์‹คํŒจ ์ด๋ฏธ์ง€ ๋ถ„์„ ๋ฐ ๊ฐœ์„ 
1230
+
1231
+ ### ์ค‘๊ธฐ (2-4์ฃผ)
1232
+ 1. **์•™์ƒ๋ธ”**: ์—ฌ๋Ÿฌ ํ•„ํ„ฐ ์กฐํ•ฉ ์•™์ƒ๋ธ”
1233
+ 2. **ํ•™์Šต ๊ธฐ๋ฐ˜ ํ›„์ฒ˜๋ฆฌ**: ๊ฐ„๋‹จํ•œ ๋ถ„๋ฅ˜๊ธฐ ์ถ”๊ฐ€ (์ƒˆ์šฐ vs ๋น„์ƒˆ์šฐ)
1234
+ 3. **์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ**: ์ˆ˜๋™ ๋ผ๋ฒจ๋ง ๊ฒฐ๊ณผ ๋ฐ˜์˜
1235
+
1236
+ ### ์žฅ๊ธฐ
1237
+ 1. **Roboflow ํŒŒ์ธํŠœ๋‹**: ๊ทผ๋ณธ์  ํ•ด๊ฒฐ
1238
+ 2. **์ž์ฒด ๋ชจ๋ธ ํ•™์Šต**: YOLOv8 ๋“ฑ
1239
+
1240
+ ---
1241
+
1242
+ ## ๐Ÿ“š ์ฐธ๊ณ 
1243
+
1244
+ - [RT-DETR Paper](https://arxiv.org/abs/2304.08069)
1245
+ - [COCO Dataset Classes](https://cocodataset.org/#explore)
1246
+ - [OpenCV Documentation](https://docs.opencv.org/)
1247
+
1248
+ ---
1249
+
1250
+ **์ž‘์„ฑ์ผ**: 2025-11-07
1251
+ **๋ฒ„์ „**: 1.0 (MECE)
1252
+ **์ž‘์„ฑ์ž**: VIDraft Team
docs/testing_framework_guide.md ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ๐Ÿงช ์ƒˆ์šฐ ๊ฒ€์ถœ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ ๊ฐ€์ด๋“œ
2
+
3
+ ## ๐Ÿ“‹ ๊ฐœ์š”
4
+
5
+ ์ด ๋ฌธ์„œ๋Š” RT-DETR + Universal Shrimp Filter ๊ฒ€์ถœ ์‹œ์Šคํ…œ์˜ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ ํ”„๋ ˆ์ž„์›Œํฌ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.
6
+
7
+ ์ž‘์„ฑ์ผ: 2025-11-09
8
+
9
+ ---
10
+
11
+ ## ๐ŸŽฏ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ ๊ตฌ์„ฑ
12
+
13
+ ### 1. **์‹œ๊ฐ์  ๊ฒ€์ฆ** (`test_visual_validation.py`)
14
+ - **๋ชฉ์ **: ๊ฒ€์ถœ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ฏธ์ง€๋กœ ์‹œ๊ฐํ™”ํ•˜์—ฌ ๋น ๋ฅด๊ฒŒ ํ™•์ธ
15
+ - **์‚ฌ์šฉ ์‹œ๊ธฐ**: ์ดˆ๊ธฐ ํ…Œ์ŠคํŠธ, ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ • ์ „ํ›„ ๋น„๊ต
16
+ - **์ถœ๋ ฅ**: ๋ณ‘๋ ฌ ๋น„๊ต ์ด๋ฏธ์ง€ (์ „์ฒด ๊ฒ€์ถœ vs ํ•„ํ„ฐ๋ง ํ›„)
17
+
18
+ ### 2. **์ •๋Ÿ‰์  ํ‰๊ฐ€** (`test_quantitative_evaluation.py`)
19
+ - **๋ชฉ์ **: Ground Truth์™€ ๋น„๊ตํ•˜์—ฌ ์ •ํ™•๋„ ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ
20
+ - **์‚ฌ์šฉ ์‹œ๊ธฐ**: ์ •ํ™•ํ•œ ์„ฑ๋Šฅ ์ธก์ •, ๋ชจ๋ธ ํŠœ๋‹
21
+ - **์ถœ๋ ฅ**: Precision, Recall, F1 Score, Confusion Matrix, PR Curve
22
+
23
+ ### 3. **๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ** (`interactive_validation.py`)
24
+ - **๋ชฉ์ **: ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •ํ•˜๋ฉฐ ๊ฒฐ๊ณผ ํ™•์ธ
25
+ - **์‚ฌ์šฉ ์‹œ๊ธฐ**: ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™”, ๋””๋ฒ„๊น…
26
+ - **์ถœ๋ ฅ**: Gradio ์›น ์ธํ„ฐํŽ˜์ด์Šค
27
+
28
+ ---
29
+
30
+ ## ๐Ÿš€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
31
+
32
+ ### 1๏ธโƒฃ ์‹œ๊ฐ์  ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ
33
+
34
+ **์‹คํ–‰:**
35
+ ```bash
36
+ python test_visual_validation.py
37
+ ```
38
+
39
+ **์„ค์ • ๋ณ€๊ฒฝ:**
40
+ ```python
41
+ run_visual_test(
42
+ test_image_dir="imgs", # ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€ ๋””๋ ‰ํ† ๋ฆฌ
43
+ confidence=0.3, # RT-DETR ์‹ ๋ขฐ๋„ (0.1~0.9)
44
+ filter_threshold=50 # ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’ (20~80)
45
+ )
46
+ ```
47
+
48
+ **์ถœ๋ ฅ:**
49
+ - `test_results/result_*.png` - ์‹œ๊ฐํ™” ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€
50
+ - `test_results/test_results_YYYYMMDD_HHMMSS.json` - ์ƒ์„ธ ๊ฒ€์ถœ ์ •๋ณด
51
+
52
+ **๊ฒฐ๊ณผ ํ•ด์„:**
53
+ - **์™ผ์ชฝ ์ด๋ฏธ์ง€**: ์ „์ฒด ๊ฒ€์ถœ (ํšŒ์ƒ‰ ๋ฐ•์Šค) + ํ•„ํ„ฐ๋ง ํ†ต๊ณผ (์ปฌ๋Ÿฌ ๋ฐ•์Šค)
54
+ - **์˜ค๋ฅธ์ชฝ ์ด๋ฏธ์ง€**: ํ•„ํ„ฐ๋ง ํ†ต๊ณผ๋งŒ ํ‘œ์‹œ
55
+ - **์ƒ‰์ƒ ์˜๋ฏธ**:
56
+ - ๐ŸŸข ๋…น์ƒ‰ (75์  ์ด์ƒ): ๋†’์€ ํ™•๋ฅ ๋กœ ์ƒˆ์šฐ
57
+ - ๐ŸŸก ๋…ธ๋ž€์ƒ‰ (50~74์ ): ์ค‘๊ฐ„ ํ™•๋ฅ 
58
+ - ๐ŸŸ  ์ฃผํ™ฉ์ƒ‰ (50์  ๋ฏธ๋งŒ): ๋‚ฎ์€ ํ™•๋ฅ 
59
+
60
+ ---
61
+
62
+ ### 2๏ธโƒฃ ์ •๋Ÿ‰์  ํ‰๊ฐ€ ํ…Œ์ŠคํŠธ
63
+
64
+ **์‚ฌ์ „ ์ค€๋น„:**
65
+ 1. `ground_truth.json` ํŒŒ์ผ ์ƒ์„ฑ
66
+ 2. ์ˆ˜๋™์œผ๋กœ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ผ๋ฒจ๋ง
67
+
68
+ **ground_truth.json ํ˜•์‹:**
69
+ ```json
70
+ {
71
+ "test_shrimp_tank.png": [
72
+ {"bbox": [100, 150, 200, 180], "label": "shrimp"},
73
+ {"bbox": [300, 250, 420, 290], "label": "shrimp"}
74
+ ],
75
+ "image (1).webp": [
76
+ {"bbox": [500, 600, 800, 700], "label": "shrimp"}
77
+ ]
78
+ }
79
+ ```
80
+
81
+ **์‹คํ–‰:**
82
+ ```bash
83
+ python test_quantitative_evaluation.py
84
+ ```
85
+
86
+ **์ถœ๋ ฅ:**
87
+ - `test_results/quantitative_*/eval_*.png` - ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
88
+ - `test_results/quantitative_*/confusion_matrix.png` - ํ˜ผ๋™ ํ–‰๋ ฌ
89
+ - `test_results/quantitative_*/evaluation_summary.json` - ํ‰๊ฐ€ ์š”์•ฝ
90
+
91
+ **๋ฉ”ํŠธ๋ฆญ ํ•ด์„:**
92
+ - **Precision (์ •๋ฐ€๋„)**: ๊ฒ€์ถœํ•œ ๊ฒƒ ์ค‘ ์‹ค์ œ ์ƒˆ์šฐ ๋น„์œจ (๋†’์„์ˆ˜๋ก ์˜ค๊ฒ€์ถœ ์ ์Œ)
93
+ - **Recall (์žฌํ˜„์œจ)**: ์‹ค์ œ ์ƒˆ์šฐ ์ค‘ ๊ฒ€์ถœํ•œ ๋น„์œจ (๋†’์„์ˆ˜๋ก ๋ฏธ๊ฒ€์ถœ ์ ์Œ)
94
+ - **F1 Score**: Precision๊ณผ Recall์˜ ์กฐํ™” ํ‰๊ท  (์ „์ฒด ์„ฑ๋Šฅ ์ง€ํ‘œ)
95
+
96
+ ---
97
+
98
+ ### 3๏ธโƒฃ ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ
99
+
100
+ **์‹คํ–‰:**
101
+ ```bash
102
+ python interactive_validation.py
103
+ ```
104
+
105
+ **์ ‘์†:**
106
+ ```
107
+ http://localhost:7861
108
+ ```
109
+
110
+ **์‚ฌ์šฉ๋ฒ•:**
111
+
112
+ **ํƒญ 1: ๐Ÿค– ์ž๋™ ๊ฒ€์ถœ**
113
+ 1. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
114
+ 2. ์Šฌ๋ผ์ด๋”๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •:
115
+ - **RT-DETR ์‹ ๋ขฐ๋„** (0.1~0.9): ๋‚ฎ์„์ˆ˜๋ก ๋” ๋งŽ์ด ๊ฒ€์ถœ
116
+ - **ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’** (20~80): ๋†’์„์ˆ˜๋ก ์—„๊ฒฉํ•˜๊ฒŒ ํ•„ํ„ฐ๋ง
117
+ 3. "์ „์ฒด ๊ฒ€์ถœ ๊ฒฐ๊ณผ ํ‘œ์‹œ" ์ฒดํฌ๋ฐ•์Šค: ํ•„ํ„ฐ๋ง ์ „ํ›„ ๋น„๊ต
118
+ 4. "๐Ÿš€ ๊ฒ€์ถœ ์‹คํ–‰" ํด๋ฆญ
119
+
120
+ **ํƒญ 2: ๐Ÿ”ฌ ์ˆ˜๋™ ๋ถ„์„**
121
+ 1. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
122
+ 2. ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒํ‘œ ์ž…๋ ฅ (x1, y1, x2, y2)
123
+ 3. "๐Ÿ” ๋ถ„์„ ์‹คํ–‰" ํด๋ฆญ
124
+ 4. ํ•ด๋‹น ์˜์—ญ์˜ ํŠน์ง• ๋ถ„์„ ๊ฒฐ๊ณผ ํ™•์ธ
125
+
126
+ ---
127
+
128
+ ## ๐Ÿ“Š ์‹ค์ œ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์˜ˆ์‹œ
129
+
130
+ ### ํ…Œ์ŠคํŠธ ์š”์•ฝ (2025-11-09)
131
+
132
+ **์„ค์ •:**
133
+ - RT-DETR Confidence: 0.3
134
+ - Filter Threshold: 50
135
+
136
+ **๊ฒฐ๊ณผ:**
137
+
138
+ | ์ด๋ฏธ์ง€ | ์ „์ฒด ๊ฒ€์ถœ | ํ•„ํ„ฐ๋ง ํ›„ | ํ•„ํ„ฐ๋ง๋ฅ  |
139
+ |--------|----------|-----------|---------|
140
+ | test_shrimp_tank.png | 7๊ฐœ | 2๊ฐœ | 71% |
141
+ | ํ™”๋ฉด ์บก์ฒ˜ 2025-10-15 142552.png | 3๊ฐœ | 2๊ฐœ | 33% |
142
+ | ํ™”๋ฉด ์บก์ฒ˜ 2025-10-15 170508.png | 2๊ฐœ | 1๊ฐœ | 50% |
143
+ | test_shrimp_tank_roboflow_result.jpg | 8๊ฐœ | 3๊ฐœ | 63% |
144
+ | image (1).webp | 2๊ฐœ | 2๊ฐœ | 0% |
145
+ | image.webp | 7๊ฐœ | 1๊ฐœ | 86% |
146
+ | image_original.webp | 1๊ฐœ | 1๊ฐœ | 0% |
147
+ | preview.webp | 7๊ฐœ | 2๊ฐœ | 71% |
148
+
149
+ **ํ‰๊ท  ํ•„ํ„ฐ๋ง๋ฅ : 47%** (RT-DETR ์˜ค๊ฒ€์ถœ์˜ ์ ˆ๋ฐ˜์„ ์ œ๊ฑฐ)
150
+
151
+ ---
152
+
153
+ ## ๐Ÿ”ง ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๊ฐ€์ด๋“œ
154
+
155
+ ### ๊ฒ€์ถœ์ด ๋„ˆ๋ฌด ์ ์„ ๋•Œ
156
+
157
+ **์ฆ์ƒ:**
158
+ - ์‹ค์ œ ์ƒˆ์šฐ๊ฐ€ ๊ฒ€์ถœ๋˜์ง€ ์•Š์Œ
159
+ - Recall์ด ๋‚ฎ์Œ
160
+
161
+ **ํ•ด๊ฒฐ:**
162
+ 1. **RT-DETR ์‹ ๋ขฐ๋„ ๋‚ฎ์ถ”๊ธฐ**: 0.3 โ†’ 0.2
163
+ 2. **ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’ ๋‚ฎ์ถ”๊ธฐ**: 50 โ†’ 40
164
+ 3. **ํ•„ํ„ฐ ๊ฐ€์ค‘์น˜ ์กฐ์ •** (๊ณ ๊ธ‰):
165
+ ```python
166
+ # test_visual_validation.py์˜ apply_universal_filter() ์ˆ˜์ •
167
+ # ์—„๊ฒฉํ•œ ๊ธฐ์ค€ ์™„ํ™”
168
+ if 1.5 <= morph['aspect_ratio'] <= 12.0: # ๊ธฐ์กด: 2.0~10.0
169
+ score += 15
170
+ ```
171
+
172
+ ### ์˜ค๊ฒ€์ถœ์ด ๋งŽ์„ ๋•Œ
173
+
174
+ **์ฆ์ƒ:**
175
+ - ์ƒˆ์šฐ๊ฐ€ ์•„๋‹Œ ๊ฒƒ๋„ ๊ฒ€์ถœ๋จ
176
+ - Precision์ด ๋‚ฎ์Œ
177
+
178
+ **ํ•ด๊ฒฐ:**
179
+ 1. **ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’ ๋†’์ด๊ธฐ**: 50 โ†’ 60
180
+ 2. **RT-DETR ์‹ ๋ขฐ๋„ ๋†’์ด๊ธฐ**: 0.3 โ†’ 0.4
181
+ 3. **ํ•„ํ„ฐ ์กฐ๊ฑด ๊ฐ•ํ™”** (๊ณ ๊ธ‰):
182
+ ```python
183
+ # ์ฑ„๋„ ์กฐ๊ฑด ๊ฐ•ํ™”
184
+ if visual['saturation'] < 120: # ๊ธฐ์กด: 150
185
+ score += 20
186
+ ```
187
+
188
+ ### F1 Score๊ฐ€ ๋‚ฎ์„ ๋•Œ
189
+
190
+ **์›์ธ ๋ถ„์„:**
191
+ 1. **RT-DETR ๋ฌธ์ œ**: ๊ฒ€์ถœ ์ž์ฒด๊ฐ€ ์•ˆ ๋จ
192
+ - ํ•ด๊ฒฐ: RT-DETR ์‹ ๋ขฐ๋„ ์กฐ์ •
193
+ 2. **ํ•„ํ„ฐ ๋ฌธ์ œ**: ์ƒˆ์šฐ๋ฅผ ์ž˜๋ชป ๊ฑธ๋Ÿฌ๋ƒ„
194
+ - ํ•ด๊ฒฐ: ํ•„ํ„ฐ ์กฐ๊ฑด ์™„ํ™”
195
+ 3. **๋ฐ์ดํ„ฐ ๋ฌธ์ œ**: ํ•™์Šต ๋ฐ์ดํ„ฐ์™€ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ฐจ์ด
196
+ - ํ•ด๊ฒฐ: ๋ชจ๋ธ ์žฌํ•™์Šต (Roboflow ์ธก์ • ์ด๋ฏธ์ง€ ์ถ”๊ฐ€)
197
+
198
+ ---
199
+
200
+ ## ๐Ÿ“ˆ ์„ฑ๋Šฅ ๋ชฉํ‘œ
201
+
202
+ ### ์ˆ˜์กฐ ์ด๋ฏธ์ง€ (Live Shrimp)
203
+ - **VIDraft/Shrimp ๋ชจ๋ธ** (๊ถŒ์žฅ)
204
+ - Precision: > 95%
205
+ - Recall: > 90%
206
+ - F1 Score: > 92%
207
+
208
+ ### ์ธก์ • ์ด๋ฏธ์ง€ (Dead Shrimp)
209
+ - **RT-DETR + Universal Filter**
210
+ - Precision: > 80%
211
+ - Recall: > 75%
212
+ - F1 Score: > 77%
213
+
214
+ ### ํ˜„์žฌ ์„ฑ๋Šฅ (์ถ”์ •)
215
+ - ์‹œ๊ฐ์  ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋กœ ๋ณผ ๋•Œ **F1 ~70%** ์˜ˆ์ƒ
216
+ - ์ •๋Ÿ‰์  ํ‰๊ฐ€ ํ•„์š” (ground_truth.json ์ƒ์„ฑ ํ›„)
217
+
218
+ ---
219
+
220
+ ## ๐ŸŽจ ํ•„ํ„ฐ ์ ์ˆ˜ ๊ตฌ์„ฑ
221
+
222
+ ์ด 100์  ๋งŒ์ :
223
+
224
+ | ํŠน์ง• | ๋ฐฐ์  | ์กฐ๊ฑด |
225
+ |------|------|------|
226
+ | ์ข…ํšก๋น„ (Aspect Ratio) | 15์  | 2.0 ~ 10.0 |
227
+ | ์„ธ์žฅ๋„ (Compactness) | 15์  | < 0.25 |
228
+ | ๋ฉด์ ๋น„ (Area Ratio) | 10์  | 5% ~ 50% |
229
+ | ์ฑ„๋„ (Saturation) | 20์  | < 150 |
230
+ | ์ƒ‰์ƒ ์ผ๊ด€์„ฑ (Color Std) | 15์  | < 30 |
231
+ | RT-DETR ์‹ ๋ขฐ๋„ | 25์  | confidence ร— 25 |
232
+
233
+ **ํ•ฉ๊ณ„:** 100์ 
234
+
235
+ **์ž„๊ณ„๊ฐ’:**
236
+ - **75์  ์ด์ƒ**: ๋†’์€ ํ™•๋ฅ ๋กœ ์ƒˆ์šฐ โœ…
237
+ - **50~74์ **: ์ค‘๊ฐ„ ํ™•๋ฅ  โš ๏ธ
238
+ - **50์  ๋ฏธ๋งŒ**: ์ƒˆ์šฐ ์•„๋‹˜ โŒ
239
+
240
+ ---
241
+
242
+ ## ๐Ÿ› ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…
243
+
244
+ ### ๋ฌธ์ œ 1: ModuleNotFoundError
245
+
246
+ **์—๋Ÿฌ:**
247
+ ```
248
+ ModuleNotFoundError: No module named 'cv2'
249
+ ```
250
+
251
+ **ํ•ด๊ฒฐ:**
252
+ ```bash
253
+ pip install opencv-python matplotlib seaborn transformers torch torchvision
254
+ ```
255
+
256
+ ### ๋ฌธ์ œ 2: Ground Truth ํŒŒ์ผ ์—†์Œ
257
+
258
+ **์—๋Ÿฌ:**
259
+ ```
260
+ โš ๏ธ Ground truth ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค: ground_truth.json
261
+ ```
262
+
263
+ **ํ•ด๊ฒฐ:**
264
+ 1. `ground_truth.json` ํŒŒ์ผ ์ƒ์„ฑ
265
+ 2. ๊ฐ ์ด๋ฏธ์ง€์˜ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒํ‘œ ์ž…๋ ฅ
266
+ 3. ์ขŒํ‘œ๋Š” ์ด๋ฏธ์ง€ ๋ทฐ์–ด๋‚˜ Gradio ๋Œ€ํ™”ํ˜• ๋„๊ตฌ๋กœ ํ™•์ธ
267
+
268
+ ### ๋ฌธ์ œ 3: ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ ๋А๋ฆผ
269
+
270
+ **์ฆ์ƒ:**
271
+ RT-DETR ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ์— ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ
272
+
273
+ **ํ•ด๊ฒฐ:**
274
+ - ์ฒ˜์Œ ํ•œ ๋ฒˆ๋งŒ ๋‹ค์šด๋กœ๋“œ๋จ (์บ์‹œ ์‚ฌ์šฉ)
275
+ - ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ํ™•์ธ
276
+ - Hugging Face Hub ์ ‘์† ํ™•์ธ
277
+
278
+ ---
279
+
280
+ ## ๐Ÿ“ ํŒŒ์ผ ๊ตฌ์กฐ
281
+
282
+ ```
283
+ D:\Project\VIDraft\Shrimp\
284
+ โ”œโ”€โ”€ test_visual_validation.py # ์‹œ๊ฐ์  ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ
285
+ โ”œโ”€โ”€ test_quantitative_evaluation.py # ์ •๋Ÿ‰์  ํ‰๊ฐ€ ์Šคํฌ๋ฆฝํŠธ
286
+ โ”œโ”€โ”€ interactive_validation.py # ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ ์ธํ„ฐํŽ˜์ด์Šค
287
+ โ”œโ”€โ”€ ground_truth.json # Ground Truth ๋ผ๋ฒจ (์ˆ˜๋™ ์ƒ์„ฑ)
288
+ โ”œโ”€โ”€ imgs/ # ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€
289
+ โ”‚ โ”œโ”€โ”€ test_shrimp_tank.png
290
+ โ”‚ โ”œโ”€โ”€ image (1).webp
291
+ โ”‚ โ””โ”€โ”€ ...
292
+ โ”œโ”€โ”€ test_results/ # ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ (์ž๋™ ์ƒ์„ฑ)
293
+ โ”‚ โ”œโ”€โ”€ result_*.png
294
+ โ”‚ โ”œโ”€โ”€ test_results_*.json
295
+ โ”‚ โ””โ”€โ”€ quantitative_*/
296
+ โ”‚ โ”œโ”€โ”€ eval_*.png
297
+ โ”‚ โ”œโ”€โ”€ confusion_matrix.png
298
+ โ”‚ โ””โ”€โ”€ evaluation_summary.json
299
+ โ””โ”€โ”€ docs/
300
+ โ”œโ”€โ”€ testing_framework_guide.md # ์ด ๋ฌธ์„œ
301
+ โ””โ”€โ”€ detection_testing_and_validation.md
302
+ ```
303
+
304
+ ---
305
+
306
+ ## ๐Ÿ”„ ์›Œํฌํ”Œ๋กœ์šฐ ๊ถŒ์žฅ์‚ฌํ•ญ
307
+
308
+ ### 1๋‹จ๊ณ„: ์ดˆ๊ธฐ ๊ฒ€์ฆ (์‹œ๊ฐ์ )
309
+ ```bash
310
+ python test_visual_validation.py
311
+ ```
312
+ - ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€ ํ™•์ธ
313
+ - ์˜ค๊ฒ€์ถœ/๋ฏธ๊ฒ€์ถœ ํŒจํ„ด ํŒŒ์•…
314
+
315
+ ### 2๋‹จ๊ณ„: ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™” (๋Œ€ํ™”ํ˜•)
316
+ ```bash
317
+ python interactive_validation.py
318
+ ```
319
+ - ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •
320
+ - ์ตœ์  ์„ค์ • ์ฐพ๊ธฐ
321
+
322
+ ### 3๋‹จ๊ณ„: ์ •ํ™•๋„ ์ธก์ • (์ •๋Ÿ‰์ )
323
+ ```bash
324
+ # ground_truth.json ์ƒ์„ฑ ํ›„
325
+ python test_quantitative_evaluation.py
326
+ ```
327
+ - Precision, Recall, F1 ๊ณ„์‚ฐ
328
+ - ์„ฑ๋Šฅ ๋ชฉํ‘œ ๋Œ€๋น„ ํ‰๊ฐ€
329
+
330
+ ### 4๋‹จ๊ณ„: ๋ฐ˜๋ณต ๊ฐœ์„ 
331
+ - ์„ฑ๋Šฅ ๋ฏธ๋‹ฌ ์‹œ ํŒŒ๋ผ๋ฏธํ„ฐ ์žฌ์กฐ์ •
332
+ - ํ•„์š” ์‹œ ํ•„ํ„ฐ ๋กœ์ง ์ˆ˜์ •
333
+ - ์žฌํ…Œ์ŠคํŠธ
334
+
335
+ ---
336
+
337
+ ## ๐Ÿ“ ์ถ”๊ฐ€ ๊ฐœ์„  ์‚ฌํ•ญ
338
+
339
+ ### ๋‹จ๊ธฐ (์ฆ‰์‹œ ๊ฐ€๋Šฅ)
340
+ 1. โœ… ์‹œ๊ฐ์  ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ - **์™„๋ฃŒ**
341
+ 2. โœ… ์ •๋Ÿ‰์  ํ‰๊ฐ€ ์Šคํฌ๋ฆฝํŠธ - **์™„๋ฃŒ**
342
+ 3. โœ… ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ ์ธํ„ฐํŽ˜์ด์Šค - **์™„๋ฃŒ**
343
+ 4. โณ Ground Truth ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - **์ง„ํ–‰ ํ•„์š”**
344
+ 5. โณ ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ - **์ง„ํ–‰ ํ•„์š”**
345
+
346
+ ### ์ค‘๊ธฐ (1-2์ฃผ)
347
+ 1. โณ ์ธก์ • ์ด๋ฏธ์ง€ 50~100์žฅ์œผ๋กœ Roboflow ๋ชจ๋ธ ์žฌํ•™์Šต
348
+ 2. โณ ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์…‹ ํ…Œ์ŠคํŠธ (100+ ์ด๋ฏธ์ง€)
349
+ 3. โณ ๊ต์ฐจ ๊ฒ€์ฆ (Cross-validation) ๊ตฌํ˜„
350
+
351
+ ### ์žฅ๊ธฐ (1๊ฐœ์›”+)
352
+ 1. โณ ์•™์ƒ๋ธ” ๋ชจ๋ธ (Roboflow + RT-DETR ์กฐํ•ฉ)
353
+ 2. โณ ์ž๋™ ๋ผ๋ฒจ๋ง ๋„๊ตฌ ๊ฐœ๋ฐœ
354
+ 3. โณ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•
355
+
356
+ ---
357
+
358
+ ## ๐Ÿ™‹ FAQ
359
+
360
+ ### Q1: ๋ช‡ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๊ฐ€ ํ•„์š”ํ•œ๊ฐ€์š”?
361
+
362
+ **A:**
363
+ - **์ตœ์†Œ**: 20~30์žฅ (๋‹ค์–‘ํ•œ ๋ฐฐ๊ฒฝ, ์กฐ๋ช…, ๊ฐ๋„)
364
+ - **๊ถŒ์žฅ**: 50~100์žฅ (ํ†ต๊ณ„์  ์‹ ๋ขฐ๋„ ํ™•๋ณด)
365
+ - **์ด์ƒ์ **: 200+ ์žฅ (ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€)
366
+
367
+ ### Q2: Ground Truth๋Š” ์–ด๋–ป๊ฒŒ ๋งŒ๋“œ๋‚˜์š”?
368
+
369
+ **A:**
370
+ 1. **์ˆ˜๋™ ๋ฐฉ๋ฒ•**: ๊ทธ๋ฆผํŒ/ํฌํ† ์ƒต์œผ๋กœ ์ขŒํ‘œ ํ™•์ธ
371
+ 2. **๋Œ€๏ฟฝ๏ฟฝ๏ฟฝํ˜• ๋ฐฉ๋ฒ•**: `interactive_validation.py`์˜ "์ˆ˜๋™ ๋ถ„์„" ํƒญ ํ™œ์šฉ
372
+ 3. **๋ผ๋ฒจ๋ง ๋„๊ตฌ**: [LabelImg](https://github.com/tzutalin/labelImg), [CVAT](https://github.com/opencv/cvat) ์‚ฌ์šฉ
373
+ 4. JSON ํ˜•์‹์œผ๋กœ ์ €์žฅ
374
+
375
+ ### Q3: ์–ด๋–ค ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ํ•ด์•ผ ํ•˜๋‚˜์š”?
376
+
377
+ **A:**
378
+ 1. **์‹œ์ž‘**: ์‹œ๊ฐ์  ๊ฒ€์ฆ (๋น ๋ฅด๊ณ  ์ง๊ด€์ )
379
+ 2. **์ตœ์ ํ™”**: ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ (ํŒŒ๋ผ๋ฏธํ„ฐ ์ฐพ๊ธฐ)
380
+ 3. **๊ฒ€์ฆ**: ์ •๋Ÿ‰์  ํ‰๊ฐ€ (์ •ํ™•๋„ ํ™•์ธ)
381
+
382
+ ### Q4: ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’์€ ์–ด๋–ป๊ฒŒ ์ •ํ•˜๋‚˜์š”?
383
+
384
+ **A:**
385
+ - **๋ณด์ˆ˜์  (๋†’์€ ์ •๋ฐ€๋„)**: 60~70์ 
386
+ - **๊ท ํ˜•์  (๊ธฐ๋ณธ)**: 50์ 
387
+ - **๊ณต๊ฒฉ์  (๋†’์€ ์žฌํ˜„์œจ)**: 40~45์ 
388
+
389
+ PR Curve๋ฅผ ๋ณด๊ณ  ์ตœ์  F1 ์ง€์  ์„ ํƒ
390
+
391
+ ---
392
+
393
+ ## ๐Ÿ“ž ๋ฌธ์˜
394
+
395
+ ํ”„๋ ˆ์ž„์›Œํฌ ๊ด€๋ จ ๋ฌธ์ œ๋Š” ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:
396
+ 1. ์ด ๋ฌธ์„œ์˜ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์„น์…˜
397
+ 2. `docs/detection_testing_and_validation.md` (์ƒ์„ธ ๊ธฐ์ˆ  ๋ฌธ์„œ)
398
+ 3. ๊ฐ ์Šคํฌ๋ฆฝํŠธ์˜ ์ฃผ์„ ๋ฐ docstring
399
+
400
+ ---
401
+
402
+ **๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ:** 2025-11-09
403
+ **๋ฒ„์ „:** 1.0
404
+ **์ž‘์„ฑ์ž:** Claude Code
docs/universal_shrimp_detection_strategy.md ADDED
@@ -0,0 +1,883 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ๐ŸŽฏ ๋ฒ”์šฉ ์ƒˆ์šฐ ๊ฒ€์ถœ ์ „๋žต (RT-DETR + ๋ณธ์งˆ์  ํŠน์ง• ํ•„ํ„ฐ๋ง)
2
+
3
+ ## ๐Ÿ“‹ ํ•ต์‹ฌ ์›์น™
4
+
5
+ **๋ฒ”์šฉ์„ฑ (Universality)**
6
+ - โœ… ๋ฐฐ๊ฒฝ ๋ฌด๊ด€: ๋งคํŠธ, ์ˆ˜์กฐ, ์ ‘์‹œ, ์†๋ฐ”๋‹ฅ ๋“ฑ ์–ด๋””๋“ 
7
+ - โœ… ์ฃผ๋ณ€ ๊ฐ์ฒด ๋ฌด๊ด€: ์ž, ์†, ์‹ ๋ฐœ ๋“ฑ ์—†์–ด๋„ ๊ฒ€์ถœ
8
+ - โœ… ์œ„์น˜ ๋ฌด๊ด€: ์ด๋ฏธ์ง€ ์–ด๋””๋“  (์ค‘์•™, ๋ชจ์„œ๋ฆฌ ๋“ฑ)
9
+ - โœ… ์กฐ๋ช… ๋ฌด๊ด€: ๋ฐ์€ ๊ณณ, ์–ด๋‘์šด ๊ณณ ๋ชจ๋‘
10
+ - โœ… ํฌ๊ธฐ ๋ฌด๊ด€: ์ž‘์€ ์ƒˆ์šฐ๋ถ€ํ„ฐ ํฐ ์ƒˆ์šฐ๊นŒ์ง€
11
+
12
+ **ํ•ต์‹ฌ ์•„์ด๋””์–ด**
13
+ > **"์ƒˆ์šฐ์˜ ๋ณธ์งˆ์  ํŠน์ง•๋งŒ์œผ๋กœ ๊ตฌ๋ณ„"**
14
+ > - ์™ธ๋ถ€ ์ปจํ…์ŠคํŠธ(์ž, ๋งคํŠธ ๋“ฑ)์— ์˜์กดํ•˜์ง€ ์•Š์Œ
15
+ > - ์ƒˆ์šฐ ์ž์ฒด์˜ ํ˜•ํƒœํ•™์ /์‹œ๊ฐ์  ํŠน์ง• ํ™œ์šฉ
16
+
17
+ ---
18
+
19
+ ## ๐Ÿ”ฌ ์ƒˆ์šฐ์˜ ๋ณธ์งˆ์  ํŠน์ง• (Intrinsic Features)
20
+
21
+ ### 1. ํ˜•ํƒœํ•™์  ํŠน์ง• (Morphological)
22
+
23
+ ```
24
+ ์ƒˆ์šฐ ํ˜•ํƒœ์˜ ๋ถˆ๋ณ€ ํŠน์„ฑ (๋ฐฐ๊ฒฝ/์œ„์น˜ ๋ฌด๊ด€)
25
+ โ”œโ”€โ”€ 1.1 ์„ธ์žฅํ˜• (Elongated)
26
+ โ”‚ โ”œโ”€โ”€ ์ข…ํšก๋น„: 2:1 ~ 10:1 (๊ฐ€๋กœ๋กœ ๊ธธ๋‹ค)
27
+ โ”‚ โ””โ”€โ”€ ๋Œ€์นญ์„ฑ: ์ขŒ์šฐ ๋Œ€์นญ
28
+ โ”‚
29
+ โ”œโ”€โ”€ 1.2 ๋ถ„์ ˆ ๊ตฌ์กฐ (Segmented Body)
30
+ โ”‚ โ”œโ”€โ”€ ๋จธ๋ฆฌ-๊ฐ€์Šด-๊ผฌ๋ฆฌ 3๋ถ„ํ• 
31
+ โ”‚ โ””โ”€โ”€ ์ฃผ๊ธฐ์  ํŒจํ„ด (๋งˆ๋””)
32
+ โ”‚
33
+ โ”œโ”€โ”€ 1.3 ๊ณก์„ ํ˜• ์™ธ๊ณฝ์„  (Curved Contour)
34
+ โ”‚ โ”œโ”€โ”€ ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„  (์ง์„  ์—†์Œ)
35
+ โ”‚ โ””โ”€โ”€ 'C์ž' ๋˜๋Š” 'ใ„ฑ์ž' ํ˜•ํƒœ (์ฃฝ์€ ์ƒˆ์šฐ)
36
+ โ”‚
37
+ โ””โ”€โ”€ 1.4 ํฌ๊ธฐ ๋ฒ”์œ„
38
+ โ”œโ”€โ”€ ์ ˆ๋Œ€ ํฌ๊ธฐ: 3cm ~ 20cm (์‹ค์ œ)
39
+ โ””โ”€โ”€ ์ด๋ฏธ์ง€ ๋‚ด ๋น„์œจ: 5% ~ 40%
40
+ ```
41
+
42
+ ### 2. ์‹œ๊ฐ์  ํŠน์ง• (Visual)
43
+
44
+ ```
45
+ ์ƒˆ์šฐ ์™ธํ˜•์˜ ๋ถˆ๋ณ€ ํŠน์„ฑ
46
+ โ”œโ”€โ”€ 2.1 ์ƒ‰์ƒ (Color)
47
+ โ”‚ โ”œโ”€โ”€ ์‚ด์•„์žˆ์Œ: ํšŒ์ƒ‰/๊ฐˆ์ƒ‰ (๋‚ฎ์€ ์ฑ„๋„)
48
+ โ”‚ โ”œโ”€โ”€ ์ฃฝ์Œ: ํฐ์ƒ‰/ํˆฌ๋ช… (๋งค์šฐ ๋‚ฎ์€ ์ฑ„๋„)
49
+ โ”‚ โ””โ”€โ”€ ๊ณตํ†ต: ๋‹จ์ƒ‰ ๋˜๋Š” ๋ฏธ์„ธํ•œ ๊ทธ๋ผ๋ฐ์ด์…˜
50
+ โ”‚
51
+ โ”œโ”€โ”€ 2.2 ํˆฌ๋ช…๋„/๋ฐ˜ํˆฌ๋ช… (Translucency)
52
+ โ”‚ โ”œโ”€โ”€ ์ฃฝ์€ ์ƒˆ์šฐ: ๋†’์€ ํˆฌ๋ช…๋„
53
+ โ”‚ โ””โ”€โ”€ ๋ฐฐ๊ฒฝ์ด ๋น„์นจ
54
+ โ”‚
55
+ โ”œโ”€โ”€ 2.3 ํ…์Šค์ฒ˜ (Texture)
56
+ โ”‚ โ”œโ”€โ”€ ๋ถ€๋“œ๋Ÿฌ์šด ํ‘œ๋ฉด (๋‚ฎ์€ ํ‘œ์ค€ํŽธ์ฐจ)
57
+ โ”‚ โ”œโ”€โ”€ ๋ฏธ์„ธํ•œ ์ค„๋ฌด๋Šฌ (๋ถ„์ ˆ)
58
+ โ”‚ โ””โ”€โ”€ ๊ด‘ํƒ (ํ•˜์ด๋ผ์ดํŠธ)
59
+ โ”‚
60
+ โ””โ”€โ”€ 2.4 ๊ฒฝ๊ณ„์„  (Boundary)
61
+ โ”œโ”€โ”€ ๋ถ€๋“œ๋Ÿฌ์šด ์—์ง€ (soft edge)
62
+ โ””โ”€โ”€ ๋ช…ํ™•ํ•œ ์‹ค๋ฃจ์—ฃ
63
+ ```
64
+
65
+ ### 3. ๊ตฌ์กฐ์  ํŠน์ง• (Structural)
66
+
67
+ ```
68
+ ์ƒˆ์šฐ ๋‚ด๋ถ€ ๊ตฌ์กฐ (์ฃฝ์€ ์ƒˆ์šฐ ํŠนํ™”)
69
+ โ”œโ”€โ”€ 3.1 ๋‚ด๋ถ€ ์žฅ๊ธฐ ๊ฐ€์‹œ์„ฑ
70
+ โ”‚ โ”œโ”€โ”€ ๊ฒ€์€ ์„  (์†Œํ™”๊ด€)
71
+ โ”‚ โ””โ”€โ”€ ํˆฌ๋ช…ํ•œ ๋ชธ์ฒด ๋‚ด๋ถ€ ๊ตฌ์กฐ
72
+ โ”‚
73
+ โ”œโ”€โ”€ 3.2 ๋‹ค๋ฆฌ/๋”๋“ฌ์ด
74
+ โ”‚ โ”œโ”€โ”€ ๊ฐ€๋Š” ์„  ํ˜•ํƒœ
75
+ โ”‚ โ””โ”€โ”€ ๋ชธ์ฒด์—์„œ ๋ฐฉ์‚ฌํ˜•์œผ๋กœ ๋ป—์Œ
76
+ โ”‚
77
+ โ””โ”€โ”€ 3.3 ๊ผฌ๋ฆฌ ๋ถ€์ฑ„ ๊ตฌ์กฐ
78
+ โ””โ”€โ”€ ๋ถ€์ฑ„๊ผด ํŽผ์ณ์ง„ ํ˜•ํƒœ
79
+ ```
80
+
81
+ ---
82
+
83
+ ## ๐Ÿ—๏ธ ๋ฒ”์šฉ ํ•„ํ„ฐ๋ง ์ „๋žต (MECE)
84
+
85
+ ### Level 1: ํ•„ํ„ฐ ๋ถ„๋ฅ˜ (๋ฐฐ๊ฒฝ ์˜์กด๋„ ๊ธฐ์ค€)
86
+
87
+ ```
88
+ ๋ฐฐ๊ฒฝ ๋…๋ฆฝ ํ•„ํ„ฐ (Background-Independent) โญโญโญ
89
+ โ”œโ”€โ”€ ํ˜•ํƒœ ํ•„ํ„ฐ (Shape Filter)
90
+ โ”œโ”€โ”€ ์ƒ‰์ƒ ํ•„ํ„ฐ (Color Filter)
91
+ โ””โ”€โ”€ ํ…์Šค์ฒ˜ ํ•„ํ„ฐ (Texture Filter)
92
+
93
+ ๋ฐฐ๊ฒฝ ์˜์กด ํ•„ํ„ฐ (Background-Dependent) โš ๏ธ
94
+ โ””โ”€โ”€ ์‚ฌ์šฉ ์•ˆ ํ•จ (๋ฒ”์šฉ์„ฑ ์ €ํ•ด)
95
+ ```
96
+
97
+ ### Level 2: ํ•„ํ„ฐ๋ง ํŒŒ์ดํ”„๋ผ์ธ (3๋‹จ๊ณ„)
98
+
99
+ ```
100
+ Step 1: ํ˜•ํƒœ ๊ธฐ๋ฐ˜ 1์ฐจ ํ•„ํ„ฐ (Shape-based Primary Filter)
101
+ โ†’ ์ƒˆ์šฐ์™€ ๋ช…๋ฐฑํžˆ ๋‹ค๋ฅธ ํ˜•ํƒœ ์ œ๊ฑฐ
102
+ โ†’ ์ข…ํšก๋น„, ํฌ๊ธฐ, ์™ธ๊ณฝ์„  ๊ณก๋ฅ 
103
+
104
+ Step 2: ์ƒ‰์ƒ/ํ…์Šค์ฒ˜ ๊ธฐ๋ฐ˜ 2์ฐจ ํ•„ํ„ฐ (Visual Secondary Filter)
105
+ โ†’ ์ƒˆ์šฐ์˜ ์‹œ๊ฐ์  ํŠน์ง•์œผ๋กœ ๊ฒ€์ฆ
106
+ โ†’ HSV ์ƒ‰๊ณต๊ฐ„, ํ…์Šค์ฒ˜ ๋ถ„์„
107
+
108
+ Step 3: ์ •๋ฐ€ ๊ฒ€์ฆ (Fine-grained Verification)
109
+ โ†’ ๊ตฌ์กฐ์  ํŠน์ง•์œผ๋กœ ์ตœ์ข… ํ™•์ธ
110
+ โ†’ ๋‚ด๋ถ€ ๊ตฌ์กฐ, ๋ถ„์ ˆ ํŒจํ„ด
111
+ ```
112
+
113
+ ---
114
+
115
+ ## ๐Ÿ’ป ๋ฒ”์šฉ ํ•„ํ„ฐ ๊ตฌํ˜„
116
+
117
+ ### Step 1: ํ˜•ํƒœ ๊ธฐ๋ฐ˜ 1์ฐจ ํ•„ํ„ฐ
118
+
119
+ #### 1.1 ์ข…ํšก๋น„ ํ•„ํ„ฐ (Aspect Ratio)
120
+
121
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ํ•ญ์ƒ ๊ฐ€๋กœ๋กœ ๊ธธ๋‹ค (๋ฐฐ๊ฒฝ ๋ฌด๊ด€)
122
+
123
+ ```python
124
+ def filter_by_aspect_ratio(detections, min_ratio=2.0, max_ratio=10.0):
125
+ """
126
+ ์ข…ํšก๋น„ ํ•„ํ„ฐ
127
+
128
+ Args:
129
+ min_ratio: ์ตœ์†Œ ๊ฐ€๋กœ/์„ธ๋กœ ๋น„์œจ (๊ธฐ๋ณธ 2.0)
130
+ max_ratio: ์ตœ๋Œ€ ๊ฐ€๋กœ/์„ธ๋กœ ๋น„์œจ (๊ธฐ๋ณธ 10.0)
131
+ """
132
+ filtered = []
133
+
134
+ for det in detections:
135
+ bbox = det['bbox']
136
+ width = bbox[2] - bbox[0]
137
+ height = bbox[3] - bbox[1]
138
+
139
+ if height == 0:
140
+ continue
141
+
142
+ # ๊ฐ€๋กœ/์„ธ๋กœ ๋น„์œจ ๊ณ„์‚ฐ
143
+ aspect_ratio = width / height
144
+
145
+ # ์„ธ๋กœ๋กœ ๊ธด ๊ฒฝ์šฐ๋„ ๊ณ ๋ ค (์ƒˆ์šฐ๊ฐ€ ์„ธ๋กœ๋กœ ๋†“์ธ ๊ฒฝ์šฐ)
146
+ if aspect_ratio < 1.0:
147
+ aspect_ratio = 1.0 / aspect_ratio
148
+
149
+ # ๋ฒ”์œ„ ์ฒดํฌ
150
+ if min_ratio <= aspect_ratio <= max_ratio:
151
+ det['aspect_ratio'] = aspect_ratio
152
+ filtered.append(det)
153
+
154
+ return filtered
155
+ ```
156
+
157
+ #### 1.2 ์™ธ๊ณฝ์„  ๊ณก๋ฅ  ํ•„ํ„ฐ (Contour Curvature)
158
+
159
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„ , ์ž๋‚˜ ์ฑ… ๋“ฑ์€ ์ง์„ 
160
+
161
+ ```python
162
+ def filter_by_curvature(image, detections, max_line_ratio=0.3):
163
+ """
164
+ ์™ธ๊ณฝ์„  ๊ณก๋ฅ  ํ•„ํ„ฐ
165
+
166
+ Args:
167
+ max_line_ratio: ์ตœ๋Œ€ ์ง์„  ๋น„์œจ (0~1)
168
+ """
169
+ import cv2
170
+ import numpy as np
171
+
172
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
173
+ filtered = []
174
+
175
+ for det in detections:
176
+ bbox = det['bbox']
177
+ x1, y1, x2, y2 = map(int, bbox)
178
+
179
+ # ROI ์ถ”์ถœ
180
+ roi = gray[y1:y2, x1:x2]
181
+ if roi.size == 0:
182
+ continue
183
+
184
+ # ์™ธ๊ณฝ์„  ๊ฒ€์ถœ
185
+ _, binary = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
186
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
187
+
188
+ if not contours:
189
+ continue
190
+
191
+ # ๊ฐ€์žฅ ํฐ ์™ธ๊ณฝ์„ 
192
+ contour = max(contours, key=cv2.contourArea)
193
+
194
+ # ์™ธ๊ณฝ์„  ๊ทผ์‚ฌ (์ง์„  ๊ฒ€์ถœ)
195
+ epsilon = 0.02 * cv2.arcLength(contour, True)
196
+ approx = cv2.approxPolyDP(contour, epsilon, True)
197
+
198
+ # ์ง์„  ๋น„์œจ = ๊ทผ์‚ฌ ์  ๊ฐœ์ˆ˜ / ์›๋ณธ ์  ๊ฐœ์ˆ˜
199
+ line_ratio = len(approx) / len(contour) if len(contour) > 0 else 1.0
200
+
201
+ # ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„  = ์ง์„  ๋น„์œจ ๋‚ฎ์Œ
202
+ if line_ratio < max_line_ratio:
203
+ det['curvature_score'] = 1.0 - line_ratio
204
+ filtered.append(det)
205
+
206
+ return filtered
207
+ ```
208
+
209
+ #### 1.3 ์ปดํŒฉํŠธ๋‹ˆ์Šค ํ•„ํ„ฐ (Compactness)
210
+
211
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ์„ธ์žฅํ˜•์ด๋ฏ€๋กœ compactness๊ฐ€ ๋‚ฎ์Œ
212
+
213
+ ```python
214
+ def filter_by_compactness(image, detections, max_compactness=0.25):
215
+ """
216
+ ์ปดํŒฉํŠธ๋‹ˆ์Šค ํ•„ํ„ฐ
217
+
218
+ Compactness = 4ฯ€ ร— Area / Perimeterยฒ
219
+ - ์›: 1.0
220
+ - ์ •์‚ฌ๊ฐํ˜•: 0.785
221
+ - ์„ธ์žฅํ˜•: < 0.3
222
+
223
+ Args:
224
+ max_compactness: ์ตœ๋Œ€ ์ปดํŒฉํŠธ๋‹ˆ์Šค (๊ธฐ๋ณธ 0.25)
225
+ """
226
+ import cv2
227
+ import numpy as np
228
+
229
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
230
+ filtered = []
231
+
232
+ for det in detections:
233
+ bbox = det['bbox']
234
+ x1, y1, x2, y2 = map(int, bbox)
235
+
236
+ roi = gray[y1:y2, x1:x2]
237
+ if roi.size == 0:
238
+ continue
239
+
240
+ # ์™ธ๊ณฝ์„  ๊ฒ€์ถœ
241
+ _, binary = cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
242
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
243
+
244
+ if not contours:
245
+ continue
246
+
247
+ contour = max(contours, key=cv2.contourArea)
248
+ area = cv2.contourArea(contour)
249
+ perimeter = cv2.arcLength(contour, True)
250
+
251
+ if perimeter == 0:
252
+ continue
253
+
254
+ # ์ปดํŒฉํŠธ๋‹ˆ์Šค ๊ณ„์‚ฐ
255
+ compactness = (4 * np.pi * area) / (perimeter ** 2)
256
+
257
+ # ์„ธ์žฅํ˜• = ๋‚ฎ์€ ์ปดํŒฉํŠธ๋‹ˆ์Šค
258
+ if compactness < max_compactness:
259
+ det['compactness'] = compactness
260
+ filtered.append(det)
261
+
262
+ return filtered
263
+ ```
264
+
265
+ #### 1.4 ํฌ๊ธฐ ์ ์‘ํ˜• ํ•„ํ„ฐ (Adaptive Size Filter)
266
+
267
+ **์›๋ฆฌ**: ์ด๋ฏธ์ง€ ํ•ด์ƒ๋„์— ๋”ฐ๋ผ ์ ์‘์ ์œผ๋กœ ํฌ๊ธฐ ํŒ๋‹จ
268
+
269
+ ```python
270
+ def filter_by_adaptive_size(detections, image_shape, min_ratio=0.05, max_ratio=0.50):
271
+ """
272
+ ์ ์‘ํ˜• ํฌ๊ธฐ ํ•„ํ„ฐ
273
+
274
+ Args:
275
+ min_ratio: ์ตœ์†Œ ๋ฉด์  ๋น„์œจ (์ด๋ฏธ์ง€์˜ 5%)
276
+ max_ratio: ์ตœ๋Œ€ ๋ฉด์  ๋น„์œจ (์ด๋ฏธ์ง€์˜ 50%)
277
+ """
278
+ h, w = image_shape[:2]
279
+ image_area = h * w
280
+
281
+ filtered = []
282
+
283
+ for det in detections:
284
+ bbox = det['bbox']
285
+ bbox_area = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1])
286
+ area_ratio = bbox_area / image_area
287
+
288
+ # ๋„ˆ๋ฌด ์ž‘๊ฑฐ๋‚˜ ํฌ๋ฉด ์ œ์™ธ
289
+ if min_ratio <= area_ratio <= max_ratio:
290
+ det['area_ratio'] = area_ratio
291
+ filtered.append(det)
292
+
293
+ return filtered
294
+ ```
295
+
296
+ ---
297
+
298
+ ### Step 2: ์ƒ‰์ƒ/ํ…์Šค์ฒ˜ ๊ธฐ๋ฐ˜ 2์ฐจ ํ•„ํ„ฐ
299
+
300
+ #### 2.1 ์ƒ‰์ƒ ์ผ๊ด€์„ฑ ํ•„ํ„ฐ (Color Consistency)
301
+
302
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ๋‹จ์ƒ‰ ๋˜๋Š” ๋ฏธ์„ธํ•œ ๊ทธ๋ผ๋ฐ์ด์…˜ (๊ฐ•ํ•œ ํŒจํ„ด ์—†์Œ)
303
+
304
+ ```python
305
+ def filter_by_color_consistency(image, detections, max_std=50):
306
+ """
307
+ ์ƒ‰์ƒ ์ผ๊ด€์„ฑ ํ•„ํ„ฐ
308
+
309
+ Args:
310
+ max_std: ์ตœ๋Œ€ ์ƒ‰์ƒ ํ‘œ์ค€ํŽธ์ฐจ (๋‹จ์ƒ‰์— ๊ฐ€๊นŒ์›€)
311
+ """
312
+ import cv2
313
+ import numpy as np
314
+
315
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
316
+ filtered = []
317
+
318
+ for det in detections:
319
+ bbox = det['bbox']
320
+ x1, y1, x2, y2 = map(int, bbox)
321
+
322
+ roi_hsv = hsv_image[y1:y2, x1:x2]
323
+ if roi_hsv.size == 0:
324
+ continue
325
+
326
+ # ์ƒ‰์ƒ(H), ์ฑ„๋„(S), ๋ช…๋„(V) ํ‘œ์ค€ํŽธ์ฐจ
327
+ h_std = np.std(roi_hsv[:, :, 0])
328
+ s_std = np.std(roi_hsv[:, :, 1])
329
+ v_std = np.std(roi_hsv[:, :, 2])
330
+
331
+ # ํ‰๊ท  ํ‘œ์ค€ํŽธ์ฐจ
332
+ avg_std = (h_std + s_std + v_std) / 3
333
+
334
+ # ๋‹จ์ƒ‰์— ๊ฐ€๊นŒ์›€ = ๋‚ฎ์€ ํ‘œ์ค€ํŽธ์ฐจ
335
+ if avg_std < max_std:
336
+ det['color_consistency'] = 1.0 - (avg_std / max_std)
337
+ filtered.append(det)
338
+
339
+ return filtered
340
+ ```
341
+
342
+ #### 2.2 ์ฑ„๋„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ (Saturation-based)
343
+
344
+ **์›๋ฆฌ**: ์ฃฝ์€ ์ƒˆ์šฐ = ๋งค์šฐ ๋‚ฎ์€ ์ฑ„๋„ (ํฐ์ƒ‰/ํšŒ์ƒ‰)
345
+
346
+ ```python
347
+ def filter_by_saturation(image, detections, max_saturation=150):
348
+ """
349
+ ์ฑ„๋„ ํ•„ํ„ฐ (์ฃฝ์€ ์ƒˆ์šฐ ์ „์šฉ)
350
+
351
+ Args:
352
+ max_saturation: ์ตœ๋Œ€ ์ฑ„๋„ (0~255)
353
+ """
354
+ import cv2
355
+
356
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
357
+ filtered = []
358
+
359
+ for det in detections:
360
+ bbox = det['bbox']
361
+ x1, y1, x2, y2 = map(int, bbox)
362
+
363
+ roi_hsv = hsv_image[y1:y2, x1:x2]
364
+ if roi_hsv.size == 0:
365
+ continue
366
+
367
+ # ํ‰๊ท  ์ฑ„๋„
368
+ mean_saturation = roi_hsv[:, :, 1].mean()
369
+
370
+ # ๋‚ฎ์€ ์ฑ„๋„ = ์ฃฝ์€ ์ƒˆ์šฐ
371
+ if mean_saturation < max_saturation:
372
+ det['saturation_score'] = 1.0 - (mean_saturation / 255)
373
+ filtered.append(det)
374
+
375
+ return filtered
376
+ ```
377
+
378
+ #### 2.3 ํ…์Šค์ฒ˜ ๊ท ์งˆ์„ฑ ํ•„ํ„ฐ (Texture Homogeneity)
379
+
380
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ๋ถ€๋“œ๋Ÿฌ์šด ํ…์Šค์ฒ˜ (์ž๋‚˜ ๋งคํŠธ๋Š” ๊ฐ•ํ•œ ํŒจํ„ด)
381
+
382
+ ```python
383
+ def filter_by_texture_homogeneity(image, detections, max_texture_std=40):
384
+ """
385
+ ํ…์Šค์ฒ˜ ๊ท ์งˆ์„ฑ ํ•„ํ„ฐ
386
+
387
+ Args:
388
+ max_texture_std: ์ตœ๋Œ€ ํ…์Šค์ฒ˜ ํ‘œ์ค€ํŽธ์ฐจ
389
+ """
390
+ import cv2
391
+ import numpy as np
392
+
393
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
394
+ filtered = []
395
+
396
+ for det in detections:
397
+ bbox = det['bbox']
398
+ x1, y1, x2, y2 = map(int, bbox)
399
+
400
+ roi = gray[y1:y2, x1:x2]
401
+ if roi.size == 0:
402
+ continue
403
+
404
+ # ํ…์Šค์ฒ˜ ๋ณต์žก๋„ (ํ‘œ์ค€ํŽธ์ฐจ)
405
+ texture_std = np.std(roi)
406
+
407
+ # ๋ถ€๋“œ๋Ÿฌ์šด ํ…์Šค์ฒ˜ = ๋‚ฎ์€ ํ‘œ์ค€ํŽธ์ฐจ
408
+ if texture_std < max_texture_std:
409
+ det['texture_homogeneity'] = 1.0 - (texture_std / 100)
410
+ filtered.append(det)
411
+
412
+ return filtered
413
+ ```
414
+
415
+ #### 2.4 ์—์ง€ ํŒจํ„ด ํ•„ํ„ฐ (Edge Pattern)
416
+
417
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ๋ถ€๋“œ๋Ÿฌ์šด ๊ณก์„  ์—์ง€ (์ง์„  ์—์ง€ ์—†์Œ)
418
+
419
+ ```python
420
+ def filter_by_edge_pattern(image, detections, min_edge_smoothness=0.6):
421
+ """
422
+ ์—์ง€ ํŒจํ„ด ํ•„ํ„ฐ
423
+
424
+ Args:
425
+ min_edge_smoothness: ์ตœ์†Œ ์—์ง€ ๋ถ€๋“œ๋Ÿฌ์›€ (0~1)
426
+ """
427
+ import cv2
428
+ import numpy as np
429
+
430
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
431
+ edges = cv2.Canny(gray, 50, 150)
432
+
433
+ filtered = []
434
+
435
+ for det in detections:
436
+ bbox = det['bbox']
437
+ x1, y1, x2, y2 = map(int, bbox)
438
+
439
+ roi_edges = edges[y1:y2, x1:x2]
440
+ if roi_edges.size == 0:
441
+ continue
442
+
443
+ # Hough Line ๋ณ€ํ™˜์œผ๋กœ ์ง์„  ๊ฒ€์ถœ
444
+ lines = cv2.HoughLinesP(roi_edges, 1, np.pi/180, threshold=30,
445
+ minLineLength=20, maxLineGap=5)
446
+
447
+ # ์ง์„  ๊ฐœ์ˆ˜
448
+ num_lines = len(lines) if lines is not None else 0
449
+
450
+ # ์—์ง€ ํ”ฝ์…€ ๊ฐœ์ˆ˜
451
+ total_edges = np.sum(roi_edges > 0)
452
+
453
+ if total_edges == 0:
454
+ continue
455
+
456
+ # ์ง์„  ๋น„์œจ = ์ง์„  ๊ฐœ์ˆ˜ / ์ด ์—์ง€ ํ”ฝ์…€
457
+ # (๋‚ฎ์„์ˆ˜๋ก ๊ณก์„ ํ˜•)
458
+ line_ratio = num_lines / (total_edges / 100) # ์ •๊ทœํ™”
459
+
460
+ edge_smoothness = max(0, 1.0 - line_ratio)
461
+
462
+ if edge_smoothness > min_edge_smoothness:
463
+ det['edge_smoothness'] = edge_smoothness
464
+ filtered.append(det)
465
+
466
+ return filtered
467
+ ```
468
+
469
+ ---
470
+
471
+ ### Step 3: ์ •๋ฐ€ ๊ฒ€์ฆ
472
+
473
+ #### 3.1 ๋‚ด๋ถ€ ๊ตฌ์กฐ ๊ฒ€์ฆ (Internal Structure)
474
+
475
+ **์›๋ฆฌ**: ์ฃฝ์€ ์ƒˆ์šฐ๋Š” ํˆฌ๋ช…ํ•˜์—ฌ ๋‚ด๋ถ€ ์†Œํ™”๊ด€(๊ฒ€์€ ์„ ) ๋ณด์ž„
476
+
477
+ ```python
478
+ def verify_internal_structure(image, detections, threshold=0.3):
479
+ """
480
+ ๋‚ด๋ถ€ ๊ตฌ์กฐ ๊ฒ€์ฆ (์ฃฝ์€ ์ƒˆ์šฐ ์ „์šฉ)
481
+
482
+ Args:
483
+ threshold: ๊ฒ€์€ ์„  ๊ฒ€์ถœ ์ž„๊ณ„๊ฐ’
484
+ """
485
+ import cv2
486
+ import numpy as np
487
+
488
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
489
+ filtered = []
490
+
491
+ for det in detections:
492
+ bbox = det['bbox']
493
+ x1, y1, x2, y2 = map(int, bbox)
494
+
495
+ roi = gray[y1:y2, x1:x2]
496
+ if roi.size == 0:
497
+ continue
498
+
499
+ # ์–ด๋‘์šด ํ”ฝ์…€ (์†Œํ™”๊ด€) ๋น„์œจ
500
+ dark_pixels = np.sum(roi < 100)
501
+ total_pixels = roi.size
502
+ dark_ratio = dark_pixels / total_pixels
503
+
504
+ # ์ ๋‹นํ•œ ์–ด๋‘์šด ์˜์—ญ (์†Œํ™”๊ด€)
505
+ if 0.05 < dark_ratio < 0.40:
506
+ det['internal_structure_score'] = dark_ratio
507
+ filtered.append(det)
508
+ else:
509
+ # ๋‚ด๋ถ€ ๊ตฌ์กฐ ์—†์–ด๋„ ํ†ต๊ณผ (ํˆฌ๋ช…ํ•˜์ง€ ์•Š์€ ์ƒˆ์šฐ)
510
+ filtered.append(det)
511
+
512
+ return filtered
513
+ ```
514
+
515
+ #### 3.2 ๋Œ€์นญ์„ฑ ๊ฒ€์ฆ (Symmetry)
516
+
517
+ **์›๋ฆฌ**: ์ƒˆ์šฐ๋Š” ์ขŒ์šฐ ๋Œ€์นญ (์œ„์•„๋ž˜๋Š” ๋น„๋Œ€์นญ)
518
+
519
+ ```python
520
+ def verify_symmetry(image, detections, min_symmetry=0.6):
521
+ """
522
+ ๋Œ€์นญ์„ฑ ๊ฒ€์ฆ
523
+
524
+ Args:
525
+ min_symmetry: ์ตœ์†Œ ๋Œ€์นญ์„ฑ ์ ์ˆ˜ (0~1)
526
+ """
527
+ import cv2
528
+ import numpy as np
529
+
530
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
531
+ filtered = []
532
+
533
+ for det in detections:
534
+ bbox = det['bbox']
535
+ x1, y1, x2, y2 = map(int, bbox)
536
+
537
+ roi = gray[y1:y2, x1:x2]
538
+ if roi.size == 0:
539
+ continue
540
+
541
+ h, w = roi.shape
542
+
543
+ # ์ขŒ์šฐ ๋Œ€์นญ ๋น„๊ต
544
+ left_half = roi[:, :w//2]
545
+ right_half = roi[:, w//2:]
546
+ right_half_flipped = np.fliplr(right_half)
547
+
548
+ # ํฌ๊ธฐ ๋งž์ถ”๊ธฐ
549
+ min_width = min(left_half.shape[1], right_half_flipped.shape[1])
550
+ left_half = left_half[:, :min_width]
551
+ right_half_flipped = right_half_flipped[:, :min_width]
552
+
553
+ # ์ƒ๊ด€๊ณ„์ˆ˜ (๋Œ€์นญ๋„)
554
+ correlation = np.corrcoef(left_half.flatten(), right_half_flipped.flatten())[0, 1]
555
+
556
+ if not np.isnan(correlation) and correlation > min_symmetry:
557
+ det['symmetry_score'] = correlation
558
+ filtered.append(det)
559
+
560
+ return filtered
561
+ ```
562
+
563
+ ---
564
+
565
+ ## ๐Ÿ”— ํ†ตํ•ฉ ํŒŒ์ดํ”„๋ผ์ธ
566
+
567
+ ```python
568
+ class UniversalShrimpFilter:
569
+ """๋ฒ”์šฉ ์ƒˆ์šฐ ๊ฒ€์ถœ ํ•„ํ„ฐ (๋ฐฐ๊ฒฝ/์œ„์น˜ ๋ฌด๊ด€)"""
570
+
571
+ def __init__(self):
572
+ # ํŒŒ๋ผ๋ฏธํ„ฐ (์กฐ์ • ๊ฐ€๋Šฅ)
573
+ self.params = {
574
+ # Step 1: ํ˜•ํƒœ ํ•„ํ„ฐ
575
+ 'min_aspect_ratio': 2.0,
576
+ 'max_aspect_ratio': 10.0,
577
+ 'max_line_ratio': 0.3,
578
+ 'max_compactness': 0.25,
579
+ 'min_area_ratio': 0.05,
580
+ 'max_area_ratio': 0.50,
581
+
582
+ # Step 2: ์ƒ‰์ƒ/ํ…์Šค์ฒ˜ ํ•„ํ„ฐ
583
+ 'max_color_std': 50,
584
+ 'max_saturation': 150,
585
+ 'max_texture_std': 40,
586
+ 'min_edge_smoothness': 0.6,
587
+
588
+ # Step 3: ์ •๋ฐ€ ๊ฒ€์ฆ
589
+ 'min_symmetry': 0.6,
590
+
591
+ # ์ข…ํ•ฉ
592
+ 'min_total_score': 60, # 0~100
593
+ }
594
+
595
+ def filter(self, image, detections):
596
+ """
597
+ ๋ฒ”์šฉ ํ•„ํ„ฐ๋ง ํŒŒ์ดํ”„๋ผ์ธ
598
+
599
+ Args:
600
+ image: ์ž…๋ ฅ ์ด๋ฏธ์ง€ (BGR)
601
+ detections: RT-DETR ๊ฒ€์ถœ ๊ฒฐ๊ณผ
602
+
603
+ Returns:
604
+ ํ•„ํ„ฐ๋ง๋œ ๊ฒ€์ถœ (์ƒˆ์šฐ๋งŒ)
605
+ """
606
+ import numpy as np
607
+
608
+ print(f"[INFO] Initial detections: {len(detections)}")
609
+
610
+ # Step 1: ํ˜•ํƒœ ํ•„ํ„ฐ
611
+ detections = filter_by_aspect_ratio(
612
+ detections,
613
+ self.params['min_aspect_ratio'],
614
+ self.params['max_aspect_ratio']
615
+ )
616
+ print(f"[STEP 1-1] After aspect ratio filter: {len(detections)}")
617
+
618
+ if not detections:
619
+ return []
620
+
621
+ detections = filter_by_curvature(
622
+ image, detections,
623
+ self.params['max_line_ratio']
624
+ )
625
+ print(f"[STEP 1-2] After curvature filter: {len(detections)}")
626
+
627
+ if not detections:
628
+ return []
629
+
630
+ detections = filter_by_compactness(
631
+ image, detections,
632
+ self.params['max_compactness']
633
+ )
634
+ print(f"[STEP 1-3] After compactness filter: {len(detections)}")
635
+
636
+ if not detections:
637
+ return []
638
+
639
+ detections = filter_by_adaptive_size(
640
+ detections, image.shape,
641
+ self.params['min_area_ratio'],
642
+ self.params['max_area_ratio']
643
+ )
644
+ print(f"[STEP 1-4] After size filter: {len(detections)}")
645
+
646
+ if not detections:
647
+ return []
648
+
649
+ # Step 2: ์ƒ‰์ƒ/ํ…์Šค์ฒ˜ ํ•„ํ„ฐ
650
+ detections = filter_by_color_consistency(
651
+ image, detections,
652
+ self.params['max_color_std']
653
+ )
654
+ print(f"[STEP 2-1] After color consistency filter: {len(detections)}")
655
+
656
+ if not detections:
657
+ return []
658
+
659
+ detections = filter_by_saturation(
660
+ image, detections,
661
+ self.params['max_saturation']
662
+ )
663
+ print(f"[STEP 2-2] After saturation filter: {len(detections)}")
664
+
665
+ if not detections:
666
+ return []
667
+
668
+ detections = filter_by_texture_homogeneity(
669
+ image, detections,
670
+ self.params['max_texture_std']
671
+ )
672
+ print(f"[STEP 2-3] After texture filter: {len(detections)}")
673
+
674
+ if not detections:
675
+ return []
676
+
677
+ # Step 3: ์ •๋ฐ€ ๊ฒ€์ฆ (์„ ํƒ์ )
678
+ detections = verify_internal_structure(image, detections)
679
+ print(f"[STEP 3-1] After internal structure verify: {len(detections)}")
680
+
681
+ detections = verify_symmetry(
682
+ image, detections,
683
+ self.params['min_symmetry']
684
+ )
685
+ print(f"[STEP 3-2] After symmetry verify: {len(detections)}")
686
+
687
+ # ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋ฐ ์ตœ์ข… ์„ ํƒ
688
+ detections = self._calculate_total_scores(detections)
689
+ detections = [d for d in detections if d['total_score'] >= self.params['min_total_score']]
690
+ detections = sorted(detections, key=lambda x: x['total_score'], reverse=True)
691
+
692
+ print(f"[FINAL] After scoring: {len(detections)}")
693
+
694
+ return detections[:1] if detections else [] # ์ƒ์œ„ 1๊ฐœ
695
+
696
+ def _calculate_total_scores(self, detections):
697
+ """์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ (0~100)"""
698
+
699
+ # ๊ฐ€์ค‘์น˜
700
+ WEIGHTS = {
701
+ 'aspect_ratio': 0.15,
702
+ 'curvature': 0.15,
703
+ 'compactness': 0.10,
704
+ 'area_ratio': 0.10,
705
+ 'color_consistency': 0.15,
706
+ 'saturation': 0.10,
707
+ 'texture_homogeneity': 0.10,
708
+ 'edge_smoothness': 0.05,
709
+ 'symmetry': 0.10,
710
+ }
711
+
712
+ for det in detections:
713
+ score = 0.0
714
+
715
+ # ๊ฐ ์ ์ˆ˜ ํ•ฉ์‚ฐ
716
+ if 'aspect_ratio' in det:
717
+ # ์ด์ƒ์  ์ข…ํšก๋น„์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋†’์€ ์ ์ˆ˜
718
+ ideal_aspect = 4.5
719
+ fitness = max(0, 1 - abs(det['aspect_ratio'] - ideal_aspect) / ideal_aspect)
720
+ score += fitness * WEIGHTS['aspect_ratio']
721
+
722
+ if 'curvature_score' in det:
723
+ score += det['curvature_score'] * WEIGHTS['curvature']
724
+
725
+ if 'compactness' in det:
726
+ # ๋‚ฎ์„์ˆ˜๋ก ์ข‹์Œ
727
+ fitness = max(0, 1 - det['compactness'] / 0.25)
728
+ score += fitness * WEIGHTS['compactness']
729
+
730
+ if 'area_ratio' in det:
731
+ # ์ด์ƒ์  ํฌ๊ธฐ: 15% ์ •๋„
732
+ ideal_area = 0.15
733
+ fitness = max(0, 1 - abs(det['area_ratio'] - ideal_area) / ideal_area)
734
+ score += fitness * WEIGHTS['area_ratio']
735
+
736
+ if 'color_consistency' in det:
737
+ score += det['color_consistency'] * WEIGHTS['color_consistency']
738
+
739
+ if 'saturation_score' in det:
740
+ score += det['saturation_score'] * WEIGHTS['saturation']
741
+
742
+ if 'texture_homogeneity' in det:
743
+ score += det['texture_homogeneity'] * WEIGHTS['texture_homogeneity']
744
+
745
+ if 'edge_smoothness' in det:
746
+ score += det['edge_smoothness'] * WEIGHTS['edge_smoothness']
747
+
748
+ if 'symmetry_score' in det:
749
+ score += det['symmetry_score'] * WEIGHTS['symmetry']
750
+
751
+ det['total_score'] = score * 100
752
+
753
+ return detections
754
+
755
+
756
+ # ์‚ฌ์šฉ ์˜ˆ์‹œ
757
+ if __name__ == "__main__":
758
+ import cv2
759
+
760
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ
761
+ image = cv2.imread("shrimp_image.jpg")
762
+
763
+ # RT-DETR ๊ฒ€์ถœ (๊ฐ€์ƒ)
764
+ rtdetr_detections = [
765
+ {'bbox': [100, 200, 500, 280], 'confidence': 0.85, 'class_id': 1},
766
+ {'bbox': [50, 50, 150, 500], 'confidence': 0.92, 'class_id': 2}, # ์ž (์ œ๊ฑฐ๋จ)
767
+ ]
768
+
769
+ # ๋ฒ”์šฉ ํ•„ํ„ฐ ์ ์šฉ
770
+ universal_filter = UniversalShrimpFilter()
771
+ shrimp_detections = universal_filter.filter(image, rtdetr_detections)
772
+
773
+ print(f"\nโœ… Final shrimp detections: {len(shrimp_detections)}")
774
+ for i, det in enumerate(shrimp_detections, 1):
775
+ print(f" Shrimp #{i}: score={det['total_score']:.2f}/100")
776
+ ```
777
+
778
+ ---
779
+
780
+ ## ๐Ÿ“Š ๋ฒ”์šฉ์„ฑ ๊ฒ€์ฆ ์‹œ๋‚˜๋ฆฌ์˜ค
781
+
782
+ ### ๋ฐฐ๊ฒฝ ์œ ํ˜•๋ณ„ ํ…Œ์ŠคํŠธ (MECE)
783
+
784
+ ```
785
+ A. ๋ฐฐ๊ฒฝ ์œ ํ˜•
786
+ โ”œโ”€โ”€ A1. ์ธก์ • ๋งคํŠธ (ํŒŒ๋ž€์ƒ‰)
787
+ โ”œโ”€โ”€ A2. ํฐ์ƒ‰ ์ ‘์‹œ
788
+ โ”œโ”€โ”€ A3. ๋‚˜๋ฌด ๋„๋งˆ
789
+ โ”œโ”€โ”€ A4. ์†๋ฐ”๋‹ฅ
790
+ โ”œโ”€โ”€ A5. ์ˆ˜์กฐ (๋ฌผ์†)
791
+ โ””โ”€โ”€ A6. ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ (์žก์ง€, ์‹ ๋ฌธ ๋“ฑ)
792
+
793
+ B. ์ฃผ๋ณ€ ๊ฐ์ฒด
794
+ โ”œโ”€โ”€ B1. ์ƒˆ์šฐ๋งŒ
795
+ โ”œโ”€โ”€ B2. ์ƒˆ์šฐ + ์ž
796
+ โ”œโ”€โ”€ B3. ์ƒˆ์šฐ + ์†
797
+ โ”œโ”€โ”€ B4. ์ƒˆ์šฐ + ๋‹ค์–‘ํ•œ ๋ฌผ์ฒด
798
+ โ””โ”€โ”€ B5. ์ƒˆ์šฐ ์—†์Œ (Negative)
799
+
800
+ C. ์ƒˆ์šฐ ์œ„์น˜
801
+ โ”œโ”€โ”€ C1. ์ค‘์•™
802
+ โ”œโ”€โ”€ C2. ๋ชจ์„œ๋ฆฌ
803
+ โ”œโ”€โ”€ C3. ๊ฐ€์žฅ์ž๋ฆฌ
804
+ โ””โ”€โ”€ C4. ์—ฌ๋Ÿฌ ๋งˆ๋ฆฌ (์‚ฐ์žฌ)
805
+ ```
806
+
807
+ ### ์˜ˆ์ƒ ์„ฑ๋Šฅ
808
+
809
+ | ์‹œ๋‚˜๋ฆฌ์˜ค | Precision | Recall | F1 |
810
+ |---------|-----------|--------|-----|
811
+ | ๋งคํŠธ ์œ„ (๊นจ๋—) | 85% | 90% | 87% |
812
+ | ์ ‘์‹œ ์œ„ | 80% | 85% | 82% |
813
+ | ์†๋ฐ”๋‹ฅ ์œ„ | 75% | 80% | 77% |
814
+ | ๋ณต์žกํ•œ ๋ฐฐ๊ฒฝ | 60% | 70% | 65% |
815
+ | **ํ‰๊ท ** | **75%** | **81%** | **78%** |
816
+
817
+ ---
818
+
819
+ ## ๐ŸŽฏ ํ•ต์‹ฌ ์žฅ์ 
820
+
821
+ ### โœ… ๋ฒ”์šฉ์„ฑ (Universality)
822
+ 1. **๋ฐฐ๊ฒฝ ๋…๋ฆฝ**: ๋งคํŠธ, ์ ‘์‹œ, ์†๋ฐ”๋‹ฅ ๋“ฑ ์–ด๋””๋“ 
823
+ 2. **๊ฐ์ฒด ๋…๋ฆฝ**: ์ž, ์† ๋“ฑ ์ฃผ๋ณ€ ๊ฐ์ฒด ๋ถˆํ•„์š”
824
+ 3. **์œ„์น˜ ๋…๋ฆฝ**: ์ด๋ฏธ์ง€ ์–ด๋””์— ์žˆ๋“  ๊ฒ€์ถœ
825
+ 4. **ํฌ๊ธฐ ๋…๋ฆฝ**: ์ž‘์€/ํฐ ์ƒˆ์šฐ ๋ชจ๋‘ ๊ฒ€์ถœ
826
+
827
+ ### โœ… ๊ฐ•๊ฑด์„ฑ (Robustness)
828
+ 1. **์กฐ๋ช… ๋ณ€ํ™”**: HSV ์ƒ‰๊ณต๊ฐ„์œผ๋กœ ์กฐ๋ช… ๋ฌด๊ด€
829
+ 2. **ํšŒ์ „ ๋ณ€ํ™”**: ์ข…ํšก๋น„๋Š” ๋ฐฉํ–ฅ ๊ณ ๋ ค
830
+ 3. **์Šค์ผ€์ผ ๋ณ€ํ™”**: ์ ์‘ํ˜• ํฌ๊ธฐ ํ•„ํ„ฐ
831
+
832
+ ### โœ… ์ •๋ฐ€์„ฑ (Precision)
833
+ 1. **๋‹ค๋‹จ๊ณ„ ํ•„ํ„ฐ**: ํ˜•ํƒœ โ†’ ์ƒ‰์ƒ โ†’ ๊ตฌ์กฐ
834
+ 2. **์ข…ํ•ฉ ์ ์ˆ˜**: ์—ฌ๋Ÿฌ ํŠน์ง•์˜ ๊ฐ€์ค‘ ํ‰๊ท 
835
+ 3. **์ž„๊ณ„๊ฐ’ ์กฐ์ •**: ์ƒํ™ฉ์— ๋งž๊ฒŒ ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹
836
+
837
+ ---
838
+
839
+ ## ๐Ÿ”ง ํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹ ๊ฐ€์ด๋“œ
840
+
841
+ ```python
842
+ # ๊ฒ€์ถœ ์•ˆ ๋จ (Recall ๋‚ฎ์Œ)
843
+ โ†’ ์ž„๊ณ„๊ฐ’ ์™„ํ™”
844
+ - min_aspect_ratio: 2.0 โ†’ 1.5
845
+ - max_compactness: 0.25 โ†’ 0.30
846
+ - min_total_score: 60 โ†’ 50
847
+
848
+ # ๊ณผ๋‹ค ๊ฒ€์ถœ (Precision ๋‚ฎ์Œ)
849
+ โ†’ ์ž„๊ณ„๊ฐ’ ๊ฐ•ํ™”
850
+ - min_aspect_ratio: 2.0 โ†’ 2.5
851
+ - max_saturation: 150 โ†’ 120
852
+ - min_total_score: 60 โ†’ 70
853
+
854
+ # ์†๋„ ๊ฐœ์„ 
855
+ โ†’ ํ•„ํ„ฐ ์ˆœ์„œ ์ตœ์ ํ™”
856
+ 1. ๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ํšจ๊ณผ์ ์ธ ํ•„ํ„ฐ ๋จผ์ €
857
+ 2. ๋น„์šฉ ๋†’์€ ํ•„ํ„ฐ(์™ธ๊ณฝ์„ , ๋Œ€์นญ์„ฑ)๋Š” ํ›„๋ฐ˜
858
+ ```
859
+
860
+ ---
861
+
862
+ ## ๐Ÿ“ˆ ์„ฑ๋Šฅ ๋น„๊ต
863
+
864
+ | ์ „๋žต | ๋ฒ”์šฉ์„ฑ | ์ •ํ™•๋„ | ์†๋„ | ๊ตฌํ˜„ ๋‚œ์ด๋„ |
865
+ |------|-------|--------|------|------------|
866
+ | **์ปจํ…์ŠคํŠธ ์˜์กด** (์ž, ๋งคํŠธ ๋“ฑ) | โญโญ | โญโญโญโญ | โญโญโญโญ | โญโญ |
867
+ | **๋ณธ์งˆ์  ํŠน์ง• ๊ธฐ๋ฐ˜** (ํ˜„์žฌ) | โญโญโญโญโญ | โญโญโญ | โญโญโญ | โญโญโญ |
868
+ | **Roboflow ํŒŒ์ธํŠœ๋‹** | โญโญโญโญโญ | โญโญโญโญโญ | โญโญโญโญ | โญโญโญโญ |
869
+
870
+ ---
871
+
872
+ ## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„
873
+
874
+ 1. **์ฆ‰์‹œ ๊ตฌํ˜„**: ๋ฒ”์šฉ ํ•„ํ„ฐ ์ฝ”๋“œ ์ ์šฉ
875
+ 2. **ํ…Œ์ŠคํŠธ**: ๋‹ค์–‘ํ•œ ๋ฐฐ๊ฒฝ/์œ„์น˜ ์ด๋ฏธ์ง€๋กœ ๊ฒ€์ฆ
876
+ 3. **ํŠœ๋‹**: ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™”
877
+ 4. **์ค‘๊ธฐ**: Roboflow ํŒŒ์ธํŠœ๋‹์œผ๋กœ ์ •ํ™•๋„ ํ–ฅ์ƒ
878
+
879
+ ---
880
+
881
+ **์ž‘์„ฑ์ผ**: 2025-11-07
882
+ **๋ฒ„์ „**: 2.0 (Universal)
883
+ **์ž‘์„ฑ์ž**: VIDraft Team
interactive_validation.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ ์ธํ„ฐํŽ˜์ด์Šค
4
+ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐ์ •ํ•˜๋ฉฐ ๊ฒ€์ถœ ๊ฒฐ๊ณผ ํ™•์ธ
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import gradio as gr
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ import numpy as np
12
+ from test_visual_validation import (
13
+ load_rtdetr_model,
14
+ detect_with_rtdetr,
15
+ apply_universal_filter,
16
+ calculate_morphological_features,
17
+ calculate_visual_features
18
+ )
19
+
20
+ # ์ „์—ญ ๋ชจ๋ธ (ํ•œ ๋ฒˆ๋งŒ ๋กœ๋“œ)
21
+ processor, model = load_rtdetr_model()
22
+
23
+ def interactive_detect(image, confidence, filter_threshold, show_all=False):
24
+ """๋Œ€ํ™”ํ˜• ๊ฒ€์ถœ"""
25
+ if image is None:
26
+ return None, "โš ๏ธ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”."
27
+
28
+ try:
29
+ # RT-DETR ๊ฒ€์ถœ
30
+ all_detections = detect_with_rtdetr(image, processor, model, confidence)
31
+
32
+ # ํ•„ํ„ฐ ์ ์šฉ
33
+ filtered_detections = apply_universal_filter(all_detections, image, filter_threshold)
34
+
35
+ # ์‹œ๊ฐํ™”
36
+ img = image.copy()
37
+ draw = ImageDraw.Draw(img)
38
+
39
+ try:
40
+ font = ImageFont.truetype("arial.ttf", 14)
41
+ font_large = ImageFont.truetype("arial.ttf", 18)
42
+ font_small = ImageFont.truetype("arial.ttf", 10)
43
+ except:
44
+ font = ImageFont.load_default()
45
+ font_large = ImageFont.load_default()
46
+ font_small = ImageFont.load_default()
47
+
48
+ # ์ „์ฒด ๊ฒ€์ถœ ํ‘œ์‹œ (์˜ต์…˜)
49
+ if show_all:
50
+ for det in all_detections:
51
+ x1, y1, x2, y2 = det['bbox']
52
+ draw.rectangle([x1, y1, x2, y2], outline="gray", width=1)
53
+
54
+ # ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ
55
+ for idx, det in enumerate(filtered_detections, 1):
56
+ x1, y1, x2, y2 = det['bbox']
57
+ score = det['filter_score']
58
+
59
+ # ์ ์ˆ˜์— ๋”ฐ๋ผ ์ƒ‰์ƒ
60
+ if score >= 75:
61
+ color = "lime"
62
+ elif score >= 50:
63
+ color = "yellow"
64
+ else:
65
+ color = "orange"
66
+
67
+ # ๋ฐ•์Šค
68
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
69
+
70
+ # ๋ผ๋ฒจ
71
+ label = f"#{idx} {score:.0f}์ "
72
+ bbox = draw.textbbox((x1, y1 - 25), label, font=font)
73
+ draw.rectangle(bbox, fill=color)
74
+ draw.text((x1, y1 - 25), label, fill="black", font=font)
75
+
76
+ # ์„ธ๋ถ€ ์ •๋ณด (์ž‘๊ฒŒ) - RT-DETR ์‹ ๋ขฐ๋„ ๋ช…์‹œ
77
+ details = f"RT-DETR:{det['confidence']:.0%}"
78
+ draw.text((x1, y2 + 5), details, fill=color, font=font_small)
79
+
80
+ # ํ—ค๋”
81
+ header = f"๊ฒ€์ถœ: {len(filtered_detections)}๊ฐœ (์ „์ฒด: {len(all_detections)}๊ฐœ)"
82
+ header_bbox = draw.textbbox((10, 10), header, font=font_large)
83
+ draw.rectangle([5, 5, header_bbox[2]+10, header_bbox[3]+10],
84
+ fill="black", outline="lime", width=2)
85
+ draw.text((10, 10), header, fill="lime", font=font_large)
86
+
87
+ # ์ •๋ณด ์ƒ์„ฑ
88
+ info = f"""
89
+ ### ๐Ÿ“Š ๊ฒ€์ถœ ๊ฒฐ๊ณผ
90
+
91
+ - **์ „์ฒด ๊ฒ€์ถœ**: {len(all_detections)}๊ฐœ
92
+ - **ํ•„ํ„ฐ๋ง ํ›„**: {len(filtered_detections)}๊ฐœ
93
+ - **์ œ๊ฑฐ๋จ**: {len(all_detections) - len(filtered_detections)}๊ฐœ
94
+
95
+ ---
96
+
97
+ ### ๐ŸŽฏ ๊ฒ€์ถœ๋œ ๊ฐ์ฒด ์ƒ์„ธ
98
+
99
+ """
100
+
101
+ for idx, det in enumerate(filtered_detections, 1):
102
+ info += f"""
103
+ **#{idx} - ์ ์ˆ˜: {det['filter_score']:.0f}์ ** (RT-DETR ์‹ ๋ขฐ๋„: {det['confidence']:.0%})
104
+
105
+ """
106
+ # ์ฃผ์š” ํŠน์ง•๋งŒ 3๊ฐœ
107
+ for reason in det['filter_reasons'][:3]:
108
+ info += f"- {reason}\n"
109
+
110
+ if not filtered_detections:
111
+ info += """
112
+ โš ๏ธ **๊ฒ€์ถœ๋œ ๊ฐ์ฒด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.**
113
+
114
+ **์กฐ์ • ๋ฐฉ๋ฒ•:**
115
+ 1. **์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’**์„ ๋‚ฎ์ถฐ๋ณด์„ธ์š” (0.2~0.3)
116
+ 2. **ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’**์„ ๋‚ฎ์ถฐ๋ณด์„ธ์š” (30~40)
117
+ 3. "์ „์ฒด ๊ฒ€์ถœ ํ‘œ์‹œ"๋ฅผ ์ผœ์„œ ์›๋ณธ ๊ฒ€์ถœ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์„ธ์š”
118
+ """
119
+
120
+ info += f"""
121
+
122
+ ---
123
+
124
+ ### โš™๏ธ ํ˜„์žฌ ์„ค์ •
125
+
126
+ - **RT-DETR ์‹ ๋ขฐ๋„**: {confidence:.0%}
127
+ - **ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’**: {filter_threshold}์ 
128
+ - **์ „์ฒด ๊ฒ€์ถœ ํ‘œ์‹œ**: {'์ผœ์ง' if show_all else '๊บผ์ง'}
129
+ """
130
+
131
+ return img, info
132
+
133
+ except Exception as e:
134
+ import traceback
135
+ error_detail = traceback.format_exc()
136
+ return None, f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:\n\n```\n{error_detail}\n```"
137
+
138
+ def analyze_single_detection(image, x1, y1, x2, y2):
139
+ """๋‹จ์ผ ๊ฒ€์ถœ ์˜์—ญ ๋ถ„์„ (์ˆ˜๋™ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค)"""
140
+ if image is None:
141
+ return "โš ๏ธ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜์„ธ์š”."
142
+
143
+ try:
144
+ bbox = [float(x1), float(y1), float(x2), float(y2)]
145
+
146
+ # ํ˜•ํƒœํ•™์  ํŠน์ง•
147
+ morph = calculate_morphological_features(bbox, image.size)
148
+
149
+ # ์‹œ๊ฐ์  ํŠน์ง•
150
+ visual = calculate_visual_features(image, bbox)
151
+
152
+ analysis = f"""
153
+ ### ๐Ÿ”ฌ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ๋ถ„์„
154
+
155
+ **์ขŒํ‘œ**: ({x1:.0f}, {y1:.0f}) โ†’ ({x2:.0f}, {y2:.0f})
156
+
157
+ ---
158
+
159
+ ### ๐Ÿ“ ํ˜•ํƒœํ•™์  ํŠน์ง•
160
+
161
+ - **์ข…ํšก๋น„**: {morph['aspect_ratio']:.2f} {'โœ… (2~10 ๋ฒ”์œ„)' if 2 <= morph['aspect_ratio'] <= 10 else 'โŒ'}
162
+ - **์„ธ์žฅ๋„ (Compactness)**: {morph['compactness']:.3f} {'โœ… (<0.25)' if morph['compactness'] < 0.25 else 'โŒ'}
163
+ - **๋ฉด์ ๋น„**: {morph['area_ratio']*100:.1f}% {'โœ… (5~50%)' if 0.05 <= morph['area_ratio'] <= 0.5 else 'โŒ'}
164
+ - **๋„ˆ๋น„ร—๋†’์ด**: {morph['width']:.0f} ร— {morph['height']:.0f}
165
+
166
+ ---
167
+
168
+ ### ๐ŸŽจ ์‹œ๊ฐ์  ํŠน์ง•
169
+
170
+ - **์ฑ„๋„ (Saturation)**: {visual['saturation']:.0f} {'โœ… (<150)' if visual['saturation'] < 150 else 'โŒ'}
171
+ - **์ƒ‰์ƒ ์ผ๊ด€์„ฑ (Std)**: {visual['color_std']:.1f} {'โœ… (<30)' if visual['color_std'] < 30 else 'โŒ'}
172
+
173
+ ---
174
+
175
+ ### ๐Ÿ’ฏ ์˜ˆ์ƒ ์ ์ˆ˜
176
+
177
+ """
178
+ # ์ ์ˆ˜ ๊ณ„์‚ฐ
179
+ score = 0
180
+ if 2.0 <= morph['aspect_ratio'] <= 10.0:
181
+ score += 15
182
+ if morph['compactness'] < 0.25:
183
+ score += 15
184
+ if 0.05 <= morph['area_ratio'] <= 0.50:
185
+ score += 10
186
+ if visual['saturation'] < 150:
187
+ score += 20
188
+ if visual['color_std'] < 30:
189
+ score += 15
190
+
191
+ analysis += f"**์ด์ **: {score}/75์  (RT-DETR ์‹ ๋ขฐ๋„ ์ œ์™ธ)\n\n"
192
+
193
+ if score >= 50:
194
+ analysis += "โœ… **ํŒ์ •**: ์ƒˆ์šฐ๋กœ ๋ถ„๋ฅ˜๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค."
195
+ else:
196
+ analysis += "โŒ **ํŒ์ •**: ์ƒˆ์šฐ๊ฐ€ ์•„๋‹ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค."
197
+
198
+ return analysis
199
+
200
+ except Exception as e:
201
+ return f"โŒ ์˜ค๋ฅ˜: {str(e)}"
202
+
203
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค
204
+ with gr.Blocks(title="๐Ÿงช ์ƒˆ์šฐ ๊ฒ€์ถœ ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ", theme=gr.themes.Soft()) as demo:
205
+
206
+ gr.Markdown("""
207
+ # ๐Ÿงช ์ƒˆ์šฐ ๊ฒ€์ถœ ๋Œ€ํ™”ํ˜• ๊ฒ€์ฆ ๋„๊ตฌ
208
+
209
+ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐ์ •ํ•˜๋ฉฐ ๊ฒ€์ถœ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
210
+
211
+ ---
212
+ """)
213
+
214
+ with gr.Tabs():
215
+ # ํƒญ 1: ์ž๋™ ๊ฒ€์ถœ
216
+ with gr.TabItem("๐Ÿค– ์ž๋™ ๊ฒ€์ถœ"):
217
+ with gr.Row():
218
+ with gr.Column():
219
+ input_image = gr.Image(label="์ž…๋ ฅ ์ด๋ฏธ์ง€", type="pil")
220
+
221
+ confidence_slider = gr.Slider(
222
+ 0.1, 0.9, 0.3,
223
+ label="RT-DETR ์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’",
224
+ info="๋‚ฎ์„์ˆ˜๋ก ๋” ๋งŽ์ด ๊ฒ€์ถœ"
225
+ )
226
+
227
+ filter_slider = gr.Slider(
228
+ 20, 80, 50,
229
+ label="ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’",
230
+ info="๋†’์„์ˆ˜๋ก ์—„๊ฒฉํ•˜๊ฒŒ ํ•„ํ„ฐ๋ง"
231
+ )
232
+
233
+ show_all_check = gr.Checkbox(
234
+ label="์ „์ฒด ๊ฒ€์ถœ ๊ฒฐ๊ณผ ํ‘œ์‹œ (ํšŒ์ƒ‰)",
235
+ value=False
236
+ )
237
+
238
+ detect_btn = gr.Button("๐Ÿš€ ๊ฒ€์ถœ ์‹คํ–‰", variant="primary", size="lg")
239
+
240
+ # ์˜ˆ์ œ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€
241
+ import os
242
+ import glob
243
+ example_images = sorted(glob.glob("data/251015/251015_*.jpg"))
244
+ if example_images:
245
+ # ์ฒ˜์Œ 5๊ฐœ๋งŒ ์˜ˆ์ œ๋กœ ํ‘œ์‹œ
246
+ examples_list = [[img, 0.3, 75, False] for img in example_images[:5]]
247
+ gr.Examples(
248
+ examples=examples_list,
249
+ inputs=[input_image, confidence_slider, filter_slider, show_all_check],
250
+ label="๐Ÿ“ท ์˜ˆ์ œ ์ด๋ฏธ์ง€ (ํด๋ฆญํ•˜์—ฌ ๋ฐ”๋กœ ํ…Œ์ŠคํŠธ)"
251
+ )
252
+
253
+ with gr.Column():
254
+ output_image = gr.Image(label="๊ฒ€์ถœ ๊ฒฐ๊ณผ")
255
+ output_info = gr.Markdown()
256
+
257
+ detect_btn.click(
258
+ interactive_detect,
259
+ [input_image, confidence_slider, filter_slider, show_all_check],
260
+ [output_image, output_info]
261
+ )
262
+
263
+ gr.Markdown("""
264
+ ### ๐Ÿ’ก ์‚ฌ์šฉ ํŒ
265
+
266
+ - **๊ฒ€์ถœ์ด ๋„ˆ๋ฌด ์ ์„ ๋•Œ**: ์‹ ๋ขฐ๋„์™€ ํ•„ํ„ฐ ์ ์ˆ˜๋ฅผ ๋‚ฎ์ถ”์„ธ์š”
267
+ - **์˜ค๊ฒ€์ถœ์ด ๋งŽ์„ ๋•Œ**: ํ•„ํ„ฐ ์ ์ˆ˜๋ฅผ ๋†’์ด์„ธ์š”
268
+ - **ํŒŒ๋ผ๋ฏธํ„ฐ ํšจ๊ณผ ํ™•์ธ**: "์ „์ฒด ๊ฒ€์ถœ ํ‘œ์‹œ"๋ฅผ ์ผœ์„œ ํ•„ํ„ฐ๋ง ์ „ํ›„๋ฅผ ๋น„๊ตํ•˜์„ธ์š”
269
+ """)
270
+
271
+ # ํƒญ 2: ์ˆ˜๋™ ๋ถ„์„
272
+ with gr.TabItem("๐Ÿ”ฌ ์ˆ˜๋™ ๋ถ„์„"):
273
+ gr.Markdown("""
274
+ ์ˆ˜๋™์œผ๋กœ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋ฅผ ์ง€์ •ํ•˜์—ฌ ํ•ด๋‹น ์˜์—ญ์˜ ํŠน์ง•์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค.
275
+ ์ด๋ฏธ์ง€ ์ขŒํ‘œ๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•˜์„ธ์š”.
276
+ """)
277
+
278
+ with gr.Row():
279
+ with gr.Column():
280
+ manual_image = gr.Image(label="๋ถ„์„ํ•  ์ด๋ฏธ์ง€", type="pil")
281
+
282
+ with gr.Row():
283
+ x1_input = gr.Number(label="x1 (์ขŒ์ƒ๋‹จ X)", value=0)
284
+ y1_input = gr.Number(label="y1 (์ขŒ์ƒ๋‹จ Y)", value=0)
285
+
286
+ with gr.Row():
287
+ x2_input = gr.Number(label="x2 (์šฐํ•˜๋‹จ X)", value=100)
288
+ y2_input = gr.Number(label="y2 (์šฐํ•˜๋‹จ Y)", value=100)
289
+
290
+ analyze_btn = gr.Button("๐Ÿ” ๋ถ„์„ ์‹คํ–‰", variant="secondary", size="lg")
291
+
292
+ with gr.Column():
293
+ analysis_output = gr.Markdown()
294
+
295
+ analyze_btn.click(
296
+ analyze_single_detection,
297
+ [manual_image, x1_input, y1_input, x2_input, y2_input],
298
+ analysis_output
299
+ )
300
+
301
+ gr.Markdown("""
302
+ ### ๐Ÿ“ ์ขŒํ‘œ ํ™•์ธ ๋ฐฉ๋ฒ•
303
+
304
+ ์ด๋ฏธ์ง€ ๋ทฐ์–ด๋‚˜ ๊ทธ๋ฆผํŒ์—์„œ ํ”ฝ์…€ ์ขŒํ‘œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
305
+ - **x1, y1**: ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒ์ƒ๋‹จ ์ขŒํ‘œ
306
+ - **x2, y2**: ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์šฐํ•˜๋‹จ ์ขŒํ‘œ
307
+ """)
308
+
309
+ gr.Markdown("""
310
+ ---
311
+
312
+ ### ๐Ÿ“– ํ•„ํ„ฐ ์ ์ˆ˜ ๊ธฐ์ค€
313
+
314
+ - **75์  ์ด์ƒ**: ๋†’์€ ํ™•๋ฅ ๋กœ ์ƒˆ์šฐ (๋…น์ƒ‰)
315
+ - **50~74์ **: ์ค‘๊ฐ„ ํ™•๋ฅ  (๋…ธ๋ž€์ƒ‰)
316
+ - **50์  ๋ฏธ๋งŒ**: ๋‚ฎ์€ ํ™•๋ฅ  (์ฃผํ™ฉ์ƒ‰)
317
+
318
+ **์ ์ˆ˜ ๊ตฌ์„ฑ** (์ด 100์ ):
319
+ - ์ข…ํšก๋น„ (2~10): 15์ 
320
+ - ์„ธ์žฅ๋„ (<0.25): 15์ 
321
+ - ๋ฉด์ ๋น„ (5~50%): 10์ 
322
+ - ์ฑ„๋„ (<150): 20์ 
323
+ - ์ƒ‰์ƒ ์ผ๊ด€์„ฑ (<30): 15์ 
324
+ - RT-DETR ์‹ ๋ขฐ๋„: ์ตœ๋Œ€ 25์ 
325
+ """)
326
+
327
+ if __name__ == "__main__":
328
+ demo.launch(
329
+ server_name="0.0.0.0",
330
+ server_port=7861, # ๋ฉ”์ธ ์•ฑ๊ณผ ๋‹ค๋ฅธ ํฌํŠธ
331
+ share=False
332
+ )
requirements.txt CHANGED
@@ -8,6 +8,9 @@ transformers>=4.41.0
8
 
9
  # Image Processing
10
  pillow>=10.0.0
 
 
 
11
 
12
  # Inference
13
  inference-sdk>=0.9.0
 
8
 
9
  # Image Processing
10
  pillow>=10.0.0
11
+ opencv-python>=4.8.0
12
+ matplotlib>=3.7.0
13
+ seaborn>=0.12.0
14
 
15
  # Inference
16
  inference-sdk>=0.9.0
test_quantitative_evaluation.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ์ •๋Ÿ‰์  ํ‰๊ฐ€ ์Šคํฌ๋ฆฝํŠธ
4
+ Ground Truth์™€ ๋น„๊ตํ•˜์—ฌ Precision, Recall, F1 Score ๊ณ„์‚ฐ
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import os
10
+ import json
11
+ import numpy as np
12
+ from PIL import Image, ImageDraw, ImageFont
13
+ import matplotlib.pyplot as plt
14
+ import seaborn as sns
15
+ from datetime import datetime
16
+ from test_visual_validation import (
17
+ load_rtdetr_model,
18
+ detect_with_rtdetr,
19
+ apply_universal_filter
20
+ )
21
+
22
+ def calculate_iou(bbox1, bbox2):
23
+ """IoU (Intersection over Union) ๊ณ„์‚ฐ"""
24
+ x1_min, y1_min, x1_max, y1_max = bbox1
25
+ x2_min, y2_min, x2_max, y2_max = bbox2
26
+
27
+ # ๊ต์ง‘ํ•ฉ ์˜์—ญ
28
+ inter_x_min = max(x1_min, x2_min)
29
+ inter_y_min = max(y1_min, y2_min)
30
+ inter_x_max = min(x1_max, x2_max)
31
+ inter_y_max = min(y1_max, y2_max)
32
+
33
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
34
+ return 0.0
35
+
36
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
37
+
38
+ # ํ•ฉ์ง‘ํ•ฉ ์˜์—ญ
39
+ bbox1_area = (x1_max - x1_min) * (y1_max - y1_min)
40
+ bbox2_area = (x2_max - x2_min) * (y2_max - y2_min)
41
+ union_area = bbox1_area + bbox2_area - inter_area
42
+
43
+ return inter_area / union_area if union_area > 0 else 0.0
44
+
45
+ def evaluate_detection(predictions, ground_truths, iou_threshold=0.5):
46
+ """๊ฒ€์ถœ ๊ฒฐ๊ณผ ํ‰๊ฐ€"""
47
+ tp = 0 # True Positive
48
+ fp = 0 # False Positive
49
+ fn = 0 # False Negative
50
+
51
+ matched_gt = set()
52
+
53
+ # ๊ฐ ์˜ˆ์ธก์— ๋Œ€ํ•ด
54
+ for pred in predictions:
55
+ pred_bbox = pred['bbox']
56
+ matched = False
57
+
58
+ # Ground truth์™€ ๋งค์นญ
59
+ for gt_idx, gt in enumerate(ground_truths):
60
+ if gt_idx in matched_gt:
61
+ continue
62
+
63
+ gt_bbox = gt['bbox']
64
+ iou = calculate_iou(pred_bbox, gt_bbox)
65
+
66
+ if iou >= iou_threshold:
67
+ tp += 1
68
+ matched_gt.add(gt_idx)
69
+ matched = True
70
+ break
71
+
72
+ if not matched:
73
+ fp += 1
74
+
75
+ # ๋งค์นญ ์•ˆ ๋œ ground truth = False Negative
76
+ fn = len(ground_truths) - len(matched_gt)
77
+
78
+ # ๋ฉ”ํŠธ๋ฆญ ๊ณ„์‚ฐ
79
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
80
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
81
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
82
+
83
+ return {
84
+ 'tp': tp,
85
+ 'fp': fp,
86
+ 'fn': fn,
87
+ 'precision': precision,
88
+ 'recall': recall,
89
+ 'f1': f1
90
+ }
91
+
92
+ def load_ground_truth(json_path):
93
+ """Ground truth JSON ๋กœ๋“œ
94
+
95
+ ์˜ˆ์ƒ ํ˜•์‹:
96
+ {
97
+ "image1.png": [
98
+ {"bbox": [x1, y1, x2, y2], "label": "shrimp"},
99
+ ...
100
+ ],
101
+ ...
102
+ }
103
+ """
104
+ if not os.path.exists(json_path):
105
+ print(f"โš ๏ธ Ground truth ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค: {json_path}")
106
+ print("๐Ÿ“ ์ƒ์„ฑ ๋ฐฉ๋ฒ•:")
107
+ print(" ground_truth.json ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑ:")
108
+ print(' {"image.png": [{"bbox": [x1, y1, x2, y2], "label": "shrimp"}]}')
109
+ return {}
110
+
111
+ with open(json_path, 'r', encoding='utf-8') as f:
112
+ return json.load(f)
113
+
114
+ def generate_confusion_matrix(results, output_dir):
115
+ """Confusion Matrix ์ƒ์„ฑ"""
116
+ all_tp = sum(r['metrics']['tp'] for r in results)
117
+ all_fp = sum(r['metrics']['fp'] for r in results)
118
+ all_fn = sum(r['metrics']['fn'] for r in results)
119
+ tn = 0 # True Negative (๊ฐ์ฒด ๊ฒ€์ถœ์—์„œ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์˜๋ฏธ ์—†์Œ)
120
+
121
+ matrix = np.array([
122
+ [all_tp, all_fp],
123
+ [all_fn, tn]
124
+ ])
125
+
126
+ plt.figure(figsize=(8, 6))
127
+ sns.heatmap(matrix, annot=True, fmt='d', cmap='Blues',
128
+ xticklabels=['Positive', 'Negative'],
129
+ yticklabels=['Predicted Positive', 'Predicted Negative'])
130
+ plt.title('Confusion Matrix')
131
+ plt.ylabel('Actual')
132
+ plt.xlabel('Predicted')
133
+
134
+ output_path = os.path.join(output_dir, 'confusion_matrix.png')
135
+ plt.savefig(output_path, dpi=150, bbox_inches='tight')
136
+ plt.close()
137
+ print(f" ๐Ÿ“Š Confusion Matrix ์ €์žฅ: {output_path}")
138
+
139
+ def generate_pr_curve(results_by_threshold, output_dir):
140
+ """Precision-Recall Curve ์ƒ์„ฑ"""
141
+ thresholds = sorted(results_by_threshold.keys())
142
+ precisions = [results_by_threshold[t]['avg_precision'] for t in thresholds]
143
+ recalls = [results_by_threshold[t]['avg_recall'] for t in thresholds]
144
+
145
+ plt.figure(figsize=(10, 6))
146
+ plt.plot(recalls, precisions, 'b-o', linewidth=2, markersize=6)
147
+ plt.xlabel('Recall', fontsize=12)
148
+ plt.ylabel('Precision', fontsize=12)
149
+ plt.title('Precision-Recall Curve', fontsize=14)
150
+ plt.grid(True, alpha=0.3)
151
+ plt.xlim([0, 1])
152
+ plt.ylim([0, 1])
153
+
154
+ # ์ตœ์  ์ง€์  ํ‘œ์‹œ
155
+ f1_scores = [2*p*r/(p+r) if (p+r)>0 else 0 for p, r in zip(precisions, recalls)]
156
+ best_idx = np.argmax(f1_scores)
157
+ plt.plot(recalls[best_idx], precisions[best_idx], 'r*', markersize=15,
158
+ label=f'Best F1={f1_scores[best_idx]:.2f} (threshold={thresholds[best_idx]})')
159
+ plt.legend()
160
+
161
+ output_path = os.path.join(output_dir, 'pr_curve.png')
162
+ plt.savefig(output_path, dpi=150, bbox_inches='tight')
163
+ plt.close()
164
+ print(f" ๐Ÿ“ˆ PR Curve ์ €์žฅ: {output_path}")
165
+
166
+ def visualize_evaluation_result(image_path, predictions, ground_truths, metrics, output_path):
167
+ """ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”"""
168
+ image = Image.open(image_path).convert('RGB')
169
+ draw = ImageDraw.Draw(image)
170
+
171
+ try:
172
+ font = ImageFont.truetype("arial.ttf", 12)
173
+ font_large = ImageFont.truetype("arial.ttf", 16)
174
+ except:
175
+ font = ImageFont.load_default()
176
+ font_large = ImageFont.load_default()
177
+
178
+ # Ground Truth (๋…น์ƒ‰)
179
+ for gt in ground_truths:
180
+ x1, y1, x2, y2 = gt['bbox']
181
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=3)
182
+ draw.text((x1, y1 - 15), "GT", fill="lime", font=font)
183
+
184
+ # Predictions (ํŒŒ๋ž€์ƒ‰)
185
+ for pred in predictions:
186
+ x1, y1, x2, y2 = pred['bbox']
187
+ draw.rectangle([x1, y1, x2, y2], outline="cyan", width=2)
188
+ label = f"{pred['filter_score']:.0f}"
189
+ draw.text((x1, y2 + 5), label, fill="cyan", font=font)
190
+
191
+ # ๋ฉ”ํŠธ๋ฆญ ํ—ค๋”
192
+ header = f"P={metrics['precision']:.2f} R={metrics['recall']:.2f} F1={metrics['f1']:.2f}"
193
+ draw.rectangle([5, 5, 300, 35], fill="black", outline="white", width=2)
194
+ draw.text((10, 10), header, fill="white", font=font_large)
195
+
196
+ image.save(output_path, quality=95)
197
+
198
+ def run_quantitative_test(test_image_dir, ground_truth_path, confidence=0.3, filter_threshold=50, iou_threshold=0.5):
199
+ """์ •๋Ÿ‰์  ํ‰๊ฐ€ ์‹คํ–‰"""
200
+ print("\n" + "="*60)
201
+ print("๐Ÿ“Š ์ •๋Ÿ‰์  ํ‰๊ฐ€ ํ…Œ์ŠคํŠธ ์‹œ์ž‘")
202
+ print("="*60)
203
+
204
+ # Ground truth ๋กœ๋“œ
205
+ ground_truths = load_ground_truth(ground_truth_path)
206
+ if not ground_truths:
207
+ return
208
+
209
+ # ๋ชจ๋ธ ๋กœ๋“œ
210
+ processor, model = load_rtdetr_model()
211
+
212
+ # ๊ฒฐ๊ณผ ๋””๋ ‰ํ† ๋ฆฌ
213
+ output_dir = f"test_results/quantitative_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
214
+ os.makedirs(output_dir, exist_ok=True)
215
+
216
+ print(f"\nโš™๏ธ ์„ค์ •:")
217
+ print(f" - RT-DETR Confidence: {confidence}")
218
+ print(f" - Filter Threshold: {filter_threshold}")
219
+ print(f" - IoU Threshold: {iou_threshold}")
220
+ print(f" - Ground Truth ์ด๋ฏธ์ง€: {len(ground_truths)}๊ฐœ\n")
221
+
222
+ results = []
223
+
224
+ for filename, gt_list in ground_truths.items():
225
+ img_path = os.path.join(test_image_dir, filename)
226
+ if not os.path.exists(img_path):
227
+ print(f"โš ๏ธ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {img_path}")
228
+ continue
229
+
230
+ print(f"\n๐Ÿ–ผ๏ธ ํ‰๊ฐ€ ์ค‘: {filename}")
231
+ print(f" Ground Truth: {len(gt_list)}๊ฐœ")
232
+
233
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ ๋ฐ ๊ฒ€์ถœ
234
+ image = Image.open(img_path).convert('RGB')
235
+ all_detections = detect_with_rtdetr(image, processor, model, confidence)
236
+ filtered_detections = apply_universal_filter(all_detections, image, filter_threshold)
237
+
238
+ print(f" ๊ฒ€์ถœ ๊ฒฐ๊ณผ: {len(filtered_detections)}๊ฐœ")
239
+
240
+ # ํ‰๊ฐ€
241
+ metrics = evaluate_detection(filtered_detections, gt_list, iou_threshold)
242
+
243
+ print(f" ๐Ÿ“Š Precision: {metrics['precision']:.2%}")
244
+ print(f" ๐Ÿ“Š Recall: {metrics['recall']:.2%}")
245
+ print(f" ๐Ÿ“Š F1 Score: {metrics['f1']:.2%}")
246
+ print(f" TP={metrics['tp']}, FP={metrics['fp']}, FN={metrics['fn']}")
247
+
248
+ # ์‹œ๊ฐํ™”
249
+ output_path = os.path.join(output_dir, f"eval_{filename}")
250
+ visualize_evaluation_result(img_path, filtered_detections, gt_list, metrics, output_path)
251
+ print(f" ๐Ÿ’พ ์ €์žฅ: {output_path}")
252
+
253
+ results.append({
254
+ 'filename': filename,
255
+ 'metrics': metrics,
256
+ 'gt_count': len(gt_list),
257
+ 'pred_count': len(filtered_detections)
258
+ })
259
+
260
+ # ์ „์ฒด ํ‰๊ท 
261
+ if results:
262
+ avg_precision = np.mean([r['metrics']['precision'] for r in results])
263
+ avg_recall = np.mean([r['metrics']['recall'] for r in results])
264
+ avg_f1 = np.mean([r['metrics']['f1'] for r in results])
265
+
266
+ print("\n" + "="*60)
267
+ print("๐Ÿ“Š ์ „์ฒด ํ‰๊ท  ์„ฑ๋Šฅ")
268
+ print("="*60)
269
+ print(f"Precision: {avg_precision:.2%}")
270
+ print(f"Recall: {avg_recall:.2%}")
271
+ print(f"F1 Score: {avg_f1:.2%}")
272
+ print("="*60)
273
+
274
+ # Confusion Matrix ์ƒ์„ฑ
275
+ generate_confusion_matrix(results, output_dir)
276
+
277
+ # ๊ฒฐ๊ณผ ์ €์žฅ
278
+ summary = {
279
+ 'config': {
280
+ 'confidence': confidence,
281
+ 'filter_threshold': filter_threshold,
282
+ 'iou_threshold': iou_threshold
283
+ },
284
+ 'avg_metrics': {
285
+ 'precision': avg_precision,
286
+ 'recall': avg_recall,
287
+ 'f1': avg_f1
288
+ },
289
+ 'per_image_results': results
290
+ }
291
+
292
+ json_path = os.path.join(output_dir, 'evaluation_summary.json')
293
+ with open(json_path, 'w', encoding='utf-8') as f:
294
+ json.dump(summary, f, ensure_ascii=False, indent=2)
295
+
296
+ print(f"\n๐Ÿ“„ ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์ €์žฅ: {json_path}")
297
+
298
+ if __name__ == "__main__":
299
+ # ํ…Œ์ŠคํŠธ ์‹คํ–‰
300
+ TEST_DIR = "imgs"
301
+ GT_PATH = "ground_truth.json"
302
+
303
+ if not os.path.exists(GT_PATH):
304
+ print("โš ๏ธ ground_truth.json ํŒŒ์ผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
305
+ print("\n๐Ÿ“ ground_truth.json ์ƒ์„ฑ ์˜ˆ์‹œ:")
306
+ print("""
307
+ {
308
+ "test_shrimp_tank.png": [
309
+ {"bbox": [100, 150, 200, 180], "label": "shrimp"},
310
+ {"bbox": [300, 250, 420, 290], "label": "shrimp"}
311
+ ],
312
+ "image (1).webp": [
313
+ {"bbox": [500, 600, 800, 700], "label": "shrimp"}
314
+ ]
315
+ }
316
+ """)
317
+ else:
318
+ run_quantitative_test(
319
+ test_image_dir=TEST_DIR,
320
+ ground_truth_path=GT_PATH,
321
+ confidence=0.3,
322
+ filter_threshold=50,
323
+ iou_threshold=0.5
324
+ )
test_visual_validation.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ์‹œ๊ฐ์  ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ
4
+ RT-DETR + Universal Shrimp Filter์˜ ๊ฒ€์ถœ ๊ฒฐ๊ณผ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๊ฒ€์ฆ
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import os
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ import json
12
+ from datetime import datetime
13
+ import cv2
14
+ import numpy as np
15
+ from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
16
+ import torch
17
+
18
+ # ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •
19
+ OUTPUT_DIR = "test_results"
20
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
21
+
22
+ def load_rtdetr_model():
23
+ """RT-DETR ๋ชจ๋ธ ๋กœ๋“œ"""
24
+ print("๐Ÿ”„ RT-DETR ๋ชจ๋ธ ๋กœ๋”ฉ ์ค‘...")
25
+ processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
26
+ model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
27
+ model.eval()
28
+ print("โœ… RT-DETR ๋กœ๋”ฉ ์™„๋ฃŒ")
29
+ return processor, model
30
+
31
+ def detect_with_rtdetr(image, processor, model, confidence=0.3):
32
+ """RT-DETR๋กœ ๊ฐ์ฒด ๊ฒ€์ถœ"""
33
+ inputs = processor(images=image, return_tensors="pt")
34
+ with torch.no_grad():
35
+ outputs = model(**inputs)
36
+
37
+ target_sizes = torch.tensor([image.size[::-1]])
38
+ results = processor.post_process_object_detection(
39
+ outputs,
40
+ target_sizes=target_sizes,
41
+ threshold=confidence
42
+ )[0]
43
+
44
+ detections = []
45
+ for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
46
+ x1, y1, x2, y2 = box.tolist()
47
+ detections.append({
48
+ 'bbox': [x1, y1, x2, y2],
49
+ 'confidence': score.item(),
50
+ 'label': label.item()
51
+ })
52
+
53
+ return detections
54
+
55
+ def calculate_morphological_features(bbox, image_size):
56
+ """ํ˜•ํƒœํ•™์  ํŠน์ง• ๊ณ„์‚ฐ"""
57
+ x1, y1, x2, y2 = bbox
58
+ width = x2 - x1
59
+ height = y2 - y1
60
+
61
+ # Aspect ratio (๊ธด ์ชฝ / ์งง์€ ์ชฝ)
62
+ aspect_ratio = max(width, height) / max(min(width, height), 1)
63
+
64
+ # Area ratio (์ด๋ฏธ์ง€ ๋Œ€๋น„ ๋ฉด์ )
65
+ img_w, img_h = image_size
66
+ area_ratio = (width * height) / (img_w * img_h)
67
+
68
+ # Compactness (4ฯ€ * Area / Perimeterยฒ)
69
+ perimeter = 2 * (width + height)
70
+ compactness = (4 * np.pi * width * height) / max(perimeter ** 2, 1)
71
+
72
+ return {
73
+ 'aspect_ratio': aspect_ratio,
74
+ 'area_ratio': area_ratio,
75
+ 'compactness': compactness,
76
+ 'width': width,
77
+ 'height': height
78
+ }
79
+
80
+ def calculate_visual_features(image_pil, bbox):
81
+ """์‹œ๊ฐ์  ํŠน์ง• ๊ณ„์‚ฐ (์ƒ‰์ƒ, ํ…์Šค์ฒ˜)"""
82
+ # PIL โ†’ OpenCV
83
+ image_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
84
+ x1, y1, x2, y2 = [int(v) for v in bbox]
85
+
86
+ # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์˜์—ญ ์ถ”์ถœ
87
+ roi = image_cv[y1:y2, x1:x2]
88
+ if roi.size == 0:
89
+ return {'saturation': 255, 'color_std': 255}
90
+
91
+ # HSV ๋ณ€ํ™˜
92
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
93
+
94
+ # ์ฑ„๋„ (Saturation)
95
+ saturation = np.mean(hsv[:, :, 1])
96
+
97
+ # ์ƒ‰์ƒ ์ผ๊ด€์„ฑ (ํ‘œ์ค€ํŽธ์ฐจ)
98
+ color_std = np.std(hsv[:, :, 0])
99
+
100
+ return {
101
+ 'saturation': saturation,
102
+ 'color_std': color_std
103
+ }
104
+
105
+ def apply_universal_filter(detections, image, threshold=50):
106
+ """๋ฒ”์šฉ ์ƒˆ์šฐ ํ•„ํ„ฐ ์ ์šฉ"""
107
+ img_size = image.size
108
+ filtered = []
109
+
110
+ for det in detections:
111
+ bbox = det['bbox']
112
+
113
+ # 1. ํ˜•ํƒœํ•™์  ํŠน์ง•
114
+ morph = calculate_morphological_features(bbox, img_size)
115
+
116
+ # 2. ์‹œ๊ฐ์  ํŠน์ง•
117
+ visual = calculate_visual_features(image, bbox)
118
+
119
+ # 3. ์ ์ˆ˜ ๊ณ„์‚ฐ
120
+ score = 0
121
+ reasons = []
122
+
123
+ # Aspect ratio (3:1 ~ 10:1) - ์ž(2:1~3:1) ์ œ์™ธํ•˜๊ธฐ ์œ„ํ•ด ํ•˜ํ•œ ์ƒํ–ฅ
124
+ if 3.0 <= morph['aspect_ratio'] <= 10.0:
125
+ score += 15
126
+ reasons.append(f"โœ“ ์ข…ํšก๋น„ {morph['aspect_ratio']:.1f}")
127
+ elif 2.0 <= morph['aspect_ratio'] < 3.0:
128
+ # ์•ฝ๊ฐ„ ๊ฐ์  (์ž์ผ ๊ฐ€๋Šฅ์„ฑ)
129
+ score += 8
130
+ reasons.append(f"โ–ณ ์ข…ํšก๋น„ {morph['aspect_ratio']:.1f}")
131
+ else:
132
+ reasons.append(f"โœ— ์ข…ํšก๋น„ {morph['aspect_ratio']:.1f}")
133
+
134
+ # Compactness (< 0.50, ๊ธด ํ˜•ํƒœ) - ์ธก์ •์šฉ ์ƒˆ์šฐ๋Š” ๋ชธ์ด ํŽด์ ธ์žˆ์Œ, ์ž(0.6~0.8) ์ œ์™ธ
135
+ if morph['compactness'] < 0.50:
136
+ score += 20 # ๊ฐ€์ค‘์น˜ ์ฆ๊ฐ€: 15 โ†’ 20
137
+ reasons.append(f"โœ“ ์„ธ์žฅ๋„ {morph['compactness']:.2f}")
138
+ else:
139
+ reasons.append(f"โœ— ์„ธ์žฅ๋„ {morph['compactness']:.2f}")
140
+ score -= 10 # ํŒจ๋„ํ‹ฐ ์ถ”๊ฐ€
141
+
142
+ # Area ratio (5% ~ 50%)
143
+ if 0.05 <= morph['area_ratio'] <= 0.50:
144
+ score += 10
145
+ reasons.append(f"โœ“ ๋ฉด์ ๋น„ {morph['area_ratio']*100:.1f}%")
146
+ else:
147
+ reasons.append(f"โœ— ๋ฉด์ ๋น„ {morph['area_ratio']*100:.1f}%")
148
+
149
+ # Saturation (๋‚ฎ์„์ˆ˜๋ก ์ฃฝ์€ ์ƒˆ์šฐ) - ์™„ํ™”: ํˆฌ๋ช…ํ•œ ์ƒˆ์šฐ๋„ ํ—ˆ์šฉ
150
+ if visual['saturation'] < 180:
151
+ score += 20
152
+ reasons.append(f"โœ“ ์ฑ„๋„ {visual['saturation']:.0f}")
153
+ else:
154
+ reasons.append(f"โœ— ์ฑ„๋„ {visual['saturation']:.0f}")
155
+
156
+ # Color consistency (๋‚ฎ์„์ˆ˜๋ก ์ผ๊ด€์„ฑ ๋†’์Œ) - ์™„ํ™”: ๋‚ด๋ถ€ ์žฅ๊ธฐ ๋ณด์ด๋Š” ํˆฌ๋ช… ์ƒˆ์šฐ ํ—ˆ์šฉ
157
+ if visual['color_std'] < 60:
158
+ score += 15
159
+ reasons.append(f"โœ“ ์ƒ‰์ƒ์ผ๊ด€์„ฑ {visual['color_std']:.1f}")
160
+ else:
161
+ reasons.append(f"โœ— ์ƒ‰์ƒ์ผ๊ด€์„ฑ {visual['color_std']:.1f}")
162
+
163
+ # RT-DETR confidence
164
+ score += det['confidence'] * 25
165
+
166
+ det['filter_score'] = score
167
+ det['filter_reasons'] = reasons
168
+ det['morph_features'] = morph
169
+ det['visual_features'] = visual
170
+
171
+ if score >= threshold:
172
+ filtered.append(det)
173
+
174
+ # ์ ์ˆ˜ ์ˆœ์œผ๋กœ ์ •๋ ฌ
175
+ filtered.sort(key=lambda x: x['filter_score'], reverse=True)
176
+
177
+ return filtered
178
+
179
+ def visualize_results(image_path, all_detections, filtered_detections, output_path):
180
+ """๊ฒ€์ถœ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”"""
181
+ image = Image.open(image_path).convert('RGB')
182
+
183
+ # 2๊ฐœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (์›๋ณธ ๊ฒ€์ถœ vs ํ•„ํ„ฐ๋ง ํ›„)
184
+ img_all = image.copy()
185
+ img_filtered = image.copy()
186
+
187
+ draw_all = ImageDraw.Draw(img_all)
188
+ draw_filtered = ImageDraw.Draw(img_filtered)
189
+
190
+ try:
191
+ font = ImageFont.truetype("arial.ttf", 12)
192
+ font_large = ImageFont.truetype("arial.ttf", 16)
193
+ except:
194
+ font = ImageFont.load_default()
195
+ font_large = ImageFont.load_default()
196
+
197
+ # ์ „์ฒด ๊ฒ€์ถœ ๊ฒฐ๊ณผ (ํšŒ์ƒ‰)
198
+ for det in all_detections:
199
+ x1, y1, x2, y2 = det['bbox']
200
+ draw_all.rectangle([x1, y1, x2, y2], outline="gray", width=2)
201
+ label = f"{det['confidence']:.0%}"
202
+ draw_all.text((x1, y1 - 15), label, fill="gray", font=font)
203
+
204
+ # ํ•„ํ„ฐ๋ง๋œ ๊ฒฐ๊ณผ (์ƒ‰์ƒ๋ณ„)
205
+ for idx, det in enumerate(filtered_detections, 1):
206
+ x1, y1, x2, y2 = det['bbox']
207
+ score = det['filter_score']
208
+
209
+ # ์ ์ˆ˜์— ๋”ฐ๋ผ ์ƒ‰์ƒ
210
+ if score >= 75:
211
+ color = "lime"
212
+ elif score >= 50:
213
+ color = "yellow"
214
+ else:
215
+ color = "orange"
216
+
217
+ # ๋‘ ์ด๋ฏธ์ง€ ๋ชจ๋‘์— ๊ทธ๋ฆฌ๊ธฐ
218
+ for draw in [draw_all, draw_filtered]:
219
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
220
+ label = f"#{idx} {score:.0f}์ "
221
+ bbox = draw.textbbox((x1, y1 - 25), label, font=font)
222
+ draw.rectangle(bbox, fill=color)
223
+ draw.text((x1, y1 - 25), label, fill="black", font=font)
224
+
225
+ # ํ—ค๋”
226
+ header_all = f"์ „์ฒด ๊ฒ€์ถœ: {len(all_detections)}๊ฐœ"
227
+ header_filtered = f"ํ•„ํ„ฐ๋ง ํ›„: {len(filtered_detections)}๊ฐœ"
228
+
229
+ draw_all.rectangle([5, 5, 200, 35], fill="black", outline="white", width=2)
230
+ draw_all.text((10, 10), header_all, fill="white", font=font_large)
231
+
232
+ draw_filtered.rectangle([5, 5, 200, 35], fill="black", outline="lime", width=2)
233
+ draw_filtered.text((10, 10), header_filtered, fill="lime", font=font_large)
234
+
235
+ # ๋ณ‘๋ ฌ ์ €์žฅ
236
+ combined_width = img_all.width * 2 + 20
237
+ combined_height = img_all.height
238
+ combined = Image.new('RGB', (combined_width, combined_height), color='white')
239
+
240
+ combined.paste(img_all, (0, 0))
241
+ combined.paste(img_filtered, (img_all.width + 20, 0))
242
+
243
+ combined.save(output_path, quality=95)
244
+ print(f" ๐Ÿ’พ ์ €์žฅ: {output_path}")
245
+
246
+ return len(all_detections), len(filtered_detections)
247
+
248
+ def run_visual_test(test_image_dir, confidence=0.3, filter_threshold=75):
249
+ """์‹œ๊ฐ์  ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ์‹คํ–‰"""
250
+ print("\n" + "="*60)
251
+ print("๐Ÿงช ์‹œ๊ฐ์  ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ์‹œ์ž‘")
252
+ print("="*60)
253
+
254
+ # ๋ชจ๋ธ ๋กœ๋“œ
255
+ processor, model = load_rtdetr_model()
256
+
257
+ # ์ด๋ฏธ์ง€ ํŒŒ์ผ ์ฐพ๊ธฐ
258
+ image_files = []
259
+ for ext in ['*.png', '*.jpg', '*.jpeg', '*.webp']:
260
+ import glob
261
+ image_files.extend(glob.glob(os.path.join(test_image_dir, ext)))
262
+
263
+ if not image_files:
264
+ print(f"โŒ {test_image_dir}์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
265
+ return
266
+
267
+ print(f"\n๐Ÿ“‚ ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€: {len(image_files)}๊ฐœ")
268
+ print(f"โš™๏ธ ์„ค์ •: Confidence={confidence}, Filter Threshold={filter_threshold}\n")
269
+
270
+ # ๊ฒฐ๊ณผ ์ €์žฅ
271
+ results = []
272
+
273
+ for img_path in image_files:
274
+ filename = os.path.basename(img_path)
275
+ print(f"\n๐Ÿ–ผ๏ธ ์ฒ˜๋ฆฌ ์ค‘: {filename}")
276
+
277
+ # ์ด๋ฏธ์ง€ ๋กœ๋“œ
278
+ image = Image.open(img_path).convert('RGB')
279
+
280
+ # RT-DETR ๊ฒ€์ถœ
281
+ all_detections = detect_with_rtdetr(image, processor, model, confidence)
282
+ print(f" ๐Ÿ” RT-DETR ๊ฒ€์ถœ: {len(all_detections)}๊ฐœ")
283
+
284
+ # ํ•„ํ„ฐ ์ ์šฉ
285
+ filtered_detections = apply_universal_filter(all_detections, image, filter_threshold)
286
+ print(f" โœ… ํ•„ํ„ฐ๋ง ํ›„: {len(filtered_detections)}๊ฐœ")
287
+
288
+ # ์‹œ๊ฐํ™”
289
+ output_path = os.path.join(OUTPUT_DIR, f"result_{filename}")
290
+ all_count, filtered_count = visualize_results(
291
+ img_path, all_detections, filtered_detections, output_path
292
+ )
293
+
294
+ # ํ•„ํ„ฐ๋ง๋œ ๊ฐ์ฒด ์„ธ๋ถ€ ์ •๋ณด
295
+ if filtered_detections:
296
+ print(f"\n ๐Ÿ“‹ ๊ฒ€์ถœ๋œ ๏ฟฝ๏ฟฝ๏ฟฝ์šฐ:")
297
+ for idx, det in enumerate(filtered_detections[:3], 1): # ์ƒ์œ„ 3๊ฐœ๋งŒ
298
+ print(f" #{idx} ์ ์ˆ˜: {det['filter_score']:.0f}์ ")
299
+ for reason in det['filter_reasons'][:3]:
300
+ print(f" {reason}")
301
+
302
+ # ๊ฒฐ๊ณผ ๊ธฐ๋ก
303
+ results.append({
304
+ 'filename': filename,
305
+ 'all_detections': all_count,
306
+ 'filtered_detections': filtered_count,
307
+ 'details': [{
308
+ 'bbox': det['bbox'],
309
+ 'confidence': det['confidence'],
310
+ 'filter_score': det['filter_score'],
311
+ 'reasons': det['filter_reasons']
312
+ } for det in filtered_detections]
313
+ })
314
+
315
+ # JSON ์ €์žฅ
316
+ result_json_path = os.path.join(OUTPUT_DIR, f"test_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
317
+ with open(result_json_path, 'w', encoding='utf-8') as f:
318
+ json.dump(results, f, ensure_ascii=False, indent=2)
319
+
320
+ print("\n" + "="*60)
321
+ print("โœ… ํ…Œ์ŠคํŠธ ์™„๋ฃŒ")
322
+ print(f"๐Ÿ“ ๊ฒฐ๊ณผ ์ €์žฅ ์œ„์น˜: {OUTPUT_DIR}/")
323
+ print(f"๐Ÿ“„ JSON ๊ฒฐ๊ณผ: {result_json_path}")
324
+ print("="*60)
325
+
326
+ if __name__ == "__main__":
327
+ # ํ…Œ์ŠคํŠธ ์‹คํ–‰
328
+ TEST_DIR = "imgs" # ํ…Œ์ŠคํŠธํ•  ์ด๋ฏธ์ง€ ๋””๋ ‰ํ† ๋ฆฌ
329
+
330
+ if not os.path.exists(TEST_DIR):
331
+ print(f"โŒ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {TEST_DIR}")
332
+ print("๐Ÿ’ก ์‚ฌ์šฉ๋ฒ•: python test_visual_validation.py")
333
+ print(" imgs/ ๋””๋ ‰ํ† ๋ฆฌ์— ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€๋ฅผ ๋„ฃ์–ด์ฃผ์„ธ์š”.")
334
+ else:
335
+ run_visual_test(
336
+ test_image_dir=TEST_DIR,
337
+ confidence=0.3, # RT-DETR ์‹ ๋ขฐ๋„
338
+ filter_threshold=50 # ํ•„ํ„ฐ ์ ์ˆ˜ ์ž„๊ณ„๊ฐ’
339
+ )