minh9972t12 commited on
Commit
e6535be
Β·
1 Parent(s): 6640d16

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +249 -178
main.py CHANGED
@@ -1,11 +1,11 @@
1
  import io
2
- from typing import List
3
  import uvicorn
4
  import numpy as np
5
  import uuid
6
  from datetime import datetime
7
  from fastapi import FastAPI, UploadFile, File, HTTPException, Form
8
- from fastapi.responses import JSONResponse, FileResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.staticfiles import StaticFiles
11
  from PIL import Image
@@ -15,11 +15,14 @@ from src.detection import YOLOv11Detector
15
  from src.comparison import DamageComparator
16
  from src.visualization import DamageVisualizer
17
  from pathlib import Path
 
 
 
18
 
19
  app = FastAPI(
20
  title="Car Damage Detection API",
21
- description="YOLOv11-based car damage detection and comparison system with PyTorch and ONNX support (Optimized for ONNX Runtime v1.19 + opset 21)",
22
- version="1.2.0"
23
  )
24
 
25
  # Add CORS middleware
@@ -31,10 +34,10 @@ app.add_middleware(
31
  allow_headers=["*"],
32
  )
33
 
34
- # Initialize components
35
  detector = None
36
- comparator = DamageComparator()
37
- visualizer = DamageVisualizer()
38
 
39
  # Model paths mapping - PT and ONNX versions
40
  MODEL_PATHS = {
@@ -81,7 +84,6 @@ PT_TO_ONNX_MAPPING = {
81
  6: 11 # Medium v3 -> ONNX
82
  }
83
 
84
-
85
  def get_optimal_model_index(select_models: int, prefer_onnx: bool = True) -> int:
86
  """
87
  Enhanced model selection with performance optimization info
@@ -116,14 +118,10 @@ def get_optimal_model_index(select_models: int, prefer_onnx: bool = True) -> int
116
  # No suitable file found
117
  raise FileNotFoundError(f"Requested PT model index {select_models} not found at {MODEL_PATHS.get(select_models)}")
118
 
119
-
120
  def load_detector(select_models: int = 2, prefer_onnx: bool = True):
121
  """
122
-
123
- Args:
124
- select_models: Model selection
125
-
126
- prefer_onnx: Whether to prefer ONNX format for fallback
127
  """
128
  global detector, comparator, visualizer
129
 
@@ -144,7 +142,20 @@ def load_detector(select_models: int = 2, prefer_onnx: bool = True):
144
  with open(temp_config, 'w') as f:
145
  yaml.dump(config, f, default_flow_style=False)
146
 
147
- # Reload all components with new config
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  detector = YOLOv11Detector(config_path=temp_config)
149
  comparator = DamageComparator(config_path=temp_config)
150
  visualizer = DamageVisualizer(config_path=temp_config)
@@ -152,8 +163,8 @@ def load_detector(select_models: int = 2, prefer_onnx: bool = True):
152
  # Log model info with optimization status
153
  model_type = "ONNX" if MODEL_PATHS[actual_model_index].endswith('.onnx') else "PyTorch"
154
  model_labels = [
155
- "Small v1", "Small v2","Small v3", "Medium v1", "Medium v2", "Medium v3",
156
- "Small v1 ONNX", "Small v2 ONNX","Small v3 ONNX", "Medium v1 ONNX", "Medium v2 ONNX", "Medium v3 ONNX"
157
  ]
158
 
159
  if 0 <= select_models < len(model_labels):
@@ -161,23 +172,15 @@ def load_detector(select_models: int = 2, prefer_onnx: bool = True):
161
  else:
162
  raise ValueError(f"select_models={select_models} must be 0-11")
163
 
164
- # Enhanced logging for optimization status
165
  optimization_status = "πŸš€ MAXIMUM OPTIMIZATIONS" if model_type == "ONNX" else "πŸ“¦ Standard PyTorch"
166
  print(f"Loaded {model_size} model in {model_type} format - {optimization_status}")
167
-
168
- # Show performance info for ONNX models
169
- if model_type == "ONNX" and hasattr(detector, 'get_performance_info'):
170
- perf_info = detector.get_performance_info()
171
- if 'providers' in perf_info:
172
- print(f"Provider: {perf_info['providers'][0]}")
173
- if 'optimization_level' in perf_info:
174
- print(f"Graph optimizations: {perf_info['optimization_level']}")
175
 
176
  return detector
177
 
178
 
179
  # Initialize default detector with medium model (preferring ONNX for performance)
180
- print("πŸš€ Initializing API with optimized ONNX Runtime support...")
181
  detector = load_detector(2, prefer_onnx=True)
182
  comparator = DamageComparator(config_path=CONFIG_PATHS[2])
183
  visualizer = DamageVisualizer(config_path=CONFIG_PATHS[2])
@@ -196,16 +199,18 @@ app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
196
  async def root():
197
  """Root endpoint with enhanced model info"""
198
  return {
199
- "message": "Car Damage Detection API with YOLOv11 (ONNX Runtime v1.19 optimized)",
200
- "version": "1.2.0",
201
  "optimizations": {
202
  "onnx_runtime": "v1.19+ with opset 21 support",
 
 
203
  "performance_features": [
204
  "Graph optimizations (ALL level)",
205
- "Dynamic thread pool with load balancing",
206
- "Memory arena optimizations",
207
- "CPU spinning for low latency",
208
- "OpenMP with ACTIVE wait policy"
209
  ]
210
  },
211
  "model_options": {
@@ -223,7 +228,7 @@ async def root():
223
  "endpoints": {
224
  "/docs": "API documentation",
225
  "/detect": "Single/Multi image detection",
226
- "/compare": "Compare before/after images (6 pairs)",
227
  "/uploads/{filename}": "Access saved visualization images",
228
  "/health": "Health check",
229
  "/model-info": "Get current model information",
@@ -238,7 +243,9 @@ async def health_check():
238
  health_info = {
239
  "status": "healthy",
240
  "model": "YOLOv11",
241
- "backend": "ONNX/PyTorch"
 
 
242
  }
243
 
244
  if detector and hasattr(detector, 'get_performance_info'):
@@ -251,6 +258,31 @@ async def health_check():
251
  return health_info
252
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  @app.post("/detect")
256
  async def detect_single_image(
@@ -259,23 +291,14 @@ async def detect_single_image(
259
  select_models: int = Form(2),
260
  prefer_onnx: bool = Form(True)
261
  ):
262
- """
263
- Multi-view detection with ONNX Runtime optimizations
264
- Args:
265
- file: Single image (backward compatibility)
266
- files: Multiple images for multi-view detection
267
- select_models: Model selection
268
- - 0-4: PyTorch models (standard performance)
269
- - 5-8: ONNX models (maximum optimizations)
270
- prefer_onnx: Whether to prefer ONNX format (default: True for better performance)
271
- """
272
  try:
273
  # Validate select_models
274
  if select_models not in list(range(0, 12)):
275
  raise HTTPException(status_code=400,
276
- detail="select_models must be 0-10 (0-5=PyTorch, 6-11=ONNX optimized)")
277
 
278
- # Load appropriate detector
279
  current_detector = load_detector(select_models, prefer_onnx)
280
 
281
  # Case 1: Single image (backward compatibility)
@@ -296,7 +319,6 @@ async def detect_single_image(
296
  output_path = UPLOADS_DIR / filename
297
  cv2.imwrite(str(output_path), visualized)
298
 
299
- # Enhanced response with optimization info
300
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
301
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
302
 
@@ -311,52 +333,39 @@ async def detect_single_image(
311
  },
312
  "visualized_image_path": f"uploads/{filename}",
313
  "visualized_image_url": f"http://localhost:8000/uploads/{filename}",
314
- "filename": filename,
315
- "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (6-11) for better performance"
316
  })
317
 
318
- # Case 2: Multiple images - MULTI-VIEW DETECTION with ReID
319
- elif files is not None and len(files) > 0:
320
- print(f"\nMulti-view detection with {len(files)} images")
321
-
322
- images_list = []
323
  detections_list = []
 
324
 
325
- # Process all images
326
- for idx, img_file in enumerate(files):
327
- contents = await img_file.read()
328
  image = Image.open(io.BytesIO(contents)).convert("RGB")
329
  image_np = np.array(image)
330
  image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
331
-
332
- images_list.append(image_bgr)
333
  detections = current_detector.detect(image_bgr)
334
  detections_list.append(detections)
335
 
336
- print(f" View {idx + 1}: {len(detections['boxes'])} detections")
337
-
338
- # DEDUPLICATION using ReID
339
- print("\nPerforming cross-view deduplication...")
340
- unique_damages = comparator.deduplicate_detections_across_views(
341
- detections_list, images_list
342
- )
343
 
344
  # Create combined visualization
345
- combined_height = max(img.shape[0] for img in images_list)
346
- combined_width = sum(img.shape[1] for img in images_list)
347
- combined_img = np.ones((combined_height, combined_width, 3), dtype=np.uint8) * 255
348
-
 
349
  x_offset = 0
350
- for img_idx, (image, detections) in enumerate(zip(images_list, detections_list)):
351
- # Resize if needed
352
- h, w = image.shape[:2]
353
- if h != combined_height:
354
- scale = combined_height / h
355
- new_w = int(w * scale)
356
- image = cv2.resize(image, (new_w, combined_height))
357
- w = new_w
358
 
359
- # Draw on combined image
 
 
 
 
360
  combined_img[:, x_offset:x_offset + w] = image
361
 
362
  # Draw detections with unique IDs
@@ -395,16 +404,17 @@ async def detect_single_image(
395
  output_path = UPLOADS_DIR / filename
396
  cv2.imwrite(str(output_path), combined_img)
397
 
398
- # Return results with optimization info
399
  total_detections = sum(len(d['boxes']) for d in detections_list)
400
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
401
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
402
 
403
  return JSONResponse({
404
  "status": "success",
405
- "mode": "multi_view_with_reid",
406
  "model_type": model_type,
407
  "optimization_status": optimization_status,
 
408
  "total_detections_all_views": total_detections,
409
  "unique_damages_count": len(unique_damages),
410
  "unique_damages": {
@@ -420,8 +430,7 @@ async def detect_single_image(
420
  "visualized_image_path": f"uploads/{filename}",
421
  "visualized_image_url": f"http://localhost:8000/uploads/{filename}",
422
  "message": f"Detected {total_detections} damages across {len(files)} views, "
423
- f"identified {len(unique_damages)} unique damages using ReID",
424
- "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (5-8) for better performance"
425
  })
426
 
427
  else:
@@ -431,6 +440,84 @@ async def detect_single_image(
431
  raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}")
432
 
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  @app.post("/compare")
435
  async def compare_vehicle_damages(
436
  # Before delivery images (6 positions)
@@ -452,34 +539,24 @@ async def compare_vehicle_damages(
452
  prefer_onnx: bool = Form(True)
453
  ):
454
  """
455
- Enhanced comparison with ONNX Runtime optimizations and ReID
456
- Args:
457
- before_1-6: Before delivery images from 6 positions
458
- after_1-6: After delivery images from 6 positions
459
- select_models: Model selection (0-4=PyTorch, 5-8=ONNX optimized)
460
- prefer_onnx: Whether to prefer ONNX format (default: True)
461
  """
462
  try:
463
  # Validate select_models
464
  if select_models not in list(range(0, 12)):
465
  raise HTTPException(status_code=400,
466
- detail="select_models must be 0-11 (0-5=PyTorch, 6-11=ONNX optimized)")
467
 
468
- # Load appropriate detector
469
  current_detector = load_detector(select_models, prefer_onnx)
470
 
471
  before_images = [before_1, before_2, before_3, before_4, before_5, before_6]
472
  after_images = [after_1, after_2, after_3, after_4, after_5, after_6]
473
 
474
- position_results = []
475
- all_visualizations = []
476
- image_pairs = []
477
-
478
- # Collect all before/after images and detections
479
- all_before_images = []
480
- all_after_images = []
481
- all_before_detections = []
482
- all_after_detections = []
483
 
484
  # Overall statistics
485
  total_new_damages = 0
@@ -489,89 +566,72 @@ async def compare_vehicle_damages(
489
  session_id = str(uuid.uuid4())[:8]
490
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
491
 
492
- # Process each position pair
493
- for i in range(6):
494
- before_contents = await before_images[i].read()
495
- after_contents = await after_images[i].read()
496
-
497
- before_img = Image.open(io.BytesIO(before_contents)).convert("RGB")
498
- after_img = Image.open(io.BytesIO(after_contents)).convert("RGB")
499
-
500
- before_np = np.array(before_img)
501
- after_np = np.array(after_img)
502
-
503
- before_bgr = cv2.cvtColor(before_np, cv2.COLOR_RGB2BGR)
504
- after_bgr = cv2.cvtColor(after_np, cv2.COLOR_RGB2BGR)
505
-
506
- # Store for multi-view analysis
507
- all_before_images.append(before_bgr)
508
- all_after_images.append(after_bgr)
509
-
510
- image_pairs.append((before_bgr, after_bgr))
511
-
512
- # Detect damages
513
- before_detections = current_detector.detect(before_bgr)
514
- after_detections = current_detector.detect(after_bgr)
515
-
516
- all_before_detections.append(before_detections)
517
- all_after_detections.append(after_detections)
518
-
519
- # Enhanced comparison with ReID
520
- comparison = comparator.analyze_damage_status(
521
- before_detections, after_detections,
522
- before_bgr, after_bgr
523
- )
524
-
525
- # Update statistics
526
- total_new_damages += len(comparison['new_damages'])
527
- total_existing_damages += len(comparison['repaired_damages'])
528
- total_matched_damages += len(comparison['matched_damages'])
529
 
530
- # Create visualization
531
- vis_img = visualizer.create_comparison_visualization(
532
- before_bgr, after_bgr,
533
- before_detections, after_detections,
534
- comparison
535
- )
536
-
537
- vis_filename = f"comparison_{timestamp_str}_{session_id}_pos{i + 1}.jpg"
538
- vis_path = UPLOADS_DIR / vis_filename
539
- cv2.imwrite(str(vis_path), vis_img)
540
-
541
- vis_url = f"http://localhost:8000/uploads/{vis_filename}"
542
- all_visualizations.append(vis_url)
543
-
544
- # Store position result with ReID info
545
- position_results.append({
546
- f"position_{i + 1}": {
547
- "case": comparison['case'],
548
- "message": comparison['message'],
549
- "statistics": comparison['statistics'],
550
- "new_damages": comparison['new_damages'],
551
- "matched_damages": comparison['matched_damages'],
552
- "repaired_damages": comparison['repaired_damages'],
553
- "using_reid": comparison['statistics'].get('using_reid', True),
554
- "visualization_path": f"uploads/{vis_filename}",
555
- "visualization_url": vis_url,
556
- "filename": vis_filename
557
- }
558
- })
559
 
560
- # Deduplicate BEFORE damages across all 6 views
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  unique_before = comparator.deduplicate_detections_across_views(
562
  all_before_detections, all_before_images
563
  )
564
 
565
- # Deduplicate AFTER damages across all 6 views
566
  unique_after = comparator.deduplicate_detections_across_views(
567
  all_after_detections, all_after_images
568
  )
569
 
570
- print(f"Before: {sum(len(d['boxes']) for d in all_before_detections)} detections β†’ {len(unique_before)} unique")
571
- print(f"After: {sum(len(d['boxes']) for d in all_after_detections)} detections β†’ {len(unique_after)} unique")
 
572
 
573
  # Determine overall case with deduplication
574
- actual_new_damages = len(unique_after) - len(unique_before)
575
 
576
  overall_case = "CASE_3_SUCCESS"
577
  overall_message = "Successful delivery - No damage detected"
@@ -584,7 +644,7 @@ async def compare_vehicle_damages(
584
  overall_message = "Existing damages from beginning β†’ Delivery completed"
585
 
586
  # Create summary grid
587
- grid_results = [res[f"position_{i + 1}"] for i, res in enumerate(position_results)]
588
  grid_img = visualizer.create_summary_grid(grid_results, image_pairs)
589
 
590
  grid_filename = f"summary_grid_{timestamp_str}_{session_id}.jpg"
@@ -595,7 +655,12 @@ async def compare_vehicle_damages(
595
 
596
  timestamp = datetime.now().isoformat()
597
 
598
- # Enhanced response with optimization info
 
 
 
 
 
599
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
600
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
601
 
@@ -606,6 +671,8 @@ async def compare_vehicle_damages(
606
  "model_type": model_type,
607
  "optimization_status": optimization_status,
608
  "reid_enabled": True,
 
 
609
  "overall_result": {
610
  "case": overall_case,
611
  "message": overall_message,
@@ -615,10 +682,11 @@ async def compare_vehicle_damages(
615
  "total_repaired_damages": int(total_existing_damages),
616
  "unique_damages_before": int(len(unique_before)),
617
  "unique_damages_after": int(len(unique_after)),
618
- "actual_new_unique_damages": int(max(0, len(unique_after) - len(unique_before)))
619
  }
620
  },
621
  "deduplication_info": {
 
622
  "before_total_detections": int(sum(len(d['boxes']) for d in all_before_detections)),
623
  "before_unique_damages": int(len(unique_before)),
624
  "after_total_detections": int(sum(len(d['boxes']) for d in all_after_detections)),
@@ -635,14 +703,17 @@ async def compare_vehicle_damages(
635
  "suggested_action": "Investigate delivery process" if actual_new_damages > 0
636
  else "Proceed with delivery completion"
637
  },
638
- "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (5-8) for better performance"
639
  })
640
 
641
  except Exception as e:
 
 
 
 
642
  raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
643
 
644
 
645
-
646
  if __name__ == "__main__":
647
  import os
648
  uvicorn.run(
@@ -651,4 +722,4 @@ if __name__ == "__main__":
651
  port=int(os.environ.get("PORT", 7860)),
652
  reload=False,
653
  log_level="info"
654
- )
 
1
  import io
2
+ from typing import List, Dict
3
  import uvicorn
4
  import numpy as np
5
  import uuid
6
  from datetime import datetime
7
  from fastapi import FastAPI, UploadFile, File, HTTPException, Form
8
+ from fastapi.responses import JSONResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.staticfiles import StaticFiles
11
  from PIL import Image
 
15
  from src.comparison import DamageComparator
16
  from src.visualization import DamageVisualizer
17
  from pathlib import Path
18
+ from concurrent.futures import ThreadPoolExecutor, as_completed
19
+ import torch
20
+ import gc
21
 
22
  app = FastAPI(
23
  title="Car Damage Detection API",
24
+ description="YOLOv11-based car damage detection with DINOv2 ReID (Memory Optimized)",
25
+ version="1.3.0"
26
  )
27
 
28
  # Add CORS middleware
 
34
  allow_headers=["*"],
35
  )
36
 
37
+ # GLOBAL COMPONENTS - Load once at startup
38
  detector = None
39
+ comparator = None
40
+ visualizer = None
41
 
42
  # Model paths mapping - PT and ONNX versions
43
  MODEL_PATHS = {
 
84
  6: 11 # Medium v3 -> ONNX
85
  }
86
 
 
87
  def get_optimal_model_index(select_models: int, prefer_onnx: bool = True) -> int:
88
  """
89
  Enhanced model selection with performance optimization info
 
118
  # No suitable file found
119
  raise FileNotFoundError(f"Requested PT model index {select_models} not found at {MODEL_PATHS.get(select_models)}")
120
 
 
121
  def load_detector(select_models: int = 2, prefer_onnx: bool = True):
122
  """
123
+ Load detector with optimized ONNX Runtime v1.19 support
124
+ IMPORTANT: This loads GLOBAL instances that are shared across threads
 
 
 
125
  """
126
  global detector, comparator, visualizer
127
 
 
142
  with open(temp_config, 'w') as f:
143
  yaml.dump(config, f, default_flow_style=False)
144
 
145
+ # Clear previous models from memory before loading new ones
146
+ if detector is not None:
147
+ del detector
148
+ if comparator is not None:
149
+ del comparator
150
+ if visualizer is not None:
151
+ del visualizer
152
+
153
+ # Force garbage collection
154
+ gc.collect()
155
+ if torch.cuda.is_available():
156
+ torch.cuda.empty_cache()
157
+
158
+ # Load all components with new config
159
  detector = YOLOv11Detector(config_path=temp_config)
160
  comparator = DamageComparator(config_path=temp_config)
161
  visualizer = DamageVisualizer(config_path=temp_config)
 
163
  # Log model info with optimization status
164
  model_type = "ONNX" if MODEL_PATHS[actual_model_index].endswith('.onnx') else "PyTorch"
165
  model_labels = [
166
+ "Small v1", "Small v2", "Small v3", "Medium v1", "Medium v2", "Medium v3",
167
+ "Small v1 ONNX", "Small v2 ONNX", "Small v3 ONNX", "Medium v1 ONNX", "Medium v2 ONNX", "Medium v3 ONNX"
168
  ]
169
 
170
  if 0 <= select_models < len(model_labels):
 
172
  else:
173
  raise ValueError(f"select_models={select_models} must be 0-11")
174
 
 
175
  optimization_status = "πŸš€ MAXIMUM OPTIMIZATIONS" if model_type == "ONNX" else "πŸ“¦ Standard PyTorch"
176
  print(f"Loaded {model_size} model in {model_type} format - {optimization_status}")
177
+ print(f"βœ… DINOv2 ReID enabled for damage comparison")
 
 
 
 
 
 
 
178
 
179
  return detector
180
 
181
 
182
  # Initialize default detector with medium model (preferring ONNX for performance)
183
+ print("πŸš€ Initializing API with optimized ONNX Runtime and DINOv2 ReID support...")
184
  detector = load_detector(2, prefer_onnx=True)
185
  comparator = DamageComparator(config_path=CONFIG_PATHS[2])
186
  visualizer = DamageVisualizer(config_path=CONFIG_PATHS[2])
 
199
  async def root():
200
  """Root endpoint with enhanced model info"""
201
  return {
202
+ "message": "Car Damage Detection API with YOLOv11 + DINOv2 ReID (Memory Optimized)",
203
+ "version": "1.3.0",
204
  "optimizations": {
205
  "onnx_runtime": "v1.19+ with opset 21 support",
206
+ "reid_model": "DINOv2 (Meta) - Superior visual feature extraction",
207
+ "memory_management": "Global model loading with ThreadPoolExecutor",
208
  "performance_features": [
209
  "Graph optimizations (ALL level)",
210
+ "DINOv2 ReID for cross-view damage matching",
211
+ "Memory-efficient threading",
212
+ "torch.no_grad() context for inference",
213
+ "Automatic CUDA cache clearing"
214
  ]
215
  },
216
  "model_options": {
 
228
  "endpoints": {
229
  "/docs": "API documentation",
230
  "/detect": "Single/Multi image detection",
231
+ "/compare": "Compare before/after images (6 pairs) with DINOv2 ReID",
232
  "/uploads/{filename}": "Access saved visualization images",
233
  "/health": "Health check",
234
  "/model-info": "Get current model information",
 
243
  health_info = {
244
  "status": "healthy",
245
  "model": "YOLOv11",
246
+ "reid": "DINOv2",
247
+ "backend": "ONNX/PyTorch",
248
+ "memory_optimization": "ThreadPoolExecutor with global models"
249
  }
250
 
251
  if detector and hasattr(detector, 'get_performance_info'):
 
258
  return health_info
259
 
260
 
261
+ @app.get("/model-info")
262
+ async def get_model_info():
263
+ """Get comprehensive information about currently loaded model"""
264
+ if detector is None:
265
+ return {"error": "No model loaded"}
266
+
267
+ model_path = detector.model_path
268
+ model_type = "ONNX" if model_path.endswith('.onnx') else "PyTorch"
269
+
270
+ info = {
271
+ "model_path": model_path,
272
+ "model_type": model_type,
273
+ "confidence_threshold": detector.confidence,
274
+ "iou_threshold": detector.iou_threshold,
275
+ "classes": detector.classes,
276
+ "reid_model": "DINOv2",
277
+ "optimization_status": "Optimized" if model_type == "ONNX" else "Standard"
278
+ }
279
+
280
+ if hasattr(detector, 'get_performance_info'):
281
+ perf_info = detector.get_performance_info()
282
+ info.update(perf_info)
283
+
284
+ return info
285
+
286
 
287
  @app.post("/detect")
288
  async def detect_single_image(
 
291
  select_models: int = Form(2),
292
  prefer_onnx: bool = Form(True)
293
  ):
294
+ """Multi-view detection with ONNX Runtime optimizations and DINOv2 ReID"""
 
 
 
 
 
 
 
 
 
295
  try:
296
  # Validate select_models
297
  if select_models not in list(range(0, 12)):
298
  raise HTTPException(status_code=400,
299
+ detail="select_models must be 0-8 (0-4=PyTorch, 5-8=ONNX optimized)")
300
 
301
+ # Load appropriate detector (if different from current)
302
  current_detector = load_detector(select_models, prefer_onnx)
303
 
304
  # Case 1: Single image (backward compatibility)
 
319
  output_path = UPLOADS_DIR / filename
320
  cv2.imwrite(str(output_path), visualized)
321
 
 
322
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
323
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
324
 
 
333
  },
334
  "visualized_image_path": f"uploads/{filename}",
335
  "visualized_image_url": f"http://localhost:8000/uploads/{filename}",
336
+ "filename": filename
 
337
  })
338
 
339
+ # Case 2: Multiple images with DINOv2 deduplication
340
+ elif files is not None:
 
 
 
341
  detections_list = []
342
+ images = []
343
 
344
+ for idx, f in enumerate(files):
345
+ contents = await f.read()
 
346
  image = Image.open(io.BytesIO(contents)).convert("RGB")
347
  image_np = np.array(image)
348
  image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
349
+ images.append(image_bgr)
 
350
  detections = current_detector.detect(image_bgr)
351
  detections_list.append(detections)
352
 
353
+ # Deduplicate across views using DINOv2
354
+ unique_damages = comparator.deduplicate_detections_across_views(detections_list, images)
 
 
 
 
 
355
 
356
  # Create combined visualization
357
+ heights = [img.shape[0] for img in images]
358
+ widths = [img.shape[1] for img in images]
359
+ max_height = max(heights)
360
+ total_width = sum(widths)
361
+ combined_img = np.zeros((max_height, total_width, 3), dtype=np.uint8)
362
  x_offset = 0
 
 
 
 
 
 
 
 
363
 
364
+ for img_idx, image in enumerate(images):
365
+ h, w = image.shape[:2]
366
+ if h != max_height:
367
+ image = cv2.resize(image, (w, max_height))
368
+ detections = detections_list[img_idx]
369
  combined_img[:, x_offset:x_offset + w] = image
370
 
371
  # Draw detections with unique IDs
 
404
  output_path = UPLOADS_DIR / filename
405
  cv2.imwrite(str(output_path), combined_img)
406
 
407
+ # Return results
408
  total_detections = sum(len(d['boxes']) for d in detections_list)
409
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
410
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
411
 
412
  return JSONResponse({
413
  "status": "success",
414
+ "mode": "multi_view_with_dinov2_reid",
415
  "model_type": model_type,
416
  "optimization_status": optimization_status,
417
+ "reid_model": "DINOv2",
418
  "total_detections_all_views": total_detections,
419
  "unique_damages_count": len(unique_damages),
420
  "unique_damages": {
 
430
  "visualized_image_path": f"uploads/{filename}",
431
  "visualized_image_url": f"http://localhost:8000/uploads/{filename}",
432
  "message": f"Detected {total_detections} damages across {len(files)} views, "
433
+ f"identified {len(unique_damages)} unique damages using DINOv2 ReID"
 
434
  })
435
 
436
  else:
 
440
  raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}")
441
 
442
 
443
+ def process_single_position_threaded(
444
+ i: int,
445
+ before_contents: bytes,
446
+ after_contents: bytes,
447
+ timestamp_str: str,
448
+ session_id: str
449
+ ) -> Dict:
450
+ """
451
+ Process single position comparison using GLOBAL models (thread-safe)
452
+ No model loading here - uses global instances
453
+ """
454
+ # Use GLOBAL instances - no loading
455
+ global detector, comparator, visualizer
456
+
457
+ # Preprocess images
458
+ before_img = Image.open(io.BytesIO(before_contents)).convert("RGB")
459
+ after_img = Image.open(io.BytesIO(after_contents)).convert("RGB")
460
+ before_np = np.array(before_img)
461
+ after_np = np.array(after_img)
462
+ before_bgr = cv2.cvtColor(before_np, cv2.COLOR_RGB2BGR)
463
+ after_bgr = cv2.cvtColor(after_np, cv2.COLOR_RGB2BGR)
464
+
465
+ # Detect using global detector
466
+ before_detections = detector.detect(before_bgr)
467
+ after_detections = detector.detect(after_bgr)
468
+
469
+ # Compare using global comparator with DINOv2 ReID
470
+ comparison = comparator.analyze_damage_status(
471
+ before_detections, after_detections,
472
+ before_bgr, after_bgr
473
+ )
474
+
475
+ # Visualize using global visualizer
476
+ vis_img = visualizer.create_comparison_visualization(
477
+ before_bgr, after_bgr,
478
+ before_detections, after_detections,
479
+ comparison
480
+ )
481
+
482
+ vis_filename = f"comparison_{timestamp_str}_{session_id}_pos{i + 1}.jpg"
483
+ vis_path = UPLOADS_DIR / vis_filename
484
+ cv2.imwrite(str(vis_path), vis_img)
485
+ vis_url = f"http://localhost:8000/uploads/{vis_filename}"
486
+
487
+ # Clear any GPU memory if used
488
+ if torch.cuda.is_available():
489
+ torch.cuda.empty_cache()
490
+
491
+ # Return result
492
+ return {
493
+ f"position_{i + 1}": {
494
+ "case": comparison['case'],
495
+ "message": comparison['message'],
496
+ "statistics": comparison['statistics'],
497
+ "new_damages": comparison['new_damages'],
498
+ "matched_damages": comparison['matched_damages'],
499
+ "repaired_damages": comparison['repaired_damages'],
500
+ "using_reid": comparison['statistics'].get('using_reid', True),
501
+ "reid_model": comparison['statistics'].get('reid_model', 'DINOv2'),
502
+ "visualization_path": f"uploads/{vis_filename}",
503
+ "visualization_url": vis_url,
504
+ "filename": vis_filename
505
+ },
506
+ "before_detections": {
507
+ "boxes": [np.array(box).tolist() for box in before_detections['boxes']],
508
+ "confidences": [float(c) for c in before_detections['confidences']],
509
+ "classes": before_detections['classes']
510
+ },
511
+ "after_detections": {
512
+ "boxes": [np.array(box).tolist() for box in after_detections['boxes']],
513
+ "confidences": [float(c) for c in after_detections['confidences']],
514
+ "classes": after_detections['classes']
515
+ },
516
+ "_before_bgr": before_bgr, # chỉ dΓΉng nα»™i bα»™
517
+ "_after_bgr": after_bgr # chỉ dΓΉng nα»™i bα»™
518
+ }
519
+
520
+
521
  @app.post("/compare")
522
  async def compare_vehicle_damages(
523
  # Before delivery images (6 positions)
 
539
  prefer_onnx: bool = Form(True)
540
  ):
541
  """
542
+ Enhanced comparison with DINOv2 ReID and Memory Optimization
543
+ Uses ThreadPoolExecutor with global models to avoid OOM
 
 
 
 
544
  """
545
  try:
546
  # Validate select_models
547
  if select_models not in list(range(0, 12)):
548
  raise HTTPException(status_code=400,
549
+ detail="select_models must be 0-10 (0-5=PyTorch, 6-11=ONNX optimized)")
550
 
551
+ # Load appropriate detector if different from current
552
  current_detector = load_detector(select_models, prefer_onnx)
553
 
554
  before_images = [before_1, before_2, before_3, before_4, before_5, before_6]
555
  after_images = [after_1, after_2, after_3, after_4, after_5, after_6]
556
 
557
+ # Read contents first
558
+ before_contents_list = [await img.read() for img in before_images]
559
+ after_contents_list = [await img.read() for img in after_images]
 
 
 
 
 
 
560
 
561
  # Overall statistics
562
  total_new_damages = 0
 
566
  session_id = str(uuid.uuid4())[:8]
567
  timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
568
 
569
+ position_results = []
570
+ all_visualizations = []
571
+ image_pairs = []
572
+ all_before_images = []
573
+ all_after_images = []
574
+ all_before_detections = []
575
+ all_after_detections = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
+ # Use ThreadPoolExecutor to share memory (avoid OOM)
578
+ print(f"πŸ”„ Processing {len(before_images)} image pairs using ThreadPoolExecutor...")
579
+
580
+ with ThreadPoolExecutor(max_workers=3) as executor: # Limit workers to avoid memory issues
581
+ futures = [
582
+ executor.submit(
583
+ process_single_position_threaded,
584
+ i,
585
+ before_contents_list[i],
586
+ after_contents_list[i],
587
+ timestamp_str,
588
+ session_id
589
+ )
590
+ for i in range(6)
591
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
 
593
+ for future in as_completed(futures):
594
+ result = future.result()
595
+ pos_key = list(result.keys())[0] # e.g., 'position_1'
596
+ position_results.append(result)
597
+ all_visualizations.append(result[pos_key]["visualization_url"])
598
+
599
+ # Collect for deduplication
600
+ image_pairs.append((result["_before_bgr"], result["_after_bgr"]))
601
+ all_before_images.append(result["_before_bgr"])
602
+ all_after_images.append(result["_after_bgr"])
603
+ result.pop("_before_bgr", None)
604
+ result.pop("_after_bgr", None)
605
+
606
+ all_before_detections.append(result["before_detections"])
607
+ all_after_detections.append(result["after_detections"])
608
+
609
+ # Update statistics
610
+ comparison = result[pos_key]
611
+ total_new_damages += len(comparison["new_damages"])
612
+ total_existing_damages += len(comparison["repaired_damages"])
613
+ total_matched_damages += len(comparison["matched_damages"])
614
+
615
+ # Sort position_results by position number
616
+ position_results.sort(key=lambda x: int(list(x.keys())[0].split('_')[1]))
617
+
618
+ # Deduplicate BEFORE damages across all 6 views using DINOv2
619
+ print("πŸ” Deduplicating damages across views using DINOv2...")
620
  unique_before = comparator.deduplicate_detections_across_views(
621
  all_before_detections, all_before_images
622
  )
623
 
624
+ # Deduplicate AFTER damages across all 6 views using DINOv2
625
  unique_after = comparator.deduplicate_detections_across_views(
626
  all_after_detections, all_after_images
627
  )
628
 
629
+ print(
630
+ f"βœ… Before: {sum(len(d['boxes']) for d in all_before_detections)} detections β†’ {len(unique_before)} unique")
631
+ print(f"βœ… After: {sum(len(d['boxes']) for d in all_after_detections)} detections β†’ {len(unique_after)} unique")
632
 
633
  # Determine overall case with deduplication
634
+ actual_new_damages = max(0, len(unique_after) - len(unique_before))
635
 
636
  overall_case = "CASE_3_SUCCESS"
637
  overall_message = "Successful delivery - No damage detected"
 
644
  overall_message = "Existing damages from beginning β†’ Delivery completed"
645
 
646
  # Create summary grid
647
+ grid_results = [res[list(res.keys())[0]] for res in position_results]
648
  grid_img = visualizer.create_summary_grid(grid_results, image_pairs)
649
 
650
  grid_filename = f"summary_grid_{timestamp_str}_{session_id}.jpg"
 
655
 
656
  timestamp = datetime.now().isoformat()
657
 
658
+ # Clean up memory
659
+ gc.collect()
660
+ if torch.cuda.is_available():
661
+ torch.cuda.empty_cache()
662
+
663
+ # Enhanced response
664
  model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch"
665
  optimization_status = "πŸš€ OPTIMIZED" if model_type == "ONNX" else "πŸ“¦ Standard"
666
 
 
671
  "model_type": model_type,
672
  "optimization_status": optimization_status,
673
  "reid_enabled": True,
674
+ "reid_model": "DINOv2",
675
+ "memory_optimization": "ThreadPoolExecutor with global models",
676
  "overall_result": {
677
  "case": overall_case,
678
  "message": overall_message,
 
682
  "total_repaired_damages": int(total_existing_damages),
683
  "unique_damages_before": int(len(unique_before)),
684
  "unique_damages_after": int(len(unique_after)),
685
+ "actual_new_unique_damages": int(actual_new_damages)
686
  }
687
  },
688
  "deduplication_info": {
689
+ "model": "DINOv2",
690
  "before_total_detections": int(sum(len(d['boxes']) for d in all_before_detections)),
691
  "before_unique_damages": int(len(unique_before)),
692
  "after_total_detections": int(sum(len(d['boxes']) for d in all_after_detections)),
 
703
  "suggested_action": "Investigate delivery process" if actual_new_damages > 0
704
  else "Proceed with delivery completion"
705
  },
706
+ "performance_note": f"Using {model_type} + DINOv2 ReID with memory optimization"
707
  })
708
 
709
  except Exception as e:
710
+ # Clean up on error
711
+ gc.collect()
712
+ if torch.cuda.is_available():
713
+ torch.cuda.empty_cache()
714
  raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}")
715
 
716
 
 
717
  if __name__ == "__main__":
718
  import os
719
  uvicorn.run(
 
722
  port=int(os.environ.get("PORT", 7860)),
723
  reload=False,
724
  log_level="info"
725
+ )