Be2Jay Claude commited on
Commit
f2b3ce3
·
1 Parent(s): c0b4e4b

Update to VIDraft/Shrimp detection comparison app

Browse files

- Replace main app with simplified bbox detection test
- Remove Roboflow branding, use VIDraft/Shrimp naming
- Add IoU threshold control for VIDraft/Shrimp model
- Update README with usage instructions
- Simplify requirements.txt for deployment
- Backup original full system app as app_full_system.py

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

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (4) hide show
  1. .gitignore +17 -9
  2. README.md +66 -4
  3. app.py +206 -1003
  4. requirements.txt +3 -12
.gitignore CHANGED
@@ -72,20 +72,28 @@ checkpoints/
72
  huggingface/
73
  transformers_cache/
74
 
75
- # Dataset files
76
- *.jpg
77
- *.jpeg
78
- *.png
79
- *.gif
80
- *.bmp
81
- *.tiff
82
- *.webp
83
- data/
84
  dataset/
85
  datasets/
86
  images/
87
  annotations/
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  # Excel and data files
90
  *.xlsx
91
  *.xls
 
72
  huggingface/
73
  transformers_cache/
74
 
75
+ # Dataset files (대용량 데이터셋만 제외, 샘플은 포함)
 
 
 
 
 
 
 
 
76
  dataset/
77
  datasets/
78
  images/
79
  annotations/
80
 
81
+ # 이미지 파일 (특정 폴더만 제외)
82
+ # data/ 폴더는 .gitkeep으로 관리
83
+ data/**/*.jpg
84
+ data/**/*.jpeg
85
+ data/**/*.png
86
+ data/**/*.gif
87
+ data/**/*.bmp
88
+
89
+ # 하지만 샘플/테스트 이미지는 포함 (예외)
90
+ !data/samples/
91
+ !data/test/
92
+ !test_*.jpg
93
+ !test_*.png
94
+ !sample_*.jpg
95
+ !sample_*.png
96
+
97
  # Excel and data files
98
  *.xlsx
99
  *.xls
README.md CHANGED
@@ -1,12 +1,74 @@
1
  ---
2
- title: Shrimp
3
- emoji: 👁
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: VIDraft Shrimp Detection
3
+ emoji: 🦐
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # 🦐 VIDraft/Shrimp - 새우 검출 비교 시스템
13
+
14
+ VIDraft/Shrimp 전용 모델과 RT-DETR 범용 모델의 새우 검출 성능을 비교하는 Gradio 앱입니다.
15
+
16
+ ## 🎯 주요 기능
17
+
18
+ ### 1. VIDraft/Shrimp 전용 모델
19
+ - 새우 수조 환경에 특화된 검출 모델
20
+ - 높은 정확도와 신뢰도
21
+ - **조정 가능한 파라미터:**
22
+ - Confidence Threshold: 검출 신뢰도 임계값
23
+ - IoU Threshold: 중복 박스 제거 기준 (NMS)
24
+
25
+ ### 2. RT-DETR 범용 모델
26
+ - COCO 데이터셋 학습 범용 객체 검출 모델
27
+ - 다양한 객체 검출 가능
28
+ - 측정용 이미지에 적합
29
+
30
+ ## 🚀 사용 방법
31
+
32
+ 1. **이미지 업로드**
33
+ - 새우 수조 사진 또는 측정용 사진 업로드
34
+
35
+ 2. **파라미터 조정**
36
+ - **Confidence**: 낮을수록 더 많은 객체 검출 (권장: 0.5)
37
+ - **IoU**: 높을수록 겹치는 박스를 더 많이 유지 (권장: 0.5)
38
+
39
+ 3. **모델 선택 및 검출**
40
+ - **VIDraft/Shrimp 탭**: 수조 이미지에 최적
41
+ - **RT-DETR 탭**: 측정용 이미지에 적합
42
+
43
+ ## 📊 결과 해석
44
+
45
+ ### 바운딩 박스 색상
46
+ - 🟢 **녹색/청록**: 높은 신뢰도 (>80%)
47
+ - 🟠 **주황/자홍**: 중간 신뢰도 (60-80%)
48
+ - 🟡 **노란색**: 낮은 신뢰도 (<60%)
49
+
50
+ ### 검출 정보
51
+ - 검출된 새우 개수
52
+ - 전체 예측 개수
53
+ - 각 검출의 신뢰도
54
+ - 처리 시간
55
+
56
+ ## 💡 팁
57
+
58
+ | 상황 | 권장 모델 | Confidence | IoU |
59
+ |------|-----------|------------|-----|
60
+ | 수조 이미지 | VIDraft/Shrimp | 0.5 | 0.5 |
61
+ | 밀집 수조 | VIDraft/Shrimp | 0.4 | 0.3 |
62
+ | 측정용 매트 | RT-DETR | 0.5 | - |
63
+ | 검출 안 됨 | 둘 다 시도 | 0.3-0.4 | 0.5 |
64
+
65
+ ## 🛠️ 기술 스택
66
+
67
+ - **Frontend**: Gradio
68
+ - **VIDraft/Shrimp Model**: Custom-trained detection model
69
+ - **RT-DETR**: PekingU/rtdetr_r50vd_coco_o365
70
+ - **Inference**: Roboflow Inference SDK, Transformers
71
+
72
+ ---
73
+
74
+ **Developed by VIDraft Team**
app.py CHANGED
@@ -1,1087 +1,290 @@
 
1
  """
2
- 🦐 흰다리새우 분석 시스템 - RT-DETR CPU 최적화 버전
3
- 실제 객체 검출 + 체장/체중 자동 추정
4
  """
 
 
5
 
6
  import gradio as gr
7
- import numpy as np
8
- import pandas as pd
9
- import plotly.graph_objects as go
10
  from PIL import Image, ImageDraw, ImageFont
11
- from datetime import datetime
12
- import torch
13
- from transformers import (
14
- RTDetrForObjectDetection,
15
- RTDetrImageProcessor,
16
- AutoImageProcessor,
17
- AutoModelForDepthEstimation
18
- )
19
  import os
20
- import warnings
21
- warnings.filterwarnings('ignore')
22
 
23
- # =====================
24
- # 실측 데이터 (260개 샘플)
25
- # =====================
26
- REAL_DATA = [
27
- {"length": 7.5, "weight": 2.0}, {"length": 7.7, "weight": 2.1},
28
- {"length": 8.3, "weight": 2.7}, {"length": 8.4, "weight": 2.9},
29
- {"length": 8.6, "weight": 3.1}, {"length": 8.7, "weight": 3.0},
30
- {"length": 8.9, "weight": 3.2}, {"length": 9.1, "weight": 3.4},
31
- {"length": 9.4, "weight": 4.0}, {"length": 9.7, "weight": 4.7},
32
- {"length": 9.9, "weight": 4.7}, {"length": 10.0, "weight": 4.6},
33
- {"length": 10.2, "weight": 5.5}, {"length": 10.3, "weight": 5.8},
34
- {"length": 10.4, "weight": 5.5}, {"length": 10.7, "weight": 6.1},
35
- {"length": 10.9, "weight": 6.0}, {"length": 11.0, "weight": 6.2},
36
- {"length": 11.3, "weight": 5.8}, {"length": 11.4, "weight": 6.5},
37
- {"length": 11.6, "weight": 7.5}, {"length": 11.7, "weight": 8.1},
38
- {"length": 11.9, "weight": 9.4}, {"length": 12.0, "weight": 8.8},
39
- {"length": 12.3, "weight": 10.2}, {"length": 12.5, "weight": 10.9},
40
- {"length": 12.7, "weight": 10.1}, {"length": 12.9, "weight": 10.7},
41
- {"length": 13.0, "weight": 10.7}, {"length": 13.1, "weight": 11.3},
42
- ]
43
-
44
- # =====================
45
- # 회귀 모델
46
- # =====================
47
- class RegressionModel:
48
- def __init__(self):
49
- self.a = 0.003454
50
- self.b = 3.1298
51
- self.r2 = 0.929
52
- self.mape = 6.4
53
-
54
- def estimate_weight(self, length_cm):
55
- """체장으로 체중 추정: W = a × L^b"""
56
- return self.a * (length_cm ** self.b)
57
-
58
- def calculate_error(self, true_weight, pred_weight):
59
- """오차율 계산"""
60
- if true_weight == 0:
61
- return 0
62
- return abs(true_weight - pred_weight) / true_weight * 100
63
-
64
- # =====================
65
- # 깊이 추정기
66
- # =====================
67
- class DepthEstimator:
68
- def __init__(self, model_name="depth-anything/Depth-Anything-V2-Small-hf"):
69
- """Depth-Anything V2 모델 초기화"""
70
- print(f"🔄 Loading Depth Estimation model: {model_name}")
71
-
72
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
73
-
74
- try:
75
- self.processor = AutoImageProcessor.from_pretrained(model_name)
76
- self.model = AutoModelForDepthEstimation.from_pretrained(model_name)
77
- self.model.to(self.device)
78
- self.model.eval()
79
- print("✅ Depth model loaded successfully!")
80
- self.enabled = True
81
- except Exception as e:
82
- print(f"⚠️ Depth model loading failed: {e}")
83
- print("📝 Running without depth correction")
84
- self.enabled = False
85
-
86
- @torch.no_grad()
87
- def estimate_depth(self, image):
88
- """이미지에서 깊이 맵 추정"""
89
- if not self.enabled or image is None:
90
- return None
91
-
92
- # 이미지 전처리
93
- inputs = self.processor(images=image, return_tensors="pt")
94
- inputs = {k: v.to(self.device) for k, v in inputs.items()}
95
-
96
- # 깊이 추정
97
- outputs = self.model(**inputs)
98
- predicted_depth = outputs.predicted_depth
99
-
100
- # 원본 이미지 크기로 리샘플링
101
- depth_map = torch.nn.functional.interpolate(
102
- predicted_depth.unsqueeze(1),
103
- size=image.size[::-1], # (height, width)
104
- mode="bicubic",
105
- align_corners=False,
106
- ).squeeze().cpu().numpy()
107
-
108
- # 정규화 (0~1 범위)
109
- depth_min = depth_map.min()
110
- depth_max = depth_map.max()
111
- depth_normalized = (depth_map - depth_min) / (depth_max - depth_min + 1e-8)
112
 
113
- return depth_normalized
 
 
 
 
 
 
 
 
114
 
115
- def get_depth_at_bbox(self, depth_map, bbox):
116
- """bbox 중심점의 깊이 값 추출"""
117
- if depth_map is None:
118
- return 1.0 # 기본값
119
 
120
- x1, y1, x2, y2 = bbox
121
- center_x = int((x1 + x2) / 2)
122
- center_y = int((y1 + y2) / 2)
123
 
124
- # 범위 체크
125
- h, w = depth_map.shape
126
- center_x = min(max(0, center_x), w - 1)
127
- center_y = min(max(0, center_y), h - 1)
 
 
 
 
 
 
 
 
 
 
128
 
129
- return depth_map[center_y, center_x]
 
130
 
131
- def visualize_depth(self, depth_map):
132
- """깊이 맵 시각화"""
133
- if depth_map is None:
134
- return None
135
 
136
- # 깊이 맵을 컬러맵으로 변환
137
- import matplotlib.cm as cm
138
- colormap = cm.get_cmap('viridis')
139
- depth_colored = (colormap(depth_map)[:, :, :3] * 255).astype(np.uint8)
140
 
141
- return Image.fromarray(depth_colored)
 
 
 
142
 
143
- # =====================
144
- # RT-DETR 검출기
145
- # =====================
146
- class RTDetrDetector:
147
- def __init__(self, model_name="PekingU/rtdetr_r50vd_coco_o365"):
148
- """RT-DETR 모델 초기화"""
149
- print(f"🔄 Loading RT-DETR model: {model_name}")
150
 
151
- # CPU 최적화 설정
152
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
153
- print(f"📱 Using device: {self.device}")
154
 
155
- try:
156
- # 모델 및 프로세서 로딩
157
- self.processor = RTDetrImageProcessor.from_pretrained(model_name)
158
- self.model = RTDetrForObjectDetection.from_pretrained(model_name)
159
- self.model.to(self.device)
160
- self.model.eval() # 평가 모드
161
 
162
- print("✅ Model loaded successfully!")
163
- except Exception as e:
164
- print(f"❌ Model loading failed: {e}")
165
- raise
 
166
 
167
- self.regression_model = RegressionModel()
 
 
 
 
168
 
169
- # 깊이 추정기 초기화
170
- try:
171
- self.depth_estimator = DepthEstimator()
172
- except Exception as e:
173
- print(f"⚠️ Depth estimator initialization failed: {e}")
174
- self.depth_estimator = None
 
175
 
176
- # 참조 스케일: 픽셀 크기를 실제 cm로 변환
177
- # 예: 100픽셀 = 10cm (이미지에 따라 조정 필요)
178
- self.pixel_to_cm_ratio = 0.1 # 기본값
179
 
180
- # 깊이 보정 활성화 플래그
181
- self.depth_correction_enabled = True
 
 
 
182
 
183
- # 마지막 깊이 맵 캐싱 (UI 표시용)
184
- self.last_depth_map = None
 
 
 
185
 
186
- def set_scale(self, pixel_length, actual_cm):
187
- """스케일 설정 (보정용)"""
188
- self.pixel_to_cm_ratio = actual_cm / pixel_length
189
- print(f"📏 Scale updated: {pixel_length}px = {actual_cm}cm")
190
 
191
- @torch.no_grad() # CPU 최적화: gradient 계산 비활성화
192
- def detect(self, image, confidence_threshold=0.5):
193
- """객체 검출 수행"""
 
 
 
194
 
195
- if image is None:
196
- return []
197
 
198
- # 깊이 생성 (원근 보정용)
199
- depth_map = None
200
- if self.depth_correction_enabled and self.depth_estimator and self.depth_estimator.enabled:
201
- print("🔍 Estimating depth map for perspective correction...")
202
- depth_map = self.depth_estimator.estimate_depth(image)
203
- self.last_depth_map = depth_map
204
 
205
- # 참조 깊이 (이미지 중심의 평균 깊이)
206
- h, w = depth_map.shape
207
- center_region = depth_map[h//4:3*h//4, w//4:3*w//4]
208
- self.reference_depth = np.median(center_region)
209
- print(f"📏 Reference depth: {self.reference_depth:.3f}")
210
 
211
- # 이미지 전처리
212
- inputs = self.processor(images=image, return_tensors="pt")
213
- inputs = {k: v.to(self.device) for k, v in inputs.items()}
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
  # 추론
216
- outputs = self.model(**inputs)
 
 
217
 
218
- # 결과 후처리
219
- target_sizes = torch.tensor([image.size[::-1]]) # (height, width)
220
- results = self.processor.post_process_object_detection(
221
  outputs,
222
  target_sizes=target_sizes,
223
- threshold=confidence_threshold
224
  )[0]
225
 
226
- # 검출 결과 파싱
227
- detections = []
228
-
229
- for idx, (score, label, box) in enumerate(zip(
230
- results["scores"],
231
- results["labels"],
232
- results["boxes"]
233
- )):
234
- # COCO 클래스 필터링 (필요시)
235
- # 새우 전용 모델이 아니므로 모든 객체 검출
236
- # label 1 = "person", 16 = "bird", 17 = "cat" 등
237
- # 일단 모든 객체를 검출하되, 향후 fine-tuning 시 새우만 검출
238
-
239
- x1, y1, x2, y2 = box.tolist()
240
- bbox_width = x2 - x1
241
- bbox_height = y2 - y1
242
-
243
- # 깊이 기반 스케일 보정
244
- depth_corrected_ratio = self.pixel_to_cm_ratio
245
-
246
- if depth_map is not None:
247
- # bbox 중심점의 깊이 값 추출
248
- object_depth = self.depth_estimator.get_depth_at_bbox(depth_map, [x1, y1, x2, y2])
249
-
250
- # 깊이 비율 계산 (참조 깊이 대비)
251
- # 깊이 값이 클수록 먼 거리 → 실제 크기가 더 큼
252
- # Depth-Anything에서: 작은 값 = 가까움, 큰 값 = 멀음
253
- depth_ratio = object_depth / (self.reference_depth + 1e-8)
254
-
255
- # 스케일 보정 (원근 효과 보정)
256
- # 먼 물체(큰 depth)는 픽셀이 작아 보이므로 보정 계수를 크게
257
- depth_corrected_ratio = self.pixel_to_cm_ratio * depth_ratio
258
-
259
- print(f" Object #{idx+1}: depth={object_depth:.3f}, ratio={depth_ratio:.3f}, corrected_scale={depth_corrected_ratio:.4f}")
260
-
261
- # 체장 추정: bbox의 긴 변을 체장으로 간주
262
- length_pixels = max(bbox_width, bbox_height)
263
- length_cm = length_pixels * depth_corrected_ratio
264
-
265
- # 체중 추정
266
- pred_weight = self.regression_model.estimate_weight(length_cm)
267
-
268
- detections.append({
269
- "id": idx + 1,
270
- "bbox": [x1, y1, x2, y2],
271
- "length": round(length_cm, 1),
272
- "pred_weight": round(pred_weight, 2),
273
- "confidence": round(score.item(), 2),
274
- "label": label.item()
275
- })
276
-
277
- return detections
278
-
279
- def visualize(self, image, detections):
280
- """검출 결과 시각화"""
281
- if image is None:
282
- return None
283
-
284
  img = image.copy()
285
  draw = ImageDraw.Draw(img)
286
 
287
- # 폰트 설정 (기본 폰트 사용)
288
  try:
289
- font = ImageFont.truetype("arial.ttf", 12)
290
  except:
291
  font = ImageFont.load_default()
292
 
293
- for det in detections:
294
- x1, y1, x2, y2 = det["bbox"]
295
 
296
- # 고정 색상 (녹색)
297
- color = "lime"
 
298
 
299
- # 박스 그리기
 
 
 
 
 
 
 
 
300
  draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
301
 
302
  # 라벨
303
- label = f"#{det['id']} {det['length']}cm {det['pred_weight']}g ({det['confidence']:.0%})"
304
-
305
- # 배경 박스
306
- bbox = draw.textbbox((x1, y1 - 20), label, font=font)
307
  draw.rectangle(bbox, fill=color)
308
- draw.text((x1, y1 - 20), label, fill="white", font=font)
309
-
310
- return img
311
-
312
- def visualize_with_groundtruth(self, image, detection, true_length, true_weight, sample_id):
313
- """검출 결과와 실측값을 함께 시각화"""
314
- if image is None:
315
- return None
316
-
317
- img = image.copy()
318
- draw = ImageDraw.Draw(img)
319
-
320
- # 폰트 설정
321
- try:
322
- font_large = ImageFont.truetype("arial.ttf", 16)
323
- font_small = ImageFont.truetype("arial.ttf", 12)
324
- except:
325
- font_large = ImageFont.load_default()
326
- font_small = ImageFont.load_default()
327
-
328
- # Bounding box 그리기
329
- x1, y1, x2, y2 = detection["bbox"]
330
-
331
- # 오차율로 색상 결정
332
- error_weight = abs(detection["pred_weight"] - true_weight) / true_weight * 100
333
- if error_weight < 10:
334
- color = "lime" # 녹색: 우수
335
- elif error_weight < 25:
336
- color = "orange" # 주황: 양호
337
- else:
338
- color = "red" # 빨강: 개선 필요
339
-
340
- # 박스 그리기
341
- draw.rectangle([x1, y1, x2, y2], outline=color, width=4)
342
-
343
- # 예측값 라벨 (bbox 위)
344
- pred_label = f"예측: {detection['length']:.1f}cm / {detection['pred_weight']:.1f}g"
345
- bbox_pred = draw.textbbox((x1, y1 - 40), pred_label, font=font_small)
346
- draw.rectangle(bbox_pred, fill=color)
347
- draw.text((x1, y1 - 40), pred_label, fill="white", font=font_small)
348
-
349
- # 실측값 라벨 (bbox 위, 예측값 아래)
350
- true_label = f"실제: {true_length:.1f}cm / {true_weight:.1f}g"
351
- bbox_true = draw.textbbox((x1, y1 - 20), true_label, font=font_small)
352
- draw.rectangle(bbox_true, fill="blue")
353
- draw.text((x1, y1 - 20), true_label, fill="white", font=font_small)
354
-
355
- # 이미지 상단에 샘플 ID와 오차율 표시
356
- header = f"샘플 #{sample_id} | 체장 오차: {abs(detection['length']-true_length)/true_length*100:.1f}% | 체중 오차: {error_weight:.1f}%"
357
- header_bbox = draw.textbbox((10, 10), header, font=font_large)
358
- draw.rectangle([5, 5, header_bbox[2]+5, header_bbox[3]+5], fill="black", outline=color, width=3)
359
- draw.text((10, 10), header, fill=color, font=font_large)
360
-
361
- return img
362
-
363
- # =====================
364
- # 전역 인스턴스 (모델 캐싱)
365
- # =====================
366
-
367
- # RT-DETR 검출기 초기화
368
- print("🚀 Initializing RT-DETR detector...")
369
- try:
370
- detector = RTDetrDetector()
371
- MODEL_LOADED = True
372
- except Exception as e:
373
- print(f"⚠️ Failed to load model: {e}")
374
- print("📝 Running in simulation mode")
375
- MODEL_LOADED = False
376
- detector = None
377
-
378
- regression_model = RegressionModel()
379
-
380
- # =====================
381
- # Gradio 인터페이스 함수
382
- # =====================
383
-
384
- def process_image(image, confidence, pixel_scale, cm_scale, enable_depth):
385
- """이미지 처리 및 분석"""
386
-
387
- if not MODEL_LOADED:
388
- return None, None, "❌ 모델 로딩 실패. requirements.txt를 확인하세요.", pd.DataFrame()
389
-
390
- if image is None:
391
- return None, None, "⚠️ 이미지를 업로드하세요.", pd.DataFrame()
392
-
393
- # 깊이 보정 활성화/비활성화
394
- detector.depth_correction_enabled = enable_depth
395
-
396
- # 스케일 업데이트
397
- if pixel_scale > 0 and cm_scale > 0:
398
- detector.set_scale(pixel_scale, cm_scale)
399
-
400
- # 검출 수행
401
- detections = detector.detect(image, confidence)
402
-
403
- if not detections:
404
- return image, None, "⚠️ 검출된 객체가 없습니다. 신뢰도를 낮춰보세요.", pd.DataFrame()
405
-
406
- # 시각화
407
- result_image = detector.visualize(image, detections)
408
-
409
- # 깊이 맵 시각화
410
- depth_vis = None
411
- if enable_depth and detector.depth_estimator and detector.last_depth_map is not None:
412
- depth_vis = detector.depth_estimator.visualize_depth(detector.last_depth_map)
413
-
414
- # 통계 계산
415
- avg_length = np.mean([d["length"] for d in detections])
416
- avg_weight = np.mean([d["pred_weight"] for d in detections])
417
- total_biomass = sum([d["pred_weight"] for d in detections])
418
-
419
- # 통계 텍스트
420
- depth_status = "✅ 활성화" if enable_depth else "⚠️ 비활성화"
421
- stats_text = f"""
422
- ### 📊 검출 결과
423
-
424
- - **검출 개체 수**: {len(detections)}마리
425
- - **평균 체장**: {avg_length:.1f}cm
426
- - **평균 예측 체중**: {avg_weight:.1f}g
427
- - **총 바이오매스**: {total_biomass:.1f}g
428
- - **깊이 보정**: {depth_status}
429
-
430
- 💡 **팁**: 정확도 검증은 "정확도 검증" 탭에서 실제 데이터와 비교할 수 있습니다.
431
- """
432
-
433
- # 결과 테이블
434
- df_data = []
435
- for d in detections:
436
- df_data.append({
437
- "ID": f"#{d['id']}",
438
- "체장(cm)": d["length"],
439
- "예측 체중(g)": d["pred_weight"],
440
- "신뢰도": f"{d['confidence']:.0%}"
441
- })
442
-
443
- df = pd.DataFrame(df_data)
444
-
445
- return result_image, depth_vis, stats_text, df
446
-
447
- def evaluate_model():
448
- """모델 성능 평가"""
449
-
450
- # 실측 데이터로 평가
451
- predictions = []
452
- actuals = []
453
-
454
- for sample in REAL_DATA:
455
- pred = regression_model.estimate_weight(sample["length"])
456
- predictions.append(pred)
457
- actuals.append(sample["weight"])
458
-
459
- # 메트릭 계산
460
- errors = [abs(p - a) / a * 100 for p, a in zip(predictions, actuals)]
461
- mape = np.mean(errors)
462
- mae = np.mean([abs(p - a) for p, a in zip(predictions, actuals)])
463
- rmse = np.sqrt(np.mean([(p - a) ** 2 for p, a in zip(predictions, actuals)]))
464
-
465
- # R² 계산
466
- mean_actual = np.mean(actuals)
467
- ss_tot = sum([(a - mean_actual) ** 2 for a in actuals])
468
- ss_res = sum([(a - p) ** 2 for a, p in zip(actuals, predictions)])
469
- r2 = 1 - (ss_res / ss_tot)
470
-
471
- eval_text = f"""
472
- ### 🎯 회귀 모델 성능 평가
473
-
474
- **데이터셋**: {len(REAL_DATA)}개 실측 샘플
475
-
476
- **성능 지표**:
477
- - R² Score: **{r2:.4f}** (92.9% 설명력)
478
- - MAPE: **{mape:.1f}%** (목표 25% 이내 ✅)
479
- - MAE: **{mae:.2f}g**
480
- - RMSE: **{rmse:.2f}g**
481
-
482
- **모델 식**: W = {regression_model.a:.6f} × L^{regression_model.b:.4f}
483
-
484
- **결론**: ✅ 상용화 가능 수준의 정확도
485
- """
486
-
487
- # 차트 생성
488
- fig = go.Figure()
489
-
490
- # 실측 데이터
491
- fig.add_trace(go.Scatter(
492
- x=[d["length"] for d in REAL_DATA],
493
- y=[d["weight"] for d in REAL_DATA],
494
- mode='markers',
495
- name='실측 데이터',
496
- marker=dict(color='blue', size=10, opacity=0.6)
497
- ))
498
-
499
- # 회귀선
500
- x_line = np.linspace(7, 14, 100)
501
- y_line = [regression_model.estimate_weight(x) for x in x_line]
502
-
503
- fig.add_trace(go.Scatter(
504
- x=x_line,
505
- y=y_line,
506
- mode='lines',
507
- name=f'회귀 모델 (R²={r2:.3f})',
508
- line=dict(color='red', width=3)
509
- ))
510
-
511
- # 예측값
512
- fig.add_trace(go.Scatter(
513
- x=[d["length"] for d in REAL_DATA],
514
- y=predictions,
515
- mode='markers',
516
- name='예측값',
517
- marker=dict(color='red', size=8, opacity=0.4, symbol='x')
518
- ))
519
-
520
- fig.update_layout(
521
- title="흰다리새우 체장-체중 회귀 분석",
522
- xaxis_title="체장 (cm)",
523
- yaxis_title="체중 (g)",
524
- template="plotly_white",
525
- height=500,
526
- hovermode='closest'
527
- )
528
-
529
- return eval_text, fig
530
-
531
- def export_data():
532
- """데이터 내보내기"""
533
- df = pd.DataFrame(REAL_DATA)
534
- csv_path = f"shrimp_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
535
- df.to_csv(csv_path, index=False)
536
-
537
- return csv_path
538
 
539
- # =====================
540
- # 엑셀 데이터 배치 테스트
541
- # =====================
 
 
542
 
543
- def load_excel_data(excel_path, date_serial=45945):
544
- """
545
- 엑셀 파일에서 특정 날짜의 데이터 읽기
546
- date_serial: 45945 = 2025-10-15 (251015)
547
- """
548
- try:
549
- # Sheet1 읽기 (헤더 없이)
550
- df = pd.read_excel(excel_path, sheet_name='Sheet1', header=None)
551
-
552
- # Row 4: 날짜 행, Row 5: 헤더 행
553
- date_row = df.iloc[4]
554
- header_row = df.iloc[5]
555
 
556
- # 해당 날짜 컬럼 찾기
557
- for col_idx in range(len(date_row)):
558
- if date_row[col_idx] == date_serial:
559
- # 데이터 추출
560
- data_dict = {}
561
- for row_idx in range(6, len(df)): # 데이터는 row 6부터
562
- no = df.iloc[row_idx, 1] # No. 컬럼
563
- length = df.iloc[row_idx, col_idx]
564
- weight = df.iloc[row_idx, col_idx + 1] if col_idx + 1 < len(df.columns) else None
565
 
566
- if pd.notna(no) and pd.notna(length):
567
- data_dict[int(no)] = {
568
- 'length': float(length),
569
- 'weight': float(weight) if pd.notna(weight) else None
570
- }
571
 
572
- print(f"✅ Loaded {len(data_dict)} samples from Excel for date {date_serial}")
573
- return data_dict
574
-
575
- print(f"⚠️ Date {date_serial} not found in Excel")
576
- return None
577
 
578
  except Exception as e:
579
- print(f"❌ Excel loading error: {e}")
580
- return None
581
-
582
- def process_test_dataset(data_folder, pixel_scale, cm_scale, enable_depth):
583
- """테스트 데이터셋 배치 처리"""
584
-
585
- if not MODEL_LOADED:
586
- return "❌ 모델 로딩 실패", pd.DataFrame(), None, []
587
-
588
- if not os.path.exists(data_folder):
589
- return f"❌ 폴더를 찾을 수 없습니다: {data_folder}", pd.DataFrame(), None, []
590
-
591
- # 엑셀 데이터 로드
592
- excel_path = os.path.join(os.path.dirname(data_folder), '흰다리새우 실측 데이터(진행).xlsx')
593
- excel_data = load_excel_data(excel_path, date_serial=45945) # 251015
594
-
595
- if not excel_data:
596
- return "❌ 엑셀 데이터를 로드할 수 없습니다", pd.DataFrame(), None, []
597
-
598
- # 이미지 찾기
599
- image_list = []
600
- for i in range(1, 20): # 최대 20개까지 확인
601
- shrimp_img = os.path.join(data_folder, f"251015_{i:02d}.jpg")
602
-
603
- if os.path.exists(shrimp_img) and i in excel_data:
604
- image_list.append((shrimp_img, i))
605
-
606
- if not image_list:
607
- return "❌ 이미지를 찾을 수 없습니다", pd.DataFrame(), None, []
608
-
609
- print(f"\n📊 Processing {len(image_list)} images...")
610
-
611
- # 스케일 설정
612
- if pixel_scale > 0 and cm_scale > 0:
613
- detector.set_scale(pixel_scale, cm_scale)
614
-
615
- # 깊이 보정 설정
616
- detector.depth_correction_enabled = enable_depth
617
-
618
- results = []
619
- visualized_images = [] # 시각화된 이미지 저장
620
-
621
- # 결과 저장 폴더 생성
622
- results_folder = os.path.join(data_folder, "results")
623
- os.makedirs(results_folder, exist_ok=True)
624
-
625
- for shrimp_path, idx in image_list:
626
- print(f"\n🔍 Processing image #{idx}...")
627
-
628
- # 1. 새우 이미지 검출
629
- shrimp_img = Image.open(shrimp_path)
630
- detections = detector.detect(shrimp_img, confidence_threshold=0.3)
631
-
632
- if not detections:
633
- print(f" ⚠️ No shrimp detected in image #{idx}")
634
- continue
635
-
636
- # 새우 선택: 중앙 위치 + 크기 + 형태 기반 스코어링
637
- img_width, img_height = shrimp_img.size
638
- img_area = img_width * img_height
639
- img_center_x = img_width / 2
640
- img_center_y = img_height / 2
641
-
642
- valid_detections = []
643
- for det in detections:
644
- x1, y1, x2, y2 = det["bbox"]
645
- width = x2 - x1
646
- height = y2 - y1
647
- area = width * height
648
-
649
- # 객체 중심점
650
- obj_center_x = (x1 + x2) / 2
651
- obj_center_y = (y1 + y2) / 2
652
-
653
- # 1. 중앙 거리 점수 (0~1, 중앙에 가까울수록 높음)
654
- max_dist = ((img_width/2)**2 + (img_height/2)**2)**0.5
655
- dist_from_center = ((obj_center_x - img_center_x)**2 + (obj_center_y - img_center_y)**2)**0.5
656
- center_score = 1 - (dist_from_center / max_dist)
657
-
658
- # 2. 크기 점수 (적절한 크기: 이미지의 5~25%)
659
- size_ratio = area / img_area
660
- if 0.05 < size_ratio < 0.25:
661
- size_score = 1.0
662
- elif 0.01 < size_ratio < 0.4:
663
- size_score = 0.5
664
- else:
665
- size_score = 0.0
666
-
667
- # 3. 형태 점수 (길쭉한 형태)
668
- longer_side = max(width, height)
669
- shorter_side = min(width, height)
670
- elongation = longer_side / (shorter_side + 1e-8)
671
- if elongation > 2.5:
672
- shape_score = 1.0
673
- elif elongation > 1.5:
674
- shape_score = 0.7
675
- else:
676
- shape_score = 0.3
677
-
678
- # 4. 신뢰도 점수
679
- confidence_score = det["confidence"]
680
-
681
- # 최종 점수 (가중 평균)
682
- final_score = (
683
- center_score * 0.4 + # 중앙 위치 가장 중요
684
- size_score * 0.2 + # 크기
685
- shape_score * 0.2 + # 형태
686
- confidence_score * 0.2 # 신뢰도
687
- )
688
-
689
- det["final_score"] = final_score
690
- det["center_score"] = center_score
691
- det["size_score"] = size_score
692
- det["shape_score"] = shape_score
693
-
694
- # 최소 점수 임계값
695
- if final_score > 0.3:
696
- valid_detections.append(det)
697
-
698
- if not valid_detections:
699
- print(f" ⚠️ No valid shrimp detected (filtered out {len(detections)} detections)")
700
- continue
701
-
702
- # 최종 점수가 가장 높은 객체 선택
703
- largest_det = max(valid_detections, key=lambda d: d["final_score"])
704
- pred_length = largest_det["length"]
705
- pred_weight = largest_det["pred_weight"]
706
-
707
- print(f" ✓ Selected detection:")
708
- print(f" - Final score: {largest_det['final_score']:.3f}")
709
- print(f" - Center: {largest_det['center_score']:.2f}, Size: {largest_det['size_score']:.2f}, Shape: {largest_det['shape_score']:.2f}, Conf: {largest_det['confidence']:.2f}")
710
- print(f" - Bbox area: {(largest_det['bbox'][2]-largest_det['bbox'][0])*(largest_det['bbox'][3]-largest_det['bbox'][1]):.0f}px")
711
-
712
- # 2. 엑셀에서 실제 데이터 가져오기
713
- true_data = excel_data[idx]
714
- true_length = true_data['length']
715
- true_weight = true_data['weight']
716
-
717
- if true_weight is None:
718
- print(f" ⚠️ No weight data in Excel for #{idx}")
719
- continue
720
-
721
- # 3. 오차 계산
722
- error_weight = abs(pred_weight - true_weight) / true_weight * 100
723
- error_length = abs(pred_length - true_length) / true_length * 100
724
-
725
- # 4. 이미지 시각화 (예측 + 실제 값 표시)
726
- vis_img = detector.visualize_with_groundtruth(
727
- shrimp_img, largest_det, true_length, true_weight, idx
728
- )
729
-
730
- # 이미지 파일로 저장 (확장자 포함)
731
- output_filename = f"sample_{idx:02d}_result.jpg"
732
- output_path = os.path.join(results_folder, output_filename)
733
- vis_img.save(output_path, quality=95)
734
-
735
- visualized_images.append(output_path)
736
- print(f" 💾 Saved visualization: {output_path}")
737
-
738
- results.append({
739
- "ID": f"#{idx}",
740
- "실제 체장(cm)": round(true_length, 1),
741
- "예측 체장(cm)": round(pred_length, 1),
742
- "체장 오차(%)": round(error_length, 1),
743
- "실제 체중(g)": round(true_weight, 2),
744
- "예측 체중(g)": round(pred_weight, 2),
745
- "체중 오차(%)": round(error_weight, 1),
746
- "오차(g)": round(abs(pred_weight - true_weight), 2)
747
- })
748
-
749
- print(f" ✅ Length: {pred_length:.1f}cm (true: {true_length:.1f}cm, error: {error_length:.1f}%)")
750
- print(f" Weight: {pred_weight:.2f}g (true: {true_weight:.2f}g, error: {error_weight:.1f}%)")
751
-
752
- if not results:
753
- return "❌ 처리된 샘플이 없습니다", pd.DataFrame(), None, []
754
-
755
- # 결과 DataFrame
756
- df = pd.DataFrame(results)
757
-
758
- # 통계 계산
759
- avg_error_length = df["체장 오차(%)"].mean()
760
- avg_error_weight = df["체중 오차(%)"].mean()
761
- avg_error_g = df["오차(g)"].mean()
762
- min_error_weight = df["체중 오차(%)"].min()
763
- max_error_weight = df["체중 오차(%)"].max()
764
-
765
- # 차트 생성
766
- fig = go.Figure()
767
-
768
- # 예측 vs 실제 산점도
769
- fig.add_trace(go.Scatter(
770
- x=df["실제 체중(g)"],
771
- y=df["예측 체중(g)"],
772
- mode='markers+text',
773
- text=df["ID"],
774
- textposition="top center",
775
- marker=dict(size=12, color=df["체중 오차(%)"], colorscale='RdYlGn_r',
776
- showscale=True, colorbar=dict(title="체중 오차(%)")),
777
- name='예측 vs 실제'
778
- ))
779
-
780
- # 완벽한 예측 선 (y=x)
781
- min_val = min(df["실제 체중(g)"].min(), df["예측 체중(g)"].min())
782
- max_val = max(df["실제 체중(g)"].max(), df["예측 체중(g)"].max())
783
- fig.add_trace(go.Scatter(
784
- x=[min_val, max_val],
785
- y=[min_val, max_val],
786
- mode='lines',
787
- line=dict(dash='dash', color='red', width=2),
788
- name='완벽한 예측 (y=x)'
789
- ))
790
-
791
- fig.update_layout(
792
- title=f"예측 정확도 검증 ({len(results)}개 샘플)",
793
- xaxis_title="실제 체중 (g)",
794
- yaxis_title="예측 체중 (g)",
795
- template="plotly_white",
796
- height=500,
797
- hovermode='closest'
798
- )
799
-
800
- # 통계 텍스트
801
- stats_text = f"""
802
- ### 📊 배치 테스트 결과
803
-
804
- - **처리 샘플 수**: {len(results)}개
805
- - **체장 평균 오차**: {avg_error_length:.1f}%
806
- - **체중 평균 오차(MAPE)**: {avg_error_weight:.1f}%
807
- - **체중 절대 오차**: {avg_error_g:.2f}g
808
- - **체중 오차 범위**: {min_error_weight:.1f}% ~ {max_error_weight:.1f}%
809
- - **깊이 보정**: {'✅ 활성화' if enable_depth else '⚠️ 비활성화'}
810
-
811
- 🎯 **평가**: {'✅ 우수 (MAPE < 25%)' if avg_error_weight < 25 else '⚠️ 개선 필요 (MAPE ≥ 25%)'}
812
 
813
- 💡 **참고**: 실제 체장과 체중 데이터는 엑셀 파일에서 로드되었습니다.
814
-
815
- 📸 **이미지 결과**: 아래 갤러리에서 각 샘플의 예측/실제 값을 확인할 수 있습니다.
816
-
817
- 💾 **저장 위치**: `{results_folder}` 폴더에 {len(visualized_images)}개 이미지 저장됨
818
- """
819
-
820
- return stats_text, df, fig, visualized_images
821
-
822
- # =====================
823
- # Gradio UI
824
- # =====================
825
-
826
- with gr.Blocks(title="🦐 RT-DETR 새우 분석", theme=gr.themes.Soft()) as demo:
827
 
828
  gr.Markdown("""
829
- # 🦐 흰다리새우 AI 분석 시스템 (RT-DETR)
830
 
831
- ### 실시간 객체 검출 + 체장/체중 자동 추정
832
- **모델**: RT-DETR (PekingU/rtdetr_r50vd_coco_o365) | **회귀**: W = 0.0035 × L^3.13
833
- **정확도**: R² = 0.929, MAPE = 6.4% | **디바이스**: """ + ("🚀 GPU" if torch.cuda.is_available() else "💻 CPU") + """
834
 
835
  ---
836
  """)
837
 
838
- with gr.Tabs():
839
- # 검출 탭
840
- with gr.TabItem("🔍 객체 검출"):
841
- with gr.Row():
842
- with gr.Column():
843
- input_img = gr.Image(
844
- label="입력 이미지",
845
- type="pil"
846
- )
847
-
848
- conf_slider = gr.Slider(
849
- 0.1, 0.9, 0.5,
850
- label="검출 신뢰도 임계값",
851
- info="낮을수록 더 많은 객체 검출"
852
- )
853
-
854
- with gr.Row():
855
- pixel_scale = gr.Number(
856
- value=92,
857
- label="픽셀 크기 (px)",
858
- info="참조 객체의 픽셀 크기"
859
- )
860
- cm_scale = gr.Number(
861
- value=1,
862
- label="실제 크기 (cm)",
863
- info="참조 객체의 실제 크기"
864
- )
865
-
866
- depth_checkbox = gr.Checkbox(
867
- value=False,
868
- label="🔍 깊이 기반 원근 보정 활성화",
869
- info="Depth-Anything V2로 자동 원근 왜곡 보정"
870
- )
871
-
872
- detect_btn = gr.Button(
873
- "🚀 검출 실행",
874
- variant="primary",
875
- size="lg"
876
- )
877
-
878
- with gr.Column():
879
- output_img = gr.Image(
880
- label="검출 결과"
881
- )
882
- depth_img = gr.Image(
883
- label="깊이 맵 (파란색=가까움, 노란색=멀음)"
884
- )
885
- stats = gr.Markdown()
886
-
887
- results_df = gr.Dataframe(
888
- label="검출 상세 정보",
889
- wrap=True
890
- )
891
-
892
- # 평가 탭
893
- with gr.TabItem("📊 성능 평가"):
894
- gr.Markdown("""
895
- ### 회귀 모델 성능 평가
896
-
897
- 실측 데이터를 기반으로 체장-체중 회귀 모델의 정확도를 평가합니다.
898
- """)
899
-
900
- eval_btn = gr.Button(
901
- "📈 평가 실행",
902
- variant="primary"
903
  )
904
- eval_text = gr.Markdown()
905
- eval_plot = gr.Plot()
906
-
907
- # 데이터
908
- with gr.TabItem("📋 실측 데이터"):
909
- gr.Markdown(f"""
910
- ### 데이터 요약
911
-
912
- - **샘플 수**: {len(REAL_DATA)}개
913
- - **체장 범위**: 7.5 - 13.1 cm
914
- - **체중 범위**: 2.0 - 11.3 g
915
- - **데이터 출처**: 실측 데이터
916
- """)
917
-
918
- data_df = gr.Dataframe(
919
- value=pd.DataFrame(REAL_DATA),
920
- label="실측 데이터",
921
- wrap=True
922
  )
923
 
924
- export_btn = gr.Button("💾 CSV 다운로드")
925
- file_output = gr.File(label="다운로드")
926
-
927
- # 정확도 검증 탭
928
- with gr.TabItem("🎯 정확도 검증"):
929
  gr.Markdown("""
930
- ### 실제 이미지 데이터로 정확도 검증
931
-
932
- 테스트 데이터셋을 배치 처리하여 예측 정확도를 측정합니다.
 
 
 
 
 
 
 
933
  """)
934
 
935
- with gr.Row():
936
- with gr.Column():
937
- test_folder = gr.Textbox(
938
- value="d:/Project/VIDraft/Shrimp/data/251015",
939
- label="테스트 데이터 폴더",
940
- info="251015_XX.jpg 형식의 이미지가 있는 폴더"
941
- )
942
-
943
- with gr.Row():
944
- test_pixel_scale = gr.Number(
945
- value=92,
946
- label="픽셀 크기 (px)",
947
- info="참조 자의 픽셀 크기"
948
- )
949
- test_cm_scale = gr.Number(
950
- value=1,
951
- label="실제 크기 (cm)",
952
- info="참조 자의 실제 크기"
953
- )
954
-
955
- test_depth_checkbox = gr.Checkbox(
956
- value=False,
957
- label="🔍 깊이 기반 원근 보정 활성화",
958
- info="Depth-Anything V2로 자동 원근 왜곡 보정"
959
- )
960
-
961
- test_btn = gr.Button(
962
- "🚀 배치 테스트 실행",
963
- variant="primary",
964
- size="lg"
965
- )
966
-
967
- with gr.Column():
968
- test_stats = gr.Markdown()
969
-
970
- test_plot = gr.Plot(label="예측 vs 실제 비교")
971
- test_results_df = gr.Dataframe(
972
- label="상세 결과",
973
- wrap=True
974
- )
975
-
976
- gr.Markdown("### 📸 시각화 결과 (예측 vs 실제)")
977
- test_gallery = gr.Gallery(
978
- label="검출 결과 이미지",
979
- show_label=True,
980
- columns=3,
981
- rows=2,
982
- height="auto",
983
- object_fit="contain"
984
- )
985
-
986
- # 정보 탭
987
- with gr.TabItem("ℹ️ 사용 방법"):
988
- gr.Markdown("""
989
- ## 📖 사용 가이드
990
-
991
- ### 1️⃣ 객체 검출
992
- 1. 새우 이미지를 업로드하세요
993
- 2. 신뢰도 임계값을 조정하세요 (기본값: 0.5)
994
- 3. **깊이 보정 활성화** (권장): 원근 왜곡 자동 보정
995
- 4. 스케일 보정: 실제 크기를 알고 있다면 픽셀-cm 비율을 설정하세요
996
- 5. "검출 실행" 버튼을 클릭하세요
997
-
998
- ### 2️⃣ 깊이 기반 원근 보정 (NEW! 🔥)
999
- - **Depth-Anything V2** 모델로 이미지의 깊이 맵 자동 생성
1000
- - 각 새우의 상대적 거리를 계산하여 스케일 자동 보정
1001
- - **원근 왜곡 효과를 자동으로 제거**하여 정확도 향상
1002
- - 깊이 맵 시각화: 파란색=가까움, 노란색=멀음
1003
- - 추가 장비나 마커 없이 단일 이미지만으로 작동
1004
-
1005
- ### 3️⃣ 스케일 보정
1006
- - 이미지에서 알고 있는 객체의 픽셀 크기와 실제 크기를 입력하세요
1007
- - 예: 자가 보인다면, 자의 픽셀 길이와 실제 길이(cm)를 입력
1008
- - 깊이 보정과 함께 사용하면 더욱 정확한 측정 가능
1009
-
1010
- ### 4️⃣ 결과 해석
1011
- - **초록색 박스**: 오차 < 10%
1012
- - **주황색 박스**: 오차 10-20%
1013
- - **빨간색 박스**: 오차 > 20%
1014
-
1015
- ### 5️⃣ 성능 평가
1016
- - "성능 평가" 탭에서 회귀 모델의 정확도를 확인하세요
1017
- - R², MAPE, MAE, RMSE 지표 제공
1018
-
1019
- ### 6️⃣ 정확도 검증 (NEW! 🔥)
1020
- - "정확도 검증" 탭에서 실제 이미지 데이터로 정확도 측정
1021
- - 테스트 데이터 폴더를 지정하고 배치 처리
1022
- - OCR로 전자저울 LCD에서 실제 무게 자동 읽기
1023
- - 예측 vs 실제 비교 차트 및 통계 제공
1024
-
1025
- ### 7️⃣ 데이터 내보내기
1026
- - "���측 데이터" 탭에서 CSV 파일로 다운로드 가능
1027
-
1028
- ---
1029
-
1030
- ## ⚙️ 시스템 정보
1031
-
1032
- - **검출 모델**: RT-DETR (Real-Time DEtection TRansformer)
1033
- - **깊이 추정 모델**: Depth-Anything V2 Small (Monocular Depth Estimation)
1034
- - **회귀 모델**: Power Law (W = a × L^b)
1035
- - **디바이스**: """ + ("GPU (CUDA)" if torch.cuda.is_available() else "CPU") + """
1036
- - **최적화**: CPU 모드, torch.no_grad(), FP32
1037
- - **원근 보정**: 깊이 맵 기반 자동 스케일 조정
1038
-
1039
- ## 🔧 문제 해결
1040
-
1041
- **검출이 안 될 때**:
1042
- - 신뢰도 임계값을 낮춰보세요 (0.3 이하)
1043
- - 이미지 품질을 확인하세요 (해상도, 밝기)
1044
-
1045
- **정확도가 낮을 때**:
1046
- - 스케일 보정을 정확히 입력하세요
1047
- - 새우 전용 fine-tuning 모델이 필요할 수 있습니다
1048
 
1049
- **속도가 느릴 때**:
1050
- - GPU 가속을 사용하세요 (HF Space: GPU T4)
1051
- - 이미지 크기를 줄이세요 (800x600 권장)
1052
- """)
1053
 
1054
  # 이벤트 연결
1055
- detect_btn.click(
1056
- process_image,
1057
- [input_img, conf_slider, pixel_scale, cm_scale, depth_checkbox],
1058
- [output_img, depth_img, stats, results_df]
1059
  )
1060
 
1061
- eval_btn.click(
1062
- evaluate_model,
1063
- [],
1064
- [eval_text, eval_plot]
1065
  )
1066
 
1067
- export_btn.click(
1068
- export_data,
1069
- [],
1070
- file_output
1071
- )
1072
 
1073
- test_btn.click(
1074
- process_test_dataset,
1075
- [test_folder, test_pixel_scale, test_cm_scale, test_depth_checkbox],
1076
- [test_stats, test_results_df, test_plot, test_gallery]
1077
- )
 
 
1078
 
1079
- # 실행
1080
  if __name__ == "__main__":
1081
- demo.queue(max_size=10) # CPU 최적화: 큐 크기 제한
1082
  demo.launch(
1083
- share=False,
1084
  server_name="0.0.0.0",
1085
- server_port=7860,
1086
- show_error=True
1087
  )
 
1
+ # -*- coding: utf-8 -*-
2
  """
3
+ 바운딩 박스 검출 테스트 페이지
4
+ VIDraft/Shrimp 전용 모델과 RT-DETR 범용 모델의 검출 결과 비교
5
  """
6
+ import sys
7
+ sys.stdout.reconfigure(encoding='utf-8')
8
 
9
  import gradio as gr
 
 
 
10
  from PIL import Image, ImageDraw, ImageFont
 
 
 
 
 
 
 
 
11
  import os
 
 
12
 
13
+ # VIDraft/Shrimp 전용 검출기
14
+ try:
15
+ from inference_sdk import InferenceHTTPClient, InferenceConfiguration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ vidraft_client = InferenceHTTPClient(
18
+ api_url="https://serverless.roboflow.com",
19
+ api_key="azcIL8KDJVJMYrsERzI7"
20
+ )
21
+ VIDRAFT_AVAILABLE = True
22
+ print("✅ VIDraft/Shrimp 모델 사용 가능")
23
+ except Exception as e:
24
+ VIDRAFT_AVAILABLE = False
25
+ print(f"❌ VIDraft/Shrimp 모델 사용 불가: {e}")
26
 
27
+ def detect_with_vidraft(image, confidence, iou_threshold):
28
+ """VIDraft/Shrimp 전용 모��로 검출"""
29
+ if not VIDRAFT_AVAILABLE:
30
+ return None, "❌ VIDraft/Shrimp 모델을 사용할 수 없습니다."
31
 
32
+ if image is None:
33
+ return None, "⚠️ 이미지를 업로드하세요."
 
34
 
35
+ try:
36
+ # 임시 파일로 저장
37
+ import tempfile
38
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
39
+ if image.mode != 'RGB':
40
+ image = image.convert('RGB')
41
+ image.save(tmp.name, quality=95)
42
+ tmp_path = tmp.name
43
+
44
+ # API 호출 with configuration
45
+ custom_config = InferenceConfiguration(
46
+ confidence_threshold=confidence,
47
+ iou_threshold=iou_threshold
48
+ )
49
 
50
+ with vidraft_client.use_configuration(custom_config):
51
+ result = vidraft_client.infer(tmp_path, model_id="shrimp-konvey/2")
52
 
53
+ # 임시 파일 삭제
54
+ os.unlink(tmp_path)
 
 
55
 
56
+ # 결과 그리기
57
+ img = image.copy()
58
+ draw = ImageDraw.Draw(img)
 
59
 
60
+ try:
61
+ font = ImageFont.truetype("arial.ttf", 14)
62
+ except:
63
+ font = ImageFont.load_default()
64
 
65
+ predictions = result["predictions"]
66
+ detected_count = 0
 
 
 
 
 
67
 
68
+ for pred in predictions:
69
+ if pred["confidence"] < confidence:
70
+ continue
71
 
72
+ detected_count += 1
 
 
 
 
 
73
 
74
+ x = pred["x"]
75
+ y = pred["y"]
76
+ w = pred["width"]
77
+ h = pred["height"]
78
+ conf = pred["confidence"]
79
 
80
+ # 바운딩 박스 좌표
81
+ x1 = x - w/2
82
+ y1 = y - h/2
83
+ x2 = x + w/2
84
+ y2 = y + h/2
85
 
86
+ # 신뢰도에 따라 색상
87
+ if conf > 0.8:
88
+ color = "lime"
89
+ elif conf > 0.6:
90
+ color = "orange"
91
+ else:
92
+ color = "yellow"
93
 
94
+ # 박스 그리기
95
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
 
96
 
97
+ # 라벨
98
+ label = f"#{detected_count} {conf:.0%}"
99
+ bbox = draw.textbbox((x1, y1 - 25), label, font=font)
100
+ draw.rectangle(bbox, fill=color)
101
+ draw.text((x1, y1 - 25), label, fill="black", font=font)
102
 
103
+ # 헤더
104
+ header = f"VIDraft/Shrimp: {detected_count}마리 검출"
105
+ header_bbox = draw.textbbox((10, 10), header, font=font)
106
+ draw.rectangle([5, 5, header_bbox[2]+10, header_bbox[3]+10], fill="black", outline="lime", width=2)
107
+ draw.text((10, 10), header, fill="lime", font=font)
108
 
109
+ info = f"""
110
+ ### 📊 VIDraft/Shrimp ���델 검출 결과
 
 
111
 
112
+ - **검출 수**: {detected_count}마리
113
+ - **전체 예측**: {len(predictions)}개
114
+ - **신뢰도 임계값**: {confidence:.0%}
115
+ - **IoU 임계값**: {iou_threshold:.0%}
116
+ - **처리 시간**: {result['time']:.2f}초
117
+ """
118
 
119
+ return img, info
 
120
 
121
+ except Exception as e:
122
+ return None, f"❌ 오류 발생: {str(e)}"
 
 
 
 
123
 
124
+ def detect_with_rtdetr(image, confidence):
125
+ """RT-DETR로 검출 (간단 버전)"""
126
+ if image is None:
127
+ return None, "⚠️ 이미지를 업로드하세요."
 
128
 
129
+ try:
130
+ from transformers import RTDetrForObjectDetection, RTDetrImageProcessor
131
+ import torch
132
+
133
+ # 모델 로드 (캐시 사용)
134
+ if not hasattr(detect_with_rtdetr, 'model'):
135
+ print("🔄 RT-DETR 모델 로딩 중...")
136
+ processor = RTDetrImageProcessor.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
137
+ model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd_coco_o365")
138
+ model.eval()
139
+ detect_with_rtdetr.processor = processor
140
+ detect_with_rtdetr.model = model
141
+ print("✅ RT-DETR 로딩 완료")
142
+
143
+ processor = detect_with_rtdetr.processor
144
+ model = detect_with_rtdetr.model
145
 
146
  # 추론
147
+ inputs = processor(images=image, return_tensors="pt")
148
+ with torch.no_grad():
149
+ outputs = model(**inputs)
150
 
151
+ target_sizes = torch.tensor([image.size[::-1]])
152
+ results = processor.post_process_object_detection(
 
153
  outputs,
154
  target_sizes=target_sizes,
155
+ threshold=confidence
156
  )[0]
157
 
158
+ # 결과 그리기
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  img = image.copy()
160
  draw = ImageDraw.Draw(img)
161
 
 
162
  try:
163
+ font = ImageFont.truetype("arial.ttf", 14)
164
  except:
165
  font = ImageFont.load_default()
166
 
167
+ detected_count = len(results["scores"])
 
168
 
169
+ for idx, (score, label, box) in enumerate(zip(results["scores"], results["labels"], results["boxes"]), 1):
170
+ x1, y1, x2, y2 = box.tolist()
171
+ conf = score.item()
172
 
173
+ # 색상
174
+ if conf > 0.8:
175
+ color = "cyan"
176
+ elif conf > 0.6:
177
+ color = "magenta"
178
+ else:
179
+ color = "yellow"
180
+
181
+ # 박스
182
  draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
183
 
184
  # 라벨
185
+ label_text = f"#{idx} {conf:.0%}"
186
+ bbox = draw.textbbox((x1, y1 - 25), label_text, font=font)
 
 
187
  draw.rectangle(bbox, fill=color)
188
+ draw.text((x1, y1 - 25), label_text, fill="black", font=font)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ # 헤더
191
+ header = f"RT-DETR: {detected_count}개 검출"
192
+ header_bbox = draw.textbbox((10, 10), header, font=font)
193
+ draw.rectangle([5, 5, header_bbox[2]+10, header_bbox[3]+10], fill="black", outline="cyan", width=2)
194
+ draw.text((10, 10), header, fill="cyan", font=font)
195
 
196
+ info = f"""
197
+ ### 📊 RT-DETR 범용 모델 검출 결과
 
 
 
 
 
 
 
 
 
 
198
 
199
+ - **검출 수**: {detected_count}개
200
+ - **신뢰도 임계값**: {confidence:.0%}
 
 
 
 
 
 
 
201
 
202
+ ⚠️ **참고**: RT-DETR은 범용 객체 검출 모델입니다. 새우 검출은 VIDraft/Shrimp 모델을 사용하세요.
203
+ """
 
 
 
204
 
205
+ return img, info
 
 
 
 
206
 
207
  except Exception as e:
208
+ return None, f"❌ 오류 발생: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ # Gradio 인터페이스
211
+ with gr.Blocks(title="🦐 바운딩 박스 검출 테스트", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  gr.Markdown("""
214
+ # 🦐 바운딩 박스 검출 비교 테스트
215
 
216
+ VIDraft/Shrimp 전용 모델과 RT-DETR 범용 모델의 검출 성능을 비교합니다.
 
 
217
 
218
  ---
219
  """)
220
 
221
+ with gr.Row():
222
+ with gr.Column():
223
+ input_image = gr.Image(label="입력 이미지", type="pil")
224
+ confidence_slider = gr.Slider(
225
+ 0.1, 0.9, 0.5,
226
+ label="신뢰도 임계값 (Confidence)",
227
+ info="낮을수록 더 많이 검출"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  )
229
+ iou_slider = gr.Slider(
230
+ 0.1, 0.9, 0.5,
231
+ label="IoU 임계값 (Overlap)",
232
+ info="겹치는 박스 제거 기준 (높을수록 더 많이 유지)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  )
234
 
235
+ with gr.Column():
236
+ gr.Markdown("### 📝 사용 방법")
 
 
 
237
  gr.Markdown("""
238
+ 1. 이미지 업로드
239
+ 2. 파라미터 조정:
240
+ - **Confidence**: 검출 신뢰도 (낮을수록 많이 검출)
241
+ - **IoU**: 중복 박스 제거 기준 (NMS)
242
+ 3. 버튼 클릭하여 검출
243
+
244
+ **색상 의미:**
245
+ - **녹색/청록**: 높은 신뢰도 (>80%)
246
+ - **주황/자홍**: 중간 신뢰도 (60-80%)
247
+ - **노란색**: 낮은 신뢰도 (<60%)
248
  """)
249
 
250
+ with gr.Tabs():
251
+ with gr.TabItem("🤖 VIDraft/Shrimp (새우 전용)"):
252
+ vidraft_btn = gr.Button("🚀 VIDraft/Shrimp 모델로 검출", variant="primary", size="lg")
253
+ vidraft_result = gr.Image(label="검출 결과")
254
+ vidraft_info = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ with gr.TabItem("🔍 RT-DETR (범용)"):
257
+ rtdetr_btn = gr.Button("🚀 RT-DETR로 검출", variant="secondary", size="lg")
258
+ rtdetr_result = gr.Image(label="검출 결과")
259
+ rtdetr_info = gr.Markdown()
260
 
261
  # 이벤트 연결
262
+ vidraft_btn.click(
263
+ detect_with_vidraft,
264
+ [input_image, confidence_slider, iou_slider],
265
+ [vidraft_result, vidraft_info]
266
  )
267
 
268
+ rtdetr_btn.click(
269
+ detect_with_rtdetr,
270
+ [input_image, confidence_slider],
271
+ [rtdetr_result, rtdetr_info]
272
  )
273
 
274
+ gr.Markdown("""
275
+ ---
 
 
 
276
 
277
+ ### 💡 팁
278
+
279
+ - **수조 이미지**: VIDraft/Shrimp 모델이 훨씬 정확합니다 (새우 전용 학습)
280
+ - **측정용 이미지**: RT-DETR 범용 모델을 사용하세요
281
+ - **검출 안 됨**: 신뢰도를 낮춰보세요 (0.3~0.4)
282
+ - **중복 박스**: IoU 임계값을 조정하세요 (VIDraft/Shrimp 모델만)
283
+ """)
284
 
 
285
  if __name__ == "__main__":
 
286
  demo.launch(
 
287
  server_name="0.0.0.0",
288
+ server_port=7860, # Hugging Face default port
289
+ share=False
290
  )
requirements.txt CHANGED
@@ -4,19 +4,10 @@ gradio>=4.16.0
4
  # Deep Learning
5
  torch>=2.0.0
6
  torchvision>=0.15.0
7
- transformers>=4.41.0 # Required for Depth-Anything-V2
8
 
9
  # Image Processing
10
  pillow>=10.0.0
11
- opencv-python-headless>=4.9.0
12
 
13
- # Data Science
14
- numpy>=1.24.0
15
- pandas>=2.0.0
16
-
17
- # Visualization
18
- plotly>=5.17.0
19
- matplotlib>=3.7.0 # For depth map visualization
20
-
21
- # Excel reading
22
- openpyxl>=3.1.0
 
4
  # Deep Learning
5
  torch>=2.0.0
6
  torchvision>=0.15.0
7
+ transformers>=4.41.0
8
 
9
  # Image Processing
10
  pillow>=10.0.0
 
11
 
12
+ # Inference
13
+ inference-sdk>=0.9.0