| from fastapi import FastAPI, File, UploadFile, HTTPException
|
| import torch
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from fastapi.responses import FileResponse
|
| import os
|
| from uuid import uuid4
|
| from ultralytics import YOLO
|
| import logging
|
| import glob
|
| import ultralytics
|
| from PIL import Image
|
| import numpy as np
|
| import matplotlib.pyplot as plt
|
| import cv2
|
| from fastapi.staticfiles import StaticFiles
|
| import traceback
|
| import json
|
|
|
| app = FastAPI()
|
|
|
|
|
| logging.basicConfig(
|
| level=logging.INFO,
|
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| )
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| origins = [
|
| "http://localhost",
|
| "http://localhost:8000",
|
| "http://127.0.0.1",
|
| "http://127.0.0.1:8000",
|
| "http://localhost:5500",
|
| "http://127.0.0.1:5500",
|
| "*"
|
| ]
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=origins,
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| expose_headers=["*"]
|
| )
|
|
|
|
|
| os.makedirs("uploads", exist_ok=True)
|
| os.makedirs("results", exist_ok=True)
|
| os.makedirs("results/explanations", exist_ok=True)
|
| os.makedirs("results/gradcam", exist_ok=True)
|
|
|
|
|
| model = None
|
|
|
| @app.on_event("startup")
|
| async def load_model():
|
| global model
|
| try:
|
| logger.info("Loading YOLOv8 model...")
|
| logger.info(f"Ultralytics version: {ultralytics.__version__}")
|
| model = YOLO('./Final.pt')
|
| logger.info("Model loaded successfully.")
|
| except Exception as e:
|
| logger.error(f"Error loading model: {e}")
|
| logger.error(traceback.format_exc())
|
| raise e
|
|
|
|
|
| app.mount("/results", StaticFiles(directory="results"), name="results")
|
|
|
| app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
|
|
|
|
| ANALYSIS_COUNT_FILE = "analysis_count.json"
|
|
|
| def get_analysis_count():
|
| if os.path.exists(ANALYSIS_COUNT_FILE):
|
| with open(ANALYSIS_COUNT_FILE, "r") as f:
|
| return json.load(f).get("count", 0)
|
| return 0
|
|
|
| def increment_analysis_count():
|
| count = get_analysis_count()
|
| count += 1
|
| with open(ANALYSIS_COUNT_FILE, "w") as f:
|
| json.dump({"count": count}, f)
|
| return count
|
|
|
| @app.get("/")
|
| async def root():
|
| return {"message": "Welcome to the YOLOv8 detection API!"}
|
|
|
| @app.get("/status")
|
| async def get_status():
|
| return {"message": "Server is running", "status": "success"}
|
|
|
|
|
| @app.get("/health")
|
| async def health_check():
|
| return {"status": "healthy", "message": "API is running properly"}
|
|
|
| @app.get("/total_analyses")
|
| async def get_total_analyses():
|
| return {"total_analyses": get_analysis_count()}
|
|
|
| @app.post("/detect")
|
| async def detect(file: UploadFile = File(...)):
|
|
|
| if not file.content_type.startswith("image/"):
|
| raise HTTPException(status_code=400, detail="File provided is not an image")
|
|
|
|
|
| increment_analysis_count()
|
|
|
|
|
| detection_id = str(uuid4())
|
|
|
|
|
| upload_folder = "uploads"
|
| os.makedirs(upload_folder, exist_ok=True)
|
|
|
| file_extension = os.path.splitext(file.filename)[1]
|
| input_file_path = f"{upload_folder}/{detection_id}{file_extension}"
|
|
|
|
|
| with open(input_file_path, "wb") as buffer:
|
| buffer.write(await file.read())
|
|
|
| try:
|
|
|
| image = Image.open(input_file_path).convert("RGB")
|
| image_array = np.array(image)
|
|
|
|
|
| results = model(image_array)
|
|
|
|
|
| output_file_path = f"results/{detection_id}_result.jpg"
|
| results_plotted = results[0].plot()
|
| cv2.imwrite(output_file_path, results_plotted)
|
|
|
|
|
| detections = []
|
| detection_boxes = []
|
|
|
| for result in results:
|
| boxes = result.boxes.cpu().numpy()
|
| for i, box in enumerate(boxes):
|
| x1, y1, x2, y2 = box.xyxy[0]
|
| confidence = box.conf[0]
|
| if confidence > 0.5:
|
| class_id = int(box.cls[0])
|
| class_name = result.names[class_id]
|
|
|
| detection_boxes.append([x1, y1, x2, y2])
|
|
|
| detections.append({
|
| "id": i,
|
| "class": class_name,
|
| "confidence": float(confidence),
|
| "box": {
|
| "x1": float(x1),
|
| "y1": float(y1),
|
| "x2": float(x2),
|
| "y2": float(y2)
|
| }
|
| })
|
|
|
|
|
| explanation_file_path = None
|
| if len(detection_boxes) > 0:
|
|
|
| explanation_img = image_array.copy()
|
|
|
|
|
| for box in detection_boxes:
|
| x1, y1, x2, y2 = [int(coord) for coord in box]
|
| cv2.rectangle(explanation_img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
|
|
| highlight = np.zeros_like(explanation_img, dtype=np.uint8)
|
|
|
| pad = 10
|
| cv2.rectangle(highlight, (max(0, x1-pad), max(0, y1-pad)),
|
| (min(explanation_img.shape[1], x2+pad), min(explanation_img.shape[0], y2+pad)),
|
| (0, 0, 255), -1)
|
|
|
| explanation_img = cv2.addWeighted(explanation_img, 1, highlight, 0.3, 0)
|
|
|
| explanation_file_path = f"results/explanations/{detection_id}_explanation.jpg"
|
| cv2.imwrite(explanation_file_path, explanation_img)
|
|
|
|
|
| gradcam_file_path = None
|
| if len(detection_boxes) > 0:
|
|
|
| gradcam_img = generate_gradcam(image_array, detection_boxes, results[0])
|
|
|
| gradcam_file_path = f"results/gradcam/{detection_id}_gradcam.jpg"
|
| cv2.imwrite(gradcam_file_path, gradcam_img)
|
|
|
| return {
|
| "detection_id": detection_id,
|
| "message": "Detection completed successfully",
|
| "result_image": f"/results/{detection_id}_result.jpg",
|
| "explanation_image": f"/results/explanations/{detection_id}_explanation.jpg" if explanation_file_path else None,
|
| "gradcam_image": f"/results/gradcam/{detection_id}_gradcam.jpg" if gradcam_file_path else None,
|
| "detections": detections
|
| }
|
|
|
| except Exception as e:
|
| logger.error(f"Error during detection: {str(e)}")
|
| logger.error(traceback.format_exc())
|
| raise HTTPException(status_code=500, detail=f"Error during detection: {str(e)}")
|
|
|
| def generate_gradcam(image, boxes, result):
|
| """
|
| Generate a Grad-CAM visualization for the detected fractures.
|
|
|
| This is a simplified implementation - in a production environment, you would use
|
| actual gradients from the model's final convolutional layer.
|
| """
|
|
|
| vis_img = image.copy()
|
|
|
|
|
| if len(vis_img.shape) == 2:
|
| vis_img = cv2.cvtColor(vis_img, cv2.COLOR_GRAY2RGB)
|
| elif vis_img.shape[2] == 1:
|
| vis_img = cv2.cvtColor(vis_img, cv2.COLOR_GRAY2RGB)
|
|
|
|
|
| height, width = vis_img.shape[:2]
|
| heatmap = np.zeros((height, width), dtype=np.float32)
|
|
|
|
|
| for box in boxes:
|
| x1, y1, x2, y2 = [int(coord) for coord in box]
|
|
|
|
|
| center_x = (x1 + x2) // 2
|
| center_y = (y1 + y2) // 2
|
|
|
|
|
| box_width = x2 - x1
|
| box_height = y2 - y1
|
|
|
|
|
| y, x = np.ogrid[:height, :width]
|
|
|
| sigma_x = box_width / 6
|
| sigma_y = box_height / 6
|
|
|
|
|
| sigma_x = max(sigma_x, 10)
|
| sigma_y = max(sigma_y, 10)
|
|
|
|
|
| gaussian = np.exp(-(
|
| ((x - center_x) ** 2) / (2 * sigma_x ** 2) +
|
| ((y - center_y) ** 2) / (2 * sigma_y ** 2)
|
| ))
|
|
|
|
|
| heatmap = np.maximum(heatmap, gaussian)
|
|
|
|
|
| heatmap = np.uint8(255 * heatmap)
|
|
|
|
|
| heatmap_colored = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
|
|
|
|
|
| alpha = 0.4
|
| gradcam_visualization = cv2.addWeighted(vis_img, 1 - alpha, heatmap_colored, alpha, 0)
|
|
|
|
|
| for box in boxes:
|
| x1, y1, x2, y2 = [int(coord) for coord in box]
|
| cv2.rectangle(gradcam_visualization, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
|
|
| return gradcam_visualization
|
|
|
| @app.get("/gradcam/{image_id}")
|
| async def get_gradcam(image_id: str):
|
| """
|
| Generate custom Grad-CAM visualization for a previously detected image
|
| """
|
|
|
| result_path = f"results/{image_id}_result.jpg"
|
| if not os.path.exists(result_path):
|
| raise HTTPException(status_code=404, detail="Image not found")
|
|
|
|
|
| gradcam_path = f"results/gradcam/{image_id}_gradcam.jpg"
|
|
|
|
|
| if os.path.exists(gradcam_path):
|
| return FileResponse(gradcam_path)
|
|
|
|
|
|
|
| raise HTTPException(status_code=404, detail="Grad-CAM not available for this image")
|
|
|