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 +23 -2
- analyze_fp_patterns.py +284 -0
- app_demo.py +163 -0
- check_250818_labeling.py +83 -0
- check_gt_split.py +38 -0
- check_labeling_quality.py +181 -0
- convert_gt_to_yolo.py +185 -0
- data/251015/251015_01-1.jpg +0 -3
- data/251015/251015_01.jpg +0 -3
- data/251015/251015_02-1.jpg +0 -3
- data/251015/251015_02.jpg +0 -3
- data/251015/251015_03-1.jpg +0 -3
- data/251015/251015_03.jpg +0 -3
- data/251015/251015_04-1.jpg +0 -3
- data/251015/251015_04.jpg +0 -3
- data/251015/251015_05-1.jpg +0 -3
- data/251015/251015_05.jpg +0 -3
- data/251015/251015_06-1.jpg +0 -3
- data/251015/251015_06.jpg +0 -3
- data/251015/251015_07-1.jpg +0 -3
- data/251015/251015_07.jpg +0 -3
- data/251015/251015_08-1.jpg +0 -3
- data/251015/251015_08.jpg +0 -3
- data/251015/251015_09-1.jpg +0 -3
- data/251015/251015_09.jpg +0 -3
- data/251015/251015_10-1.jpg +0 -3
- data/251015/251015_10.jpg +0 -3
- debug_roboflow_api.py +38 -0
- ground_truth.json +110 -0
- imgs/image.webp +2 -2
- optimize_yolov8_confidence.py +217 -0
- optimize_yolov8_confidence_val_only.py +204 -0
- quick_test_roboflow.py +89 -0
- quick_test_save_result.py +127 -0
- test_10_images.py +205 -0
- test_curl_roboflow.py +82 -0
- test_parameter_sweep.py +219 -0
- test_roboflow_model.py +177 -0
- test_roboflow_save_results.py +183 -0
- test_yolo_with_filter.py +336 -0
- test_yolov8_val_results.py +234 -0
- test_yolov8m_trained.py +112 -0
- test_yolov8m_unseen.py +144 -0
- test_yolov8m_with_filter.py +367 -0
- validate_ground_truth.py +190 -0
- visualize_yolo_dataset.py +135 -0
.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
|
data/251015/251015_01.jpg
DELETED
Git LFS Details
|
data/251015/251015_02-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_02.jpg
DELETED
Git LFS Details
|
data/251015/251015_03-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_03.jpg
DELETED
Git LFS Details
|
data/251015/251015_04-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_04.jpg
DELETED
Git LFS Details
|
data/251015/251015_05-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_05.jpg
DELETED
Git LFS Details
|
data/251015/251015_06-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_06.jpg
DELETED
Git LFS Details
|
data/251015/251015_07-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_07.jpg
DELETED
Git LFS Details
|
data/251015/251015_08-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_08.jpg
DELETED
Git LFS Details
|
data/251015/251015_09-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_09.jpg
DELETED
Git LFS Details
|
data/251015/251015_10-1.jpg
DELETED
Git LFS Details
|
data/251015/251015_10.jpg
DELETED
Git LFS Details
|
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
|
|
Git LFS Details
|
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()
|