Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- Dockerfile +26 -0
- requirements.txt +23 -0
- scripts/gradcam.py +100 -0
- scripts/load_models.py +104 -0
- scripts/prediction_helper.py +212 -0
- scripts/yolo_predict.py +64 -0
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# --- SYSTEM DEPENDENCIES (CRITICAL FOR OPENCV / YOLO) ---
|
| 9 |
+
RUN apt-get update && apt-get install -y \
|
| 10 |
+
build-essential \
|
| 11 |
+
gcc \
|
| 12 |
+
libgl1 \
|
| 13 |
+
libglib2.0-0 \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# --- PYTHON DEPENDENCIES ---
|
| 17 |
+
COPY requirements.txt .
|
| 18 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
| 19 |
+
&& pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# --- APP CODE ---
|
| 22 |
+
COPY . .
|
| 23 |
+
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
transformers
|
| 4 |
+
fastapi
|
| 5 |
+
uvicorn
|
| 6 |
+
dotenv
|
| 7 |
+
matplotlib
|
| 8 |
+
opencv-python
|
| 9 |
+
python-multipart
|
| 10 |
+
ultralytics
|
| 11 |
+
plotly
|
| 12 |
+
pandas
|
| 13 |
+
scikit-learn
|
| 14 |
+
seaborn
|
| 15 |
+
huggingface_hub
|
| 16 |
+
onnx
|
| 17 |
+
onnxruntime
|
| 18 |
+
pillow
|
| 19 |
+
onnxscript
|
| 20 |
+
huggingface_hub
|
| 21 |
+
python-dotenv
|
| 22 |
+
onnxconverter-common
|
| 23 |
+
streamlit
|
scripts/gradcam.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class _GradCAMHook:
|
| 12 |
+
__slots__ = ("activation", "gradient", "fwd_handle", "bwd_handle")
|
| 13 |
+
|
| 14 |
+
def __init__(self, target_layer):
|
| 15 |
+
self.activation = None
|
| 16 |
+
self.gradient = None
|
| 17 |
+
self.fwd_handle = target_layer.register_forward_hook(self._fwd_hook)
|
| 18 |
+
self.bwd_handle = None
|
| 19 |
+
|
| 20 |
+
def _fwd_hook(self, module, inp, out):
|
| 21 |
+
self.activation = out
|
| 22 |
+
self.bwd_handle = out.register_hook(self._bwd_hook)
|
| 23 |
+
|
| 24 |
+
def _bwd_hook(self, grad):
|
| 25 |
+
self.gradient = grad
|
| 26 |
+
|
| 27 |
+
def remove(self):
|
| 28 |
+
self.fwd_handle.remove()
|
| 29 |
+
if self.bwd_handle is not None:
|
| 30 |
+
self.bwd_handle.remove()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _postprocess_cam(cam_tensor, original_img, output_path, alpha=0.6, beta=0.4):
|
| 34 |
+
h, w = original_img.height, original_img.width
|
| 35 |
+
|
| 36 |
+
cam_min = cam_tensor.min()
|
| 37 |
+
cam_max = cam_tensor.max()
|
| 38 |
+
if cam_max > cam_min:
|
| 39 |
+
cam_tensor = (cam_tensor - cam_min) / (cam_max - cam_min)
|
| 40 |
+
else:
|
| 41 |
+
cam_tensor = torch.zeros_like(cam_tensor)
|
| 42 |
+
|
| 43 |
+
cam_np = cam_tensor.numpy()
|
| 44 |
+
cam_np = cv2.resize(cam_np, (w, h), interpolation=cv2.INTER_LINEAR)
|
| 45 |
+
|
| 46 |
+
cam_np = np.uint8(255 * cam_np)
|
| 47 |
+
heatmap = cv2.applyColorMap(cam_np, cv2.COLORMAP_JET)
|
| 48 |
+
|
| 49 |
+
original_bgr = cv2.cvtColor(np.array(original_img), cv2.COLOR_RGB2BGR)
|
| 50 |
+
overlay = cv2.addWeighted(original_bgr, alpha, heatmap, beta, 0)
|
| 51 |
+
cv2.imwrite(output_path, overlay)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_resnet_gradcam(image_path, predictor, output_path):
|
| 55 |
+
"""Generates Grad-CAM visual heatmap using the PyTorch ResNet classifier."""
|
| 56 |
+
logger.info("Starting ResNet Grad-CAM generation...")
|
| 57 |
+
|
| 58 |
+
model = predictor.model
|
| 59 |
+
model.eval()
|
| 60 |
+
|
| 61 |
+
target_layer = model.model.layer4[-1]
|
| 62 |
+
hook = _GradCAMHook(target_layer)
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
original_img = Image.open(image_path).convert("RGB")
|
| 66 |
+
input_tensor = predictor.test_transforms(original_img).unsqueeze(0).to(predictor.device)
|
| 67 |
+
|
| 68 |
+
output = model(input_tensor)
|
| 69 |
+
score, pred_class_idx = output[0].max(dim=0)
|
| 70 |
+
|
| 71 |
+
logger.info(f"Predicted class index for Grad-CAM: {pred_class_idx.item()}")
|
| 72 |
+
score.backward()
|
| 73 |
+
|
| 74 |
+
if hook.activation is None or hook.gradient is None:
|
| 75 |
+
raise RuntimeError("Failed to capture activations or gradients.")
|
| 76 |
+
|
| 77 |
+
acts = hook.activation[0].detach().cpu().float()
|
| 78 |
+
grads = hook.gradient[0].detach().cpu().float()
|
| 79 |
+
|
| 80 |
+
weights = grads.mean(dim=(1, 2), keepdim=True)
|
| 81 |
+
cam = (weights * acts).sum(dim=0)
|
| 82 |
+
cam = F.relu(cam)
|
| 83 |
+
|
| 84 |
+
_postprocess_cam(cam, original_img, output_path)
|
| 85 |
+
|
| 86 |
+
logger.info(f"ResNet Grad-CAM heatmap saved to: {output_path}")
|
| 87 |
+
return True
|
| 88 |
+
|
| 89 |
+
except Exception as e:
|
| 90 |
+
logger.exception("ResNet Grad-CAM generation failed.")
|
| 91 |
+
raise RuntimeError(f"ResNet Grad-CAM failed: {e}") from e
|
| 92 |
+
|
| 93 |
+
finally:
|
| 94 |
+
hook.remove()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_fusion_gradcam(image_path, predictor, output_path):
|
| 98 |
+
"""Fallback handler mapping to ResNet Grad-CAM (since single Grad-CAM architecture is used)."""
|
| 99 |
+
logger.info("Fusion Grad-CAM requested. Directing to ResNet Grad-CAM engine...")
|
| 100 |
+
return get_resnet_gradcam(image_path, predictor, output_path)
|
scripts/load_models.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import onnxruntime as ort
|
| 4 |
+
from huggingface_hub import hf_hub_download
|
| 5 |
+
from ultralytics import YOLO
|
| 6 |
+
|
| 7 |
+
from .prediction_helper import ResnetCarDamagePredictor
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
MODEL_CONFIG = {
|
| 12 |
+
"resnet_onnx": {
|
| 13 |
+
"repo_id": "junaid17/car-damage-classifier",
|
| 14 |
+
"filename": "car-damage-classifier.onnx",
|
| 15 |
+
},
|
| 16 |
+
"fusion_onnx": {
|
| 17 |
+
"repo_id": "junaid17/best_fusion_model_fp16",
|
| 18 |
+
"filename": "fusion_model.onnx",
|
| 19 |
+
},
|
| 20 |
+
"yolo_onnx": {
|
| 21 |
+
"repo_id": "junaid17/Yolo_Model",
|
| 22 |
+
"filename": "damage_detector.onnx",
|
| 23 |
+
},
|
| 24 |
+
"resnet_pt": {
|
| 25 |
+
"repo_id": "junaid17/car-damage-classifier",
|
| 26 |
+
"filename": "car-damage-classifier.pt",
|
| 27 |
+
},
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_checkpoint_path(model_key: str) -> Path:
|
| 32 |
+
if model_key not in MODEL_CONFIG:
|
| 33 |
+
raise ValueError(f"Unknown model key: {model_key}")
|
| 34 |
+
|
| 35 |
+
config = MODEL_CONFIG[model_key]
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
logger.info(f"Fetching {model_key} model from Hugging Face Hub...")
|
| 39 |
+
logger.info(f"Repo: {config['repo_id']} | File: {config['filename']}")
|
| 40 |
+
|
| 41 |
+
local_path = hf_hub_download(
|
| 42 |
+
repo_id=config["repo_id"],
|
| 43 |
+
filename=config["filename"],
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
logger.info(f"{model_key} model downloaded to: {local_path}")
|
| 47 |
+
return Path(local_path)
|
| 48 |
+
|
| 49 |
+
except Exception as e:
|
| 50 |
+
logger.exception(f"Failed to fetch {model_key} model.")
|
| 51 |
+
raise RuntimeError(f"Failed to load {model_key} checkpoint: {str(e)}")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ModelLoader:
|
| 55 |
+
def __init__(self):
|
| 56 |
+
logger.info("Initializing ModelLoader...")
|
| 57 |
+
|
| 58 |
+
def get_model_path(self, model_key: str) -> Path:
|
| 59 |
+
return get_checkpoint_path(model_key)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def initialize_models(class_map):
|
| 63 |
+
logger.info("Starting model initialization pipeline...")
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
# 1. Download/Fetch all 4 model file paths
|
| 67 |
+
resnet_onnx_path = get_checkpoint_path("resnet_onnx")
|
| 68 |
+
fusion_onnx_path = get_checkpoint_path("fusion_onnx")
|
| 69 |
+
yolo_onnx_path = get_checkpoint_path("yolo_onnx")
|
| 70 |
+
resnet_pt_path = get_checkpoint_path("resnet_pt")
|
| 71 |
+
|
| 72 |
+
# Define ONNX Execution Providers (GPU if available, fallback to CPU)
|
| 73 |
+
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
| 74 |
+
|
| 75 |
+
# 2. Initialize ONNX Runtime Sessions for Classifier Models
|
| 76 |
+
logger.info("Initializing ResNet ONNX Session...")
|
| 77 |
+
resnet_onnx_session = ort.InferenceSession(str(resnet_onnx_path), providers=providers)
|
| 78 |
+
|
| 79 |
+
logger.info("Initializing Fusion ONNX Session...")
|
| 80 |
+
fusion_onnx_session = ort.InferenceSession(str(fusion_onnx_path), providers=providers)
|
| 81 |
+
|
| 82 |
+
# 3. Initialize YOLO ONNX Model via Ultralytics (enables ONNX runtime with .predict API)
|
| 83 |
+
logger.info("Initializing YOLO ONNX Model via Ultralytics...")
|
| 84 |
+
yolo_onnx_model = YOLO(str(yolo_onnx_path), task="detect")
|
| 85 |
+
|
| 86 |
+
# 4. Initialize PyTorch ResNet Predictor (specifically reserved for Grad-CAM)
|
| 87 |
+
logger.info("Initializing PyTorch ResNet model for Grad-CAM...")
|
| 88 |
+
resnet_gradcam_predictor = ResnetCarDamagePredictor(
|
| 89 |
+
checkpoint_path=resnet_pt_path,
|
| 90 |
+
class_map=class_map
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
logger.info("All 4 models (3 ONNX + 1 PyTorch Grad-CAM) initialized successfully.")
|
| 94 |
+
|
| 95 |
+
return {
|
| 96 |
+
"resnet_onnx": resnet_onnx_session,
|
| 97 |
+
"fusion_onnx": fusion_onnx_session,
|
| 98 |
+
"yolo_onnx": yolo_onnx_model,
|
| 99 |
+
"resnet_pt": resnet_gradcam_predictor,
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.exception("Model initialization failed.")
|
| 104 |
+
raise RuntimeError(f"Model initialization failed: {str(e)}")
|
scripts/prediction_helper.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
from torchvision import transforms, models
|
| 6 |
+
from PIL import Image, UnidentifiedImageError
|
| 7 |
+
from transformers import ConvNextImageProcessor
|
| 8 |
+
import onnxruntime as ort
|
| 9 |
+
|
| 10 |
+
# ---------------- LOGGING SETUP ----------------
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=logging.INFO,
|
| 13 |
+
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# =====================================================================
|
| 20 |
+
# 1. PYTORCH RESNET MODEL & PREDICTOR (RETAINED EXCLUSIVELY FOR GRAD-CAM)
|
| 21 |
+
# =====================================================================
|
| 22 |
+
class Car_Classifier_Resnet(nn.Module):
|
| 23 |
+
def __init__(self, num_classes):
|
| 24 |
+
super().__init__()
|
| 25 |
+
logger.info("Initializing ResNet18 PyTorch architecture for Grad-CAM...")
|
| 26 |
+
|
| 27 |
+
self.model = models.resnet18(weights="DEFAULT")
|
| 28 |
+
|
| 29 |
+
for param in self.model.parameters():
|
| 30 |
+
param.requires_grad = False
|
| 31 |
+
|
| 32 |
+
for param in self.model.layer3.parameters():
|
| 33 |
+
param.requires_grad = True
|
| 34 |
+
|
| 35 |
+
for param in self.model.layer4.parameters():
|
| 36 |
+
param.requires_grad = True
|
| 37 |
+
|
| 38 |
+
self.model.fc = nn.Sequential(
|
| 39 |
+
nn.Dropout(0.5),
|
| 40 |
+
nn.Linear(self.model.fc.in_features, 256),
|
| 41 |
+
nn.ReLU(),
|
| 42 |
+
nn.Dropout(0.3),
|
| 43 |
+
nn.Linear(256, num_classes)
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def forward(self, x):
|
| 47 |
+
return self.model(x)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ResnetCarDamagePredictor:
|
| 51 |
+
"""PyTorch Predictor wrapper used strictly by Grad-CAM for backpropagation."""
|
| 52 |
+
def __init__(self, checkpoint_path, class_map):
|
| 53 |
+
logger.info("Initializing PyTorch ResNet predictor for Grad-CAM...")
|
| 54 |
+
|
| 55 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 56 |
+
self.class_map = class_map
|
| 57 |
+
|
| 58 |
+
self.test_transforms = transforms.Compose([
|
| 59 |
+
transforms.Resize((224, 224)),
|
| 60 |
+
transforms.ToTensor(),
|
| 61 |
+
transforms.Normalize(
|
| 62 |
+
[0.485, 0.456, 0.406],
|
| 63 |
+
[0.229, 0.224, 0.225]
|
| 64 |
+
)
|
| 65 |
+
])
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
self.model = Car_Classifier_Resnet(num_classes=len(class_map))
|
| 69 |
+
checkpoint = torch.load(checkpoint_path, map_location=self.device)
|
| 70 |
+
state_dict = checkpoint.get("model_state_dict", checkpoint)
|
| 71 |
+
|
| 72 |
+
self.model.load_state_dict(state_dict)
|
| 73 |
+
self.model.to(self.device)
|
| 74 |
+
self.model.eval()
|
| 75 |
+
|
| 76 |
+
logger.info("PyTorch ResNet model loaded successfully for Grad-CAM.")
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.exception("Failed to load PyTorch ResNet model.")
|
| 80 |
+
raise RuntimeError(f"Failed to load ResNet PyTorch model: {str(e)}")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# =====================================================================
|
| 84 |
+
# 2. ONNX RESNET PREDICTOR (FAST INFERENCE)
|
| 85 |
+
# =====================================================================
|
| 86 |
+
class ResnetONNXPredictor:
|
| 87 |
+
def __init__(self, session: ort.InferenceSession, class_map: dict):
|
| 88 |
+
logger.info("Initializing ResNet ONNX Predictor...")
|
| 89 |
+
self.session = session
|
| 90 |
+
self.class_map = class_map
|
| 91 |
+
self.input_name = self.session.get_inputs()[0].name
|
| 92 |
+
|
| 93 |
+
self.transforms = transforms.Compose([
|
| 94 |
+
transforms.Resize((224, 224)),
|
| 95 |
+
transforms.ToTensor(),
|
| 96 |
+
transforms.Normalize(
|
| 97 |
+
[0.485, 0.456, 0.406],
|
| 98 |
+
[0.229, 0.224, 0.225]
|
| 99 |
+
)
|
| 100 |
+
])
|
| 101 |
+
|
| 102 |
+
def predict(self, image_input):
|
| 103 |
+
logger.info("Starting ResNet ONNX prediction...")
|
| 104 |
+
try:
|
| 105 |
+
if isinstance(image_input, str):
|
| 106 |
+
image = Image.open(image_input).convert("RGB")
|
| 107 |
+
elif isinstance(image_input, Image.Image):
|
| 108 |
+
image = image_input.convert("RGB")
|
| 109 |
+
else:
|
| 110 |
+
raise TypeError("image_input must be a file path or PIL.Image")
|
| 111 |
+
|
| 112 |
+
# Preprocess to [1, 3, 224, 224] NumPy array
|
| 113 |
+
input_tensor = self.transforms(image).unsqueeze(0).numpy()
|
| 114 |
+
|
| 115 |
+
# Execute ONNX forward pass
|
| 116 |
+
outputs = self.session.run(None, {self.input_name: input_tensor})
|
| 117 |
+
logits = outputs[0][0]
|
| 118 |
+
|
| 119 |
+
# Softmax calculation
|
| 120 |
+
exp_logits = np.exp(logits - np.max(logits))
|
| 121 |
+
probs = exp_logits / np.sum(exp_logits)
|
| 122 |
+
|
| 123 |
+
class_probs = {
|
| 124 |
+
self.class_map[i]: float(probs[i])
|
| 125 |
+
for i in range(len(self.class_map))
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
sorted_probs = dict(
|
| 129 |
+
sorted(class_probs.items(), key=lambda x: x[1], reverse=True)
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return sorted_probs
|
| 133 |
+
|
| 134 |
+
except UnidentifiedImageError:
|
| 135 |
+
logger.error("Invalid image file provided to ResNet ONNX predictor.")
|
| 136 |
+
raise ValueError("Invalid image file provided")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.exception("ResNet ONNX prediction failed.")
|
| 139 |
+
raise RuntimeError(f"ResNet ONNX prediction failed: {str(e)}")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# =====================================================================
|
| 143 |
+
# 3. ONNX FUSION PREDICTOR (EFFICIENTNET + CONVNEXT)
|
| 144 |
+
# =====================================================================
|
| 145 |
+
class FusionONNXPredictor:
|
| 146 |
+
def __init__(self, session: ort.InferenceSession, class_map: dict, convnext_model_name="facebook/convnext-small-224"):
|
| 147 |
+
logger.info("Initializing Fusion ONNX Predictor...")
|
| 148 |
+
self.session = session
|
| 149 |
+
self.class_map = class_map
|
| 150 |
+
|
| 151 |
+
# Dynamically retrieve ONNX input names (EffNet & ConvNeXt)
|
| 152 |
+
self.input_eff_name = self.session.get_inputs()[0].name
|
| 153 |
+
self.input_cnx_name = self.session.get_inputs()[1].name
|
| 154 |
+
|
| 155 |
+
self.eff_normalize = transforms.Compose([
|
| 156 |
+
transforms.Resize((260, 260)),
|
| 157 |
+
transforms.ToTensor(),
|
| 158 |
+
transforms.Normalize(
|
| 159 |
+
[0.485, 0.456, 0.406],
|
| 160 |
+
[0.229, 0.224, 0.225]
|
| 161 |
+
)
|
| 162 |
+
])
|
| 163 |
+
|
| 164 |
+
self.convnext_processor = ConvNextImageProcessor.from_pretrained(convnext_model_name)
|
| 165 |
+
|
| 166 |
+
def predict(self, image_input):
|
| 167 |
+
logger.info("Starting Fusion ONNX prediction...")
|
| 168 |
+
try:
|
| 169 |
+
if isinstance(image_input, str):
|
| 170 |
+
image = Image.open(image_input).convert("RGB")
|
| 171 |
+
elif isinstance(image_input, Image.Image):
|
| 172 |
+
image = image_input.convert("RGB")
|
| 173 |
+
else:
|
| 174 |
+
raise TypeError("image_input must be a file path or PIL.Image")
|
| 175 |
+
|
| 176 |
+
# EfficientNet preprocessing -> [1, 3, 260, 260]
|
| 177 |
+
pixel_eff_np = self.eff_normalize(image).unsqueeze(0).numpy()
|
| 178 |
+
|
| 179 |
+
# ConvNeXt preprocessing -> [1, 3, 224, 224]
|
| 180 |
+
inputs_cnx = self.convnext_processor(images=image, return_tensors="pt")
|
| 181 |
+
pixel_cnx_np = inputs_cnx["pixel_values"].numpy()
|
| 182 |
+
|
| 183 |
+
# Execute dual-input ONNX forward pass
|
| 184 |
+
onnx_inputs = {
|
| 185 |
+
self.input_eff_name: pixel_eff_np,
|
| 186 |
+
self.input_cnx_name: pixel_cnx_np
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
outputs = self.session.run(None, onnx_inputs)
|
| 190 |
+
logits = outputs[0][0]
|
| 191 |
+
|
| 192 |
+
# Softmax calculation
|
| 193 |
+
exp_logits = np.exp(logits - np.max(logits))
|
| 194 |
+
probs = exp_logits / np.sum(exp_logits)
|
| 195 |
+
|
| 196 |
+
class_probs = {
|
| 197 |
+
self.class_map[i]: float(probs[i])
|
| 198 |
+
for i in range(len(self.class_map))
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
sorted_probs = dict(
|
| 202 |
+
sorted(class_probs.items(), key=lambda x: x[1], reverse=True)
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
return sorted_probs
|
| 206 |
+
|
| 207 |
+
except UnidentifiedImageError:
|
| 208 |
+
logger.error("Invalid image file provided to Fusion ONNX predictor.")
|
| 209 |
+
raise ValueError("Invalid image file provided")
|
| 210 |
+
except Exception as e:
|
| 211 |
+
logger.exception("Fusion ONNX prediction failed.")
|
| 212 |
+
raise RuntimeError(f"Fusion ONNX prediction failed: {str(e)}")
|
scripts/yolo_predict.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import logging
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from ultralytics import YOLO
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_yolo_damage_boxes(image_path, yolo_model: YOLO, output_path: str):
|
| 10 |
+
"""Runs object detection using the YOLO ONNX model."""
|
| 11 |
+
logger.info("Starting YOLO ONNX damage detection...")
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
image = Image.open(image_path).convert("RGB")
|
| 15 |
+
|
| 16 |
+
# Run ONNX inference through Ultralytics wrapper
|
| 17 |
+
results = yolo_model.predict(
|
| 18 |
+
source=image,
|
| 19 |
+
conf=0.05,
|
| 20 |
+
imgsz=640,
|
| 21 |
+
verbose=False
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
result = results[0]
|
| 25 |
+
boxes = result.boxes
|
| 26 |
+
detections = []
|
| 27 |
+
|
| 28 |
+
if boxes is not None and len(boxes) > 0:
|
| 29 |
+
logger.info(f"{len(boxes)} damage detections found.")
|
| 30 |
+
|
| 31 |
+
for box in boxes:
|
| 32 |
+
conf = float(box.conf[0])
|
| 33 |
+
cls_id = int(box.cls[0])
|
| 34 |
+
|
| 35 |
+
label = yolo_model.names[cls_id]
|
| 36 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 37 |
+
|
| 38 |
+
detections.append({
|
| 39 |
+
"label": label,
|
| 40 |
+
"confidence": round(conf, 4),
|
| 41 |
+
"box": [x1, y1, x2, y2]
|
| 42 |
+
})
|
| 43 |
+
else:
|
| 44 |
+
logger.info("No damage detections found.")
|
| 45 |
+
|
| 46 |
+
# Save bounding box visual overlay
|
| 47 |
+
plotted = result.plot()
|
| 48 |
+
cv2.imwrite(output_path, plotted)
|
| 49 |
+
|
| 50 |
+
logger.info(f"YOLO ONNX output saved to: {output_path}")
|
| 51 |
+
|
| 52 |
+
return {
|
| 53 |
+
"detections": detections,
|
| 54 |
+
"total_detections": len(detections),
|
| 55 |
+
"message": (
|
| 56 |
+
"No damage detected"
|
| 57 |
+
if len(detections) == 0
|
| 58 |
+
else "Detections found"
|
| 59 |
+
)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
except Exception as e:
|
| 63 |
+
logger.exception("YOLO ONNX detection failed.")
|
| 64 |
+
raise RuntimeError(f"YOLO detection failed: {str(e)}")
|