Spaces:
Sleeping
Sleeping
| import io | |
| from typing import List | |
| import uvicorn | |
| import numpy as np | |
| import uuid | |
| from datetime import datetime | |
| from fastapi import FastAPI, UploadFile, File, HTTPException, Form | |
| from fastapi.responses import JSONResponse, FileResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from PIL import Image | |
| import cv2 | |
| import yaml | |
| from src.detection import YOLOv11Detector | |
| from src.comparison import DamageComparator | |
| from src.visualization import DamageVisualizer | |
| from pathlib import Path | |
| app = FastAPI( | |
| title="Car Damage Detection API", | |
| description="YOLOv11-based car damage detection and comparison system with PyTorch and ONNX support (Optimized for ONNX Runtime v1.19 + opset 21)", | |
| version="1.2.0" | |
| ) | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Initialize components | |
| detector = None | |
| comparator = DamageComparator() | |
| visualizer = DamageVisualizer() | |
| # Model paths mapping - PT and ONNX versions | |
| MODEL_PATHS = { | |
| # PT models (original) | |
| 0: "models_small/best.pt", # Small v1 PT | |
| 1: "models_small_version_2/best.pt", # Small v2 PT, | |
| 2: "models_small_version3/best.pt", # Small v3 PT | |
| 3: "models_medium/best.pt", # Medium v1 PT | |
| 4: "models_medium_version_2/best.pt", # Medium v2 PT | |
| 5: "models_medium_version3/best.pt", # Medium v3 PT | |
| # ONNX models (optimized with v1.19 + opset 21) | |
| 6: "models_small/best.onnx", # Small v1 ONNX | |
| 7: "models_small_version_2/best.onnx", # Small v2 ONNX, | |
| 8: "models_small_version3/best.onnx", # Small v3 ONNX | |
| 9: "models_medium/best.onnx", # Medium v1 ONNX | |
| 10: "models_medium_version_2/best.onnx", # Medium v2 ONNX, | |
| 11: "models_medium_version3/best.onnx" # Medium v3 ONNX, | |
| } | |
| # Config paths - ONNX uses same config as PT version | |
| CONFIG_PATHS = { | |
| 0: "config.yaml", # Small v1 PT | |
| 1: "config_version2.yaml", # Small v2 PT | |
| 2: "config_version3.yaml", # Small v3 PT | |
| 3: "config.yaml", # Medium v1 PT | |
| 4: "config_version2.yaml", # Medium v2 PT | |
| 5: "config_version3.yaml", # Medium v3 PT | |
| 6: "config.yaml", # Small v1 ONNX | |
| 7: "config_version2.yaml", # Small v2 ONNX | |
| 8: "config_version3.yaml", # Small v3 ONNX | |
| 9: "config.yaml", # Medium v1 ONNX | |
| 10: "config_version2.yaml", # Medium v2 ONNX | |
| 11: "config_version3.yaml" # Medium v3 ONNX | |
| } | |
| # Mapping from PT index to ONNX index | |
| PT_TO_ONNX_MAPPING = { | |
| 0: 5, # Small v1 -> ONNX | |
| 1: 6, # Small v2 -> ONNX | |
| 2: 7, # Medium v1 -> ONNX | |
| 3: 8, # Medium v2 -> ONNX | |
| 4: 9, # Medium v3 -> ONNX | |
| 5: 10, # Medium v3 -> ONNX | |
| 6: 11 # Medium v3 -> ONNX | |
| } | |
| def get_optimal_model_index(select_models: int, prefer_onnx: bool = True) -> int: | |
| """ | |
| Enhanced model selection with performance optimization info | |
| """ | |
| # If user explicitly selects ONNX index (5..8) => use that ONNX with optimizations | |
| if select_models in (6, 7, 8, 9, 10, 11): | |
| onnx_path = Path(MODEL_PATHS.get(select_models, "")) | |
| if not onnx_path.exists(): | |
| raise FileNotFoundError( | |
| f"Requested ONNX model index {select_models} not found at {MODEL_PATHS.get(select_models)}") | |
| print(f"π Selected ONNX model with MAXIMUM optimizations: {MODEL_PATHS[select_models]}") | |
| return select_models | |
| # Normalize to valid PT indices | |
| if select_models not in (0, 1, 2, 3, 4, 5): | |
| select_models = 2 # default to medium v1 | |
| # PT preferred for 0..4 | |
| pt_path = Path(MODEL_PATHS.get(select_models, "")) | |
| if pt_path.exists(): | |
| print(f"π¦ Selected PyTorch model: {MODEL_PATHS[select_models]}") | |
| return select_models | |
| # If PT not found and prefer_onnx: fallback to ONNX with optimizations | |
| onnx_index = PT_TO_ONNX_MAPPING.get(select_models) | |
| if prefer_onnx and onnx_index is not None: | |
| onnx_path = Path(MODEL_PATHS.get(onnx_index, "")) | |
| if onnx_path.exists(): | |
| print(f"PT not found at {pt_path}, falling back to optimized ONNX {MODEL_PATHS[onnx_index]}") | |
| return onnx_index | |
| # No suitable file found | |
| raise FileNotFoundError(f"Requested PT model index {select_models} not found at {MODEL_PATHS.get(select_models)}") | |
| def load_detector(select_models: int = 2, prefer_onnx: bool = True): | |
| """ | |
| Args: | |
| select_models: Model selection | |
| prefer_onnx: Whether to prefer ONNX format for fallback | |
| """ | |
| global detector, comparator, visualizer | |
| actual_model_index = get_optimal_model_index(select_models, prefer_onnx) | |
| # Get appropriate config file | |
| config_file = CONFIG_PATHS.get(actual_model_index, "config.yaml") | |
| # Load config | |
| with open(config_file, 'r') as f: | |
| config = yaml.safe_load(f) | |
| # Update config with selected model path | |
| config['model']['path'] = MODEL_PATHS[actual_model_index] | |
| # Save updated config to temp file | |
| temp_config = f'temp_config_{actual_model_index}.yaml' | |
| with open(temp_config, 'w') as f: | |
| yaml.dump(config, f, default_flow_style=False) | |
| # Reload all components with new config | |
| detector = YOLOv11Detector(config_path=temp_config) | |
| comparator = DamageComparator(config_path=temp_config) | |
| visualizer = DamageVisualizer(config_path=temp_config) | |
| # Log model info with optimization status | |
| model_type = "ONNX" if MODEL_PATHS[actual_model_index].endswith('.onnx') else "PyTorch" | |
| model_labels = [ | |
| "Small v1", "Small v2","Small v3", "Medium v1", "Medium v2", "Medium v3",, | |
| "Small v1 ONNX", "Small v2 ONNX","Small v3 ONNX", "Medium v1 ONNX", "Medium v2 ONNX", "Medium v3 ONNX" | |
| ] | |
| if 0 <= select_models < len(model_labels): | |
| model_size = model_labels[select_models] | |
| else: | |
| raise ValueError(f"select_models={select_models} must be 0-11") | |
| # Enhanced logging for optimization status | |
| optimization_status = "π MAXIMUM OPTIMIZATIONS" if model_type == "ONNX" else "π¦ Standard PyTorch" | |
| print(f"Loaded {model_size} model in {model_type} format - {optimization_status}") | |
| # Show performance info for ONNX models | |
| if model_type == "ONNX" and hasattr(detector, 'get_performance_info'): | |
| perf_info = detector.get_performance_info() | |
| if 'providers' in perf_info: | |
| print(f"Provider: {perf_info['providers'][0]}") | |
| if 'optimization_level' in perf_info: | |
| print(f"Graph optimizations: {perf_info['optimization_level']}") | |
| return detector | |
| # Initialize default detector with medium model (preferring ONNX for performance) | |
| print("π Initializing API with optimized ONNX Runtime support...") | |
| detector = load_detector(2, prefer_onnx=True) | |
| comparator = DamageComparator(config_path=CONFIG_PATHS[2]) | |
| visualizer = DamageVisualizer(config_path=CONFIG_PATHS[2]) | |
| # Create necessary directories | |
| UPLOADS_DIR = Path("uploads") | |
| RESULTS_DIR = Path("results") | |
| UPLOADS_DIR.mkdir(exist_ok=True) | |
| RESULTS_DIR.mkdir(exist_ok=True) | |
| # Mount static files directory | |
| app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads") | |
| async def root(): | |
| """Root endpoint with enhanced model info""" | |
| return { | |
| "message": "Car Damage Detection API with YOLOv11 (ONNX Runtime v1.19 optimized)", | |
| "version": "1.2.0", | |
| "optimizations": { | |
| "onnx_runtime": "v1.19+ with opset 21 support", | |
| "performance_features": [ | |
| "Graph optimizations (ALL level)", | |
| "Dynamic thread pool with load balancing", | |
| "Memory arena optimizations", | |
| "CPU spinning for low latency", | |
| "OpenMP with ACTIVE wait policy" | |
| ] | |
| }, | |
| "model_options": { | |
| "0": "Small model v1 (PyTorch)", | |
| "1": "Small model v2 (PyTorch)", | |
| "2": "Medium model v1 (PyTorch)", | |
| "3": "Medium model v2 (PyTorch)", | |
| "4": "Large model (PyTorch only)", | |
| "5": "Small model v1 (ONNX - OPTIMIZED)", | |
| "6": "Small model v2 (ONNX - OPTIMIZED)", | |
| "7": "Medium model v1 (ONNX - OPTIMIZED)", | |
| "8": "Medium model v2 (ONNX - OPTIMIZED)" | |
| }, | |
| "recommendation": "Use indices 5-8 for maximum performance with ONNX optimizations", | |
| "endpoints": { | |
| "/docs": "API documentation", | |
| "/detect": "Single/Multi image detection", | |
| "/compare": "Compare before/after images (6 pairs)", | |
| "/uploads/{filename}": "Access saved visualization images", | |
| "/health": "Health check", | |
| "/model-info": "Get current model information", | |
| "/performance-info": "Get optimization details" | |
| } | |
| } | |
| async def health_check(): | |
| """Enhanced health check with optimization status""" | |
| health_info = { | |
| "status": "healthy", | |
| "model": "YOLOv11", | |
| "backend": "ONNX/PyTorch" | |
| } | |
| if detector and hasattr(detector, 'get_performance_info'): | |
| perf_info = detector.get_performance_info() | |
| health_info.update({ | |
| "model_type": perf_info.get("model_type", "Unknown"), | |
| "optimization_status": "Optimized" if perf_info.get("model_type") == "ONNX" else "Standard" | |
| }) | |
| return health_info | |
| async def detect_single_image( | |
| file: UploadFile = File(None), | |
| files: List[UploadFile] = File(None), | |
| select_models: int = Form(2), | |
| prefer_onnx: bool = Form(True) | |
| ): | |
| """ | |
| Multi-view detection with ONNX Runtime optimizations | |
| Args: | |
| file: Single image (backward compatibility) | |
| files: Multiple images for multi-view detection | |
| select_models: Model selection | |
| - 0-4: PyTorch models (standard performance) | |
| - 5-8: ONNX models (maximum optimizations) | |
| prefer_onnx: Whether to prefer ONNX format (default: True for better performance) | |
| """ | |
| try: | |
| # Validate select_models | |
| if select_models not in list(range(0, 10)): | |
| raise HTTPException(status_code=400, | |
| detail="select_models must be 0-10 (0-5=PyTorch, 6-11=ONNX optimized)") | |
| # Load appropriate detector | |
| current_detector = load_detector(select_models, prefer_onnx) | |
| # Case 1: Single image (backward compatibility) | |
| if file is not None: | |
| contents = await file.read() | |
| image = Image.open(io.BytesIO(contents)).convert("RGB") | |
| image_np = np.array(image) | |
| image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR) | |
| # Perform detection | |
| detections = current_detector.detect(image_bgr) | |
| # Create visualization | |
| visualized = visualizer.draw_detections(image_bgr, detections, 'new_damage') | |
| # Save and return | |
| filename = f"detection_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg" | |
| output_path = UPLOADS_DIR / filename | |
| cv2.imwrite(str(output_path), visualized) | |
| # Enhanced response with optimization info | |
| model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch" | |
| optimization_status = "π OPTIMIZED" if model_type == "ONNX" else "π¦ Standard" | |
| return JSONResponse({ | |
| "status": "success", | |
| "model_type": model_type, | |
| "optimization_status": optimization_status, | |
| "detections": detections, | |
| "statistics": { | |
| "total_damages": len(detections['boxes']), | |
| "damage_types": list(set(detections['classes'])) | |
| }, | |
| "visualized_image_path": f"uploads/{filename}", | |
| "visualized_image_url": f"http://localhost:8000/uploads/{filename}", | |
| "filename": filename, | |
| "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (6-11) for better performance" | |
| }) | |
| # Case 2: Multiple images - MULTI-VIEW DETECTION with ReID | |
| elif files is not None and len(files) > 0: | |
| print(f"\nMulti-view detection with {len(files)} images") | |
| images_list = [] | |
| detections_list = [] | |
| # Process all images | |
| for idx, img_file in enumerate(files): | |
| contents = await img_file.read() | |
| image = Image.open(io.BytesIO(contents)).convert("RGB") | |
| image_np = np.array(image) | |
| image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR) | |
| images_list.append(image_bgr) | |
| detections = current_detector.detect(image_bgr) | |
| detections_list.append(detections) | |
| print(f" View {idx + 1}: {len(detections['boxes'])} detections") | |
| # DEDUPLICATION using ReID | |
| print("\nPerforming cross-view deduplication...") | |
| unique_damages = comparator.deduplicate_detections_across_views( | |
| detections_list, images_list | |
| ) | |
| # Create combined visualization | |
| combined_height = max(img.shape[0] for img in images_list) | |
| combined_width = sum(img.shape[1] for img in images_list) | |
| combined_img = np.ones((combined_height, combined_width, 3), dtype=np.uint8) * 255 | |
| x_offset = 0 | |
| for img_idx, (image, detections) in enumerate(zip(images_list, detections_list)): | |
| # Resize if needed | |
| h, w = image.shape[:2] | |
| if h != combined_height: | |
| scale = combined_height / h | |
| new_w = int(w * scale) | |
| image = cv2.resize(image, (new_w, combined_height)) | |
| w = new_w | |
| # Draw on combined image | |
| combined_img[:, x_offset:x_offset + w] = image | |
| # Draw detections with unique IDs | |
| for det_idx, bbox in enumerate(detections['boxes']): | |
| # Find unique damage ID for this detection | |
| damage_id = None | |
| for uid, damage_info in unique_damages.items(): | |
| for d in damage_info['detections']: | |
| if d['view_idx'] == img_idx and d['bbox'] == bbox: | |
| damage_id = uid | |
| break | |
| # Draw with unique ID | |
| x1, y1, x2, y2 = bbox | |
| x1 += x_offset | |
| x2 += x_offset | |
| # Color based on unique ID | |
| if damage_id: | |
| color_hash = int(damage_id[-6:], 16) | |
| color = ((color_hash >> 16) & 255, (color_hash >> 8) & 255, color_hash & 255) | |
| else: | |
| color = (0, 0, 255) | |
| cv2.rectangle(combined_img, (x1, y1), (x2, y2), color, 2) | |
| # Label | |
| label = f"{damage_id[:8] if damage_id else 'Unknown'}" | |
| cv2.putText(combined_img, label, (x1, y1 - 5), | |
| cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) | |
| x_offset += w | |
| # Save combined visualization | |
| filename = f"multiview_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.jpg" | |
| output_path = UPLOADS_DIR / filename | |
| cv2.imwrite(str(output_path), combined_img) | |
| # Return results with optimization info | |
| total_detections = sum(len(d['boxes']) for d in detections_list) | |
| model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch" | |
| optimization_status = "π OPTIMIZED" if model_type == "ONNX" else "π¦ Standard" | |
| return JSONResponse({ | |
| "status": "success", | |
| "mode": "multi_view_with_reid", | |
| "model_type": model_type, | |
| "optimization_status": optimization_status, | |
| "total_detections_all_views": total_detections, | |
| "unique_damages_count": len(unique_damages), | |
| "unique_damages": { | |
| damage_id: { | |
| "appears_in_views": info['views'], | |
| "class": info['class'], | |
| "avg_confidence": float(info['avg_confidence']), | |
| "detection_count": len(info['detections']) | |
| } | |
| for damage_id, info in unique_damages.items() | |
| }, | |
| "reduction_rate": f"{(1 - len(unique_damages) / total_detections) * 100:.1f}%" if total_detections > 0 else "0%", | |
| "visualized_image_path": f"uploads/{filename}", | |
| "visualized_image_url": f"http://localhost:8000/uploads/{filename}", | |
| "message": f"Detected {total_detections} damages across {len(files)} views, " | |
| f"identified {len(unique_damages)} unique damages using ReID", | |
| "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (5-8) for better performance" | |
| }) | |
| else: | |
| raise HTTPException(status_code=400, detail="No image provided") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}") | |
| async def compare_vehicle_damages( | |
| # Before delivery images (6 positions) | |
| before_1: UploadFile = File(...), | |
| before_2: UploadFile = File(...), | |
| before_3: UploadFile = File(...), | |
| before_4: UploadFile = File(...), | |
| before_5: UploadFile = File(...), | |
| before_6: UploadFile = File(...), | |
| # After delivery images (6 positions) | |
| after_1: UploadFile = File(...), | |
| after_2: UploadFile = File(...), | |
| after_3: UploadFile = File(...), | |
| after_4: UploadFile = File(...), | |
| after_5: UploadFile = File(...), | |
| after_6: UploadFile = File(...), | |
| # Model selection | |
| select_models: int = Form(2), | |
| prefer_onnx: bool = Form(True) | |
| ): | |
| """ | |
| Enhanced comparison with ONNX Runtime optimizations and ReID | |
| Args: | |
| before_1-6: Before delivery images from 6 positions | |
| after_1-6: After delivery images from 6 positions | |
| select_models: Model selection (0-4=PyTorch, 5-8=ONNX optimized) | |
| prefer_onnx: Whether to prefer ONNX format (default: True) | |
| """ | |
| try: | |
| # Validate select_models | |
| if select_models not in list(range(0, 10)): | |
| raise HTTPException(status_code=400, | |
| detail="select_models must be 0-11 (0-5=PyTorch, 6-11=ONNX optimized)") | |
| # Load appropriate detector | |
| current_detector = load_detector(select_models, prefer_onnx) | |
| before_images = [before_1, before_2, before_3, before_4, before_5, before_6] | |
| after_images = [after_1, after_2, after_3, after_4, after_5, after_6] | |
| position_results = [] | |
| all_visualizations = [] | |
| image_pairs = [] | |
| # Collect all before/after images and detections | |
| all_before_images = [] | |
| all_after_images = [] | |
| all_before_detections = [] | |
| all_after_detections = [] | |
| # Overall statistics | |
| total_new_damages = 0 | |
| total_existing_damages = 0 | |
| total_matched_damages = 0 | |
| session_id = str(uuid.uuid4())[:8] | |
| timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| # Process each position pair | |
| for i in range(6): | |
| before_contents = await before_images[i].read() | |
| after_contents = await after_images[i].read() | |
| before_img = Image.open(io.BytesIO(before_contents)).convert("RGB") | |
| after_img = Image.open(io.BytesIO(after_contents)).convert("RGB") | |
| before_np = np.array(before_img) | |
| after_np = np.array(after_img) | |
| before_bgr = cv2.cvtColor(before_np, cv2.COLOR_RGB2BGR) | |
| after_bgr = cv2.cvtColor(after_np, cv2.COLOR_RGB2BGR) | |
| # Store for multi-view analysis | |
| all_before_images.append(before_bgr) | |
| all_after_images.append(after_bgr) | |
| image_pairs.append((before_bgr, after_bgr)) | |
| # Detect damages | |
| before_detections = current_detector.detect(before_bgr) | |
| after_detections = current_detector.detect(after_bgr) | |
| all_before_detections.append(before_detections) | |
| all_after_detections.append(after_detections) | |
| # Enhanced comparison with ReID | |
| comparison = comparator.analyze_damage_status( | |
| before_detections, after_detections, | |
| before_bgr, after_bgr | |
| ) | |
| # Update statistics | |
| total_new_damages += len(comparison['new_damages']) | |
| total_existing_damages += len(comparison['repaired_damages']) | |
| total_matched_damages += len(comparison['matched_damages']) | |
| # Create visualization | |
| vis_img = visualizer.create_comparison_visualization( | |
| before_bgr, after_bgr, | |
| before_detections, after_detections, | |
| comparison | |
| ) | |
| vis_filename = f"comparison_{timestamp_str}_{session_id}_pos{i + 1}.jpg" | |
| vis_path = UPLOADS_DIR / vis_filename | |
| cv2.imwrite(str(vis_path), vis_img) | |
| vis_url = f"http://localhost:8000/uploads/{vis_filename}" | |
| all_visualizations.append(vis_url) | |
| # Store position result with ReID info | |
| position_results.append({ | |
| f"position_{i + 1}": { | |
| "case": comparison['case'], | |
| "message": comparison['message'], | |
| "statistics": comparison['statistics'], | |
| "new_damages": comparison['new_damages'], | |
| "matched_damages": comparison['matched_damages'], | |
| "repaired_damages": comparison['repaired_damages'], | |
| "using_reid": comparison['statistics'].get('using_reid', True), | |
| "visualization_path": f"uploads/{vis_filename}", | |
| "visualization_url": vis_url, | |
| "filename": vis_filename | |
| } | |
| }) | |
| # Deduplicate BEFORE damages across all 6 views | |
| unique_before = comparator.deduplicate_detections_across_views( | |
| all_before_detections, all_before_images | |
| ) | |
| # Deduplicate AFTER damages across all 6 views | |
| unique_after = comparator.deduplicate_detections_across_views( | |
| all_after_detections, all_after_images | |
| ) | |
| print(f"Before: {sum(len(d['boxes']) for d in all_before_detections)} detections β {len(unique_before)} unique") | |
| print(f"After: {sum(len(d['boxes']) for d in all_after_detections)} detections β {len(unique_after)} unique") | |
| # Determine overall case with deduplication | |
| actual_new_damages = len(unique_after) - len(unique_before) | |
| overall_case = "CASE_3_SUCCESS" | |
| overall_message = "Successful delivery - No damage detected" | |
| if actual_new_damages > 0: | |
| overall_case = "CASE_2_NEW_DAMAGE" | |
| overall_message = f"Error during delivery - {actual_new_damages} new unique damage(s) detected" | |
| elif len(unique_before) > 0 and actual_new_damages <= 0: | |
| overall_case = "CASE_1_EXISTING" | |
| overall_message = "Existing damages from beginning β Delivery completed" | |
| # Create summary grid | |
| grid_results = [res[f"position_{i + 1}"] for i, res in enumerate(position_results)] | |
| grid_img = visualizer.create_summary_grid(grid_results, image_pairs) | |
| grid_filename = f"summary_grid_{timestamp_str}_{session_id}.jpg" | |
| grid_path = UPLOADS_DIR / grid_filename | |
| cv2.imwrite(str(grid_path), grid_img) | |
| grid_url = f"http://localhost:8000/uploads/{grid_filename}" | |
| timestamp = datetime.now().isoformat() | |
| # Enhanced response with optimization info | |
| model_type = "ONNX" if current_detector.model_path.endswith('.onnx') else "PyTorch" | |
| optimization_status = "π OPTIMIZED" if model_type == "ONNX" else "π¦ Standard" | |
| return JSONResponse({ | |
| "status": "success", | |
| "session_id": session_id, | |
| "timestamp": timestamp, | |
| "model_type": model_type, | |
| "optimization_status": optimization_status, | |
| "reid_enabled": True, | |
| "overall_result": { | |
| "case": overall_case, | |
| "message": overall_message, | |
| "statistics": { | |
| "total_new_damages": int(total_new_damages), | |
| "total_matched_damages": int(total_matched_damages), | |
| "total_repaired_damages": int(total_existing_damages), | |
| "unique_damages_before": int(len(unique_before)), | |
| "unique_damages_after": int(len(unique_after)), | |
| "actual_new_unique_damages": int(max(0, len(unique_after) - len(unique_before))) | |
| } | |
| }, | |
| "deduplication_info": { | |
| "before_total_detections": int(sum(len(d['boxes']) for d in all_before_detections)), | |
| "before_unique_damages": int(len(unique_before)), | |
| "after_total_detections": int(sum(len(d['boxes']) for d in all_after_detections)), | |
| "after_unique_damages": int(len(unique_after)), | |
| "duplicate_reduction_rate": f"{(1 - len(unique_after) / sum(len(d['boxes']) for d in all_after_detections)) * 100:.1f}%" | |
| if sum(len(d['boxes']) for d in all_after_detections) > 0 else "0%" | |
| }, | |
| "position_results": position_results, | |
| "summary_visualization_path": f"uploads/{grid_filename}", | |
| "summary_visualization_url": grid_url, | |
| "all_visualizations": all_visualizations, | |
| "recommendations": { | |
| "action_required": bool(actual_new_damages > 0), | |
| "suggested_action": "Investigate delivery process" if actual_new_damages > 0 | |
| else "Proceed with delivery completion" | |
| }, | |
| "performance_note": "Using ONNX optimizations" if model_type == "ONNX" else "Consider using ONNX models (5-8) for better performance" | |
| }) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Comparison failed: {str(e)}") | |
| if __name__ == "__main__": | |
| import os | |
| uvicorn.run( | |
| "main:app", | |
| host="0.0.0.0", | |
| port=int(os.environ.get("PORT", 7860)), | |
| reload=False, | |
| log_level="info" | |
| ) | |