junaid17 commited on
Commit
624fba0
·
verified ·
1 Parent(s): 09a57df

Upload 6 files

Browse files
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)}")