Be2Jay Claude commited on
Commit
4a12440
·
1 Parent(s): 94dd46f

Add analysis tools, update gitignore, and clean up data files

Browse files

- Add test and validation scripts (YOLOv8, Roboflow API testing)
- Add ground truth validation and labeling quality check tools
- Update .gitignore to exclude venv_gpu, test results, and local settings
- Remove obsolete image files and backups from git tracking
- Update ground_truth.json with latest labeling data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

.gitignore CHANGED
@@ -22,6 +22,7 @@ wheels/
22
 
23
  # Virtual Environment
24
  venv/
 
25
  env/
26
  ENV/
27
  .venv
@@ -89,8 +90,8 @@ data/**/*.bmp
89
  # 하지만 샘플/테스트 이미지는 포함 (예외)
90
  !data/samples/
91
  !data/test/
92
- !data/251015/*.jpg
93
- !data/251015/*.png
94
  !test_*.jpg
95
  !test_*.png
96
  !sample_*.jpg
@@ -105,6 +106,8 @@ data/**/*.bmp
105
  results/
106
  outputs/
107
  runs/
 
 
108
  wandb/
109
 
110
  # TensorBoard
@@ -128,3 +131,21 @@ logs/
128
  # Large documentation
129
  *.pdf
130
  *.docx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  # Virtual Environment
24
  venv/
25
+ venv_gpu/
26
  env/
27
  ENV/
28
  .venv
 
90
  # 하지만 샘플/테스트 이미지는 포함 (예외)
91
  !data/samples/
92
  !data/test/
93
+ !data/흰다리새우\ 실측\ 데이터_익투스에이아이\(주\)/251015/*.jpg
94
+ !data/흰다리새우\ 실측\ 데이터_익투스에이아이\(주\)/251015/*.png
95
  !test_*.jpg
96
  !test_*.png
97
  !sample_*.jpg
 
106
  results/
107
  outputs/
108
  runs/
109
+ test_results/
110
+ test_results_*/
111
  wandb/
112
 
113
  # TensorBoard
 
131
  # Large documentation
132
  *.pdf
133
  *.docx
134
+
135
+ # Backups (폴더 전체 제외, 로컬에만 보관)
136
+ backups/
137
+ ground_truth_backup_*.json
138
+
139
+ # Ground Truth (중요 파일은 포함)
140
+ # ground_truth.json - Git에 포함됨
141
+
142
+ # Test and analysis results (JSON)
143
+ *_results.json
144
+ *_optimization.json
145
+ fp_analysis_result.json
146
+ yolo_evaluation_results.json
147
+ yolo_with_filter_results.json
148
+ yolov8m_confidence_optimization*.json
149
+
150
+ # Claude settings
151
+ .claude/settings.local.json
analyze_fp_patterns.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ False Positive 패턴 분석
4
+ GT 박스 vs 오검출 박스의 특징 비교
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
13
+ from test_visual_validation import (
14
+ load_rtdetr_model,
15
+ detect_with_rtdetr,
16
+ apply_universal_filter,
17
+ calculate_morphological_features,
18
+ calculate_visual_features
19
+ )
20
+
21
+ def calculate_iou(bbox1, bbox2):
22
+ """IoU 계산"""
23
+ x1_min, y1_min, x1_max, y1_max = bbox1
24
+ x2_min, y2_min, x2_max, y2_max = bbox2
25
+
26
+ inter_x_min = max(x1_min, x2_min)
27
+ inter_y_min = max(y1_min, y2_min)
28
+ inter_x_max = min(x1_max, x2_max)
29
+ inter_y_max = min(y1_max, y2_max)
30
+
31
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
32
+ return 0.0
33
+
34
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
35
+ bbox1_area = (x1_max - x1_min) * (y1_max - y1_min)
36
+ bbox2_area = (x2_max - x2_min) * (y2_max - y2_min)
37
+ union_area = bbox1_area + bbox2_area - inter_area
38
+
39
+ return inter_area / union_area if union_area > 0 else 0.0
40
+
41
+ def analyze_fp_patterns(test_image_dir, ground_truth_path, confidence=0.065):
42
+ """False Positive 패턴 분석"""
43
+ print("\n" + "="*80)
44
+ print("🔍 False Positive 패턴 분석")
45
+ print("="*80)
46
+
47
+ # Ground truth 로드
48
+ with open(ground_truth_path, 'r', encoding='utf-8') as f:
49
+ ground_truths = json.load(f)
50
+
51
+ # 모델 로드
52
+ processor, model = load_rtdetr_model()
53
+
54
+ # 결과 저장
55
+ tp_features = [] # True Positive (GT와 매칭된 박스)
56
+ fp_features = [] # False Positive (오검출)
57
+ gt_features = [] # Ground Truth 박스
58
+
59
+ print(f"\n분석 중...")
60
+
61
+ for filename, gt_list in ground_truths.items():
62
+ if not gt_list:
63
+ continue
64
+
65
+ # 이미지 경로
66
+ if 'folder' in gt_list[0]:
67
+ folder = gt_list[0]['folder']
68
+ img_path = os.path.join(test_image_dir, folder, filename)
69
+ else:
70
+ img_path = os.path.join(test_image_dir, filename)
71
+
72
+ if not os.path.exists(img_path):
73
+ continue
74
+
75
+ # 이미지 로드
76
+ image = Image.open(img_path).convert('RGB')
77
+
78
+ # GT 특징 추출
79
+ for gt in gt_list:
80
+ morph = calculate_morphological_features(gt['bbox'], image.size)
81
+ visual = calculate_visual_features(image, gt['bbox'])
82
+
83
+ gt_features.append({
84
+ 'filename': filename,
85
+ 'type': 'GT',
86
+ 'bbox': gt['bbox'],
87
+ 'morph': morph,
88
+ 'visual': visual,
89
+ 'confidence': gt['confidence']
90
+ })
91
+
92
+ # 검출
93
+ all_detections = detect_with_rtdetr(image, processor, model, confidence)
94
+
95
+ # 각 검출 박스 분석
96
+ for det in all_detections:
97
+ morph = calculate_morphological_features(det['bbox'], image.size)
98
+ visual = calculate_visual_features(image, det['bbox'])
99
+
100
+ # GT와 매칭 확인
101
+ matched = False
102
+ for gt in gt_list:
103
+ iou = calculate_iou(det['bbox'], gt['bbox'])
104
+ if iou >= 0.5:
105
+ matched = True
106
+ tp_features.append({
107
+ 'filename': filename,
108
+ 'type': 'TP',
109
+ 'bbox': det['bbox'],
110
+ 'morph': morph,
111
+ 'visual': visual,
112
+ 'confidence': det['confidence'],
113
+ 'iou': iou
114
+ })
115
+ break
116
+
117
+ if not matched:
118
+ fp_features.append({
119
+ 'filename': filename,
120
+ 'type': 'FP',
121
+ 'bbox': det['bbox'],
122
+ 'morph': morph,
123
+ 'visual': visual,
124
+ 'confidence': det['confidence']
125
+ })
126
+
127
+ print(f"✅ 분석 완료")
128
+ print(f" GT: {len(gt_features)}개")
129
+ print(f" TP: {len(tp_features)}개")
130
+ print(f" FP: {len(fp_features)}개")
131
+
132
+ # 통계 비교
133
+ print("\n" + "="*80)
134
+ print("📊 특징 비교: GT vs FP")
135
+ print("="*80)
136
+
137
+ # 1. 장단축비
138
+ gt_ratios = [f['morph']['aspect_ratio'] for f in gt_features]
139
+ fp_ratios = [f['morph']['aspect_ratio'] for f in fp_features]
140
+
141
+ print(f"\n1️⃣ 장단축비 (Aspect Ratio)")
142
+ print(f" GT: 평균={np.mean(gt_ratios):.2f}, 범위=[{np.min(gt_ratios):.2f}, {np.max(gt_ratios):.2f}], std={np.std(gt_ratios):.2f}")
143
+ print(f" FP: 평균={np.mean(fp_ratios):.2f}, 범위=[{np.min(fp_ratios):.2f}, {np.max(fp_ratios):.2f}], std={np.std(fp_ratios):.2f}")
144
+ print(f" → 차이: {abs(np.mean(gt_ratios) - np.mean(fp_ratios)):.2f}")
145
+
146
+ # 2. Compactness
147
+ gt_compact = [f['morph']['compactness'] for f in gt_features]
148
+ fp_compact = [f['morph']['compactness'] for f in fp_features]
149
+
150
+ print(f"\n2️⃣ Compactness (세장도)")
151
+ print(f" GT: 평균={np.mean(gt_compact):.3f}, 범위=[{np.min(gt_compact):.3f}, {np.max(gt_compact):.3f}], std={np.std(gt_compact):.3f}")
152
+ print(f" FP: 평균={np.mean(fp_compact):.3f}, 범위=[{np.min(fp_compact):.3f}, {np.max(fp_compact):.3f}], std={np.std(fp_compact):.3f}")
153
+ print(f" → 차이: {abs(np.mean(gt_compact) - np.mean(fp_compact)):.3f}")
154
+
155
+ # 3. 면적
156
+ gt_area = [f['morph']['width'] * f['morph']['height'] for f in gt_features]
157
+ fp_area = [f['morph']['width'] * f['morph']['height'] for f in fp_features]
158
+
159
+ print(f"\n3️⃣ 면적 (px²)")
160
+ print(f" GT: 평균={np.mean(gt_area):.0f}, 범위=[{np.min(gt_area):.0f}, {np.max(gt_area):.0f}], std={np.std(gt_area):.0f}")
161
+ print(f" FP: 평균={np.mean(fp_area):.0f}, 범위=[{np.min(fp_area):.0f}, {np.max(fp_area):.0f}], std={np.std(fp_area):.0f}")
162
+ print(f" → 차이: {abs(np.mean(gt_area) - np.mean(fp_area)):.0f}")
163
+
164
+ # 4. Hue
165
+ gt_hue = [f['visual']['hue'] for f in gt_features]
166
+ fp_hue = [f['visual']['hue'] for f in fp_features]
167
+
168
+ print(f"\n4️⃣ Hue (색상)")
169
+ print(f" GT: 평균={np.mean(gt_hue):.1f}, 범위=[{np.min(gt_hue):.1f}, {np.max(gt_hue):.1f}], std={np.std(gt_hue):.1f}")
170
+ print(f" FP: 평균={np.mean(fp_hue):.1f}, 범위=[{np.min(fp_hue):.1f}, {np.max(fp_hue):.1f}], std={np.std(fp_hue):.1f}")
171
+ print(f" → 차이: {abs(np.mean(gt_hue) - np.mean(fp_hue)):.1f}")
172
+
173
+ # 5. Saturation
174
+ gt_sat = [f['visual']['saturation'] for f in gt_features]
175
+ fp_sat = [f['visual']['saturation'] for f in fp_features]
176
+
177
+ print(f"\n5️⃣ Saturation (채도)")
178
+ print(f" GT: 평균={np.mean(gt_sat):.1f}, 범위=[{np.min(gt_sat):.1f}, {np.max(gt_sat):.1f}], std={np.std(gt_sat):.1f}")
179
+ print(f" FP: 평균={np.mean(fp_sat):.1f}, 범위=[{np.min(fp_sat):.1f}, {np.max(fp_sat):.1f}], std={np.std(fp_sat):.1f}")
180
+ print(f" → 차이: {abs(np.mean(gt_sat) - np.mean(fp_sat)):.1f}")
181
+
182
+ # 6. Color std
183
+ gt_cstd = [f['visual']['color_std'] for f in gt_features]
184
+ fp_cstd = [f['visual']['color_std'] for f in fp_features]
185
+
186
+ print(f"\n6️⃣ Color Std (색상 일관성)")
187
+ print(f" GT: 평균={np.mean(gt_cstd):.1f}, 범위=[{np.min(gt_cstd):.1f}, {np.max(gt_cstd):.1f}], std={np.std(gt_cstd):.1f}")
188
+ print(f" FP: 평균={np.mean(fp_cstd):.1f}, 범위=[{np.min(fp_cstd):.1f}, {np.max(fp_cstd):.1f}], std={np.std(fp_cstd):.1f}")
189
+ print(f" → 차이: {abs(np.mean(gt_cstd) - np.mean(fp_cstd)):.1f}")
190
+
191
+ # 7. Confidence
192
+ gt_conf = [f['confidence'] for f in gt_features]
193
+ fp_conf = [f['confidence'] for f in fp_features]
194
+
195
+ print(f"\n7️⃣ RT-DETR Confidence")
196
+ print(f" GT: 평균={np.mean(gt_conf):.3f}, 범위=[{np.min(gt_conf):.3f}, {np.max(gt_conf):.3f}], std={np.std(gt_conf):.3f}")
197
+ print(f" FP: 평균={np.mean(fp_conf):.3f}, 범위=[{np.min(fp_conf):.3f}, {np.max(fp_conf):.3f}], std={np.std(fp_conf):.3f}")
198
+ print(f" → 차이: {abs(np.mean(gt_conf) - np.mean(fp_conf)):.3f}")
199
+
200
+ # 가장 차이나는 특징 찾기
201
+ print("\n" + "="*80)
202
+ print("🎯 판별력 높은 특징 (GT vs FP 차이)")
203
+ print("="*80)
204
+
205
+ differences = [
206
+ ('장단축비', abs(np.mean(gt_ratios) - np.mean(fp_ratios)) / np.mean(gt_ratios)),
207
+ ('Compactness', abs(np.mean(gt_compact) - np.mean(fp_compact)) / np.mean(gt_compact)),
208
+ ('면적', abs(np.mean(gt_area) - np.mean(fp_area)) / np.mean(gt_area)),
209
+ ('Hue', abs(np.mean(gt_hue) - np.mean(fp_hue)) / max(np.mean(gt_hue), 1)),
210
+ ('Saturation', abs(np.mean(gt_sat) - np.mean(fp_sat)) / max(np.mean(gt_sat), 1)),
211
+ ('Color Std', abs(np.mean(gt_cstd) - np.mean(fp_cstd)) / max(np.mean(gt_cstd), 1)),
212
+ ('Confidence', abs(np.mean(gt_conf) - np.mean(fp_conf)) / np.mean(gt_conf))
213
+ ]
214
+
215
+ differences.sort(key=lambda x: x[1], reverse=True)
216
+
217
+ for i, (name, diff) in enumerate(differences, 1):
218
+ print(f"{i}. {name}: {diff*100:.1f}% 차이")
219
+
220
+ # 상세 분포
221
+ print("\n" + "="*80)
222
+ print("📈 FP 상세 분포 (상위 오검출 패턴)")
223
+ print("="*80)
224
+
225
+ # 장단축비 분포
226
+ fp_ratio_dist = {
227
+ '< 3': len([r for r in fp_ratios if r < 3]),
228
+ '3-4': len([r for r in fp_ratios if 3 <= r < 4]),
229
+ '4-9': len([r for r in fp_ratios if 4 <= r < 9]),
230
+ '9-15': len([r for r in fp_ratios if 9 <= r < 15]),
231
+ '>= 15': len([r for r in fp_ratios if r >= 15])
232
+ }
233
+
234
+ print(f"\nFP 장단축비 분포:")
235
+ for range_name, count in fp_ratio_dist.items():
236
+ print(f" {range_name}: {count}개 ({count/len(fp_ratios)*100:.1f}%)")
237
+
238
+ # 추천사항
239
+ print("\n" + "="*80)
240
+ print("💡 필터 개선 제안")
241
+ print("="*80)
242
+
243
+ # 가장 차이나는 특징 기반 제안
244
+ top_diff = differences[0]
245
+ if top_diff[0] == '장단축비':
246
+ print(f"1. 장단축비 필터 강화")
247
+ print(f" - GT 범위: {np.min(gt_ratios):.2f}~{np.max(gt_ratios):.2f}")
248
+ print(f" - FP 평균: {np.mean(fp_ratios):.2f}")
249
+ if np.mean(fp_ratios) < np.mean(gt_ratios):
250
+ print(f" → FP가 더 둥글음. 하한을 {np.percentile(gt_ratios, 10):.1f}로 상향")
251
+ else:
252
+ print(f" → FP가 더 가늘음. 상한을 {np.percentile(gt_ratios, 90):.1f}로 하향")
253
+
254
+ # 결과 저장
255
+ result = {
256
+ 'gt_count': len(gt_features),
257
+ 'tp_count': len(tp_features),
258
+ 'fp_count': len(fp_features),
259
+ 'feature_comparison': {
260
+ 'aspect_ratio': {'gt': gt_ratios, 'fp': fp_ratios},
261
+ 'compactness': {'gt': gt_compact, 'fp': fp_compact},
262
+ 'area': {'gt': gt_area, 'fp': fp_area},
263
+ 'hue': {'gt': gt_hue, 'fp': fp_hue},
264
+ 'saturation': {'gt': gt_sat, 'fp': fp_sat},
265
+ 'color_std': {'gt': gt_cstd, 'fp': fp_cstd},
266
+ 'confidence': {'gt': gt_conf, 'fp': fp_conf}
267
+ },
268
+ 'discriminative_features': differences
269
+ }
270
+
271
+ with open('fp_analysis_result.json', 'w', encoding='utf-8') as f:
272
+ # numpy array를 list로 변환
273
+ for key in result['feature_comparison']:
274
+ result['feature_comparison'][key]['gt'] = [float(x) for x in result['feature_comparison'][key]['gt']]
275
+ result['feature_comparison'][key]['fp'] = [float(x) for x in result['feature_comparison'][key]['fp']]
276
+ json.dump(result, f, ensure_ascii=False, indent=2)
277
+
278
+ print(f"\n📄 분석 결과 저장: fp_analysis_result.json")
279
+
280
+ if __name__ == "__main__":
281
+ TEST_DIR = r"data\흰다리새우 실측 데이터_익투스에이아이(주)"
282
+ GT_PATH = "ground_truth.json"
283
+
284
+ analyze_fp_patterns(TEST_DIR, GT_PATH, confidence=0.065)
app_demo.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 새우 검출 시스템 데모 웹앱
4
+ 최종 최적화 버전 (Precision=44.2%, Recall=94%, F1=56.1%)
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import gradio as gr
10
+ import numpy as np
11
+ from PIL import Image, ImageDraw, ImageFont
12
+ from test_visual_validation import (
13
+ load_rtdetr_model,
14
+ detect_with_rtdetr,
15
+ apply_universal_filter
16
+ )
17
+
18
+ # 모델 로드 (시작시 한번만)
19
+ print("🔄 RT-DETR 모델 로딩 중...")
20
+ processor, model = load_rtdetr_model()
21
+ print("✅ RT-DETR 로딩 완료\n")
22
+
23
+ def detect_shrimp(image, confidence_threshold, filter_threshold):
24
+ """새우 검출 함수"""
25
+ if image is None:
26
+ return None, "이미지를 업로드하세요."
27
+
28
+ # PIL Image로 변환
29
+ if isinstance(image, np.ndarray):
30
+ image = Image.fromarray(image)
31
+
32
+ # RT-DETR 검출
33
+ all_detections = detect_with_rtdetr(image, processor, model, confidence_threshold)
34
+
35
+ # 필터 적용
36
+ filtered_detections = apply_universal_filter(all_detections, image, filter_threshold)
37
+
38
+ # 시각화
39
+ result_image = image.copy()
40
+ draw = ImageDraw.Draw(result_image)
41
+
42
+ try:
43
+ font = ImageFont.truetype("arial.ttf", 20)
44
+ font_small = ImageFont.truetype("arial.ttf", 14)
45
+ except:
46
+ font = ImageFont.load_default()
47
+ font_small = ImageFont.load_default()
48
+
49
+ # 박스 그리기
50
+ for i, det in enumerate(filtered_detections, 1):
51
+ x1, y1, x2, y2 = det['bbox']
52
+
53
+ # 박스
54
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=4)
55
+
56
+ # 라벨
57
+ score = det['filter_score']
58
+ conf = det['confidence']
59
+ label = f"#{i} | Score:{score:.0f} | Conf:{conf:.2f}"
60
+
61
+ # 배경
62
+ bbox = draw.textbbox((x1, y1-25), label, font=font_small)
63
+ draw.rectangle(bbox, fill="lime")
64
+ draw.text((x1, y1-25), label, fill="black", font=font_small)
65
+
66
+ # 결과 텍스트
67
+ info = f"""
68
+ 📊 검출 결과:
69
+ • RT-DETR 검출: {len(all_detections)}개
70
+ • 필터 통과: {len(filtered_detections)}개
71
+ • 제거됨: {len(all_detections) - len(filtered_detections)}개
72
+
73
+ ⚙️ 설정:
74
+ • RT-DETR Confidence: {confidence_threshold}
75
+ • Filter Threshold: {filter_threshold}
76
+
77
+ 🎯 성능 (50개 GT 기준):
78
+ • Precision: 44.2%
79
+ • Recall: 94.0%
80
+ • F1 Score: 56.1%
81
+ """
82
+
83
+ if len(filtered_detections) > 0:
84
+ info += f"\n✅ {len(filtered_detections)}개의 새우를 검출했습니다!"
85
+ else:
86
+ info += "\n⚠️ 새우가 검출되지 않았습니다. Threshold를 낮춰보세요."
87
+
88
+ return result_image, info
89
+
90
+ # Gradio 인터페이스
91
+ with gr.Blocks(title="🦐 새우 검출 시스템 v1.0") as demo:
92
+ gr.Markdown("""
93
+ # 🦐 새우 검출 시스템 v1.0
94
+ **RT-DETR + Universal Filter (최적화 완료)**
95
+
96
+ - **Precision**: 44.2% (검출된 박스 중 실제 새우 비율)
97
+ - **Recall**: 94.0% (실제 새우 중 검출된 비율)
98
+ - **F1 Score**: 56.1% (전체 성능)
99
+ """)
100
+
101
+ with gr.Row():
102
+ with gr.Column():
103
+ input_image = gr.Image(label="📤 이미지 업로드", type="pil")
104
+
105
+ with gr.Row():
106
+ conf_slider = gr.Slider(
107
+ minimum=0.05,
108
+ maximum=0.5,
109
+ value=0.065,
110
+ step=0.005,
111
+ label="🎯 RT-DETR Confidence",
112
+ info="낮을수록 더 많이 검출 (권장: 0.065)"
113
+ )
114
+
115
+ filter_slider = gr.Slider(
116
+ minimum=50,
117
+ maximum=100,
118
+ value=90,
119
+ step=5,
120
+ label="🔍 Filter Threshold",
121
+ info="높을수록 엄격한 필터링 (권장: 90)"
122
+ )
123
+
124
+ detect_btn = gr.Button("🚀 새우 검출 시작", variant="primary", size="lg")
125
+
126
+ with gr.Column():
127
+ output_image = gr.Image(label="📊 검출 결과")
128
+ output_text = gr.Textbox(label="📝 상세 정보", lines=15)
129
+
130
+ # 예제 이미지
131
+ gr.Examples(
132
+ examples=[
133
+ ["data/test_shrimp_tank.png", 0.065, 90],
134
+ ],
135
+ inputs=[input_image, conf_slider, filter_slider],
136
+ label="📁 예제 이미지 (클릭하여 테스트)"
137
+ )
138
+
139
+ # 이벤트 연결
140
+ detect_btn.click(
141
+ fn=detect_shrimp,
142
+ inputs=[input_image, conf_slider, filter_slider],
143
+ outputs=[output_image, output_text]
144
+ )
145
+
146
+ # 앱 실행
147
+ if __name__ == "__main__":
148
+ print("="*60)
149
+ print("🦐 새우 검출 시스템 v1.0 시작")
150
+ print("="*60)
151
+ print("⚙️ 최적 설정:")
152
+ print(" - RT-DETR Confidence: 0.065")
153
+ print(" - Filter Threshold: 90")
154
+ print("\n📊 성능 (50개 GT 기준):")
155
+ print(" - Precision: 44.2%")
156
+ print(" - Recall: 94.0%")
157
+ print(" - F1 Score: 56.1%")
158
+ print("="*60)
159
+
160
+ demo.launch(
161
+ server_name="0.0.0.0",
162
+ share=False
163
+ )
check_250818_labeling.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """250818 폴더 라벨링 검수"""
3
+ import sys
4
+ sys.stdout.reconfigure(encoding='utf-8')
5
+ import json
6
+
7
+ with open('ground_truth.json', 'r', encoding='utf-8') as f:
8
+ data = json.load(f)
9
+
10
+ print("=" * 70)
11
+ print("📋 250818 폴더 라벨링 검수")
12
+ print("=" * 70)
13
+
14
+ folder_data = {k: v for k, v in data.items() if k.startswith('250818_') and not '-' in k}
15
+
16
+ print(f"\n✅ 라벨링된 이미지: {len([v for v in folder_data.values() if v])}개")
17
+ print(f"❌ 건너뛴 이미지: {len([v for v in folder_data.values() if not v])}개")
18
+
19
+ print(f"\n{'파일명':<20} {'박스수':>6} {'종횡비':>8} {'신뢰도':>8} {'상태':>10}")
20
+ print("-" * 70)
21
+
22
+ issues = []
23
+
24
+ for filename in sorted(folder_data.keys()):
25
+ boxes = folder_data[filename]
26
+
27
+ if not boxes:
28
+ print(f"{filename:<20} {'0':>6} {'-':>8} {'-':>8} {'⚠️ 건너뜀':>10}")
29
+ issues.append(f"{filename}: 건너뛴 이미지 (새우 없음?)")
30
+ else:
31
+ for box in boxes:
32
+ bbox = box['bbox']
33
+ x1, y1, x2, y2 = bbox
34
+ width = x2 - x1
35
+ height = y2 - y1
36
+ aspect = width / height if height > 0 else 0
37
+ conf = box['confidence']
38
+
39
+ # 이상치 판단
40
+ status = "✅ 정상"
41
+ if aspect < 0.5: # 너무 세로로 긴 경우
42
+ status = "⚠️ 세로"
43
+ issues.append(f"{filename}: 종횡비 {aspect:.2f} (너무 세로로 김)")
44
+ elif aspect > 15: # 너무 가로로 긴 경우
45
+ status = "⚠️ 가로"
46
+ issues.append(f"{filename}: 종횡비 {aspect:.2f} (너무 가로로 김)")
47
+ elif conf < 0.1:
48
+ status = "⚠️ 낮음"
49
+ issues.append(f"{filename}: 신뢰도 {conf:.3f} (매우 낮음)")
50
+
51
+ print(f"{filename:<20} {len(boxes):>6} {aspect:>8.2f} {conf:>8.3f} {status:>10}")
52
+
53
+ print("\n" + "=" * 70)
54
+ if issues:
55
+ print("⚠️ 확인 필요한 항목:")
56
+ print("-" * 70)
57
+ for issue in issues:
58
+ print(f" • {issue}")
59
+ else:
60
+ print("✅ 모든 라벨링 정상!")
61
+
62
+ print("\n" + "=" * 70)
63
+ print("📊 통계")
64
+ print("-" * 70)
65
+
66
+ all_boxes = [box for boxes in folder_data.values() if boxes for box in boxes]
67
+ if all_boxes:
68
+ aspects = [((box['bbox'][2]-box['bbox'][0])/(box['bbox'][3]-box['bbox'][1]))
69
+ for box in all_boxes if (box['bbox'][3]-box['bbox'][1]) > 0]
70
+ confs = [box['confidence'] for box in all_boxes]
71
+
72
+ print(f"평균 종횡비: {sum(aspects)/len(aspects):.2f}")
73
+ print(f"평균 신뢰도: {sum(confs)/len(confs):.3f}")
74
+ print(f"최소 신뢰도: {min(confs):.3f}")
75
+ print(f"최대 신뢰도: {max(confs):.3f}")
76
+
77
+ print("\n" + "=" * 70)
78
+ print("💡 권장사항")
79
+ print("-" * 70)
80
+ print("• 종횡비 3:1 ~ 10:1 범위가 새우의 일반적인 비율")
81
+ print("• 신뢰도 0.1 이하는 오검출 가능성 높음")
82
+ print("• 건너뛴 이미지는 실제로 새우가 없는지 재확인")
83
+ print("=" * 70)
check_gt_split.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ # Ground Truth 파일 로드
5
+ with open('ground_truth.json', 'r', encoding='utf-8') as f:
6
+ gt = json.load(f)
7
+
8
+ # GT가 있는 이미지 목록
9
+ gt_images = [k for k, v in gt.items() if v]
10
+ print(f'GT 이미지 총 {len(gt_images)}장')
11
+
12
+ # Train/Val split 확인
13
+ train_images = set(os.listdir('data/yolo_dataset/images/train'))
14
+ val_images = set(os.listdir('data/yolo_dataset/images/val'))
15
+
16
+ gt_in_train = []
17
+ gt_in_val = []
18
+
19
+ for img in gt_images:
20
+ base_name = img.replace('-1.jpg', '.jpg')
21
+ if img in train_images or base_name in train_images:
22
+ gt_in_train.append(img)
23
+ elif img in val_images or base_name in val_images:
24
+ gt_in_val.append(img)
25
+
26
+ print(f'\nGT 분포:')
27
+ print(f' - Train set: {len(gt_in_train)}장')
28
+ print(f' - Val set: {len(gt_in_val)}장')
29
+
30
+ if len(gt_in_train) > 0:
31
+ print(f'\n문제: GT {len(gt_in_train)}장이 학습 데이터에 포함됨!')
32
+ print(f'해결: Val set {len(gt_in_val)}장만으로 평가해야 함')
33
+
34
+ print(f'\nVal set GT 이미지:')
35
+ for img in gt_in_val[:10]:
36
+ print(f' - {img}')
37
+ if len(gt_in_val) > 10:
38
+ print(f' ... and {len(gt_in_val)-10} more')
check_labeling_quality.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """올바른 라벨링 품질 검수"""
3
+ import sys
4
+ sys.stdout.reconfigure(encoding='utf-8')
5
+ import json
6
+ import math
7
+
8
+ def calculate_iou(box1, box2):
9
+ """IoU 계산"""
10
+ x1_min, y1_min, x1_max, y1_max = box1
11
+ x2_min, y2_min, x2_max, y2_max = box2
12
+
13
+ inter_x_min = max(x1_min, x2_min)
14
+ inter_y_min = max(y1_min, y2_min)
15
+ inter_x_max = min(x1_max, x2_max)
16
+ inter_y_max = min(y1_max, y2_max)
17
+
18
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
19
+ return 0.0
20
+
21
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
22
+ box1_area = (x1_max - x1_min) * (y1_max - y1_min)
23
+ box2_area = (x2_max - x2_min) * (y2_max - y2_min)
24
+ union_area = box1_area + box2_area - inter_area
25
+
26
+ return inter_area / union_area if union_area > 0 else 0.0
27
+
28
+ with open('ground_truth.json', 'r', encoding='utf-8') as f:
29
+ data = json.load(f)
30
+
31
+ print("=" * 80)
32
+ print("📋 라벨링 품질 검수 (올바른 기준)")
33
+ print("=" * 80)
34
+
35
+ folder_data = {k: v for k, v in data.items() if k.startswith('250818_') and not '-' in k}
36
+
37
+ print(f"\n✅ 라벨링된 이미지: {len([v for v in folder_data.values() if v])}개")
38
+ print(f"⚠️ 건너뛴 이미지: {len([v for v in folder_data.values() if not v])}개")
39
+
40
+ print(f"\n{'파일명':<20} {'박스':>4} {'장단축비':>8} {'면적':>10} {'신뢰도':>8} {'상태':>12}")
41
+ print("-" * 80)
42
+
43
+ issues = []
44
+ warnings = []
45
+
46
+ for filename in sorted(folder_data.keys()):
47
+ boxes = folder_data[filename]
48
+
49
+ if not boxes:
50
+ print(f"{filename:<20} {'0':>4} {'-':>8} {'-':>10} {'-':>8} {'⚠️ 건너뜀':>12}")
51
+ warnings.append(f"{filename}: 건너뛴 이미지")
52
+ continue
53
+
54
+ # 박스 수 확인
55
+ if len(boxes) > 10:
56
+ issues.append(f"{filename}: 박스 {len(boxes)}개 (너무 많음, 오검출 의심)")
57
+
58
+ # 중복 박스 확인
59
+ if len(boxes) > 1:
60
+ for i in range(len(boxes)):
61
+ for j in range(i+1, len(boxes)):
62
+ iou = calculate_iou(boxes[i]['bbox'], boxes[j]['bbox'])
63
+ if iou > 0.5:
64
+ issues.append(f"{filename}: 박스 #{i+1}과 #{j+1} 중첩 (IoU={iou:.2f}, 중복 선택?)")
65
+
66
+ for idx, box in enumerate(boxes):
67
+ bbox = box['bbox']
68
+ x1, y1, x2, y2 = bbox
69
+ width = x2 - x1
70
+ height = y2 - y1
71
+ area = width * height
72
+ conf = box['confidence']
73
+
74
+ # 장축/단축 비율 (방향 무관)
75
+ long_axis = max(width, height)
76
+ short_axis = min(width, height)
77
+ axis_ratio = long_axis / short_axis if short_axis > 0 else 0
78
+
79
+ # 품질 판단
80
+ status = "✅ 정상"
81
+ issue_desc = []
82
+
83
+ # 1. 면적 체크
84
+ if area < 1000:
85
+ status = "❌ 너무작음"
86
+ issues.append(f"{filename} 박스#{idx+1}: 면적 {area:.0f}px² (너무 작음, 오검출?)")
87
+ issue_desc.append("면적↓")
88
+ elif area > 1000000:
89
+ status = "❌ 너무큼"
90
+ issues.append(f"{filename} 박스#{idx+1}: 면적 {area:.0f}px² (너무 큼, 배경 포함?)")
91
+ issue_desc.append("면적↑")
92
+
93
+ # 2. 장단축 비율 (새우는 길쭉해야 함)
94
+ if axis_ratio < 2.5:
95
+ status = "⚠️ 둥글음"
96
+ warnings.append(f"{filename} 박스#{idx+1}: 장단축비 {axis_ratio:.2f} (너무 둥글음, 새우 맞나?)")
97
+ issue_desc.append("둥글음")
98
+ elif axis_ratio > 20:
99
+ status = "⚠️ 가늘음"
100
+ warnings.append(f"{filename} 박스#{idx+1}: 장단축비 {axis_ratio:.2f} (너무 가늘음)")
101
+ issue_desc.append("가늘음")
102
+
103
+ # 3. 신뢰도 체크
104
+ if conf < 0.05:
105
+ status = "❌ 신뢰도↓"
106
+ issues.append(f"{filename} 박스#{idx+1}: 신뢰도 {conf:.3f} (매우 낮음, 오검출 의심)")
107
+ issue_desc.append("신뢰↓")
108
+ elif conf < 0.15:
109
+ if status == "✅ 정상":
110
+ status = "⚠️ 신뢰도↓"
111
+ warnings.append(f"{filename} 박스#{idx+1}: 신뢰도 {conf:.3f} (낮음, 재확인 권장)")
112
+ issue_desc.append("신뢰낮음")
113
+
114
+ issue_str = ",".join(issue_desc) if issue_desc else ""
115
+ if issue_str:
116
+ status = f"⚠️ {issue_str}"
117
+
118
+ print(f"{filename:<20} {len(boxes):>4} {axis_ratio:>8.2f} {area:>10.0f} {conf:>8.3f} {status:>12}")
119
+
120
+ print("\n" + "=" * 80)
121
+ if issues:
122
+ print("❌ 심각한 문제 (재확인 필수):")
123
+ print("-" * 80)
124
+ for issue in issues[:10]: # 최대 10개만
125
+ print(f" • {issue}")
126
+ if len(issues) > 10:
127
+ print(f" ... 외 {len(issues)-10}개")
128
+ else:
129
+ print("✅ 심각한 문제 없음")
130
+
131
+ if warnings:
132
+ print(f"\n⚠️ 경고 ({len(warnings)}개, 재확인 권장):")
133
+ print("-" * 80)
134
+ for warning in warnings[:10]: # 최대 10개만
135
+ print(f" • {warning}")
136
+ if len(warnings) > 10:
137
+ print(f" ... 외 {len(warnings)-10}개")
138
+
139
+ print("\n" + "=" * 80)
140
+ print("📊 통계")
141
+ print("-" * 80)
142
+
143
+ all_boxes = [box for boxes in folder_data.values() if boxes for box in boxes]
144
+ if all_boxes:
145
+ areas = [(box['bbox'][2]-box['bbox'][0])*(box['bbox'][3]-box['bbox'][1]) for box in all_boxes]
146
+ axis_ratios = []
147
+ for box in all_boxes:
148
+ w = box['bbox'][2] - box['bbox'][0]
149
+ h = box['bbox'][3] - box['bbox'][1]
150
+ axis_ratios.append(max(w,h)/min(w,h) if min(w,h) > 0 else 0)
151
+ confs = [box['confidence'] for box in all_boxes]
152
+
153
+ print(f"총 박스: {len(all_boxes)}개")
154
+ print(f"평균 면적: {sum(areas)/len(areas):,.0f} px²")
155
+ print(f"평균 장단축비: {sum(axis_ratios)/len(axis_ratios):.2f} (정상 범위: 3~15)")
156
+ print(f"평균 신뢰도: {sum(confs)/len(confs):.3f}")
157
+ print(f"신뢰도 범위: {min(confs):.3f} ~ {max(confs):.3f}")
158
+
159
+ print("\n" + "=" * 80)
160
+ print("💡 품질 기준")
161
+ print("-" * 80)
162
+ print("✅ 장단축비: 3:1 ~ 15:1 (새우는 길쭉함, 방향 무관)")
163
+ print("✅ 면적: 1,000 ~ 1,000,000 px²")
164
+ print("✅ 신뢰도: > 0.15")
165
+ print("✅ 박스 수: 1~5개/이미지")
166
+ print("✅ 중첩: IoU < 0.5 (중복 선택 방지)")
167
+ print("=" * 80)
168
+
169
+ # 최종 평가
170
+ print("\n" + "=" * 80)
171
+ print("📋 최종 평가")
172
+ print("-" * 80)
173
+ if not issues and len(warnings) <= 3:
174
+ print("🎉 우수: 라벨링 품질이 매우 좋습니다!")
175
+ elif not issues:
176
+ print("✅ 양호: 몇 가지 재확인 권장")
177
+ elif len(issues) <= 3:
178
+ print("⚠️ 보통: 일부 박스 재확인 필요")
179
+ else:
180
+ print("❌ 불량: 많은 박스 재라벨링 필요")
181
+ print("=" * 80)
convert_gt_to_yolo.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Ground Truth를 YOLO format으로 변환
4
+ Train/Val split 포함
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import json
10
+ import os
11
+ import shutil
12
+ from pathlib import Path
13
+ import random
14
+ from PIL import Image
15
+
16
+ def convert_bbox_to_yolo(bbox, img_width, img_height):
17
+ """
18
+ [x1, y1, x2, y2] → [x_center, y_center, width, height] (normalized)
19
+ """
20
+ x1, y1, x2, y2 = bbox
21
+
22
+ # Center coordinates
23
+ x_center = (x1 + x2) / 2.0
24
+ y_center = (y1 + y2) / 2.0
25
+
26
+ # Width and height
27
+ width = x2 - x1
28
+ height = y2 - y1
29
+
30
+ # Normalize to [0, 1]
31
+ x_center_norm = x_center / img_width
32
+ y_center_norm = y_center / img_height
33
+ width_norm = width / img_width
34
+ height_norm = height / img_height
35
+
36
+ return x_center_norm, y_center_norm, width_norm, height_norm
37
+
38
+ def main():
39
+ print("=" * 60)
40
+ print("Ground Truth → YOLO Format 변환 시작")
41
+ print("=" * 60)
42
+
43
+ # 경로 설정
44
+ gt_file = "ground_truth.json"
45
+ data_base_dir = "data/흰다리새우 실측 데이터_익투스에이아이(주)"
46
+ output_base_dir = "data/yolo_dataset"
47
+
48
+ # YOLO 디렉토리 구조 생성
49
+ train_img_dir = Path(output_base_dir) / "images" / "train"
50
+ val_img_dir = Path(output_base_dir) / "images" / "val"
51
+ train_label_dir = Path(output_base_dir) / "labels" / "train"
52
+ val_label_dir = Path(output_base_dir) / "labels" / "val"
53
+
54
+ for d in [train_img_dir, val_img_dir, train_label_dir, val_label_dir]:
55
+ d.mkdir(parents=True, exist_ok=True)
56
+
57
+ # Ground Truth 로드
58
+ print(f"\n📂 {gt_file} 로딩 중...")
59
+ with open(gt_file, 'r', encoding='utf-8') as f:
60
+ gt_data = json.load(f)
61
+
62
+ # 데이터 수집
63
+ all_samples = []
64
+
65
+ for filename, annotations in gt_data.items():
66
+ if not annotations: # 빈 리스트는 건너뛰기
67
+ continue
68
+
69
+ # 첫 번째 annotation에서 폴더 정보 가져오기
70
+ folder = annotations[0].get('folder', '')
71
+
72
+ if not folder:
73
+ print(f"⚠️ {filename}: 폴더 정보 없음, 건너뜀")
74
+ continue
75
+
76
+ # 이미지 경로 확인
77
+ img_path = os.path.join(data_base_dir, folder, filename)
78
+
79
+ if not os.path.exists(img_path):
80
+ print(f"⚠️ 이미지 없음: {img_path}")
81
+ continue
82
+
83
+ all_samples.append({
84
+ 'filename': filename,
85
+ 'folder': folder,
86
+ 'img_path': img_path,
87
+ 'annotations': annotations
88
+ })
89
+
90
+ print(f"\n✅ 총 {len(all_samples)}개 샘플 수집 완료")
91
+
92
+ # Train/Val Split (80/20)
93
+ random.seed(42) # 재현성을 위해
94
+ random.shuffle(all_samples)
95
+
96
+ split_idx = int(len(all_samples) * 0.8)
97
+ train_samples = all_samples[:split_idx]
98
+ val_samples = all_samples[split_idx:]
99
+
100
+ print(f"\n📊 데이터 분할:")
101
+ print(f" - Train: {len(train_samples)}개")
102
+ print(f" - Val: {len(val_samples)}개")
103
+
104
+ # 변환 함수
105
+ def process_samples(samples, img_dir, label_dir, split_name):
106
+ print(f"\n🔄 {split_name} 데이터 변환 중...")
107
+
108
+ for idx, sample in enumerate(samples, 1):
109
+ filename = sample['filename']
110
+ img_path = sample['img_path']
111
+ annotations = sample['annotations']
112
+
113
+ # 이미지 복사
114
+ dest_img_path = img_dir / filename
115
+ shutil.copy2(img_path, dest_img_path)
116
+
117
+ # 이미지 크기 가져오기
118
+ with Image.open(img_path) as img:
119
+ img_width, img_height = img.size
120
+
121
+ # YOLO 라벨 생성
122
+ label_filename = Path(filename).stem + ".txt"
123
+ label_path = label_dir / label_filename
124
+
125
+ with open(label_path, 'w') as f:
126
+ for ann in annotations:
127
+ bbox = ann['bbox']
128
+
129
+ # YOLO format으로 변환
130
+ x_center, y_center, width, height = convert_bbox_to_yolo(
131
+ bbox, img_width, img_height
132
+ )
133
+
134
+ # YOLO 형식: class_id x_center y_center width height
135
+ # class_id=0 (shrimp)
136
+ f.write(f"0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")
137
+
138
+ if idx % 10 == 0 or idx == len(samples):
139
+ print(f" 진행: {idx}/{len(samples)}")
140
+
141
+ # Train/Val 데이터 처리
142
+ process_samples(train_samples, train_img_dir, train_label_dir, "Train")
143
+ process_samples(val_samples, val_img_dir, val_label_dir, "Val")
144
+
145
+ # data.yaml 생성
146
+ yaml_path = Path(output_base_dir) / "data.yaml"
147
+ yaml_content = f"""# Shrimp Detection Dataset
148
+ path: {output_base_dir} # dataset root dir
149
+ train: images/train # train images (relative to 'path')
150
+ val: images/val # val images (relative to 'path')
151
+
152
+ # Classes
153
+ nc: 1 # number of classes
154
+ names: ['shrimp'] # class names
155
+ """
156
+
157
+ with open(yaml_path, 'w', encoding='utf-8') as f:
158
+ f.write(yaml_content)
159
+
160
+ print(f"\n✅ data.yaml 생성 완료: {yaml_path}")
161
+
162
+ # 요약 출력
163
+ print("\n" + "=" * 60)
164
+ print("✅ 변환 완료!")
165
+ print("=" * 60)
166
+ print(f"\n📁 출력 디렉토리: {output_base_dir}")
167
+ print(f"\n📊 데이터셋 구조:")
168
+ print(f" - Train: {len(train_samples)} images")
169
+ print(f" - Val: {len(val_samples)} images")
170
+ print(f" - Total: {len(all_samples)} images")
171
+
172
+ # 샘플 확인
173
+ print(f"\n📝 샘플 라벨 확인 (Train 첫 번째):")
174
+ first_label = next(train_label_dir.glob("*.txt"))
175
+ with open(first_label, 'r') as f:
176
+ content = f.read()
177
+ print(f" {first_label.name}:")
178
+ for line in content.strip().split('\n'):
179
+ print(f" {line}")
180
+
181
+ print(f"\n🎯 다음 단계: YOLOv8 학습 실행")
182
+ print(f" python train_yolo.py")
183
+
184
+ if __name__ == "__main__":
185
+ main()
data/251015/251015_01-1.jpg DELETED

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

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

Git LFS Details

  • SHA256: ade4056ad3dbc85f2065d6597d54d1b8a86021eff867a0622e60cab8f58ee168
  • Pointer size: 131 Bytes
  • Size of remote file: 481 kB
debug_roboflow_api.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow API 응답 디버깅
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ from inference_sdk import InferenceHTTPClient
9
+ import json
10
+
11
+ # Roboflow 클라이언트
12
+ client = InferenceHTTPClient(
13
+ api_url="https://serverless.roboflow.com",
14
+ api_key="azcIL8KDJVJMYrsERzI7"
15
+ )
16
+
17
+ # 테스트 이미지
18
+ test_image = "data/yolo_dataset/images/train/250818_01.jpg"
19
+
20
+ print("="*60)
21
+ print("🔍 Roboflow API 응답 디버깅")
22
+ print("="*60)
23
+ print(f"\n📸 이미지: {test_image}")
24
+ print(f"🔗 Workflow: vidraft/find-shrimp-6")
25
+
26
+ # API 호출
27
+ result = client.run_workflow(
28
+ workspace_name="vidraft",
29
+ workflow_id="find-shrimp-6",
30
+ images={"image": test_image},
31
+ use_cache=False # 캐시 비활성화
32
+ )
33
+
34
+ print(f"\n📦 전체 응답 구조:")
35
+ print(json.dumps(result, indent=2, ensure_ascii=False))
36
+
37
+ print(f"\n{'='*60}")
38
+ print("✅ 디버깅 완료")
ground_truth.json CHANGED
@@ -1211,5 +1211,115 @@
1211
  ],
1212
  "folder": "251007"
1213
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  ]
1215
  }
 
1211
  ],
1212
  "folder": "251007"
1213
  }
1214
+ ],
1215
+ "251007_03.jpg": [
1216
+ {
1217
+ "bbox": [
1218
+ 241.99429035186768,
1219
+ 293.28845500946045,
1220
+ 350.4645299911499,
1221
+ 806.7023754119873
1222
+ ],
1223
+ "folder": "251007"
1224
+ }
1225
+ ],
1226
+ "251007_04.jpg": [
1227
+ {
1228
+ "bbox": [
1229
+ 466.5386486053467,
1230
+ 241.44012212753296,
1231
+ 999.3045139312744,
1232
+ 330.7358407974243
1233
+ ],
1234
+ "folder": "251007"
1235
+ }
1236
+ ],
1237
+ "251007_05.jpg": [
1238
+ {
1239
+ "bbox": [
1240
+ 509.04098987579346,
1241
+ 300.90571880340576,
1242
+ 922.2488784790039,
1243
+ 369.85572814941406
1244
+ ],
1245
+ "folder": "251007"
1246
+ }
1247
+ ],
1248
+ "251007_06.jpg": [
1249
+ {
1250
+ "bbox": [
1251
+ 551.3954639434814,
1252
+ 234.11472082138062,
1253
+ 945.5917358398438,
1254
+ 325.4259395599365
1255
+ ],
1256
+ "folder": "251007"
1257
+ }
1258
+ ],
1259
+ "251007_07.jpg": [
1260
+ {
1261
+ "bbox": [
1262
+ 518.1720066070557,
1263
+ 258.2105827331543,
1264
+ 1216.37056350708,
1265
+ 378.3600950241089
1266
+ ],
1267
+ "folder": "251007"
1268
+ }
1269
+ ],
1270
+ "251007_08.jpg": [
1271
+ {
1272
+ "bbox": [
1273
+ 542.6454305648804,
1274
+ 152.6955008506775,
1275
+ 1012.4309158325195,
1276
+ 244.41664218902588
1277
+ ],
1278
+ "folder": "251007"
1279
+ }
1280
+ ],
1281
+ "251007_09.jpg": [
1282
+ {
1283
+ "bbox": [
1284
+ 408.89760971069336,
1285
+ 284.97180938720703,
1286
+ 968.8659000396729,
1287
+ 380.16117095947266
1288
+ ],
1289
+ "folder": "251007"
1290
+ }
1291
+ ],
1292
+ "251007_10.jpg": [
1293
+ {
1294
+ "bbox": [
1295
+ 453.19141387939453,
1296
+ 234.03351068496704,
1297
+ 1054.6015739440918,
1298
+ 364.84920501708984
1299
+ ],
1300
+ "folder": "251007"
1301
+ }
1302
+ ],
1303
+ "251002_01.jpg": [
1304
+ {
1305
+ "bbox": [
1306
+ 580.7671642303467,
1307
+ 355.0561571121216,
1308
+ 697.6516151428223,
1309
+ 914.4206619262695
1310
+ ],
1311
+ "folder": "251002"
1312
+ }
1313
+ ],
1314
+ "251002_02.jpg": [
1315
+ {
1316
+ "bbox": [
1317
+ 387.40245819091797,
1318
+ 441.32769107818604,
1319
+ 501.0829973220825,
1320
+ 1040.6700134277344
1321
+ ],
1322
+ "folder": "251002"
1323
+ }
1324
  ]
1325
  }
imgs/image.webp CHANGED

Git LFS Details

  • SHA256: 1dc015113e9d849430ab359572221f0e6573fa8273b0ee528c86b88301e42ca6
  • Pointer size: 130 Bytes
  • Size of remote file: 90 kB

Git LFS Details

  • SHA256: 45f28785e8e4220636059e2a52d6d7d878d95825e05737c08f70de1bc77df794
  • Pointer size: 132 Bytes
  • Size of remote file: 1.23 MB
optimize_yolov8_confidence.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m Confidence Threshold 최적화
3
+ Ground Truth 기반으로 최적 confidence 값 탐색
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image
7
+ import json
8
+ import os
9
+ import glob
10
+ import numpy as np
11
+
12
+ # 학습된 모델 로드
13
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
14
+ model = YOLO(MODEL_PATH)
15
+
16
+ print(f"✅ YOLOv8m 모델 로드 완료: {MODEL_PATH}")
17
+
18
+ # Ground Truth 로드
19
+ GT_FILE = "ground_truth.json"
20
+ with open(GT_FILE, 'r', encoding='utf-8') as f:
21
+ ground_truth = json.load(f)
22
+
23
+ print(f"✅ Ground Truth 로드: {GT_FILE}")
24
+
25
+ # GT 통계
26
+ total_gt = sum(len(gts) for gts in ground_truth.values() if gts)
27
+ gt_images = [k for k, v in ground_truth.items() if v]
28
+ print(f" - GT가 있는 이미지: {len(gt_images)}장")
29
+ print(f" - 총 GT 객체: {total_gt}개")
30
+ print("-" * 60)
31
+
32
+ def calculate_iou(box1, box2):
33
+ """IoU 계산"""
34
+ x1_min, y1_min, x1_max, y1_max = box1
35
+ x2_min, y2_min, x2_max, y2_max = box2
36
+
37
+ inter_x_min = max(x1_min, x2_min)
38
+ inter_y_min = max(y1_min, y2_min)
39
+ inter_x_max = min(x1_max, x2_max)
40
+ inter_y_max = min(y1_max, y2_max)
41
+
42
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
43
+ return 0.0
44
+
45
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
46
+ box1_area = (x1_max - x1_min) * (y1_max - y1_min)
47
+ box2_area = (x2_max - x2_min) * (y2_max - y2_min)
48
+ union_area = box1_area + box2_area - inter_area
49
+
50
+ return inter_area / union_area if union_area > 0 else 0.0
51
+
52
+ def evaluate_confidence_threshold(conf_threshold, iou_threshold=0.5):
53
+ """특정 confidence threshold에서 성능 평가"""
54
+ tp = 0 # True Positive
55
+ fp = 0 # False Positive
56
+ fn = 0 # False Negative
57
+
58
+ matched_gt_count = 0
59
+ total_gt_count = 0
60
+
61
+ # GT가 있는 이미지만 테스트
62
+ for img_name in gt_images:
63
+ # 이미지 경로 찾기
64
+ img_path = None
65
+ for split in ['train', 'val']:
66
+ search_path = f"data/yolo_dataset/images/{split}/{img_name}"
67
+ if os.path.exists(search_path):
68
+ img_path = search_path
69
+ break
70
+
71
+ if not img_path:
72
+ # 파일명에서 -1 제거해서 다시 시도
73
+ base_name = img_name.replace('-1.jpg', '.jpg')
74
+ for split in ['train', 'val']:
75
+ search_path = f"data/yolo_dataset/images/{split}/{base_name}"
76
+ if os.path.exists(search_path):
77
+ img_path = search_path
78
+ break
79
+
80
+ if not img_path or not os.path.exists(img_path):
81
+ continue
82
+
83
+ # 이미지 로드
84
+ image = Image.open(img_path)
85
+
86
+ # YOLOv8 검출
87
+ results = model.predict(
88
+ source=image,
89
+ conf=conf_threshold,
90
+ iou=0.7,
91
+ device=0,
92
+ verbose=False
93
+ )
94
+
95
+ # 결과 파싱
96
+ result = results[0]
97
+ boxes = result.boxes
98
+
99
+ predictions = []
100
+ if boxes is not None and len(boxes) > 0:
101
+ for box in boxes:
102
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
103
+ confidence = box.conf[0].cpu().item()
104
+ predictions.append({
105
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
106
+ 'confidence': confidence
107
+ })
108
+
109
+ # Ground Truth
110
+ gt_boxes = ground_truth[img_name]
111
+ total_gt_count += len(gt_boxes)
112
+
113
+ # GT와 매칭
114
+ matched_gt = set()
115
+ matched_pred = set()
116
+
117
+ for pred_idx, pred in enumerate(predictions):
118
+ best_iou = 0
119
+ best_gt_idx = -1
120
+
121
+ for gt_idx, gt in enumerate(gt_boxes):
122
+ if gt_idx in matched_gt:
123
+ continue
124
+
125
+ iou = calculate_iou(pred['bbox'], gt['bbox'])
126
+ if iou > best_iou:
127
+ best_iou = iou
128
+ best_gt_idx = gt_idx
129
+
130
+ if best_iou >= iou_threshold:
131
+ tp += 1
132
+ matched_gt.add(best_gt_idx)
133
+ matched_pred.add(pred_idx)
134
+ else:
135
+ fp += 1
136
+
137
+ # 매칭되지 않은 GT = False Negative
138
+ fn += len(gt_boxes) - len(matched_gt)
139
+ matched_gt_count += len(matched_gt)
140
+
141
+ # 성능 지표 계산
142
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0
143
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 0
144
+ f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
145
+
146
+ return {
147
+ 'tp': tp,
148
+ 'fp': fp,
149
+ 'fn': fn,
150
+ 'precision': precision,
151
+ 'recall': recall,
152
+ 'f1': f1,
153
+ 'matched_gt': matched_gt_count,
154
+ 'total_gt': total_gt_count,
155
+ 'gt_match_rate': matched_gt_count / total_gt_count if total_gt_count > 0 else 0
156
+ }
157
+
158
+ # Confidence threshold sweep
159
+ print("\n🔍 Confidence Threshold 최적화 시작...\n")
160
+
161
+ confidence_thresholds = [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8]
162
+ results = []
163
+
164
+ for conf in confidence_thresholds:
165
+ metrics = evaluate_confidence_threshold(conf)
166
+ results.append({
167
+ 'confidence': conf,
168
+ **metrics
169
+ })
170
+
171
+ print(f"Conf {conf:.2f}: P={metrics['precision']:.1%} R={metrics['recall']:.1%} F1={metrics['f1']:.1%} | "
172
+ f"GT매칭={metrics['matched_gt']}/{metrics['total_gt']} ({metrics['gt_match_rate']:.1%})")
173
+
174
+ # 최적값 찾기
175
+ best_f1 = max(results, key=lambda x: x['f1'])
176
+ best_recall = max(results, key=lambda x: x['recall'])
177
+ best_precision = max(results, key=lambda x: x['precision'])
178
+ best_gt_match = max(results, key=lambda x: x['gt_match_rate'])
179
+
180
+ print("\n" + "=" * 60)
181
+ print("📊 최적화 결과:")
182
+ print("=" * 60)
183
+
184
+ print(f"\n1️⃣ 최고 F1 Score: {best_f1['f1']:.1%} (Confidence={best_f1['confidence']:.2f})")
185
+ print(f" - Precision: {best_f1['precision']:.1%}")
186
+ print(f" - Recall: {best_f1['recall']:.1%}")
187
+ print(f" - GT 매칭: {best_f1['matched_gt']}/{best_f1['total_gt']} ({best_f1['gt_match_rate']:.1%})")
188
+
189
+ print(f"\n2️⃣ 최고 Recall: {best_recall['recall']:.1%} (Confidence={best_recall['confidence']:.2f})")
190
+ print(f" - F1 Score: {best_recall['f1']:.1%}")
191
+ print(f" - Precision: {best_recall['precision']:.1%}")
192
+
193
+ print(f"\n3️⃣ 최고 Precision: {best_precision['precision']:.1%} (Confidence={best_precision['confidence']:.2f})")
194
+ print(f" - F1 Score: {best_precision['f1']:.1%}")
195
+ print(f" - Recall: {best_precision['recall']:.1%}")
196
+
197
+ print(f"\n4️⃣ 최고 GT 매칭률: {best_gt_match['gt_match_rate']:.1%} (Confidence={best_gt_match['confidence']:.2f})")
198
+ print(f" - F1 Score: {best_gt_match['f1']:.1%}")
199
+ print(f" - 매칭: {best_gt_match['matched_gt']}/{best_gt_match['total_gt']}")
200
+
201
+ print("\n💡 권장 설정:")
202
+ print(f" - 균형잡힌 성능: confidence={best_f1['confidence']:.2f} (F1={best_f1['f1']:.1%})")
203
+ print(f" - 높은 재현율: confidence={best_recall['confidence']:.2f} (Recall={best_recall['recall']:.1%})")
204
+
205
+ # 결과 저장
206
+ output_file = "yolov8m_confidence_optimization.json"
207
+ with open(output_file, 'w', encoding='utf-8') as f:
208
+ json.dump({
209
+ 'best_f1': best_f1,
210
+ 'best_recall': best_recall,
211
+ 'best_precision': best_precision,
212
+ 'best_gt_match': best_gt_match,
213
+ 'all_results': results
214
+ }, f, indent=2, ensure_ascii=False)
215
+
216
+ print(f"\n💾 결과 저장: {output_file}")
217
+ print("=" * 60)
optimize_yolov8_confidence_val_only.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m Confidence Threshold 최적화 (Validation Set만 사용)
3
+ 과적합 방지를 위해 Val set 10장만으로 평가
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image
7
+ import json
8
+ import os
9
+
10
+ # 학습된 모델 로드
11
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
12
+ model = YOLO(MODEL_PATH)
13
+
14
+ print(f"✅ YOLOv8m 모델 로드 완료: {MODEL_PATH}")
15
+
16
+ # Ground Truth 로드
17
+ GT_FILE = "ground_truth.json"
18
+ with open(GT_FILE, 'r', encoding='utf-8') as f:
19
+ ground_truth = json.load(f)
20
+
21
+ # Val set 이미지만 필터링
22
+ val_images = set(os.listdir('data/yolo_dataset/images/val'))
23
+ gt_val_only = {}
24
+
25
+ for img_name, gts in ground_truth.items():
26
+ if not gts:
27
+ continue
28
+ base_name = img_name.replace('-1.jpg', '.jpg')
29
+ if img_name in val_images or base_name in val_images:
30
+ gt_val_only[img_name] = gts
31
+
32
+ print(f"✅ Ground Truth (Val set만): {len(gt_val_only)}장")
33
+ total_gt = sum(len(gts) for gts in gt_val_only.values())
34
+ print(f" - 총 GT 객체: {total_gt}개")
35
+ print("-" * 60)
36
+
37
+ def calculate_iou(box1, box2):
38
+ """IoU 계산"""
39
+ x1_min, y1_min, x1_max, y1_max = box1
40
+ x2_min, y2_min, x2_max, y2_max = box2
41
+
42
+ inter_x_min = max(x1_min, x2_min)
43
+ inter_y_min = max(y1_min, y2_min)
44
+ inter_x_max = min(x1_max, x2_max)
45
+ inter_y_max = min(y1_max, y2_max)
46
+
47
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
48
+ return 0.0
49
+
50
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
51
+ box1_area = (x1_max - x1_min) * (y1_max - y1_min)
52
+ box2_area = (x2_max - x2_min) * (y2_max - y2_min)
53
+ union_area = box1_area + box2_area - inter_area
54
+
55
+ return inter_area / union_area if union_area > 0 else 0.0
56
+
57
+ def evaluate_confidence_threshold(conf_threshold, iou_threshold=0.5):
58
+ """특정 confidence threshold에서 성능 평가 (Val set만)"""
59
+ tp = 0
60
+ fp = 0
61
+ fn = 0
62
+
63
+ matched_gt_count = 0
64
+ total_gt_count = 0
65
+
66
+ for img_name, gt_boxes in gt_val_only.items():
67
+ # 이미지 경로
68
+ img_path = f"data/yolo_dataset/images/val/{img_name}"
69
+ base_name = img_name.replace('-1.jpg', '.jpg')
70
+ if not os.path.exists(img_path):
71
+ img_path = f"data/yolo_dataset/images/val/{base_name}"
72
+
73
+ if not os.path.exists(img_path):
74
+ continue
75
+
76
+ # 이미지 로드
77
+ image = Image.open(img_path)
78
+
79
+ # YOLOv8 검출
80
+ results = model.predict(
81
+ source=image,
82
+ conf=conf_threshold,
83
+ iou=0.7,
84
+ device=0,
85
+ verbose=False
86
+ )
87
+
88
+ result = results[0]
89
+ boxes = result.boxes
90
+
91
+ predictions = []
92
+ if boxes is not None and len(boxes) > 0:
93
+ for box in boxes:
94
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
95
+ confidence = box.conf[0].cpu().item()
96
+ predictions.append({
97
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
98
+ 'confidence': confidence
99
+ })
100
+
101
+ total_gt_count += len(gt_boxes)
102
+
103
+ # GT와 매칭
104
+ matched_gt = set()
105
+ matched_pred = set()
106
+
107
+ for pred_idx, pred in enumerate(predictions):
108
+ best_iou = 0
109
+ best_gt_idx = -1
110
+
111
+ for gt_idx, gt in enumerate(gt_boxes):
112
+ if gt_idx in matched_gt:
113
+ continue
114
+
115
+ iou = calculate_iou(pred['bbox'], gt['bbox'])
116
+ if iou > best_iou:
117
+ best_iou = iou
118
+ best_gt_idx = gt_idx
119
+
120
+ if best_iou >= iou_threshold:
121
+ tp += 1
122
+ matched_gt.add(best_gt_idx)
123
+ matched_pred.add(pred_idx)
124
+ else:
125
+ fp += 1
126
+
127
+ fn += len(gt_boxes) - len(matched_gt)
128
+ matched_gt_count += len(matched_gt)
129
+
130
+ # 성능 지표 계산
131
+ precision = tp / (tp + fp) if (tp + fp) > 0 else 0
132
+ recall = tp / (tp + fn) if (tp + fn) > 0 else 0
133
+ f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
134
+
135
+ return {
136
+ 'tp': tp,
137
+ 'fp': fp,
138
+ 'fn': fn,
139
+ 'precision': precision,
140
+ 'recall': recall,
141
+ 'f1': f1,
142
+ 'matched_gt': matched_gt_count,
143
+ 'total_gt': total_gt_count,
144
+ 'gt_match_rate': matched_gt_count / total_gt_count if total_gt_count > 0 else 0
145
+ }
146
+
147
+ # Confidence threshold sweep
148
+ print("\n🔍 Confidence Threshold 최적화 (Val set만)...\n")
149
+
150
+ confidence_thresholds = [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9]
151
+ results = []
152
+
153
+ for conf in confidence_thresholds:
154
+ metrics = evaluate_confidence_threshold(conf)
155
+ results.append({
156
+ 'confidence': conf,
157
+ **metrics
158
+ })
159
+
160
+ print(f"Conf {conf:.2f}: P={metrics['precision']:.1%} R={metrics['recall']:.1%} F1={metrics['f1']:.1%} | "
161
+ f"GT매칭={metrics['matched_gt']}/{metrics['total_gt']} ({metrics['gt_match_rate']:.1%})")
162
+
163
+ # 최적값 찾기
164
+ best_f1 = max(results, key=lambda x: x['f1'])
165
+ best_recall = max(results, key=lambda x: x['recall'])
166
+ best_precision = max(results, key=lambda x: x['precision'])
167
+
168
+ print("\n" + "=" * 60)
169
+ print("📊 최적화 결과 (Val set만, 과적합 없음):")
170
+ print("=" * 60)
171
+
172
+ print(f"\n1️⃣ 최고 F1 Score: {best_f1['f1']:.1%} (Confidence={best_f1['confidence']:.2f})")
173
+ print(f" - Precision: {best_f1['precision']:.1%}")
174
+ print(f" - Recall: {best_f1['recall']:.1%}")
175
+ print(f" - GT 매칭: {best_f1['matched_gt']}/{best_f1['total_gt']} ({best_f1['gt_match_rate']:.1%})")
176
+
177
+ print(f"\n2️⃣ 최고 Recall: {best_recall['recall']:.1%} (Confidence={best_recall['confidence']:.2f})")
178
+ print(f" - F1 Score: {best_recall['f1']:.1%}")
179
+ print(f" - Precision: {best_recall['precision']:.1%}")
180
+
181
+ print(f"\n3️⃣ 최고 Precision: {best_precision['precision']:.1%} (Confidence={best_precision['confidence']:.2f})")
182
+ print(f" - F1 Score: {best_precision['f1']:.1%}")
183
+ print(f" - Recall: {best_precision['recall']:.1%}")
184
+
185
+ print("\n💡 권장 설정 (Val set 기준, 일반화 성능):")
186
+ print(f" - 최적 confidence: {best_f1['confidence']:.2f}")
187
+ print(f" - F1 Score: {best_f1['f1']:.1%}")
188
+ print(f" - Precision: {best_f1['precision']:.1%}, Recall: {best_f1['recall']:.1%}")
189
+
190
+ # 결과 저장
191
+ output_file = "yolov8m_confidence_optimization_val_only.json"
192
+ with open(output_file, 'w', encoding='utf-8') as f:
193
+ json.dump({
194
+ 'dataset': 'validation_set_only',
195
+ 'num_images': len(gt_val_only),
196
+ 'total_gt': total_gt,
197
+ 'best_f1': best_f1,
198
+ 'best_recall': best_recall,
199
+ 'best_precision': best_precision,
200
+ 'all_results': results
201
+ }, f, indent=2, ensure_ascii=False)
202
+
203
+ print(f"\n💾 결과 저장: {output_file}")
204
+ print("=" * 60)
quick_test_roboflow.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow 모델 빠른 테스트
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import requests
9
+ import base64
10
+ from PIL import Image
11
+ from io import BytesIO
12
+ import json
13
+
14
+ # 테스트 이미지
15
+ test_image = "data/yolo_dataset/images/train/250818_04.jpg"
16
+
17
+ print("="*60)
18
+ print("🦐 Roboflow 모델 빠른 테스트")
19
+ print("="*60)
20
+ print(f"📸 이미지: {test_image}\n")
21
+
22
+ # 이미지 로드 및 리사이즈
23
+ image = Image.open(test_image)
24
+ print(f"원본 크기: {image.size}")
25
+
26
+ image.thumbnail((640, 640), Image.Resampling.LANCZOS)
27
+ print(f"리사이즈: {image.size}")
28
+
29
+ # Base64 인코딩
30
+ buffered = BytesIO()
31
+ image.save(buffered, format="JPEG", quality=80)
32
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
33
+
34
+ # API 호출
35
+ print(f"\n🔄 API 호출 중...\n")
36
+ response = requests.post(
37
+ 'https://serverless.roboflow.com/vidraft/workflows/find-shrimp-6',
38
+ headers={'Content-Type': 'application/json'},
39
+ json={
40
+ 'api_key': 'azcIL8KDJVJMYrsERzI7',
41
+ 'inputs': {
42
+ 'image': {'type': 'base64', 'value': img_base64}
43
+ }
44
+ },
45
+ timeout=30
46
+ )
47
+
48
+ if response.status_code != 200:
49
+ print(f"❌ 오류: {response.status_code}")
50
+ print(response.text)
51
+ exit(1)
52
+
53
+ result = response.json()
54
+
55
+ # predictions 추출
56
+ predictions = []
57
+ if 'outputs' in result and len(result['outputs']) > 0:
58
+ output = result['outputs'][0]
59
+ if 'predictions' in output:
60
+ pred_data = output['predictions']
61
+ if isinstance(pred_data, dict) and 'predictions' in pred_data:
62
+ predictions = pred_data['predictions']
63
+
64
+ print(f"{'='*60}")
65
+ print(f"📊 검출 결과")
66
+ print(f"{'='*60}\n")
67
+
68
+ print(f"총 검출 수: {len(predictions)}개\n")
69
+
70
+ # 상세 결과
71
+ for i, pred in enumerate(predictions, 1):
72
+ cls = pred.get('class', 'unknown')
73
+ conf = pred.get('confidence', 0)
74
+ x = pred.get('x', 0)
75
+ y = pred.get('y', 0)
76
+ w = pred.get('width', 0)
77
+ h = pred.get('height', 0)
78
+
79
+ print(f"{i}. 클래스: {cls}")
80
+ print(f" 신뢰도: {conf:.1%}")
81
+ print(f" 위치: ({x:.0f}, {y:.0f})")
82
+ print(f" 크기: {w:.0f} x {h:.0f}")
83
+ print()
84
+
85
+ # shrimp만 필터링
86
+ shrimp_count = sum(1 for p in predictions if p.get('class') == 'shrimp')
87
+ print(f"{'='*60}")
88
+ print(f"✅ shrimp 클래스: {shrimp_count}개")
89
+ print(f"{'='*60}")
quick_test_save_result.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow 모델 빠른 테스트 + 결과 이미지 저장
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import requests
9
+ import base64
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ from io import BytesIO
12
+ import json
13
+
14
+ # 테스트 이미지
15
+ test_image = "data/yolo_dataset/images/train/250818_04.jpg"
16
+
17
+ print("="*60)
18
+ print("🦐 Roboflow 모델 테스트 + 결과 저장")
19
+ print("="*60)
20
+ print(f"📸 이미지: {test_image}\n")
21
+
22
+ # 원본 이미지 로드
23
+ image_original = Image.open(test_image)
24
+ original_size = image_original.size
25
+ print(f"원본 크기: {original_size}")
26
+
27
+ # 리사이즈 (API 전송용)
28
+ image_resized = image_original.copy()
29
+ image_resized.thumbnail((640, 640), Image.Resampling.LANCZOS)
30
+ print(f"리사이즈: {image_resized.size}")
31
+
32
+ # Base64 인코딩
33
+ buffered = BytesIO()
34
+ image_resized.save(buffered, format="JPEG", quality=80)
35
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
36
+
37
+ # API 호출
38
+ print(f"\n🔄 API 호출 중...\n")
39
+ response = requests.post(
40
+ 'https://serverless.roboflow.com/vidraft/workflows/find-shrimp-6',
41
+ headers={'Content-Type': 'application/json'},
42
+ json={
43
+ 'api_key': 'azcIL8KDJVJMYrsERzI7',
44
+ 'inputs': {
45
+ 'image': {'type': 'base64', 'value': img_base64}
46
+ }
47
+ },
48
+ timeout=30
49
+ )
50
+
51
+ if response.status_code != 200:
52
+ print(f"❌ 오류: {response.status_code}")
53
+ exit(1)
54
+
55
+ result = response.json()
56
+
57
+ # predictions 추출
58
+ predictions = []
59
+ if 'outputs' in result and len(result['outputs']) > 0:
60
+ output = result['outputs'][0]
61
+ if 'predictions' in output:
62
+ pred_data = output['predictions']
63
+ if isinstance(pred_data, dict) and 'predictions' in pred_data:
64
+ predictions = pred_data['predictions']
65
+
66
+ print(f"📊 검출 수: {len(predictions)}개\n")
67
+
68
+ # 원본 이미지에 박스 그리기
69
+ draw = ImageDraw.Draw(image_original)
70
+
71
+ # 스케일 계산 (리사이즈된 좌표 → 원본 좌표)
72
+ scale_x = original_size[0] / image_resized.size[0]
73
+ scale_y = original_size[1] / image_resized.size[1]
74
+
75
+ shrimp_count = 0
76
+
77
+ for i, pred in enumerate(predictions, 1):
78
+ cls = pred.get('class', 'unknown')
79
+ conf = pred.get('confidence', 0)
80
+ x = pred.get('x', 0) * scale_x
81
+ y = pred.get('y', 0) * scale_y
82
+ w = pred.get('width', 0) * scale_x
83
+ h = pred.get('height', 0) * scale_y
84
+
85
+ print(f"{i}. 클래스: {cls}, 신뢰도: {conf:.1%}")
86
+
87
+ # shrimp만 그리기
88
+ if cls == 'shrimp':
89
+ shrimp_count += 1
90
+
91
+ # 박스 좌표
92
+ x1 = x - w / 2
93
+ y1 = y - h / 2
94
+ x2 = x + w / 2
95
+ y2 = y + h / 2
96
+
97
+ # 신뢰도별 색상
98
+ if conf >= 0.5:
99
+ color = 'lime'
100
+ elif conf >= 0.3:
101
+ color = 'yellow'
102
+ else:
103
+ color = 'orange'
104
+
105
+ # 박스 그리기
106
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=8)
107
+
108
+ # 텍스트
109
+ text = f"#{shrimp_count} {conf:.1%}"
110
+ try:
111
+ font = ImageFont.truetype("arial.ttf", 50)
112
+ except:
113
+ font = ImageFont.load_default()
114
+
115
+ # 텍스트 배경
116
+ text_bbox = draw.textbbox((x1, y1-60), text, font=font)
117
+ draw.rectangle(text_bbox, fill=color)
118
+ draw.text((x1, y1-60), text, fill='black', font=font)
119
+
120
+ # 결과 저장
121
+ output_path = "quick_test_result.jpg"
122
+ image_original.save(output_path, quality=95)
123
+
124
+ print(f"\n{'='*60}")
125
+ print(f"✅ shrimp 검출: {shrimp_count}개")
126
+ print(f"💾 결과 저장: {output_path}")
127
+ print(f"{'='*60}")
test_10_images.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow 모델 10개 이미지 테스트
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import requests
9
+ import base64
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ from io import BytesIO
12
+ import json
13
+ import glob
14
+ import os
15
+ from datetime import datetime
16
+
17
+ def test_image(image_path):
18
+ """단일 이미지 테스트"""
19
+ # 원본 이미지 로드
20
+ image_original = Image.open(image_path)
21
+ original_size = image_original.size
22
+
23
+ # 리사이즈 (API 전송용)
24
+ image_resized = image_original.copy()
25
+ image_resized.thumbnail((640, 640), Image.Resampling.LANCZOS)
26
+
27
+ # Base64 인코딩
28
+ buffered = BytesIO()
29
+ image_resized.save(buffered, format="JPEG", quality=80)
30
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
31
+
32
+ # API 호출
33
+ response = requests.post(
34
+ 'https://serverless.roboflow.com/vidraft/workflows/find-shrimp-6',
35
+ headers={'Content-Type': 'application/json'},
36
+ json={
37
+ 'api_key': 'azcIL8KDJVJMYrsERzI7',
38
+ 'inputs': {
39
+ 'image': {'type': 'base64', 'value': img_base64}
40
+ }
41
+ },
42
+ timeout=30
43
+ )
44
+
45
+ if response.status_code != 200:
46
+ return None
47
+
48
+ result = response.json()
49
+
50
+ # predictions 추출
51
+ predictions = []
52
+ if 'outputs' in result and len(result['outputs']) > 0:
53
+ output = result['outputs'][0]
54
+ if 'predictions' in output:
55
+ pred_data = output['predictions']
56
+ if isinstance(pred_data, dict) and 'predictions' in pred_data:
57
+ predictions = pred_data['predictions']
58
+
59
+ # 스케일 계산
60
+ scale_x = original_size[0] / image_resized.size[0]
61
+ scale_y = original_size[1] / image_resized.size[1]
62
+
63
+ # shrimp만 필터링
64
+ shrimp_predictions = [p for p in predictions if p.get('class') == 'shrimp']
65
+
66
+ return {
67
+ 'original': image_original,
68
+ 'predictions': shrimp_predictions,
69
+ 'scale_x': scale_x,
70
+ 'scale_y': scale_y
71
+ }
72
+
73
+ def draw_result(image, predictions, scale_x, scale_y):
74
+ """결과 그리기"""
75
+ draw = ImageDraw.Draw(image)
76
+
77
+ try:
78
+ font = ImageFont.truetype("arial.ttf", 50)
79
+ except:
80
+ font = ImageFont.load_default()
81
+
82
+ for i, pred in enumerate(predictions, 1):
83
+ conf = pred.get('confidence', 0)
84
+ x = pred.get('x', 0) * scale_x
85
+ y = pred.get('y', 0) * scale_y
86
+ w = pred.get('width', 0) * scale_x
87
+ h = pred.get('height', 0) * scale_y
88
+
89
+ # 박스 좌표
90
+ x1 = x - w / 2
91
+ y1 = y - h / 2
92
+ x2 = x + w / 2
93
+ y2 = y + h / 2
94
+
95
+ # 신뢰도별 색상
96
+ if conf >= 0.5:
97
+ color = 'lime'
98
+ elif conf >= 0.3:
99
+ color = 'yellow'
100
+ else:
101
+ color = 'orange'
102
+
103
+ # 박스 그리기
104
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=8)
105
+
106
+ # 텍스트
107
+ text = f"#{i} {conf:.1%}"
108
+ text_bbox = draw.textbbox((x1, y1-60), text, font=font)
109
+ draw.rectangle(text_bbox, fill=color)
110
+ draw.text((x1, y1-60), text, fill='black', font=font)
111
+
112
+ return image
113
+
114
+ def main():
115
+ print("="*60)
116
+ print("🦐 Roboflow 모델 10개 이미지 테스트")
117
+ print("="*60)
118
+
119
+ # 테스트 이미지 선택
120
+ image_dir = "data/yolo_dataset/images/train"
121
+ all_images = sorted(glob.glob(os.path.join(image_dir, "*.jpg")))
122
+
123
+ # roboflow_result가 아닌 원본 이미지만 선택
124
+ test_images = [img for img in all_images if 'roboflow_result' not in img][:10]
125
+
126
+ if len(test_images) < 10:
127
+ print(f"⚠️ 이미지 부족: {len(test_images)}개만 발견")
128
+ test_images = test_images[:len(test_images)]
129
+
130
+ print(f"\n📁 이미지 경로: {image_dir}")
131
+ print(f"📊 테스트 이미지 수: {len(test_images)}개\n")
132
+
133
+ # 출력 디렉토리 생성
134
+ output_dir = "test_results_10"
135
+ os.makedirs(output_dir, exist_ok=True)
136
+
137
+ results_summary = []
138
+
139
+ for idx, img_path in enumerate(test_images, 1):
140
+ img_name = os.path.basename(img_path)
141
+ print(f"[{idx}/{len(test_images)}] {img_name} 처리 중...", end=" ")
142
+
143
+ try:
144
+ # 테스트
145
+ result = test_image(img_path)
146
+
147
+ if result is None:
148
+ print("❌ API 오류")
149
+ continue
150
+
151
+ predictions = result['predictions']
152
+ shrimp_count = len(predictions)
153
+
154
+ # 결과 그리기
155
+ image_with_boxes = draw_result(
156
+ result['original'],
157
+ predictions,
158
+ result['scale_x'],
159
+ result['scale_y']
160
+ )
161
+
162
+ # 저장
163
+ output_filename = img_name.replace('.jpg', '_roboflow_result.jpg')
164
+ output_path = os.path.join(output_dir, output_filename)
165
+ image_with_boxes.save(output_path, quality=95)
166
+
167
+ print(f"✅ shrimp {shrimp_count}개")
168
+
169
+ results_summary.append({
170
+ 'image': img_name,
171
+ 'shrimp_count': shrimp_count,
172
+ 'output': output_path,
173
+ 'confidences': [p.get('confidence', 0) for p in predictions]
174
+ })
175
+
176
+ except Exception as e:
177
+ print(f"❌ 오류: {str(e)}")
178
+
179
+ # 요약
180
+ print(f"\n{'='*60}")
181
+ print("📊 테스트 요약")
182
+ print(f"{'='*60}\n")
183
+
184
+ total_shrimp = sum(r['shrimp_count'] for r in results_summary)
185
+ avg_shrimp = total_shrimp / len(results_summary) if results_summary else 0
186
+
187
+ print(f"총 처리 이미지: {len(results_summary)}개")
188
+ print(f"총 shrimp 검출: {total_shrimp}개")
189
+ print(f"평균: {avg_shrimp:.1f}개/이미지\n")
190
+
191
+ print("이미지별 결과:")
192
+ for r in results_summary:
193
+ avg_conf = sum(r['confidences']) / len(r['confidences']) if r['confidences'] else 0
194
+ print(f" {r['image']}: {r['shrimp_count']}개 (평균 신뢰도: {avg_conf:.1%})")
195
+
196
+ print(f"\n✅ 완료! 결과는 {output_dir}/ 폴더에 저장되었습니다.")
197
+
198
+ # JSON 저장
199
+ json_path = os.path.join(output_dir, f"test_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
200
+ with open(json_path, 'w', encoding='utf-8') as f:
201
+ json.dump(results_summary, f, indent=2, ensure_ascii=False)
202
+ print(f"📄 JSON 저장: {json_path}")
203
+
204
+ if __name__ == "__main__":
205
+ main()
test_curl_roboflow.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 간단한 Roboflow API 테스트 (requests 사용)
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import requests
9
+ import base64
10
+ from PIL import Image
11
+ from io import BytesIO
12
+ import json
13
+
14
+ # 테스트 이미지
15
+ test_image = "data/yolo_dataset/images/train/250818_01.jpg"
16
+
17
+ print("="*60)
18
+ print("🔍 Roboflow API 테스트 (requests)")
19
+ print("="*60)
20
+
21
+ # 이미지를 base64로 인코딩
22
+ image = Image.open(test_image)
23
+ print(f"📸 이미지: {test_image}")
24
+ print(f"🖼️ 크기: {image.size}")
25
+
26
+ # 리사이즈
27
+ image.thumbnail((640, 640), Image.Resampling.LANCZOS)
28
+ print(f"📐 리사이즈: {image.size}")
29
+
30
+ buffered = BytesIO()
31
+ image.save(buffered, format="JPEG", quality=80)
32
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
33
+
34
+ print(f"📦 Base64 크기: {len(img_base64)} bytes")
35
+
36
+ # API 요청
37
+ print(f"\n🔄 API 호출 중...")
38
+
39
+ try:
40
+ response = requests.post(
41
+ 'https://serverless.roboflow.com/vidraft/workflows/find-shrimp-6',
42
+ headers={'Content-Type': 'application/json'},
43
+ json={
44
+ 'api_key': 'azcIL8KDJVJMYrsERzI7',
45
+ 'inputs': {
46
+ 'image': {'type': 'base64', 'value': img_base64}
47
+ }
48
+ },
49
+ timeout=30
50
+ )
51
+
52
+ print(f"📡 응답 코드: {response.status_code}")
53
+
54
+ if response.status_code == 200:
55
+ result = response.json()
56
+ print(f"\n📦 응답 구조:")
57
+ print(json.dumps(result, indent=2, ensure_ascii=False)[:2000])
58
+
59
+ # predictions 추출 시도
60
+ if 'outputs' in result:
61
+ print(f"\n✅ outputs 발견: {len(result['outputs'])}개")
62
+ if len(result['outputs']) > 0:
63
+ output = result['outputs'][0]
64
+ print(f"📦 output[0] keys: {output.keys()}")
65
+ if 'predictions' in output:
66
+ pred_data = output['predictions']
67
+ print(f"📦 predictions type: {type(pred_data)}")
68
+ if isinstance(pred_data, dict):
69
+ print(f"📦 predictions keys: {pred_data.keys()}")
70
+ if 'predictions' in pred_data:
71
+ preds = pred_data['predictions']
72
+ print(f"✅ 최종 predictions: {len(preds)}개")
73
+ else:
74
+ print(f"❌ 오류: {response.text}")
75
+
76
+ except Exception as e:
77
+ print(f"❌ 예외 발생: {str(e)}")
78
+ import traceback
79
+ traceback.print_exc()
80
+
81
+ print(f"\n{'='*60}")
82
+ print("✅ 테스트 완료")
test_parameter_sweep.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 파라미터 그리드 서치
4
+ 최적의 confidence threshold와 filter threshold 조합 찾기
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 datetime import datetime
13
+ import matplotlib.pyplot as plt
14
+ import seaborn as sns
15
+ from test_quantitative_evaluation import (
16
+ load_ground_truth,
17
+ load_rtdetr_model,
18
+ detect_with_rtdetr,
19
+ apply_universal_filter,
20
+ evaluate_detection
21
+ )
22
+ from PIL import Image
23
+
24
+ def run_parameter_sweep(
25
+ test_image_dir,
26
+ ground_truth_path,
27
+ confidence_values,
28
+ filter_values,
29
+ iou_threshold=0.5
30
+ ):
31
+ """파라미터 그리드 서치 실행"""
32
+ print("\n" + "="*70)
33
+ print("🔍 파라미터 그리드 서치 시작")
34
+ print("="*70)
35
+
36
+ # Ground truth 로드
37
+ ground_truths = load_ground_truth(ground_truth_path)
38
+ if not ground_truths:
39
+ return
40
+
41
+ # 모델 로드 (한 번만)
42
+ processor, model = load_rtdetr_model()
43
+
44
+ # 결과 저장
45
+ results_grid = {}
46
+ best_f1 = 0
47
+ best_config = None
48
+
49
+ print(f"\n📊 테스트 범위:")
50
+ print(f" Confidence: {confidence_values}")
51
+ print(f" Filter Threshold: {filter_values}")
52
+ print(f" 총 {len(confidence_values) * len(filter_values)}개 조합 테스트\n")
53
+
54
+ total_tests = len(confidence_values) * len(filter_values)
55
+ current_test = 0
56
+
57
+ # 그리드 서치
58
+ for conf in confidence_values:
59
+ results_grid[conf] = {}
60
+
61
+ for filt in filter_values:
62
+ current_test += 1
63
+ print(f"\n[{current_test}/{total_tests}] 테스트 중: Conf={conf}, Filter={filt}")
64
+
65
+ metrics_list = []
66
+
67
+ for filename, gt_list in ground_truths.items():
68
+ # 이미지 경로 구성
69
+ if gt_list and 'folder' in gt_list[0]:
70
+ folder = gt_list[0]['folder']
71
+ img_path = os.path.join(test_image_dir, folder, filename)
72
+ else:
73
+ img_path = os.path.join(test_image_dir, filename)
74
+
75
+ if not os.path.exists(img_path):
76
+ continue
77
+
78
+ # 검출
79
+ image = Image.open(img_path).convert('RGB')
80
+ all_detections = detect_with_rtdetr(image, processor, model, conf)
81
+ filtered_detections = apply_universal_filter(all_detections, image, filt)
82
+
83
+ # 평가
84
+ metrics = evaluate_detection(filtered_detections, gt_list, iou_threshold)
85
+ metrics_list.append(metrics)
86
+
87
+ # 평균 계산
88
+ if metrics_list:
89
+ avg_precision = np.mean([m['precision'] for m in metrics_list])
90
+ avg_recall = np.mean([m['recall'] for m in metrics_list])
91
+ avg_f1 = np.mean([m['f1'] for m in metrics_list])
92
+
93
+ results_grid[conf][filt] = {
94
+ 'precision': avg_precision,
95
+ 'recall': avg_recall,
96
+ 'f1': avg_f1
97
+ }
98
+
99
+ print(f" → P={avg_precision:.2%}, R={avg_recall:.2%}, F1={avg_f1:.2%}")
100
+
101
+ # 최고 성능 업데이트
102
+ if avg_f1 > best_f1:
103
+ best_f1 = avg_f1
104
+ best_config = {
105
+ 'confidence': conf,
106
+ 'filter_threshold': filt,
107
+ 'metrics': {
108
+ 'precision': avg_precision,
109
+ 'recall': avg_recall,
110
+ 'f1': avg_f1
111
+ }
112
+ }
113
+
114
+ # 결과 저장
115
+ output_dir = f"test_results/parameter_sweep_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
116
+ os.makedirs(output_dir, exist_ok=True)
117
+
118
+ # JSON 저장
119
+ summary = {
120
+ 'test_range': {
121
+ 'confidence': confidence_values,
122
+ 'filter_threshold': filter_values,
123
+ 'iou_threshold': iou_threshold
124
+ },
125
+ 'best_config': best_config,
126
+ 'all_results': results_grid
127
+ }
128
+
129
+ json_path = os.path.join(output_dir, 'sweep_results.json')
130
+ with open(json_path, 'w', encoding='utf-8') as f:
131
+ json.dump(summary, f, ensure_ascii=False, indent=2)
132
+
133
+ print("\n" + "="*70)
134
+ print("🏆 최고 성능 설정")
135
+ print("="*70)
136
+ print(f"Confidence Threshold: {best_config['confidence']}")
137
+ print(f"Filter Threshold: {best_config['filter_threshold']}")
138
+ print(f"Precision: {best_config['metrics']['precision']:.2%}")
139
+ print(f"Recall: {best_config['metrics']['recall']:.2%}")
140
+ print(f"F1 Score: {best_config['metrics']['f1']:.2%}")
141
+ print("="*70)
142
+
143
+ # 히트맵 생성
144
+ generate_heatmaps(results_grid, confidence_values, filter_values, output_dir)
145
+
146
+ print(f"\n📄 결과 저장: {json_path}")
147
+ print(f"📊 히트맵 저장: {output_dir}")
148
+
149
+ return best_config, results_grid
150
+
151
+
152
+ def generate_heatmaps(results_grid, conf_values, filt_values, output_dir):
153
+ """성능 히트��� 생성"""
154
+ metrics = ['precision', 'recall', 'f1']
155
+ metric_names = {
156
+ 'precision': 'Precision (정밀도)',
157
+ 'recall': 'Recall (재현율)',
158
+ 'f1': 'F1 Score'
159
+ }
160
+
161
+ for metric in metrics:
162
+ # 데이터 행렬 생성
163
+ data = np.zeros((len(conf_values), len(filt_values)))
164
+
165
+ for i, conf in enumerate(conf_values):
166
+ for j, filt in enumerate(filt_values):
167
+ if conf in results_grid and filt in results_grid[conf]:
168
+ data[i, j] = results_grid[conf][filt][metric]
169
+
170
+ # 히트맵 그리기
171
+ plt.figure(figsize=(12, 8))
172
+ sns.heatmap(
173
+ data,
174
+ annot=True,
175
+ fmt='.2%',
176
+ cmap='RdYlGn',
177
+ xticklabels=filt_values,
178
+ yticklabels=conf_values,
179
+ vmin=0,
180
+ vmax=1,
181
+ cbar_kws={'label': metric_names[metric]}
182
+ )
183
+ plt.xlabel('Filter Threshold', fontsize=12)
184
+ plt.ylabel('Confidence Threshold', fontsize=12)
185
+ plt.title(f'{metric_names[metric]} Heatmap', fontsize=14, fontweight='bold')
186
+ plt.tight_layout()
187
+
188
+ output_path = os.path.join(output_dir, f'heatmap_{metric}.png')
189
+ plt.savefig(output_path, dpi=150)
190
+ plt.close()
191
+ print(f" 📊 {metric_names[metric]} 히트맵 저장: {output_path}")
192
+
193
+
194
+ if __name__ == "__main__":
195
+ # 테스트 범위 설정
196
+ TEST_DIR = r"data\흰다리새우 실측 데이터_익투스에이아이(주)"
197
+ GT_PATH = "ground_truth.json"
198
+
199
+ # 파라미터 범위
200
+ CONFIDENCE_VALUES = [0.3, 0.35, 0.4, 0.45, 0.5]
201
+ FILTER_VALUES = [30, 40, 50, 60, 70]
202
+
203
+ if not os.path.exists(GT_PATH):
204
+ print("⚠️ ground_truth.json 파일이 필요합니다.")
205
+ else:
206
+ best_config, all_results = run_parameter_sweep(
207
+ test_image_dir=TEST_DIR,
208
+ ground_truth_path=GT_PATH,
209
+ confidence_values=CONFIDENCE_VALUES,
210
+ filter_values=FILTER_VALUES,
211
+ iou_threshold=0.5
212
+ )
213
+
214
+ print("\n💡 다음 단계:")
215
+ print(f" 1. test_visual_validation.py 의 파라미터를 업데이트:")
216
+ print(f" - confidence_threshold = {best_config['confidence']}")
217
+ print(f" - filter_threshold = {best_config['filter_threshold']}")
218
+ print(f" 2. 업데이트 후 재평가 실행:")
219
+ print(f" python test_quantitative_evaluation.py")
test_roboflow_model.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow 모델 (find-shrimp-6) 테스트
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ from inference_sdk import InferenceHTTPClient
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ import os
11
+ import glob
12
+ import time
13
+
14
+ # Roboflow 클라이언트 초기화
15
+ client = InferenceHTTPClient(
16
+ api_url="https://serverless.roboflow.com",
17
+ api_key="azcIL8KDJVJMYrsERzI7"
18
+ )
19
+
20
+ def test_roboflow_detection(image_path):
21
+ """단일 이미지 테스트"""
22
+ print(f"\n{'='*60}")
23
+ print(f"📸 테스트 이미지: {os.path.basename(image_path)}")
24
+ print(f"{'='*60}")
25
+
26
+ # 이미지 크기 확인
27
+ image = Image.open(image_path)
28
+ print(f"🖼️ 원본 크기: {image.size}")
29
+
30
+ # Roboflow API 호출
31
+ start_time = time.time()
32
+
33
+ result = client.run_workflow(
34
+ workspace_name="vidraft",
35
+ workflow_id="find-shrimp-6",
36
+ images={"image": image_path},
37
+ use_cache=True
38
+ )
39
+
40
+ elapsed = time.time() - start_time
41
+ print(f"⏱️ 추론 시간: {elapsed:.2f}초")
42
+
43
+ # 결과 파싱
44
+ predictions = []
45
+
46
+ if isinstance(result, dict) and 'outputs' in result and len(result['outputs']) > 0:
47
+ output = result['outputs'][0]
48
+ if isinstance(output, dict) and 'predictions' in output:
49
+ pred_data = output['predictions']
50
+ if isinstance(pred_data, dict) and 'predictions' in pred_data:
51
+ predictions = pred_data['predictions']
52
+ elif isinstance(pred_data, list):
53
+ predictions = pred_data
54
+
55
+ print(f"📦 검출된 객체 수: {len(predictions)}개")
56
+
57
+ # 신뢰도별 통계
58
+ if predictions:
59
+ confidences = [pred.get('confidence', 0) for pred in predictions]
60
+ print(f"📊 신뢰도 통계:")
61
+ print(f" - 최고: {max(confidences):.1%}")
62
+ print(f" - 최저: {min(confidences):.1%}")
63
+ print(f" - 평균: {sum(confidences)/len(confidences):.1%}")
64
+
65
+ # 신뢰도별 개수
66
+ high_conf = sum(1 for c in confidences if c >= 0.5)
67
+ mid_conf = sum(1 for c in confidences if 0.2 <= c < 0.5)
68
+ low_conf = sum(1 for c in confidences if c < 0.2)
69
+
70
+ print(f"\n - 고신뢰도 (≥50%): {high_conf}개")
71
+ print(f" - 중신뢰도 (20-50%): {mid_conf}개")
72
+ print(f" - 저신뢰도 (<20%): {low_conf}개")
73
+
74
+ # 상위 5개 출력
75
+ print(f"\n🔍 상위 5개 검출 결과:")
76
+ sorted_preds = sorted(predictions, key=lambda x: x.get('confidence', 0), reverse=True)
77
+ for i, pred in enumerate(sorted_preds[:5], 1):
78
+ conf = pred.get('confidence', 0)
79
+ x = pred.get('x', 0)
80
+ y = pred.get('y', 0)
81
+ w = pred.get('width', 0)
82
+ h = pred.get('height', 0)
83
+ print(f" {i}. 신뢰도: {conf:.1%}, 위치: ({x:.0f}, {y:.0f}), 크기: {w:.0f}x{h:.0f}")
84
+ else:
85
+ print("⚠️ 검출된 객체가 없습니다!")
86
+
87
+ # 시각화
88
+ output_path = image_path.replace('.jpg', '_roboflow_result.jpg')
89
+ visualize_result(image_path, predictions, output_path)
90
+ print(f"💾 결과 저장: {output_path}")
91
+
92
+ return predictions
93
+
94
+ def visualize_result(image_path, predictions, output_path):
95
+ """결과 시각화"""
96
+ image = Image.open(image_path)
97
+ draw = ImageDraw.Draw(image)
98
+
99
+ for pred in predictions:
100
+ conf = pred.get('confidence', 0)
101
+ x = pred.get('x', 0)
102
+ y = pred.get('y', 0)
103
+ w = pred.get('width', 0)
104
+ h = pred.get('height', 0)
105
+
106
+ # 박스 좌표
107
+ x1 = x - w / 2
108
+ y1 = y - h / 2
109
+ x2 = x + w / 2
110
+ y2 = y + h / 2
111
+
112
+ # 신뢰도에 따른 색상
113
+ if conf >= 0.5:
114
+ color = 'green'
115
+ elif conf >= 0.2:
116
+ color = 'yellow'
117
+ else:
118
+ color = 'red'
119
+
120
+ # 박스 그리기
121
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
122
+
123
+ # 신뢰도 텍스트
124
+ text = f"{conf:.0%}"
125
+ draw.text((x1, y1-15), text, fill=color)
126
+
127
+ image.save(output_path, quality=95)
128
+
129
+ def main():
130
+ print("="*60)
131
+ print("🦐 Roboflow 모델 (find-shrimp-6) 테스트")
132
+ print("="*60)
133
+
134
+ # 테스트 이미지 선택 (YOLO 데이터셋에서 5개)
135
+ image_dir = "data/yolo_dataset/images/train"
136
+ test_images = sorted(glob.glob(os.path.join(image_dir, "*.jpg")))[:5]
137
+
138
+ if not test_images:
139
+ print("❌ 테스트 이미지를 찾을 수 없습니다!")
140
+ return
141
+
142
+ print(f"\n📁 테스트 이미지 경로: {image_dir}")
143
+ print(f"📊 테스트 이미지 수: {len(test_images)}개\n")
144
+
145
+ all_results = []
146
+
147
+ for img_path in test_images:
148
+ try:
149
+ predictions = test_roboflow_detection(img_path)
150
+ all_results.append({
151
+ 'image': os.path.basename(img_path),
152
+ 'count': len(predictions),
153
+ 'predictions': predictions
154
+ })
155
+ except Exception as e:
156
+ print(f"❌ 오류 발생: {str(e)}")
157
+ import traceback
158
+ traceback.print_exc()
159
+
160
+ # 전체 요약
161
+ print(f"\n{'='*60}")
162
+ print("📊 전체 테스트 요약")
163
+ print(f"{'='*60}")
164
+
165
+ total_detections = sum(r['count'] for r in all_results)
166
+ print(f"\n총 검출 수: {total_detections}개")
167
+ print(f"이미지당 평균: {total_detections/len(all_results):.1f}개")
168
+
169
+ print(f"\n이미지별 검출 수:")
170
+ for r in all_results:
171
+ print(f" - {r['image']}: {r['count']}개")
172
+
173
+ print(f"\n✅ 테스트 완료!")
174
+ print(f"📁 결과 이미지: {image_dir}/*_roboflow_result.jpg")
175
+
176
+ if __name__ == "__main__":
177
+ main()
test_roboflow_save_results.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Roboflow 모델 테스트 및 결과 이미지 저장
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import requests
9
+ import base64
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ from io import BytesIO
12
+ import json
13
+ import os
14
+ import glob
15
+
16
+ def test_and_save_result(image_path, output_dir="test_results"):
17
+ """이미지 테스트 후 결과 저장"""
18
+
19
+ # 출력 디렉토리 생성
20
+ os.makedirs(output_dir, exist_ok=True)
21
+
22
+ print(f"\n{'='*60}")
23
+ print(f"📸 테스트: {os.path.basename(image_path)}")
24
+ print(f"{'='*60}")
25
+
26
+ # 이미지 로드
27
+ image = Image.open(image_path)
28
+ original_size = image.size
29
+ print(f"🖼️ 원본 크기: {original_size}")
30
+
31
+ # 리사이즈
32
+ max_size = 640
33
+ if image.width > max_size or image.height > max_size:
34
+ image_resized = image.copy()
35
+ image_resized.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
36
+ print(f"📐 리사이즈: {image_resized.size}")
37
+ else:
38
+ image_resized = image
39
+
40
+ # Base64 인코딩
41
+ buffered = BytesIO()
42
+ image_resized.save(buffered, format="JPEG", quality=80)
43
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
44
+ print(f"📦 Base64 크기: {len(img_base64)} bytes")
45
+
46
+ # API 호출
47
+ print(f"🔄 Roboflow API 호출 중...")
48
+ response = requests.post(
49
+ 'https://serverless.roboflow.com/vidraft/workflows/find-shrimp-6',
50
+ headers={'Content-Type': 'application/json'},
51
+ json={
52
+ 'api_key': 'azcIL8KDJVJMYrsERzI7',
53
+ 'inputs': {
54
+ 'image': {'type': 'base64', 'value': img_base64}
55
+ }
56
+ },
57
+ timeout=30
58
+ )
59
+
60
+ if response.status_code != 200:
61
+ print(f"❌ API 오류: {response.status_code}")
62
+ return None
63
+
64
+ result = response.json()
65
+
66
+ # predictions 추출
67
+ predictions = []
68
+ if 'outputs' in result and len(result['outputs']) > 0:
69
+ output = result['outputs'][0]
70
+ if 'predictions' in output:
71
+ pred_data = output['predictions']
72
+ if isinstance(pred_data, dict) and 'predictions' in pred_data:
73
+ predictions = pred_data['predictions']
74
+
75
+ print(f"📦 검출 수: {len(predictions)}개")
76
+
77
+ # 원본 이미지에 박스 그리기
78
+ draw = ImageDraw.Draw(image)
79
+
80
+ # 리사이즈 비율 계산 (원본 크기로 복원)
81
+ scale_x = original_size[0] / image_resized.size[0]
82
+ scale_y = original_size[1] / image_resized.size[1]
83
+
84
+ for i, pred in enumerate(predictions, 1):
85
+ conf = pred.get('confidence', 0)
86
+ x = pred.get('x', 0) * scale_x
87
+ y = pred.get('y', 0) * scale_y
88
+ w = pred.get('width', 0) * scale_x
89
+ h = pred.get('height', 0) * scale_y
90
+
91
+ # 박스 좌표
92
+ x1 = x - w / 2
93
+ y1 = y - h / 2
94
+ x2 = x + w / 2
95
+ y2 = y + h / 2
96
+
97
+ # 신뢰도별 색상
98
+ if conf >= 0.5:
99
+ color = 'lime'
100
+ thickness = 5
101
+ elif conf >= 0.3:
102
+ color = 'yellow'
103
+ thickness = 4
104
+ else:
105
+ color = 'red'
106
+ thickness = 3
107
+
108
+ # 박스 그리기
109
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=thickness)
110
+
111
+ # 신뢰도 텍스트
112
+ text = f"#{i} {conf:.1%}"
113
+
114
+ # 텍스트 배경
115
+ try:
116
+ font = ImageFont.truetype("arial.ttf", 40)
117
+ except:
118
+ font = ImageFont.load_default()
119
+
120
+ # 텍스트 위치
121
+ text_bbox = draw.textbbox((x1, y1-50), text, font=font)
122
+ draw.rectangle(text_bbox, fill=color)
123
+ draw.text((x1, y1-50), text, fill='black', font=font)
124
+
125
+ print(f" {i}. 신뢰도: {conf:.1%}, 위치: ({x:.0f}, {y:.0f}), 크기: {w:.0f}x{h:.0f}")
126
+
127
+ # 결과 저장
128
+ output_filename = os.path.basename(image_path).replace('.jpg', '_result.jpg')
129
+ output_path = os.path.join(output_dir, output_filename)
130
+ image.save(output_path, quality=95)
131
+ print(f"💾 저장: {output_path}")
132
+
133
+ return {
134
+ 'image': os.path.basename(image_path),
135
+ 'detections': len(predictions),
136
+ 'output': output_path
137
+ }
138
+
139
+ def main():
140
+ print("="*60)
141
+ print("🦐 Roboflow 모델 테스트 및 결과 저장")
142
+ print("="*60)
143
+
144
+ # YOLO 데이터셋에서 5개 이미지 선택
145
+ image_dir = "data/yolo_dataset/images/train"
146
+ test_images = sorted(glob.glob(os.path.join(image_dir, "*.jpg")))[:5]
147
+
148
+ if not test_images:
149
+ print("❌ 테스트 이미지를 찾을 수 없습니다!")
150
+ return
151
+
152
+ print(f"\n📁 이미지 경로: {image_dir}")
153
+ print(f"📊 테스트 수: {len(test_images)}개\n")
154
+
155
+ results = []
156
+
157
+ for img_path in test_images:
158
+ try:
159
+ result = test_and_save_result(img_path)
160
+ if result:
161
+ results.append(result)
162
+ except Exception as e:
163
+ print(f"❌ 오류: {str(e)}")
164
+ import traceback
165
+ traceback.print_exc()
166
+
167
+ # 요약
168
+ print(f"\n{'='*60}")
169
+ print("📊 테스트 요약")
170
+ print(f"{'='*60}")
171
+
172
+ total_detections = sum(r['detections'] for r in results)
173
+ print(f"\n총 검출 수: {total_detections}개")
174
+ print(f"평균: {total_detections/len(results):.1f}개/이미지")
175
+
176
+ print(f"\n이미지별 결과:")
177
+ for r in results:
178
+ print(f" - {r['image']}: {r['detections']}개 → {r['output']}")
179
+
180
+ print(f"\n✅ 완료! 결과는 test_results/ 폴더에 저장되었습니다.")
181
+
182
+ if __name__ == "__main__":
183
+ main()
test_yolo_with_filter.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ YOLOv8 + Universal Filter 결합
4
+ 즉시 개선: YOLOv8의 높은 Recall + Filter의 높은 Precision
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ from ultralytics import YOLO
10
+ import json
11
+ import os
12
+ from PIL import Image
13
+ import numpy as np
14
+ from pathlib import Path
15
+ from test_visual_validation import apply_universal_filter
16
+
17
+ def calculate_iou(box1, box2):
18
+ """IoU 계산"""
19
+ x1_1, y1_1, x2_1, y2_1 = box1
20
+ x1_2, y1_2, x2_2, y2_2 = box2
21
+
22
+ x1_i = max(x1_1, x1_2)
23
+ y1_i = max(y1_1, y1_2)
24
+ x2_i = min(x2_1, x2_2)
25
+ y2_i = min(y2_1, y2_2)
26
+
27
+ if x2_i < x1_i or y2_i < y1_i:
28
+ return 0.0
29
+
30
+ intersection = (x2_i - x1_i) * (y2_i - y1_i)
31
+ area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
32
+ area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
33
+ union = area1 + area2 - intersection
34
+
35
+ return intersection / union if union > 0 else 0.0
36
+
37
+ def yolo_with_filter_evaluate(model_path, gt_file, data_base_dir,
38
+ yolo_conf=0.01, filter_threshold=90, iou_threshold=0.5):
39
+ """YOLOv8 + Universal Filter 평가"""
40
+
41
+ print(f"\n📊 YOLOv8 + Universal Filter 평가")
42
+ print(f" - YOLOv8 Confidence: {yolo_conf}")
43
+ print(f" - Filter Threshold: {filter_threshold}")
44
+ print(f" - IoU Threshold: {iou_threshold}")
45
+
46
+ # 모델 로드
47
+ model = YOLO(model_path)
48
+ print(f"✅ YOLOv8 모델 로드 완료")
49
+
50
+ # GT 로드
51
+ with open(gt_file, 'r', encoding='utf-8') as f:
52
+ gt_data = json.load(f)
53
+
54
+ # 통계
55
+ total_gt = 0
56
+ total_yolo_pred = 0
57
+ total_filtered_pred = 0
58
+
59
+ true_positives = 0
60
+ false_positives = 0
61
+ false_negatives = 0
62
+
63
+ yolo_only_tp = 0
64
+ yolo_only_fp = 0
65
+
66
+ results_detail = []
67
+
68
+ # 각 이미지 평가
69
+ for filename, gt_boxes in gt_data.items():
70
+ if not gt_boxes:
71
+ continue
72
+
73
+ folder = gt_boxes[0].get('folder', '')
74
+ if not folder:
75
+ continue
76
+
77
+ img_path = os.path.join(data_base_dir, folder, filename)
78
+ if not os.path.exists(img_path):
79
+ continue
80
+
81
+ # PIL 이미지 로드
82
+ image = Image.open(img_path)
83
+
84
+ # YOLOv8 추론
85
+ results = model(img_path, conf=yolo_conf, verbose=False)
86
+
87
+ # 예측 박스 추출
88
+ yolo_detections = []
89
+ if results and len(results) > 0:
90
+ result = results[0]
91
+ if result.boxes is not None and len(result.boxes) > 0:
92
+ boxes = result.boxes.xyxy.cpu().numpy()
93
+ confs = result.boxes.conf.cpu().numpy()
94
+
95
+ for box, conf in zip(boxes, confs):
96
+ yolo_detections.append({
97
+ 'bbox': box.tolist(),
98
+ 'confidence': float(conf)
99
+ })
100
+
101
+ # Universal Filter 적용
102
+ filtered_detections = apply_universal_filter(yolo_detections, image, threshold=filter_threshold)
103
+
104
+ # GT 박스
105
+ gt_boxes_only = [{'bbox': ann['bbox']} for ann in gt_boxes]
106
+
107
+ # YOLOv8 only 매칭 (비교용)
108
+ yolo_matched_gt = set()
109
+ yolo_matched_pred = set()
110
+
111
+ for i, pred in enumerate(yolo_detections):
112
+ best_iou = 0
113
+ best_gt_idx = -1
114
+ for j, gt in enumerate(gt_boxes_only):
115
+ if j in yolo_matched_gt:
116
+ continue
117
+ iou = calculate_iou(pred['bbox'], gt['bbox'])
118
+ if iou > best_iou:
119
+ best_iou = iou
120
+ best_gt_idx = j
121
+
122
+ if best_iou >= iou_threshold:
123
+ yolo_matched_pred.add(i)
124
+ yolo_matched_gt.add(best_gt_idx)
125
+
126
+ yolo_tp = len(yolo_matched_gt)
127
+ yolo_fp = len(yolo_detections) - len(yolo_matched_pred)
128
+
129
+ # Filtered 매칭
130
+ matched_gt = set()
131
+ matched_pred = set()
132
+
133
+ for i, pred in enumerate(filtered_detections):
134
+ best_iou = 0
135
+ best_gt_idx = -1
136
+
137
+ for j, gt in enumerate(gt_boxes_only):
138
+ if j in matched_gt:
139
+ continue
140
+ iou = calculate_iou(pred['bbox'], gt['bbox'])
141
+ if iou > best_iou:
142
+ best_iou = iou
143
+ best_gt_idx = j
144
+
145
+ if best_iou >= iou_threshold:
146
+ matched_pred.add(i)
147
+ matched_gt.add(best_gt_idx)
148
+
149
+ tp = len(matched_gt)
150
+ fp = len(filtered_detections) - len(matched_pred)
151
+ fn = len(gt_boxes_only) - len(matched_gt)
152
+
153
+ true_positives += tp
154
+ false_positives += fp
155
+ false_negatives += fn
156
+ total_gt += len(gt_boxes_only)
157
+ total_yolo_pred += len(yolo_detections)
158
+ total_filtered_pred += len(filtered_detections)
159
+
160
+ yolo_only_tp += yolo_tp
161
+ yolo_only_fp += yolo_fp
162
+
163
+ results_detail.append({
164
+ 'filename': filename,
165
+ 'gt_count': len(gt_boxes_only),
166
+ 'yolo_count': len(yolo_detections),
167
+ 'filtered_count': len(filtered_detections),
168
+ 'tp': tp,
169
+ 'fp': fp,
170
+ 'fn': fn
171
+ })
172
+
173
+ # 성능 계산
174
+ precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
175
+ recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
176
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
177
+
178
+ # YOLOv8 only 성능
179
+ yolo_precision = yolo_only_tp / (yolo_only_tp + yolo_only_fp) if (yolo_only_tp + yolo_only_fp) > 0 else 0
180
+ yolo_recall = yolo_only_tp / total_gt if total_gt > 0 else 0
181
+ yolo_f1 = 2 * yolo_precision * yolo_recall / (yolo_precision + yolo_recall) if (yolo_precision + yolo_recall) > 0 else 0
182
+
183
+ return {
184
+ 'yolo_with_filter': {
185
+ 'precision': precision,
186
+ 'recall': recall,
187
+ 'f1': f1,
188
+ 'tp': true_positives,
189
+ 'fp': false_positives,
190
+ 'fn': false_negatives,
191
+ 'total_pred': total_filtered_pred
192
+ },
193
+ 'yolo_only': {
194
+ 'precision': yolo_precision,
195
+ 'recall': yolo_recall,
196
+ 'f1': yolo_f1,
197
+ 'tp': yolo_only_tp,
198
+ 'fp': yolo_only_fp,
199
+ 'total_pred': total_yolo_pred
200
+ },
201
+ 'total_gt': total_gt
202
+ }
203
+
204
+ def main():
205
+ print("=" * 60)
206
+ print("🚀 YOLOv8 + Universal Filter 즉시 개선")
207
+ print("=" * 60)
208
+
209
+ # 경로 설정
210
+ yolo_model = "runs/train/shrimp_yolov8n/weights/best.pt"
211
+ gt_file = "ground_truth.json"
212
+ data_base_dir = "data/흰다리새우 실측 데이터_익투스에이아이(주)"
213
+
214
+ if not os.path.exists(yolo_model):
215
+ print(f"\n❌ 모델 파일 없음: {yolo_model}")
216
+ return
217
+
218
+ print(f"\n📁 YOLOv8 모델: {yolo_model}")
219
+ print(f"📁 GT: {gt_file}")
220
+
221
+ # 여러 조합 테스트
222
+ test_configs = [
223
+ {'yolo_conf': 0.01, 'filter_threshold': 70},
224
+ {'yolo_conf': 0.01, 'filter_threshold': 80},
225
+ {'yolo_conf': 0.01, 'filter_threshold': 90},
226
+ {'yolo_conf': 0.001, 'filter_threshold': 90},
227
+ {'yolo_conf': 0.005, 'filter_threshold': 90},
228
+ ]
229
+
230
+ print(f"\n🔍 최적 조합 탐색 중...")
231
+
232
+ best_f1 = 0
233
+ best_config = None
234
+ best_result = None
235
+ all_results = []
236
+
237
+ for config in test_configs:
238
+ result = yolo_with_filter_evaluate(
239
+ yolo_model, gt_file, data_base_dir,
240
+ yolo_conf=config['yolo_conf'],
241
+ filter_threshold=config['filter_threshold']
242
+ )
243
+
244
+ result['config'] = config
245
+ all_results.append(result)
246
+
247
+ filtered = result['yolo_with_filter']
248
+ print(f"\n YOLOv8(conf={config['yolo_conf']}) + Filter({config['filter_threshold']})")
249
+ print(f" P={filtered['precision']:.1%}, R={filtered['recall']:.1%}, F1={filtered['f1']:.1%}")
250
+ print(f" Pred: {result['yolo_only']['total_pred']} → {filtered['total_pred']} (Filter 제거: {result['yolo_only']['total_pred'] - filtered['total_pred']}개)")
251
+
252
+ if filtered['f1'] > best_f1:
253
+ best_f1 = filtered['f1']
254
+ best_config = config
255
+ best_result = result
256
+
257
+ # 최적 결과 출력
258
+ print("\n" + "=" * 60)
259
+ print("✅ 평가 완료!")
260
+ print("=" * 60)
261
+
262
+ print(f"\n🏆 최적 조합:")
263
+ print(f" - YOLOv8 Confidence: {best_config['yolo_conf']}")
264
+ print(f" - Filter Threshold: {best_config['filter_threshold']}")
265
+
266
+ filtered = best_result['yolo_with_filter']
267
+ yolo = best_result['yolo_only']
268
+
269
+ print(f"\n📊 YOLOv8 + Universal Filter 성능:")
270
+ print(f" - Precision: {filtered['precision']:.1%}")
271
+ print(f" - Recall: {filtered['recall']:.1%}")
272
+ print(f" - F1 Score: {filtered['f1']:.1%}")
273
+ print(f"\n - True Positives: {filtered['tp']}")
274
+ print(f" - False Positives: {filtered['fp']}")
275
+ print(f" - False Negatives: {filtered['fn']}")
276
+ print(f" - Total Predictions: {filtered['total_pred']}")
277
+
278
+ print(f"\n📊 YOLOv8 Only 비교:")
279
+ print(f" - Precision: {yolo['precision']:.1%}")
280
+ print(f" - Recall: {yolo['recall']:.1%}")
281
+ print(f" - F1 Score: {yolo['f1']:.1%}")
282
+ print(f" - Total Predictions: {yolo['total_pred']}")
283
+
284
+ print(f"\n🎯 Filter 효과:")
285
+ print(f" - FP 제거: {yolo['fp']} → {filtered['fp']} ({yolo['fp'] - filtered['fp']}개 제거)")
286
+ print(f" - Precision 향상: {yolo['precision']:.1%} → {filtered['precision']:.1%} ({(filtered['precision'] - yolo['precision'])*100:+.1f}%p)")
287
+ print(f" - F1 향상: {yolo['f1']:.1%} → {filtered['f1']:.1%} ({(filtered['f1'] - yolo['f1'])*100:+.1f}%p)")
288
+
289
+ # 전체 시스템 비교
290
+ print(f"\n📊 전체 시스템 비교:")
291
+ print(f"\n RT-DETR + Filter (기존):")
292
+ print(f" - Precision: 44.2%")
293
+ print(f" - Recall: 94.0%")
294
+ print(f" - F1 Score: 56.1%")
295
+
296
+ print(f"\n YOLOv8 + Filter (새로운):")
297
+ print(f" - Precision: {filtered['precision']:.1%}")
298
+ print(f" - Recall: {filtered['recall']:.1%}")
299
+ print(f" - F1 Score: {filtered['f1']:.1%}")
300
+
301
+ # F1 비교
302
+ baseline_f1 = 0.561
303
+ improvement = (filtered['f1'] - baseline_f1) / baseline_f1 * 100
304
+
305
+ if improvement > 0:
306
+ print(f"\n ✅ F1 개선율: {improvement:+.1f}% (YOLOv8+Filter가 더 좋음)")
307
+ else:
308
+ print(f"\n ⚠️ F1 차이: {improvement:+.1f}% (RT-DETR+Filter가 더 좋음)")
309
+
310
+ # 결과 저장
311
+ output_file = "yolo_with_filter_results.json"
312
+ with open(output_file, 'w', encoding='utf-8') as f:
313
+ json.dump({
314
+ 'best_config': best_config,
315
+ 'best_result': best_result,
316
+ 'all_results': all_results,
317
+ 'baseline': {
318
+ 'name': 'RT-DETR + Filter',
319
+ 'precision': 0.442,
320
+ 'recall': 0.940,
321
+ 'f1': 0.561
322
+ }
323
+ }, f, indent=2, ensure_ascii=False)
324
+
325
+ print(f"\n💾 결과 저장: {output_file}")
326
+
327
+ print(f"\n💡 권장 사항:")
328
+ if filtered['f1'] >= baseline_f1:
329
+ print(f" ✅ YOLOv8 + Universal Filter 사용 권장")
330
+ print(f" - 설정: YOLOv8 conf={best_config['yolo_conf']}, Filter={best_config['filter_threshold']}")
331
+ else:
332
+ print(f" ⚠️ RT-DETR + Universal Filter 계속 사용 권장")
333
+ print(f" - YOLOv8+Filter도 준수한 성능이지만 기존이 약간 더 좋음")
334
+
335
+ if __name__ == "__main__":
336
+ main()
test_yolov8_val_results.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m Val Set 결과 이미지 저장
3
+ Confidence = 0.85 사용
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ import json
8
+ import os
9
+
10
+ # 학습된 모델 로드
11
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
12
+ model = YOLO(MODEL_PATH)
13
+
14
+ # 최적 confidence
15
+ CONFIDENCE = 0.85
16
+
17
+ print(f"✅ YOLOv8m 모델 로드: {MODEL_PATH}")
18
+ print(f"🎯 Confidence Threshold: {CONFIDENCE}")
19
+
20
+ # Ground Truth 로드
21
+ with open('ground_truth.json', 'r', encoding='utf-8') as f:
22
+ ground_truth = json.load(f)
23
+
24
+ # Val set 이미지만 필터링
25
+ val_images_dir = set(os.listdir('data/yolo_dataset/images/val'))
26
+ gt_val_only = {}
27
+
28
+ for img_name, gts in ground_truth.items():
29
+ if not gts:
30
+ continue
31
+ base_name = img_name.replace('-1.jpg', '.jpg')
32
+ if img_name in val_images_dir or base_name in val_images_dir:
33
+ gt_val_only[img_name] = gts
34
+
35
+ print(f"📁 Val set GT 이미지: {len(gt_val_only)}장")
36
+
37
+ # 출력 디렉토리
38
+ output_dir = "test_results_yolov8m_val"
39
+ os.makedirs(output_dir, exist_ok=True)
40
+
41
+ # 폰트 설정
42
+ try:
43
+ font = ImageFont.truetype("malgun.ttf", 24)
44
+ font_small = ImageFont.truetype("malgun.ttf", 18)
45
+ font_tiny = ImageFont.truetype("malgun.ttf", 14)
46
+ except:
47
+ font = ImageFont.load_default()
48
+ font_small = ImageFont.load_default()
49
+ font_tiny = ImageFont.load_default()
50
+
51
+ def calculate_iou(box1, box2):
52
+ """IoU 계산"""
53
+ x1_min, y1_min, x1_max, y1_max = box1
54
+ x2_min, y2_min, x2_max, y2_max = box2
55
+
56
+ inter_x_min = max(x1_min, x2_min)
57
+ inter_y_min = max(y1_min, y2_min)
58
+ inter_x_max = min(x1_max, x2_max)
59
+ inter_y_max = min(y1_max, y2_max)
60
+
61
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
62
+ return 0.0
63
+
64
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
65
+ box1_area = (x1_max - x1_min) * (y1_max - y1_min)
66
+ box2_area = (x2_max - x2_min) * (y2_max - y2_min)
67
+ union_area = box1_area + box2_area - inter_area
68
+
69
+ return inter_area / union_area if union_area > 0 else 0.0
70
+
71
+ # 통계
72
+ total_gt = 0
73
+ total_tp = 0
74
+ total_fp = 0
75
+ total_fn = 0
76
+
77
+ print("-" * 60)
78
+
79
+ # 각 이미지 처리
80
+ for idx, (img_name, gt_boxes) in enumerate(sorted(gt_val_only.items()), 1):
81
+ print(f"\n[{idx}/{len(gt_val_only)}] {img_name}")
82
+
83
+ # 이미지 경로
84
+ img_path = f"data/yolo_dataset/images/val/{img_name}"
85
+ base_name = img_name.replace('-1.jpg', '.jpg')
86
+ if not os.path.exists(img_path):
87
+ img_path = f"data/yolo_dataset/images/val/{base_name}"
88
+
89
+ if not os.path.exists(img_path):
90
+ print(f" ⚠️ 이미지를 찾을 수 없음: {img_path}")
91
+ continue
92
+
93
+ # 이미지 로드
94
+ image = Image.open(img_path)
95
+ print(f" 📐 크기: {image.size}")
96
+
97
+ # YOLOv8 검출
98
+ results = model.predict(
99
+ source=image,
100
+ conf=CONFIDENCE,
101
+ iou=0.7,
102
+ device=0,
103
+ verbose=False
104
+ )
105
+
106
+ result = results[0]
107
+ boxes = result.boxes
108
+
109
+ predictions = []
110
+ if boxes is not None and len(boxes) > 0:
111
+ for box in boxes:
112
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
113
+ confidence = box.conf[0].cpu().item()
114
+ predictions.append({
115
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
116
+ 'confidence': confidence
117
+ })
118
+
119
+ print(f" 🦐 검출: {len(predictions)}개 (GT: {len(gt_boxes)}개)")
120
+
121
+ # GT와 매칭
122
+ matched_gt = set()
123
+ matched_pred = set()
124
+ tp = 0
125
+ fp = 0
126
+
127
+ for pred_idx, pred in enumerate(predictions):
128
+ best_iou = 0
129
+ best_gt_idx = -1
130
+
131
+ for gt_idx, gt in enumerate(gt_boxes):
132
+ if gt_idx in matched_gt:
133
+ continue
134
+
135
+ iou = calculate_iou(pred['bbox'], gt['bbox'])
136
+ if iou > best_iou:
137
+ best_iou = iou
138
+ best_gt_idx = gt_idx
139
+
140
+ if best_iou >= 0.5:
141
+ tp += 1
142
+ matched_gt.add(best_gt_idx)
143
+ matched_pred.add(pred_idx)
144
+ else:
145
+ fp += 1
146
+
147
+ fn = len(gt_boxes) - len(matched_gt)
148
+
149
+ total_gt += len(gt_boxes)
150
+ total_tp += tp
151
+ total_fp += fp
152
+ total_fn += fn
153
+
154
+ print(f" 📊 TP={tp}, FP={fp}, FN={fn}")
155
+
156
+ # 결과 이미지 그리기
157
+ result_image = image.copy()
158
+ draw = ImageDraw.Draw(result_image)
159
+
160
+ # Ground Truth (파란색, 점선 효과)
161
+ for gt_idx, gt in enumerate(gt_boxes):
162
+ x1, y1, x2, y2 = gt['bbox']
163
+
164
+ # 파란색 박스 (얇게)
165
+ for offset in range(0, 4, 2):
166
+ draw.rectangle([x1+offset, y1+offset, x2-offset, y2-offset], outline="blue", width=2)
167
+
168
+ # GT 라벨
169
+ label = f"GT#{gt_idx+1}"
170
+ bbox_label = draw.textbbox((x1, y1 - 30), label, font=font_tiny)
171
+ draw.rectangle(bbox_label, fill="blue")
172
+ draw.text((x1, y1 - 30), label, fill="white", font=font_tiny)
173
+
174
+ # 매칭 여부 표시
175
+ if gt_idx not in matched_gt:
176
+ # FN (놓침)
177
+ draw.text((x1 + 10, y1 + 10), "MISSED", fill="red", font=font_small)
178
+
179
+ # Predictions
180
+ for pred_idx, pred in enumerate(predictions):
181
+ x1, y1, x2, y2 = pred['bbox']
182
+ conf = pred['confidence']
183
+
184
+ if pred_idx in matched_pred:
185
+ # TP (올바른 검출) - 녹색
186
+ color = "lime"
187
+ label = f"✓ TP #{pred_idx+1} | {conf:.0%}"
188
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=10)
189
+ else:
190
+ # FP (잘못된 검출) - 빨간색
191
+ color = "red"
192
+ label = f"✗ FP #{pred_idx+1} | {conf:.0%}"
193
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=8)
194
+
195
+ # 라벨
196
+ bbox_label = draw.textbbox((x1, y2 + 5), label, font=font_tiny)
197
+ draw.rectangle(bbox_label, fill=color)
198
+ draw.text((x1, y2 + 5), label, fill="black" if color == "lime" else "white", font=font_tiny)
199
+
200
+ # 통계 텍스트 (상단)
201
+ stats_text = f"GT:{len(gt_boxes)} | Pred:{len(predictions)} | TP:{tp} FP:{fp} FN:{fn}"
202
+ draw.rectangle([10, 10, 800, 50], fill="black")
203
+ draw.text((15, 15), stats_text, fill="white", font=font_small)
204
+
205
+ # 이미지 정보 (하단)
206
+ info_text = f"Confidence: {CONFIDENCE} | {img_name}"
207
+ img_width, img_height = image.size
208
+ draw.rectangle([10, img_height - 50, 800, img_height - 10], fill="black")
209
+ draw.text((15, img_height - 45), info_text, fill="yellow", font=font_tiny)
210
+
211
+ # 저장
212
+ output_path = os.path.join(output_dir, f"result_{base_name}")
213
+ result_image.save(output_path, quality=95)
214
+ print(f" ✅ 저장: {output_path}")
215
+
216
+ # 최종 통계
217
+ print("\n" + "=" * 60)
218
+ print("📊 전체 결과 (Val Set 10장):")
219
+ print("=" * 60)
220
+ print(f"총 GT: {total_gt}개")
221
+ print(f"TP: {total_tp}개 (올바른 검출)")
222
+ print(f"FP: {total_fp}개 (잘못된 검출)")
223
+ print(f"FN: {total_fn}개 (놓친 GT)")
224
+
225
+ precision = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0
226
+ recall = total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0
227
+ f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
228
+
229
+ print(f"\nPrecision: {precision:.1%}")
230
+ print(f"Recall: {recall:.1%}")
231
+ print(f"F1 Score: {f1:.1%}")
232
+
233
+ print(f"\n📁 결과 저장: {output_dir}/")
234
+ print("=" * 60)
test_yolov8m_trained.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m 학습 모델 테스트 스크립트
3
+ 학습된 모델로 테스트 이미지 검출 및 결과 저장
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ import os
8
+ import glob
9
+
10
+ # 학습된 모델 로드
11
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
12
+ model = YOLO(MODEL_PATH)
13
+
14
+ print(f"✅ YOLOv8m 모델 로드 완료: {MODEL_PATH}")
15
+
16
+ # 테스트 이미지 경로
17
+ test_images_dir = "data/yolo_dataset/images/val"
18
+ output_dir = "test_results_yolov8m"
19
+
20
+ # 출력 디렉토리 생성
21
+ os.makedirs(output_dir, exist_ok=True)
22
+
23
+ # 테스트 이미지 찾기
24
+ test_images = sorted(glob.glob(os.path.join(test_images_dir, "*.jpg")))
25
+
26
+ if not test_images:
27
+ print(f"❌ 테스트 이미지를 찾을 수 없습니다: {test_images_dir}")
28
+ exit(1)
29
+
30
+ print(f"📁 테스트 이미지: {len(test_images)}장")
31
+ print(f"📂 결과 저장 경로: {output_dir}/")
32
+ print("-" * 60)
33
+
34
+ # 폰트 설정
35
+ try:
36
+ font = ImageFont.truetype("malgun.ttf", 20)
37
+ font_small = ImageFont.truetype("malgun.ttf", 14)
38
+ except:
39
+ font = ImageFont.load_default()
40
+ font_small = ImageFont.load_default()
41
+
42
+ # 각 이미지 테스트
43
+ total_detections = 0
44
+ for idx, img_path in enumerate(test_images, 1):
45
+ img_name = os.path.basename(img_path)
46
+ print(f"\n[{idx}/{len(test_images)}] {img_name}")
47
+
48
+ # 이미지 로드
49
+ image = Image.open(img_path)
50
+ print(f" 📐 이미지 크기: {image.size}")
51
+
52
+ # YOLOv8 검출 (confidence threshold=0.065)
53
+ results = model.predict(
54
+ source=image,
55
+ conf=0.065,
56
+ iou=0.7,
57
+ device=0, # GPU 사용
58
+ verbose=False
59
+ )
60
+
61
+ # 결과 파싱
62
+ result = results[0]
63
+ boxes = result.boxes
64
+
65
+ detections = []
66
+ if boxes is not None and len(boxes) > 0:
67
+ for box in boxes:
68
+ # 바운딩 박스 좌표
69
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
70
+ confidence = box.conf[0].cpu().item()
71
+ cls = int(box.cls[0].cpu().item())
72
+
73
+ detections.append({
74
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
75
+ 'confidence': confidence,
76
+ 'class': cls
77
+ })
78
+
79
+ print(f" 🦐 검출: {len(detections)}개")
80
+ total_detections += len(detections)
81
+
82
+ # 결과 이미지 그리기
83
+ result_image = image.copy()
84
+ draw = ImageDraw.Draw(result_image)
85
+
86
+ for i, det in enumerate(detections, 1):
87
+ x1, y1, x2, y2 = det['bbox']
88
+ conf = det['confidence']
89
+
90
+ # 바운딩 박스 (녹색, 굵게)
91
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=8)
92
+
93
+ # 라벨
94
+ label = f"#{i} | {conf:.2%}"
95
+ bbox = draw.textbbox((x1, y1 - 25), label, font=font_small)
96
+ draw.rectangle(bbox, fill="lime")
97
+ draw.text((x1, y1 - 25), label, fill="black", font=font_small)
98
+
99
+ print(f" #{i}: conf={conf:.2%}, bbox=[{x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}]")
100
+
101
+ # 결과 저장
102
+ output_path = os.path.join(output_dir, f"result_{img_name}")
103
+ result_image.save(output_path)
104
+ print(f" ✅ 저장: {output_path}")
105
+
106
+ print("\n" + "=" * 60)
107
+ print(f"📊 전체 결과:")
108
+ print(f" - 테스트 이미지: {len(test_images)}장")
109
+ print(f" - 총 검출: {total_detections}개")
110
+ print(f" - 평균: {total_detections / len(test_images):.1f}개/이미지")
111
+ print(f" - 결과 저장: {output_dir}/")
112
+ print("=" * 60)
test_yolov8m_unseen.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m Unseen Test Set 평가
3
+ 251010, 251017 폴더의 완전히 새로운 이미지 20개로 테스트
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ import os
8
+ import glob
9
+
10
+ # 학습된 모델 로드
11
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
12
+ model = YOLO(MODEL_PATH)
13
+
14
+ # 최적 confidence
15
+ CONFIDENCE = 0.85
16
+
17
+ print(f"✅ YOLOv8m 모델 로드: {MODEL_PATH}")
18
+ print(f"🎯 Confidence Threshold: {CONFIDENCE}")
19
+
20
+ # Unseen test 이미지 경로
21
+ test_folders = [
22
+ "data/흰다리새우 실측 데이터_익투스에이아이(주)/251010",
23
+ "data/흰다리새우 실측 데이터_익투스에이아이(주)/251017"
24
+ ]
25
+
26
+ test_images = []
27
+ for folder in test_folders:
28
+ # -1.jpg 제외, 원본 이미지만
29
+ images = sorted(glob.glob(os.path.join(folder, "*_[0-9][0-9].jpg")))
30
+ test_images.extend(images)
31
+
32
+ print(f"📁 Unseen test 이미지: {len(test_images)}장")
33
+ print("-" * 60)
34
+
35
+ # 출력 디렉토리
36
+ output_dir = "test_results_unseen"
37
+ os.makedirs(output_dir, exist_ok=True)
38
+
39
+ # 폰트 설정
40
+ try:
41
+ font = ImageFont.truetype("malgun.ttf", 24)
42
+ font_small = ImageFont.truetype("malgun.ttf", 18)
43
+ font_tiny = ImageFont.truetype("malgun.ttf", 14)
44
+ except:
45
+ font = ImageFont.load_default()
46
+ font_small = ImageFont.load_default()
47
+ font_tiny = ImageFont.load_default()
48
+
49
+ # 통계
50
+ total_detections = 0
51
+ images_with_detections = 0
52
+ images_without_detections = 0
53
+
54
+ print("\n🔍 Unseen Test 검출 시작...\n")
55
+
56
+ # 각 이미지 처리
57
+ for idx, img_path in enumerate(test_images, 1):
58
+ img_name = os.path.basename(img_path)
59
+ folder_name = os.path.basename(os.path.dirname(img_path))
60
+
61
+ print(f"[{idx}/{len(test_images)}] {folder_name}/{img_name}")
62
+
63
+ # 이미지 로드
64
+ image = Image.open(img_path)
65
+ print(f" 📐 크기: {image.size}")
66
+
67
+ # YOLOv8 검출
68
+ results = model.predict(
69
+ source=image,
70
+ conf=CONFIDENCE,
71
+ iou=0.7,
72
+ device=0,
73
+ verbose=False
74
+ )
75
+
76
+ result = results[0]
77
+ boxes = result.boxes
78
+
79
+ predictions = []
80
+ if boxes is not None and len(boxes) > 0:
81
+ for box in boxes:
82
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
83
+ confidence = box.conf[0].cpu().item()
84
+ predictions.append({
85
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
86
+ 'confidence': confidence
87
+ })
88
+
89
+ num_detections = len(predictions)
90
+ total_detections += num_detections
91
+
92
+ if num_detections > 0:
93
+ images_with_detections += 1
94
+ print(f" 🦐 검출: {num_detections}개")
95
+ else:
96
+ images_without_detections += 1
97
+ print(f" ⚪ 검출 없음")
98
+
99
+ # 결과 이미지 그리기
100
+ result_image = image.copy()
101
+ draw = ImageDraw.Draw(result_image)
102
+
103
+ # Predictions (녹색 박스)
104
+ for pred_idx, pred in enumerate(predictions, 1):
105
+ x1, y1, x2, y2 = pred['bbox']
106
+ conf = pred['confidence']
107
+
108
+ # 녹색 박스
109
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=10)
110
+
111
+ # 라벨
112
+ label = f"#{pred_idx} | {conf:.0%}"
113
+ bbox_label = draw.textbbox((x1, y2 + 5), label, font=font_small)
114
+ draw.rectangle(bbox_label, fill="lime")
115
+ draw.text((x1, y2 + 5), label, fill="black", font=font_small)
116
+
117
+ # 통계 텍스트 (상단)
118
+ stats_text = f"Detections: {num_detections} | Confidence: {CONFIDENCE}"
119
+ draw.rectangle([10, 10, 800, 50], fill="black")
120
+ draw.text((15, 15), stats_text, fill="white", font=font_small)
121
+
122
+ # 이미지 정보 (하단)
123
+ info_text = f"{folder_name}/{img_name}"
124
+ img_width, img_height = image.size
125
+ draw.rectangle([10, img_height - 50, 800, img_height - 10], fill="black")
126
+ draw.text((15, img_height - 45), info_text, fill="yellow", font=font_tiny)
127
+
128
+ # 저장
129
+ output_path = os.path.join(output_dir, f"result_{folder_name}_{img_name}")
130
+ result_image.save(output_path, quality=95)
131
+ print(f" ✅ 저장: {output_path}")
132
+
133
+ # 최종 통계
134
+ print("\n" + "=" * 60)
135
+ print("📊 Unseen Test Set 결과:")
136
+ print("=" * 60)
137
+ print(f"총 이미지: {len(test_images)}장")
138
+ print(f"총 검출: {total_detections}개")
139
+ print(f"평균 검출: {total_detections / len(test_images):.1f}개/이미지")
140
+ print(f"\n검출 있음: {images_with_detections}장 ({images_with_detections/len(test_images)*100:.1f}%)")
141
+ print(f"검출 없음: {images_without_detections}장 ({images_without_detections/len(test_images)*100:.1f}%)")
142
+
143
+ print(f"\n📁 결과 저장: {output_dir}/")
144
+ print("=" * 60)
test_yolov8m_with_filter.py ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YOLOv8m + Universal Filter 테스트
3
+ 전체 데이터셋으로 테스트 및 필터링 성능 개선
4
+ """
5
+ from ultralytics import YOLO
6
+ from PIL import Image, ImageDraw, ImageFont
7
+ import os
8
+ import glob
9
+ import numpy as np
10
+ import json
11
+
12
+ # OpenCV for filter functions
13
+ import cv2
14
+
15
+ def calculate_morphological_features(bbox, img_size):
16
+ """형태학적 특징 계산"""
17
+ x1, y1, x2, y2 = bbox
18
+ width = x2 - x1
19
+ height = y2 - y1
20
+ area = width * height
21
+
22
+ aspect_ratio = height / width if width > 0 else 0
23
+
24
+ # Compactness (0~1, 1에 가까울수록 정사각형)
25
+ perimeter = 2 * (width + height)
26
+ compactness = (4 * np.pi * area) / (perimeter ** 2) if perimeter > 0 else 0
27
+
28
+ # 이미지 내 비율
29
+ img_area = img_size[0] * img_size[1]
30
+ area_ratio = area / img_area if img_area > 0 else 0
31
+
32
+ return {
33
+ 'width': width,
34
+ 'height': height,
35
+ 'area': area,
36
+ 'aspect_ratio': aspect_ratio,
37
+ 'compactness': compactness,
38
+ 'area_ratio': area_ratio
39
+ }
40
+
41
+ def calculate_visual_features(image_pil, bbox):
42
+ """시각적 특징 계산"""
43
+ image_cv = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR)
44
+ x1, y1, x2, y2 = [int(v) for v in bbox]
45
+
46
+ roi = image_cv[y1:y2, x1:x2]
47
+ if roi.size == 0:
48
+ return {'hue': 100, 'saturation': 255, 'color_std': 255}
49
+
50
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
51
+
52
+ return {
53
+ 'hue': np.mean(hsv[:, :, 0]),
54
+ 'saturation': np.mean(hsv[:, :, 1]),
55
+ 'color_std': np.std(hsv[:, :, 0])
56
+ }
57
+
58
+ def calculate_iou_simple(bbox1, bbox2):
59
+ """간단한 IoU 계산"""
60
+ x1_min, y1_min, x1_max, y1_max = bbox1
61
+ x2_min, y2_min, x2_max, y2_max = bbox2
62
+
63
+ inter_x_min = max(x1_min, x2_min)
64
+ inter_y_min = max(y1_min, y2_min)
65
+ inter_x_max = min(x1_max, x2_max)
66
+ inter_y_max = min(y1_max, y2_max)
67
+
68
+ if inter_x_max < inter_x_min or inter_y_max < inter_y_min:
69
+ return 0.0
70
+
71
+ inter_area = (inter_x_max - inter_x_min) * (inter_y_max - inter_y_min)
72
+
73
+ bbox1_area = (x1_max - x1_min) * (y1_max - y1_min)
74
+ bbox2_area = (x2_max - x2_min) * (y2_max - y2_min)
75
+ union_area = bbox1_area + bbox2_area - inter_area
76
+
77
+ return inter_area / union_area if union_area > 0 else 0.0
78
+
79
+ def apply_universal_filter(detections, image, threshold=90):
80
+ """범용 새우 필터 적용"""
81
+ img_size = image.size
82
+ filtered = []
83
+
84
+ for det in detections:
85
+ bbox = det['bbox']
86
+ morph = calculate_morphological_features(bbox, img_size)
87
+ visual = calculate_visual_features(image, bbox)
88
+
89
+ score = 0
90
+ reasons = []
91
+
92
+ # Aspect ratio (4:1 ~ 9:1)
93
+ if 4.0 <= morph['aspect_ratio'] <= 9.0:
94
+ score += 25
95
+ reasons.append(f"✓ 종횡비 {morph['aspect_ratio']:.1f}")
96
+ elif 3.0 <= morph['aspect_ratio'] < 4.0 or 9.0 < morph['aspect_ratio'] <= 10.0:
97
+ score += 12
98
+ reasons.append(f"△ 종횡비 {morph['aspect_ratio']:.1f}")
99
+ else:
100
+ score -= 5
101
+ reasons.append(f"✗ 종횡비 {morph['aspect_ratio']:.1f}")
102
+
103
+ # Compactness (세장도)
104
+ if morph['compactness'] < 0.40:
105
+ score += 30
106
+ reasons.append(f"✓ 세장도 {morph['compactness']:.2f}")
107
+ elif 0.40 <= morph['compactness'] < 0.50:
108
+ score += 15
109
+ reasons.append(f"△ 세장도 {morph['compactness']:.2f}")
110
+ else:
111
+ score -= 20
112
+ reasons.append(f"✗ 세장도 {morph['compactness']:.2f}")
113
+
114
+ # Area
115
+ abs_area = morph['width'] * morph['height']
116
+ if 50000 <= abs_area <= 500000:
117
+ score += 35
118
+ reasons.append(f"✓ 면적 {abs_area/1000:.0f}K")
119
+ elif 500000 < abs_area <= 800000:
120
+ score -= 10
121
+ reasons.append(f"△ 면적 {abs_area/1000:.0f}K")
122
+ elif abs_area > 800000:
123
+ score -= 30
124
+ reasons.append(f"✗ 면적 {abs_area/1000:.0f}K (너무큼)")
125
+ else:
126
+ score -= 10
127
+ reasons.append(f"✗ 면적 {abs_area/1000:.0f}K (너무작음)")
128
+
129
+ # Hue (색상)
130
+ hue = visual['hue']
131
+ if hue < 40 or hue > 130:
132
+ score += 10
133
+ reasons.append(f"✓ 색상 {hue:.0f}")
134
+ elif 90 <= hue <= 130:
135
+ score -= 5
136
+ reasons.append(f"✗ 색상 {hue:.0f} (배경)")
137
+ else:
138
+ reasons.append(f"△ 색상 {hue:.0f}")
139
+
140
+ # Saturation (채도)
141
+ if visual['saturation'] < 85:
142
+ score += 20
143
+ reasons.append(f"✓ 채도 {visual['saturation']:.0f}")
144
+ elif 85 <= visual['saturation'] < 120:
145
+ score += 5
146
+ reasons.append(f"△ 채도 {visual['saturation']:.0f}")
147
+ else:
148
+ score -= 15
149
+ reasons.append(f"✗ 채도 {visual['saturation']:.0f} (높음)")
150
+
151
+ # Color consistency
152
+ if visual['color_std'] < 50:
153
+ score += 15
154
+ reasons.append(f"✓ 색상일관성 {visual['color_std']:.1f}")
155
+ elif 50 <= visual['color_std'] < 80:
156
+ score += 5
157
+ reasons.append(f"△ 색상일관성 {visual['color_std']:.1f}")
158
+ else:
159
+ score -= 10
160
+ reasons.append(f"✗ 색상일관성 {visual['color_std']:.1f}")
161
+
162
+ # YOLOv8 confidence
163
+ score += det['confidence'] * 15
164
+
165
+ det['filter_score'] = score
166
+ det['filter_reasons'] = reasons
167
+ det['morph_features'] = morph
168
+ det['visual_features'] = visual
169
+
170
+ if score >= threshold:
171
+ filtered.append(det)
172
+
173
+ # 점수 순으로 정렬
174
+ filtered.sort(key=lambda x: x['filter_score'], reverse=True)
175
+
176
+ # NMS (Non-Maximum Suppression)
177
+ filtered_nms = []
178
+ for det in filtered:
179
+ is_duplicate = False
180
+ for kept_det in filtered_nms:
181
+ iou = calculate_iou_simple(det['bbox'], kept_det['bbox'])
182
+ if iou > 0.5:
183
+ is_duplicate = True
184
+ break
185
+
186
+ if not is_duplicate:
187
+ filtered_nms.append(det)
188
+
189
+ return filtered_nms
190
+
191
+ # 학습된 모델 로드
192
+ MODEL_PATH = "runs/train/yolov8m_shrimp2/weights/best.pt"
193
+ model = YOLO(MODEL_PATH)
194
+
195
+ print(f"✅ YOLOv8m 모델 로드 완료: {MODEL_PATH}")
196
+
197
+ # 전체 데이터셋 테스트 (train + val)
198
+ test_images = []
199
+ for split in ['train', 'val']:
200
+ split_dir = f"data/yolo_dataset/images/{split}"
201
+ if os.path.exists(split_dir):
202
+ test_images.extend(sorted(glob.glob(os.path.join(split_dir, "*.jpg"))))
203
+
204
+ output_dir = "test_results_yolov8m_filtered"
205
+ os.makedirs(output_dir, exist_ok=True)
206
+
207
+ print(f"📁 테스트 이미지: {len(test_images)}장")
208
+ print(f"📂 결과 저장 경로: {output_dir}/")
209
+ print("-" * 60)
210
+
211
+ # 폰트 설정
212
+ try:
213
+ font = ImageFont.truetype("malgun.ttf", 20)
214
+ font_small = ImageFont.truetype("malgun.ttf", 14)
215
+ font_tiny = ImageFont.truetype("malgun.ttf", 12)
216
+ except:
217
+ font = ImageFont.load_default()
218
+ font_small = ImageFont.load_default()
219
+ font_tiny = ImageFont.load_default()
220
+
221
+ # 통계 변수
222
+ stats_no_filter = {'total': 0, 'per_image': []}
223
+ stats_filtered = {'total': 0, 'per_image': []}
224
+ filter_thresholds = [50, 60, 70, 80, 90] # 다양한 임계값 테스트
225
+ stats_by_threshold = {th: {'total': 0, 'per_image': []} for th in filter_thresholds}
226
+
227
+ # 각 이미지 테스트
228
+ for idx, img_path in enumerate(test_images, 1):
229
+ img_name = os.path.basename(img_path)
230
+
231
+ if idx % 10 == 0 or idx == 1:
232
+ print(f"\n[{idx}/{len(test_images)}] {img_name}")
233
+
234
+ # 이미지 로드
235
+ image = Image.open(img_path)
236
+
237
+ # YOLOv8 검출 (confidence threshold=0.065)
238
+ results = model.predict(
239
+ source=image,
240
+ conf=0.065,
241
+ iou=0.7,
242
+ device=0,
243
+ verbose=False
244
+ )
245
+
246
+ # 결과 파싱
247
+ result = results[0]
248
+ boxes = result.boxes
249
+
250
+ detections_raw = []
251
+ if boxes is not None and len(boxes) > 0:
252
+ for box in boxes:
253
+ x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
254
+ confidence = box.conf[0].cpu().item()
255
+
256
+ detections_raw.append({
257
+ 'bbox': [float(x1), float(y1), float(x2), float(y2)],
258
+ 'confidence': confidence
259
+ })
260
+
261
+ stats_no_filter['total'] += len(detections_raw)
262
+ stats_no_filter['per_image'].append(len(detections_raw))
263
+
264
+ # Universal Filter 적용
265
+ detections_scored = apply_universal_filter(detections_raw, image, threshold=0)
266
+
267
+ # 다양한 임계값으로 필터링 테스트
268
+ for threshold in filter_thresholds:
269
+ filtered = [det for det in detections_scored if det['filter_score'] >= threshold]
270
+ stats_by_threshold[threshold]['total'] += len(filtered)
271
+ stats_by_threshold[threshold]['per_image'].append(len(filtered))
272
+
273
+ # 기본 임계값 90 사용
274
+ detections_filtered = [det for det in detections_scored if det['filter_score'] >= 90]
275
+ stats_filtered['total'] += len(detections_filtered)
276
+ stats_filtered['per_image'].append(len(detections_filtered))
277
+
278
+ # 처음 10개 이미지만 결과 저장
279
+ if idx <= 10:
280
+ # 결과 이미지 그리기
281
+ result_image = image.copy()
282
+ draw = ImageDraw.Draw(result_image)
283
+
284
+ # 필터링된 검출 (녹색)
285
+ for i, det in enumerate(detections_filtered, 1):
286
+ x1, y1, x2, y2 = det['bbox']
287
+ score = det['filter_score']
288
+ conf = det['confidence']
289
+
290
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=8)
291
+ label = f"#{i} | F:{score:.0f} C:{conf:.0%}"
292
+ bbox_label = draw.textbbox((x1, y1 - 25), label, font=font_tiny)
293
+ draw.rectangle(bbox_label, fill="lime")
294
+ draw.text((x1, y1 - 25), label, fill="black", font=font_tiny)
295
+
296
+ # 제거된 검출 (빨간색, 반투명)
297
+ for det in detections_raw:
298
+ if det not in [d for d in detections_filtered]:
299
+ x1, y1, x2, y2 = det['bbox']
300
+ # 필터 점수 찾기
301
+ scored_det = next((d for d in detections_scored if d['bbox'] == det['bbox']), None)
302
+ if scored_det:
303
+ score = scored_det['filter_score']
304
+ draw.rectangle([x1, y1, x2, y2], outline="red", width=4)
305
+ label = f"X:{score:.0f}"
306
+ bbox_label = draw.textbbox((x1, y1 - 20), label, font=font_tiny)
307
+ draw.rectangle(bbox_label, fill="red")
308
+ draw.text((x1, y1 - 20), label, fill="white", font=font_tiny)
309
+
310
+ # 통계 텍스트 추가
311
+ info_text = f"Raw: {len(detections_raw)} | Filtered (90): {len(detections_filtered)}"
312
+ draw.text((10, 10), info_text, fill="yellow", font=font)
313
+
314
+ # 결과 저장
315
+ output_path = os.path.join(output_dir, f"result_{img_name}")
316
+ result_image.save(output_path)
317
+
318
+ if idx % 10 == 0 or idx == 1:
319
+ print(f" Raw: {len(detections_raw)}, Filtered: {len(detections_filtered)}")
320
+ print(f" ✅ 저장: {output_path}")
321
+
322
+ # 최종 통계 출력
323
+ print("\n" + "=" * 60)
324
+ print("📊 전체 결과 (필터링 전후 비교):")
325
+ print("=" * 60)
326
+ print(f"\n1️⃣ 필터링 전 (Raw YOLOv8):")
327
+ print(f" - 총 검출: {stats_no_filter['total']}개")
328
+ print(f" - 평균: {stats_no_filter['total'] / len(test_images):.1f}개/이미지")
329
+
330
+ print(f"\n2️⃣ 필터링 후 성능 비교:")
331
+ for threshold in filter_thresholds:
332
+ total = stats_by_threshold[threshold]['total']
333
+ avg = total / len(test_images)
334
+ reduction = (1 - total / stats_no_filter['total']) * 100 if stats_no_filter['total'] > 0 else 0
335
+ print(f" Threshold {threshold}: {total}개 (평균 {avg:.1f}/이미지, -{reduction:.1f}% 감소)")
336
+
337
+ print(f"\n3️⃣ 권장 설정 (Threshold 90):")
338
+ print(f" - 총 검출: {stats_filtered['total']}개")
339
+ print(f" - 평균: {stats_filtered['total'] / len(test_images):.1f}개/이미지")
340
+ reduction = (1 - stats_filtered['total'] / stats_no_filter['total']) * 100 if stats_no_filter['total'] > 0 else 0
341
+ print(f" - False Positive 감소: {reduction:.1f}%")
342
+
343
+ print(f"\n📁 결과 이미지 저장: {output_dir}/ (처음 10장)")
344
+ print("=" * 60)
345
+
346
+ # 통계를 JSON으로 저장
347
+ stats_summary = {
348
+ 'total_images': len(test_images),
349
+ 'no_filter': {
350
+ 'total': stats_no_filter['total'],
351
+ 'average': stats_no_filter['total'] / len(test_images)
352
+ },
353
+ 'by_threshold': {}
354
+ }
355
+
356
+ for threshold in filter_thresholds:
357
+ total = stats_by_threshold[threshold]['total']
358
+ stats_summary['by_threshold'][threshold] = {
359
+ 'total': total,
360
+ 'average': total / len(test_images),
361
+ 'reduction_pct': (1 - total / stats_no_filter['total']) * 100 if stats_no_filter['total'] > 0 else 0
362
+ }
363
+
364
+ with open(os.path.join(output_dir, 'filter_statistics.json'), 'w', encoding='utf-8') as f:
365
+ json.dump(stats_summary, f, indent=2, ensure_ascii=False)
366
+
367
+ print(f"\n💾 통계 저장: {output_dir}/filter_statistics.json")
validate_ground_truth.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Ground Truth 검증 스크립트
4
+ 라벨링 데이터의 품질과 일관성을 확인
5
+ """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
+
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ def validate_ground_truth(gt_file="ground_truth.json"):
14
+ """Ground Truth 데이터 검증"""
15
+
16
+ print("=" * 60)
17
+ print("🔍 Ground Truth 검증")
18
+ print("=" * 60)
19
+
20
+ # 파일 존재 확인
21
+ if not os.path.exists(gt_file):
22
+ print(f"❌ 파일이 존재하지 않습니다: {gt_file}")
23
+ return
24
+
25
+ # JSON 로드
26
+ with open(gt_file, 'r', encoding='utf-8') as f:
27
+ data = json.load(f)
28
+
29
+ # 기본 통계
30
+ total_images = len(data)
31
+ labeled_images = sum(1 for v in data.values() if len(v) > 0)
32
+ empty_images = sum(1 for v in data.values() if len(v) == 0)
33
+ total_boxes = sum(len(v) for v in data.values())
34
+
35
+ print(f"\n📊 기본 통계")
36
+ print(f"{'─' * 60}")
37
+ print(f"총 이미지: {total_images}개")
38
+ print(f"라벨링된 이미지: {labeled_images}개 ({labeled_images/total_images*100:.1f}%)")
39
+ print(f"빈 이미지: {empty_images}개 ({empty_images/total_images*100:.1f}%)")
40
+ print(f"총 바운딩 박스: {total_boxes}개")
41
+ if labeled_images > 0:
42
+ print(f"이미지당 평균: {total_boxes/labeled_images:.2f}개")
43
+
44
+ # 폴더별 통계
45
+ folders = {}
46
+ for filename, boxes in data.items():
47
+ if boxes:
48
+ folder = boxes[0].get('folder', 'unknown')
49
+ if folder not in folders:
50
+ folders[folder] = {'images': 0, 'boxes': 0}
51
+ folders[folder]['images'] += 1
52
+ folders[folder]['boxes'] += len(boxes)
53
+
54
+ if folders:
55
+ print(f"\n📁 폴더별 통계")
56
+ print(f"{'─' * 60}")
57
+ for folder, stats in sorted(folders.items()):
58
+ print(f"{folder}: {stats['images']}장, {stats['boxes']}박스 (평균 {stats['boxes']/stats['images']:.1f}개/이미지)")
59
+
60
+ # 신뢰도 분석
61
+ confidences = []
62
+ for boxes in data.values():
63
+ for box in boxes:
64
+ if 'confidence' in box:
65
+ confidences.append(box['confidence'])
66
+
67
+ if confidences:
68
+ print(f"\n🎯 신뢰도 분석")
69
+ print(f"{'─' * 60}")
70
+ print(f"평균 신뢰도: {sum(confidences)/len(confidences):.3f}")
71
+ print(f"최소 신뢰도: {min(confidences):.3f}")
72
+ print(f"최대 신뢰도: {max(confidences):.3f}")
73
+
74
+ # 신뢰도 분포
75
+ low = sum(1 for c in confidences if c < 0.2)
76
+ mid = sum(1 for c in confidences if 0.2 <= c < 0.4)
77
+ high = sum(1 for c in confidences if c >= 0.4)
78
+ print(f"\n신뢰도 분포:")
79
+ print(f" < 0.2: {low}개 ({low/len(confidences)*100:.1f}%)")
80
+ print(f" 0.2 ~ 0.4: {mid}개 ({mid/len(confidences)*100:.1f}%)")
81
+ print(f" >= 0.4: {high}개 ({high/len(confidences)*100:.1f}%)")
82
+
83
+ # 박스 크기 분석
84
+ print(f"\n📐 박스 크기 분석")
85
+ print(f"{'─' * 60}")
86
+
87
+ box_areas = []
88
+ box_widths = []
89
+ box_heights = []
90
+ aspect_ratios = []
91
+
92
+ for boxes in data.values():
93
+ for box in boxes:
94
+ bbox = box['bbox']
95
+ x1, y1, x2, y2 = bbox
96
+ width = x2 - x1
97
+ height = y2 - y1
98
+ area = width * height
99
+
100
+ box_areas.append(area)
101
+ box_widths.append(width)
102
+ box_heights.append(height)
103
+ if height > 0:
104
+ aspect_ratios.append(width / height)
105
+
106
+ if box_areas:
107
+ print(f"평균 면적: {sum(box_areas)/len(box_areas):.0f} px²")
108
+ print(f"평균 너비: {sum(box_widths)/len(box_widths):.0f} px")
109
+ print(f"평균 높이: {sum(box_heights)/len(box_heights):.0f} px")
110
+ print(f"평균 종횡비: {sum(aspect_ratios)/len(aspect_ratios):.2f}")
111
+ print(f" (새우는 보통 3:1 ~ 10:1)")
112
+
113
+ # 상세 데이터 (처음 5개)
114
+ print(f"\n📋 상세 데이터 (처음 5개)")
115
+ print(f"{'─' * 60}")
116
+
117
+ count = 0
118
+ for filename, boxes in data.items():
119
+ if count >= 5:
120
+ break
121
+
122
+ print(f"\n{filename}")
123
+ if not boxes:
124
+ print(" - 박스 없음 (빈 이미지 또는 건너뛰기)")
125
+ else:
126
+ for idx, box in enumerate(boxes, 1):
127
+ bbox = box['bbox']
128
+ x1, y1, x2, y2 = bbox
129
+ width = x2 - x1
130
+ height = y2 - y1
131
+ conf = box.get('confidence', 0)
132
+ print(f" #{idx}: bbox=[{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}], "
133
+ f"크기={width:.0f}x{height:.0f}, 신뢰도={conf:.3f}")
134
+ count += 1
135
+
136
+ # 검증 결과
137
+ print(f"\n{'=' * 60}")
138
+ print(f"✅ 검증 결과")
139
+ print(f"{'=' * 60}")
140
+
141
+ issues = []
142
+
143
+ # 1. 데이터가 너무 적은지 확인
144
+ if total_images < 10:
145
+ issues.append(f"⚠️ 이미지 수가 적습니다 ({total_images}개). 최소 50개 권장")
146
+
147
+ # 2. 라벨링 비율 확인
148
+ if labeled_images / total_images < 0.5:
149
+ issues.append(f"⚠️ 라벨링 비율이 낮습니다 ({labeled_images/total_images*100:.1f}%). 50% 이상 권장")
150
+
151
+ # 3. 평균 박스 수 확인
152
+ if labeled_images > 0 and total_boxes / labeled_images < 0.5:
153
+ issues.append(f"⚠️ 이미지당 평균 박스 수가 적습니다 ({total_boxes/labeled_images:.2f}개)")
154
+
155
+ # 4. 신뢰도 확인
156
+ if confidences and sum(confidences)/len(confidences) < 0.2:
157
+ issues.append(f"⚠️ 평균 신뢰도가 낮습니다 ({sum(confidences)/len(confidences):.3f}). 검출 품질 확인 필요")
158
+
159
+ if issues:
160
+ print("\n문제점:")
161
+ for issue in issues:
162
+ print(f" {issue}")
163
+ else:
164
+ print("\n✅ 모든 검증 통과!")
165
+
166
+ # 다음 단계 제안
167
+ print(f"\n{'=' * 60}")
168
+ print(f"📝 다음 단계")
169
+ print(f"{'=' * 60}")
170
+
171
+ if total_images < 50:
172
+ print(f"\n1. 더 많은 이미지 라벨링")
173
+ print(f" - 현재: {total_images}장")
174
+ print(f" - 목표: 50장 이상 (Phase 1)")
175
+ print(f" - 권장: 100~200장 (Phase 2~3)")
176
+
177
+ if labeled_images > 10:
178
+ print(f"\n2. 정량적 평가 실행")
179
+ print(f" ```bash")
180
+ print(f" python test_quantitative_evaluation.py")
181
+ print(f" ```")
182
+
183
+ print(f"\n3. 계속 라벨링")
184
+ print(f" - 브라우저에서 http://localhost:7862 접속")
185
+ print(f" - 더 많은 폴더 작업")
186
+
187
+ print(f"\n{'=' * 60}\n")
188
+
189
+ if __name__ == "__main__":
190
+ validate_ground_truth()
visualize_yolo_dataset.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ YOLO 데이터셋 바운딩 박스 시각화
4
+ """
5
+ import sys
6
+ sys.stdout.reconfigure(encoding='utf-8')
7
+
8
+ import os
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ from pathlib import Path
11
+ import random
12
+
13
+ def visualize_yolo_annotation(img_path, label_path, output_path):
14
+ """YOLO 형식 라벨을 이미지에 그리기"""
15
+
16
+ # 이미지 로드
17
+ img = Image.open(img_path)
18
+ draw = ImageDraw.Draw(img)
19
+ img_width, img_height = img.size
20
+
21
+ # 폰트 설정
22
+ try:
23
+ font = ImageFont.truetype("arial.ttf", 30)
24
+ font_small = ImageFont.truetype("arial.ttf", 20)
25
+ except:
26
+ font = ImageFont.load_default()
27
+ font_small = ImageFont.load_default()
28
+
29
+ # 라벨 파일 읽기
30
+ if not os.path.exists(label_path):
31
+ print(f"⚠️ 라벨 파일 없음: {label_path}")
32
+ return False
33
+
34
+ with open(label_path, 'r') as f:
35
+ lines = f.readlines()
36
+
37
+ print(f"\n📄 {os.path.basename(img_path)}")
38
+ print(f" 이미지 크기: {img_width} x {img_height}")
39
+ print(f" 박스 개수: {len(lines)}")
40
+
41
+ # 각 박스 그리기
42
+ for idx, line in enumerate(lines, 1):
43
+ parts = line.strip().split()
44
+ if len(parts) != 5:
45
+ continue
46
+
47
+ class_id = int(parts[0])
48
+ x_center_norm = float(parts[1])
49
+ y_center_norm = float(parts[2])
50
+ width_norm = float(parts[3])
51
+ height_norm = float(parts[4])
52
+
53
+ # 정규화된 좌표를 픽셀 좌표로 변환
54
+ x_center = x_center_norm * img_width
55
+ y_center = y_center_norm * img_height
56
+ width = width_norm * img_width
57
+ height = height_norm * img_height
58
+
59
+ # 바운딩 박스 좌표 계산
60
+ x1 = x_center - width / 2
61
+ y1 = y_center - height / 2
62
+ x2 = x_center + width / 2
63
+ y2 = y_center + height / 2
64
+
65
+ # 박스 그리기 (녹색, 두껍게)
66
+ draw.rectangle([x1, y1, x2, y2], outline="lime", width=8)
67
+
68
+ # 라벨 (검은 배경에 흰 글씨)
69
+ label = f"Shrimp #{idx}"
70
+ bbox = draw.textbbox((x1, y1 - 40), label, font=font)
71
+ draw.rectangle([bbox[0]-5, bbox[1]-5, bbox[2]+5, bbox[3]+5], fill="lime")
72
+ draw.text((x1, y1 - 40), label, fill="black", font=font)
73
+
74
+ # 좌표 정보 (작게)
75
+ info = f"YOLO: {x_center_norm:.3f} {y_center_norm:.3f} {width_norm:.3f} {height_norm:.3f}"
76
+ draw.text((x1, y2 + 10), info, fill="lime", font=font_small)
77
+
78
+ print(f" #{idx}: center=({x_center:.0f}, {y_center:.0f}), size=({width:.0f}x{height:.0f})")
79
+
80
+ # 저장
81
+ img.save(output_path, quality=95)
82
+ print(f" ✅ 저장: {output_path}")
83
+
84
+ return True
85
+
86
+ def main():
87
+ print("=" * 60)
88
+ print("📊 YOLO 데이터셋 바운딩 박스 시각화")
89
+ print("=" * 60)
90
+
91
+ # 경로 설정
92
+ dataset_dir = Path("data/yolo_dataset")
93
+ train_img_dir = dataset_dir / "images" / "train"
94
+ train_label_dir = dataset_dir / "labels" / "train"
95
+ output_dir = Path("data/yolo_visualization")
96
+ output_dir.mkdir(exist_ok=True)
97
+
98
+ # Train 이미지 리스트
99
+ img_files = list(train_img_dir.glob("*.jpg"))
100
+
101
+ if not img_files:
102
+ print("❌ Train 이미지 없음!")
103
+ return
104
+
105
+ print(f"\n📁 Train 이미지: {len(img_files)}개")
106
+
107
+ # 랜덤 3개 샘플
108
+ random.seed(42)
109
+ samples = random.sample(img_files, min(3, len(img_files)))
110
+
111
+ print(f"\n🎲 랜덤 샘플 3개 시각화:")
112
+
113
+ for img_path in samples:
114
+ # 대응하는 라벨 파일
115
+ label_filename = img_path.stem + ".txt"
116
+ label_path = train_label_dir / label_filename
117
+
118
+ # 출력 파일
119
+ output_path = output_dir / f"{img_path.stem}_visualized.jpg"
120
+
121
+ # 시각화
122
+ visualize_yolo_annotation(img_path, label_path, output_path)
123
+
124
+ print("\n" + "=" * 60)
125
+ print("✅ 시각화 완료!")
126
+ print("=" * 60)
127
+ print(f"\n📁 출력 디렉토리: {output_dir}")
128
+ print(f"\n생성된 파일:")
129
+ for file in sorted(output_dir.glob("*_visualized.jpg")):
130
+ print(f" - {file.name}")
131
+
132
+ print(f"\n💡 이미지를 열어서 바운딩 박스가 올바른지 확인하세요!")
133
+
134
+ if __name__ == "__main__":
135
+ main()