junaid17 commited on
Commit
6b2e578
·
verified ·
1 Parent(s): e252153

Upload 29 files

Browse files
Models_Conversion/base_fusion_model.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torchvision.models as models
4
+ from transformers import ConvNextModel
5
+ from huggingface_hub import hf_hub_download
6
+
7
+
8
+ # ==========================================
9
+ # 1. MODEL ARCHITECTURE DEFINITION
10
+ # ==========================================
11
+ class FusionClassifier(nn.Module):
12
+ def __init__(self, num_classes, convnext_model_name="facebook/convnext-small-224"):
13
+ super().__init__()
14
+
15
+ # EfficientNet-V2-S backbone
16
+ eff = models.efficientnet_v2_s(weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1)
17
+
18
+ for param in eff.parameters():
19
+ param.requires_grad = False
20
+
21
+ for param in eff.features[5].parameters():
22
+ param.requires_grad = True
23
+ for param in eff.features[6].parameters():
24
+ param.requires_grad = True
25
+ for param in eff.features[7].parameters():
26
+ param.requires_grad = True
27
+
28
+ self.eff_features = eff.features
29
+ self.eff_avgpool = eff.avgpool
30
+ self.eff_out_dim = eff.classifier[1].in_features
31
+
32
+ # ConvNeXt-Small backbone
33
+ cnx = ConvNextModel.from_pretrained(convnext_model_name)
34
+
35
+ for param in cnx.parameters():
36
+ param.requires_grad = False
37
+
38
+ for param in cnx.encoder.stages[2].parameters():
39
+ param.requires_grad = True
40
+ for param in cnx.encoder.stages[3].parameters():
41
+ param.requires_grad = True
42
+ for param in cnx.layernorm.parameters():
43
+ param.requires_grad = True
44
+
45
+ self.cnx_backbone = cnx
46
+ self.cnx_out_dim = 768
47
+
48
+ # Fusion head
49
+ fused_dim = self.eff_out_dim + self.cnx_out_dim
50
+ self.fusion_head = nn.Sequential(
51
+ nn.Dropout(p=0.4),
52
+ nn.Linear(fused_dim, 512),
53
+ nn.LayerNorm(512),
54
+ nn.GELU(),
55
+ nn.Dropout(p=0.3),
56
+ nn.Linear(512, 256),
57
+ nn.LayerNorm(256),
58
+ nn.GELU(),
59
+ nn.Dropout(p=0.2),
60
+ nn.Linear(256, num_classes),
61
+ )
62
+
63
+ def forward(self, pixel_values_eff, pixel_values_cnx):
64
+ x_eff = self.eff_features(pixel_values_eff)
65
+ x_eff = self.eff_avgpool(x_eff)
66
+ x_eff = torch.flatten(x_eff, 1)
67
+
68
+ cnx_out = self.cnx_backbone(pixel_values=pixel_values_cnx, return_dict=True)
69
+ x_cnx = cnx_out.pooler_output
70
+
71
+ fused = torch.cat([x_eff, x_cnx], dim=1)
72
+ logits = self.fusion_head(fused)
73
+
74
+ return logits
75
+
76
+
77
+ # ==========================================
78
+ # 2. HUGGING FACE DOWNLOAD & LOAD FUNCTION
79
+ # ==========================================
80
+ def load_fusion_model_from_hf(
81
+ repo_id: str = "junaid17/best_fusion_model_fp16",
82
+ filename: str = "best_fusion_model_fp16.pt",
83
+ num_classes: int = 10, # Replace with your actual number of classes
84
+ device: str = "cpu",
85
+ ) -> nn.Module:
86
+ """Downloads weights from Hugging Face Hub and loads into FusionClassifier."""
87
+ print(f"Downloading checkpoint from Hugging Face Hub: '{repo_id}/{filename}'...")
88
+
89
+ # Download weights file from Hugging Face Hub
90
+ checkpoint_path = hf_hub_download(repo_id=repo_id, filename=filename)
91
+
92
+ # Instantiate Model
93
+ model = FusionClassifier(num_classes=num_classes)
94
+
95
+ # Load weights
96
+ checkpoint = torch.load(checkpoint_path, map_location=device)
97
+ if isinstance(checkpoint, dict) and "state_dict" in checkpoint:
98
+ model.load_state_dict(checkpoint["state_dict"])
99
+ elif isinstance(checkpoint, dict):
100
+ model.load_state_dict(checkpoint)
101
+ else:
102
+ model = checkpoint
103
+
104
+ # Ensure model is float32 for stable ONNX export
105
+ model = model.float().to(device)
106
+ model.eval()
107
+
108
+ print("✅ Model loaded successfully from Hugging Face.")
109
+ return model
Models_Conversion/base_model_resnet.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torchvision import models
4
+ from huggingface_hub import hf_hub_download
5
+
6
+
7
+ class Car_Classifier_Resnet(nn.Module):
8
+ def __init__(self, num_classes):
9
+ super().__init__()
10
+
11
+ self.model = models.resnet18(weights="DEFAULT")
12
+
13
+ for param in self.model.parameters():
14
+ param.requires_grad = False
15
+
16
+ for param in self.model.layer3.parameters():
17
+ param.requires_grad = True
18
+
19
+ for param in self.model.layer4.parameters():
20
+ param.requires_grad = True
21
+
22
+ self.model.fc = nn.Sequential(
23
+ nn.Dropout(0.5),
24
+ nn.Linear(self.model.fc.in_features, 256),
25
+ nn.ReLU(),
26
+ nn.Dropout(0.3),
27
+ nn.Linear(256, num_classes)
28
+ )
29
+
30
+ def forward(self, x):
31
+ return self.model(x)
32
+
33
+
34
+ def load_resnet_model_from_hf(
35
+ repo_id: str,
36
+ filename: str = "car-damage-classifier.pt",
37
+ num_classes: int = 6,
38
+ device: str = "cpu",
39
+ hf_token: str = None,
40
+ ) -> torch.nn.Module:
41
+ """
42
+ Downloads and loads the state_dict for Car_Classifier_Resnet from Hugging Face Hub.
43
+ """
44
+ print(f"Downloading checkpoint '{filename}' from Hugging Face repo '{repo_id}'...")
45
+ checkpoint_path = hf_hub_download(
46
+ repo_id=repo_id,
47
+ filename=filename,
48
+ token=hf_token
49
+ )
50
+
51
+ model = Car_Classifier_Resnet(num_classes=num_classes)
52
+ state_dict = torch.load(checkpoint_path, map_location=device)
53
+
54
+ if isinstance(state_dict, dict) and "state_dict" in state_dict:
55
+ state_dict = state_dict["state_dict"]
56
+
57
+ model.load_state_dict(state_dict)
58
+ model.to(device)
59
+ model.eval()
60
+ print("✅ ResNet-18 model loaded successfully.")
61
+ return model
Models_Conversion/base_model_yolo.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from ultralytics import YOLO
3
+ from huggingface_hub import hf_hub_download
4
+
5
+
6
+ def load_yolo_model_from_hf(
7
+ repo_id: str = "junaid17/Yolo_Model",
8
+ filename: str = "damage_detector.pt",
9
+ hf_token: str = None,
10
+ ) -> YOLO:
11
+ """
12
+ Downloads and loads a trained Ultralytics YOLO model from Hugging Face Hub.
13
+ """
14
+ print(f"Downloading YOLO checkpoint '{filename}' from Hugging Face repo '{repo_id}'...")
15
+ checkpoint_path = hf_hub_download(
16
+ repo_id=repo_id,
17
+ filename=filename,
18
+ token=hf_token
19
+ )
20
+ model = YOLO(checkpoint_path)
21
+ print("✅ YOLO model loaded successfully.")
22
+ return model
Models_Conversion/base_vs_onnx_model_evaluation.ipynb ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 3,
6
+ "id": "7c5a22e0",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import os\n",
11
+ "import torch\n",
12
+ "import numpy as np\n",
13
+ "import onnxruntime as ort\n",
14
+ "from tqdm import tqdm\n",
15
+ "from torch.utils.data import DataLoader, Dataset\n",
16
+ "from torchvision import transforms\n",
17
+ "from torchvision.datasets import ImageFolder\n",
18
+ "from transformers import ConvNextImageProcessor\n",
19
+ "from huggingface_hub import hf_hub_download\n",
20
+ "from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report\n",
21
+ "from base_fusion_model import FusionClassifier\n",
22
+ "from base_model_resnet import Car_Classifier_Resnet\n",
23
+ "\n",
24
+ "# ==========================================\n",
25
+ "# IMPORT YOUR BASE PYTORCH MODELS HERE\n",
26
+ "# Example: from my_models import FusionClassifier, Car_Classifier_Resnet\n",
27
+ "# ==========================================\n",
28
+ "\n",
29
+ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
30
+ "HF_TOKEN = os.getenv(\"HF_TOKEN\") # Ensure your Hugging Face token is set in environment\n",
31
+ "\n",
32
+ "# ==========================================\n",
33
+ "# 1. IMAGE PROCESSING & TRANSFORMS\n",
34
+ "# ==========================================\n",
35
+ "\n",
36
+ "# --- Fusion Model Transforms ---\n",
37
+ "IMG_SIZE = 260\n",
38
+ "convnext_model_name = \"facebook/convnext-small-224\"\n",
39
+ "processor = ConvNextImageProcessor.from_pretrained(convnext_model_name)\n",
40
+ "\n",
41
+ "fusion_val_transforms = transforms.Compose([\n",
42
+ " transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
43
+ "])\n",
44
+ "\n",
45
+ "eff_normalize = transforms.Compose([\n",
46
+ " transforms.ToTensor(),\n",
47
+ " transforms.Normalize([0.485, 0.456, 0.406],\n",
48
+ " [0.229, 0.224, 0.225])\n",
49
+ "])\n",
50
+ "\n",
51
+ "# --- ResNet Model Transforms ---\n",
52
+ "resnet_test_transforms = transforms.Compose([\n",
53
+ " transforms.Resize((224, 224)),\n",
54
+ " transforms.ToTensor(),\n",
55
+ " transforms.Normalize([0.485, 0.456, 0.406],\n",
56
+ " [0.229, 0.224, 0.225])\n",
57
+ "])\n",
58
+ "\n",
59
+ "\n",
60
+ "# ==========================================\n",
61
+ "# 2. DATASETS\n",
62
+ "# ==========================================\n",
63
+ "\n",
64
+ "class FusionDataset(Dataset):\n",
65
+ " def __init__(self, data_dir, processor, transform=None):\n",
66
+ " self.dataset = ImageFolder(root=data_dir, transform=transform)\n",
67
+ " self.processor = processor\n",
68
+ " self.classes = self.dataset.classes\n",
69
+ "\n",
70
+ " def __len__(self):\n",
71
+ " return len(self.dataset)\n",
72
+ "\n",
73
+ " def __getitem__(self, idx):\n",
74
+ " pil_image, label = self.dataset[idx]\n",
75
+ "\n",
76
+ " # EfficientNet branch\n",
77
+ " pixel_eff = eff_normalize(pil_image)\n",
78
+ "\n",
79
+ " # ConvNeXt branch\n",
80
+ " enc = self.processor(images=pil_image, return_tensors='pt')\n",
81
+ " pixel_cnx = enc['pixel_values'].squeeze(0)\n",
82
+ "\n",
83
+ " return {\n",
84
+ " 'pixel_values_eff': pixel_eff,\n",
85
+ " 'pixel_values_cnx': pixel_cnx,\n",
86
+ " 'labels': torch.tensor(label)\n",
87
+ " }\n",
88
+ "\n",
89
+ "class ResNetDataset(Dataset):\n",
90
+ " def __init__(self, data_dir, transform=None):\n",
91
+ " self.dataset = ImageFolder(root=data_dir, transform=transform)\n",
92
+ " self.classes = self.dataset.classes\n",
93
+ "\n",
94
+ " def __len__(self):\n",
95
+ " return len(self.dataset)\n",
96
+ "\n",
97
+ " def __getitem__(self, idx):\n",
98
+ " return self.dataset[idx]\n",
99
+ "\n",
100
+ "\n",
101
+ "# ==========================================\n",
102
+ "# 3. HUGGING FACE MODEL LOADER\n",
103
+ "# ==========================================\n",
104
+ "def load_models_from_hf(repo_id, pt_filename, onnx_filename, model_instance):\n",
105
+ " \"\"\"\n",
106
+ " Downloads PyTorch and ONNX models from HF and loads PyTorch weights into the base instance.\n",
107
+ " \"\"\"\n",
108
+ " print(f\"\\n⬇️ Downloading models from Hugging Face: {repo_id}\")\n",
109
+ " \n",
110
+ " # 1. Download and load PyTorch weights\n",
111
+ " pt_path = hf_hub_download(repo_id=repo_id, filename=pt_filename, token=HF_TOKEN)\n",
112
+ " checkpoint = torch.load(pt_path, map_location=device)\n",
113
+ " \n",
114
+ " # Handle both full checkpoints dicts and direct state_dicts\n",
115
+ " if 'model_state_dict' in checkpoint:\n",
116
+ " model_instance.load_state_dict(checkpoint['model_state_dict'])\n",
117
+ " print(f\"✅ PyTorch model loaded (Val Acc: {checkpoint.get('val_acc', 'N/A')}%)\")\n",
118
+ " else:\n",
119
+ " model_instance.load_state_dict(checkpoint)\n",
120
+ " print(\"✅ PyTorch model loaded\")\n",
121
+ " \n",
122
+ " model_instance.to(device)\n",
123
+ " model_instance.eval()\n",
124
+ "\n",
125
+ " # 2. Download ONNX model\n",
126
+ " onnx_path = hf_hub_download(repo_id=repo_id, filename=onnx_filename, token=HF_TOKEN)\n",
127
+ " print(\"✅ ONNX model downloaded\")\n",
128
+ " \n",
129
+ " return model_instance, onnx_path\n",
130
+ "\n",
131
+ "\n",
132
+ "# ==========================================\n",
133
+ "# 4. EVALUATION ENGINE\n",
134
+ "# ==========================================\n",
135
+ "def evaluate_models(model_type, torch_model, onnx_model_path, dataloader, target_names):\n",
136
+ " \"\"\"\n",
137
+ " Runs evaluation for both PyTorch and ONNX models side-by-side.\n",
138
+ " model_type must be either 'fusion' or 'resnet'.\n",
139
+ " \"\"\"\n",
140
+ " providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']\n",
141
+ " ort_session = ort.InferenceSession(onnx_model_path, providers=providers)\n",
142
+ " \n",
143
+ " all_labels = []\n",
144
+ " torch_preds = []\n",
145
+ " onnx_preds = []\n",
146
+ "\n",
147
+ " print(f\"\\n🚀 Starting Evaluation for {model_type.upper()} Model...\")\n",
148
+ " \n",
149
+ " with torch.no_grad():\n",
150
+ " for batch in tqdm(dataloader, desc=\"Evaluating\"):\n",
151
+ " if model_type == 'fusion':\n",
152
+ " images_eff = batch['pixel_values_eff'].to(device)\n",
153
+ " images_cnx = batch['pixel_values_cnx'].to(device)\n",
154
+ " labels = batch['labels'].numpy()\n",
155
+ " \n",
156
+ " # PyTorch Inference\n",
157
+ " logits_torch = torch_model(images_eff, images_cnx)\n",
158
+ " \n",
159
+ " # ONNX Inference (Requires inputs matching the exported graph)\n",
160
+ " onnx_inputs = {\n",
161
+ " ort_session.get_inputs()[0].name: images_eff.cpu().numpy(),\n",
162
+ " ort_session.get_inputs()[1].name: images_cnx.cpu().numpy()\n",
163
+ " }\n",
164
+ " logits_onnx = ort_session.run(None, onnx_inputs)[0]\n",
165
+ "\n",
166
+ " elif model_type == 'resnet':\n",
167
+ " images, labels_tensor = batch\n",
168
+ " images = images.to(device)\n",
169
+ " labels = labels_tensor.numpy()\n",
170
+ " \n",
171
+ " # PyTorch Inference\n",
172
+ " logits_torch = torch_model(images)\n",
173
+ " \n",
174
+ " # ONNX Inference\n",
175
+ " onnx_inputs = {ort_session.get_inputs()[0].name: images.cpu().numpy()}\n",
176
+ " logits_onnx = ort_session.run(None, onnx_inputs)[0]\n",
177
+ "\n",
178
+ " # Collect Predictions\n",
179
+ " preds_t = torch.argmax(logits_torch, dim=1).cpu().numpy()\n",
180
+ " preds_o = np.argmax(logits_onnx, axis=1)\n",
181
+ "\n",
182
+ " all_labels.extend(labels)\n",
183
+ " torch_preds.extend(preds_t)\n",
184
+ " onnx_preds.extend(preds_o)\n",
185
+ "\n",
186
+ " # Helper function to print detailed metrics\n",
187
+ " def print_metrics(y_true, y_pred, title):\n",
188
+ " acc = accuracy_score(y_true, y_pred)\n",
189
+ " prec = precision_score(y_true, y_pred, average='macro', zero_division=0)\n",
190
+ " rec = recall_score(y_true, y_pred, average='macro', zero_division=0)\n",
191
+ " f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)\n",
192
+ "\n",
193
+ " print(f\"\\n{'='*50}\")\n",
194
+ " print(f\" {title.center(48)} \")\n",
195
+ " print(f\"{'='*50}\")\n",
196
+ " print(f\"Accuracy : {acc:.4f}\")\n",
197
+ " print(f\"Precision : {prec:.4f}\")\n",
198
+ " print(f\"Recall : {rec:.4f}\")\n",
199
+ " print(f\"F1 Score : {f1:.4f}\")\n",
200
+ " print(\"-\" * 50)\n",
201
+ " print(\"Classification Report:\")\n",
202
+ " print(classification_report(y_true, y_pred, target_names=target_names, zero_division=0))\n",
203
+ "\n",
204
+ " print_metrics(all_labels, torch_preds, f\"{model_type.upper()} - PYTORCH MODEL\")\n",
205
+ " print_metrics(all_labels, onnx_preds, f\"{model_type.upper()} - ONNX MODEL\")"
206
+ ]
207
+ },
208
+ {
209
+ "cell_type": "code",
210
+ "execution_count": 5,
211
+ "id": "375a4992",
212
+ "metadata": {},
213
+ "outputs": [
214
+ {
215
+ "data": {
216
+ "application/vnd.jupyter.widget-view+json": {
217
+ "model_id": "c428a2fa573042d590a7663a583b9c97",
218
+ "version_major": 2,
219
+ "version_minor": 0
220
+ },
221
+ "text/plain": [
222
+ "Loading weights: 0%| | 0/342 [00:00<?, ?it/s]"
223
+ ]
224
+ },
225
+ "metadata": {},
226
+ "output_type": "display_data"
227
+ },
228
+ {
229
+ "name": "stderr",
230
+ "output_type": "stream",
231
+ "text": [
232
+ "[transformers] \u001b[1mConvNextModel LOAD REPORT\u001b[0m from: facebook/convnext-small-224\n",
233
+ "Key | Status | | \n",
234
+ "------------------+------------+--+-\n",
235
+ "classifier.bias | UNEXPECTED | | \n",
236
+ "classifier.weight | UNEXPECTED | | \n",
237
+ "\n",
238
+ "Notes:\n",
239
+ "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
240
+ ]
241
+ },
242
+ {
243
+ "name": "stdout",
244
+ "output_type": "stream",
245
+ "text": [
246
+ "\n",
247
+ "⬇️ Downloading models from Hugging Face: junaid17/best_fusion_model_fp16\n",
248
+ "✅ PyTorch model loaded\n",
249
+ "✅ ONNX model downloaded\n"
250
+ ]
251
+ },
252
+ {
253
+ "name": "stderr",
254
+ "output_type": "stream",
255
+ "text": [
256
+ "d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\onnxruntime\\capi\\onnxruntime_inference_collection.py:153: UserWarning: Specified provider 'CUDAExecutionProvider' is not in available provider names.Available providers: 'AzureExecutionProvider, CPUExecutionProvider'\n",
257
+ " warnings.warn(\n"
258
+ ]
259
+ },
260
+ {
261
+ "name": "stdout",
262
+ "output_type": "stream",
263
+ "text": [
264
+ "\n",
265
+ "🚀 Starting Evaluation for FUSION Model...\n"
266
+ ]
267
+ },
268
+ {
269
+ "name": "stderr",
270
+ "output_type": "stream",
271
+ "text": [
272
+ "Evaluating: 100%|██████████| 29/29 [04:30<00:00, 9.31s/it]\n"
273
+ ]
274
+ },
275
+ {
276
+ "name": "stdout",
277
+ "output_type": "stream",
278
+ "text": [
279
+ "\n",
280
+ "==================================================\n",
281
+ " FUSION - PYTORCH MODEL \n",
282
+ "==================================================\n",
283
+ "Accuracy : 0.8413\n",
284
+ "Precision : 0.8364\n",
285
+ "Recall : 0.8342\n",
286
+ "F1 Score : 0.8348\n",
287
+ "--------------------------------------------------\n",
288
+ "Classification Report:\n",
289
+ " precision recall f1-score support\n",
290
+ "\n",
291
+ " F_Breakage 0.86 0.86 0.86 100\n",
292
+ " F_Crushed 0.78 0.78 0.78 80\n",
293
+ " F_Normal 0.91 0.92 0.92 100\n",
294
+ " R_Breakage 0.83 0.80 0.81 60\n",
295
+ " R_Crushed 0.74 0.82 0.78 60\n",
296
+ " R_Normal 0.89 0.83 0.86 60\n",
297
+ "\n",
298
+ " accuracy 0.84 460\n",
299
+ " macro avg 0.84 0.83 0.83 460\n",
300
+ "weighted avg 0.84 0.84 0.84 460\n",
301
+ "\n",
302
+ "\n",
303
+ "==================================================\n",
304
+ " FUSION - ONNX MODEL \n",
305
+ "==================================================\n",
306
+ "Accuracy : 0.8413\n",
307
+ "Precision : 0.8364\n",
308
+ "Recall : 0.8342\n",
309
+ "F1 Score : 0.8348\n",
310
+ "--------------------------------------------------\n",
311
+ "Classification Report:\n",
312
+ " precision recall f1-score support\n",
313
+ "\n",
314
+ " F_Breakage 0.86 0.86 0.86 100\n",
315
+ " F_Crushed 0.78 0.78 0.78 80\n",
316
+ " F_Normal 0.91 0.92 0.92 100\n",
317
+ " R_Breakage 0.83 0.80 0.81 60\n",
318
+ " R_Crushed 0.74 0.82 0.78 60\n",
319
+ " R_Normal 0.89 0.83 0.86 60\n",
320
+ "\n",
321
+ " accuracy 0.84 460\n",
322
+ " macro avg 0.84 0.83 0.83 460\n",
323
+ "weighted avg 0.84 0.84 0.84 460\n",
324
+ "\n",
325
+ "\n",
326
+ "⬇️ Downloading models from Hugging Face: junaid17/car-damage-classifier\n"
327
+ ]
328
+ },
329
+ {
330
+ "data": {
331
+ "application/vnd.jupyter.widget-view+json": {
332
+ "model_id": "477e596952ee4da99a21c89acb64e70e",
333
+ "version_major": 2,
334
+ "version_minor": 0
335
+ },
336
+ "text/plain": [
337
+ "car-damage-classifier.pt: reconstructing file: 0%| | 0.00B / 45.3MB "
338
+ ]
339
+ },
340
+ "metadata": {},
341
+ "output_type": "display_data"
342
+ },
343
+ {
344
+ "data": {
345
+ "application/vnd.jupyter.widget-view+json": {
346
+ "model_id": "eb1e5cde28db4feb9c1c4fba02eaef64",
347
+ "version_major": 2,
348
+ "version_minor": 0
349
+ },
350
+ "text/plain": [
351
+ "car-damage-classifier.pt: downloading bytes: | 0.00B "
352
+ ]
353
+ },
354
+ "metadata": {},
355
+ "output_type": "display_data"
356
+ },
357
+ {
358
+ "name": "stderr",
359
+ "output_type": "stream",
360
+ "text": [
361
+ "d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\huggingface_hub\\file_download.py:141: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--car-damage-classifier. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.\n",
362
+ "To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development\n",
363
+ " warnings.warn(message)\n"
364
+ ]
365
+ },
366
+ {
367
+ "name": "stdout",
368
+ "output_type": "stream",
369
+ "text": [
370
+ "✅ PyTorch model loaded\n",
371
+ "✅ ONNX model downloaded\n"
372
+ ]
373
+ },
374
+ {
375
+ "name": "stderr",
376
+ "output_type": "stream",
377
+ "text": [
378
+ "d:\\DamageLensAI\\myvenv\\Lib\\site-packages\\onnxruntime\\capi\\onnxruntime_inference_collection.py:153: UserWarning: Specified provider 'CUDAExecutionProvider' is not in available provider names.Available providers: 'AzureExecutionProvider, CPUExecutionProvider'\n",
379
+ " warnings.warn(\n"
380
+ ]
381
+ },
382
+ {
383
+ "name": "stdout",
384
+ "output_type": "stream",
385
+ "text": [
386
+ "\n",
387
+ "🚀 Starting Evaluation for RESNET Model...\n"
388
+ ]
389
+ },
390
+ {
391
+ "name": "stderr",
392
+ "output_type": "stream",
393
+ "text": [
394
+ "Evaluating: 100%|██████████| 29/29 [00:35<00:00, 1.21s/it]"
395
+ ]
396
+ },
397
+ {
398
+ "name": "stdout",
399
+ "output_type": "stream",
400
+ "text": [
401
+ "\n",
402
+ "==================================================\n",
403
+ " RESNET - PYTORCH MODEL \n",
404
+ "==================================================\n",
405
+ "Accuracy : 0.7739\n",
406
+ "Precision : 0.7734\n",
407
+ "Recall : 0.7661\n",
408
+ "F1 Score : 0.7681\n",
409
+ "--------------------------------------------------\n",
410
+ "Classification Report:\n",
411
+ " precision recall f1-score support\n",
412
+ "\n",
413
+ " F_Breakage 0.83 0.78 0.80 100\n",
414
+ " F_Crushed 0.63 0.75 0.69 80\n",
415
+ " F_Normal 0.89 0.85 0.87 100\n",
416
+ " R_Breakage 0.81 0.72 0.76 60\n",
417
+ " R_Crushed 0.67 0.72 0.69 60\n",
418
+ " R_Normal 0.81 0.78 0.80 60\n",
419
+ "\n",
420
+ " accuracy 0.77 460\n",
421
+ " macro avg 0.77 0.77 0.77 460\n",
422
+ "weighted avg 0.78 0.77 0.78 460\n",
423
+ "\n",
424
+ "\n",
425
+ "==================================================\n",
426
+ " RESNET - ONNX MODEL \n",
427
+ "==================================================\n",
428
+ "Accuracy : 0.7739\n",
429
+ "Precision : 0.7734\n",
430
+ "Recall : 0.7661\n",
431
+ "F1 Score : 0.7681\n",
432
+ "--------------------------------------------------\n",
433
+ "Classification Report:\n",
434
+ " precision recall f1-score support\n",
435
+ "\n",
436
+ " F_Breakage 0.83 0.78 0.80 100\n",
437
+ " F_Crushed 0.63 0.75 0.69 80\n",
438
+ " F_Normal 0.89 0.85 0.87 100\n",
439
+ " R_Breakage 0.81 0.72 0.76 60\n",
440
+ " R_Crushed 0.67 0.72 0.69 60\n",
441
+ " R_Normal 0.81 0.78 0.80 60\n",
442
+ "\n",
443
+ " accuracy 0.77 460\n",
444
+ " macro avg 0.77 0.77 0.77 460\n",
445
+ "weighted avg 0.78 0.77 0.78 460\n",
446
+ "\n"
447
+ ]
448
+ },
449
+ {
450
+ "name": "stderr",
451
+ "output_type": "stream",
452
+ "text": [
453
+ "\n"
454
+ ]
455
+ }
456
+ ],
457
+ "source": [
458
+ "if __name__ == \"__main__\":\n",
459
+ " eval_dir = \"D:\\\\DamageLensAI\\\\val\" # Adjust to your evaluation folder\n",
460
+ "\n",
461
+ " # ---------------------------------------------------------\n",
462
+ " # EVALUATE FUSION MODEL\n",
463
+ " # ---------------------------------------------------------\n",
464
+ " fusion_dataset = FusionDataset(data_dir=eval_dir, processor=processor, transform=fusion_val_transforms)\n",
465
+ " fusion_loader = DataLoader(fusion_dataset, batch_size=16, shuffle=False)\n",
466
+ " \n",
467
+ " # Initialize your base Fusion PyTorch architecture\n",
468
+ " # Replace 'FusionClassifier' with your actual class name and arguments\n",
469
+ " base_fusion = FusionClassifier(num_classes=len(fusion_dataset.classes), convnext_model_name=convnext_model_name)\n",
470
+ " \n",
471
+ " # Load weights and get ONNX path from Hub\n",
472
+ " torch_fusion, onnx_fusion_path = load_models_from_hf(\n",
473
+ " repo_id=\"junaid17/best_fusion_model_fp16\",\n",
474
+ " pt_filename=\"best_fusion_model_fp16.pt\", # Update if named differently on HF\n",
475
+ " onnx_filename=\"fusion_model.onnx\",\n",
476
+ " model_instance=base_fusion\n",
477
+ " )\n",
478
+ " \n",
479
+ " evaluate_models('fusion', torch_fusion, onnx_fusion_path, fusion_loader, fusion_dataset.classes)\n",
480
+ "\n",
481
+ " # ---------------------------------------------------------\n",
482
+ " # EVALUATE RESNET MODEL\n",
483
+ " # ---------------------------------------------------------\n",
484
+ " resnet_dataset = ResNetDataset(data_dir=eval_dir, transform=resnet_test_transforms)\n",
485
+ " resnet_loader = DataLoader(resnet_dataset, batch_size=16, shuffle=False)\n",
486
+ " \n",
487
+ " # Initialize your base ResNet PyTorch architecture\n",
488
+ " # Replace 'get_resnet_model' with how you initialize your ResNet\n",
489
+ " base_resnet = Car_Classifier_Resnet(num_classes=len(resnet_dataset.classes))\n",
490
+ " \n",
491
+ " # Load weights and get ONNX path from Hub\n",
492
+ " torch_resnet, onnx_resnet_path = load_models_from_hf(\n",
493
+ " repo_id=\"junaid17/car-damage-classifier\",\n",
494
+ " pt_filename=\"car-damage-classifier.pt\", # Update if named differently on HF\n",
495
+ " onnx_filename=\"car-damage-classifier.onnx\",\n",
496
+ " model_instance=base_resnet\n",
497
+ " )\n",
498
+ " \n",
499
+ " evaluate_models('resnet', torch_resnet, onnx_resnet_path, resnet_loader, resnet_dataset.classes)"
500
+ ]
501
+ },
502
+ {
503
+ "cell_type": "code",
504
+ "execution_count": null,
505
+ "id": "36697686",
506
+ "metadata": {},
507
+ "outputs": [],
508
+ "source": []
509
+ }
510
+ ],
511
+ "metadata": {
512
+ "kernelspec": {
513
+ "display_name": "myvenv (3.11.0)",
514
+ "language": "python",
515
+ "name": "python3"
516
+ },
517
+ "language_info": {
518
+ "codemirror_mode": {
519
+ "name": "ipython",
520
+ "version": 3
521
+ },
522
+ "file_extension": ".py",
523
+ "mimetype": "text/x-python",
524
+ "name": "python",
525
+ "nbconvert_exporter": "python",
526
+ "pygments_lexer": "ipython3",
527
+ "version": "3.11.0"
528
+ }
529
+ },
530
+ "nbformat": 4,
531
+ "nbformat_minor": 5
532
+ }
Models_Conversion/model_onnx_conversion.ipynb ADDED
@@ -0,0 +1,1171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "ac6e10d4",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Fusion Model Conversion"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": 4,
14
+ "id": "0f63f311",
15
+ "metadata": {},
16
+ "outputs": [
17
+ {
18
+ "name": "stdout",
19
+ "output_type": "stream",
20
+ "text": [
21
+ "Downloading checkpoint from Hugging Face Hub: 'junaid17/best_fusion_model_fp16/best_fusion_model_fp16.pt'...\n"
22
+ ]
23
+ },
24
+ {
25
+ "name": "stderr",
26
+ "output_type": "stream",
27
+ "text": [
28
+ "Loading weights: 100%|██████████| 342/342 [00:00<00:00, 1952.82it/s]\n",
29
+ "[transformers] \u001b[1mConvNextModel LOAD REPORT\u001b[0m from: facebook/convnext-small-224\n",
30
+ "Key | Status | | \n",
31
+ "------------------+------------+--+-\n",
32
+ "classifier.weight | UNEXPECTED | | \n",
33
+ "classifier.bias | UNEXPECTED | | \n",
34
+ "\n",
35
+ "Notes:\n",
36
+ "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n"
37
+ ]
38
+ },
39
+ {
40
+ "name": "stdout",
41
+ "output_type": "stream",
42
+ "text": [
43
+ "✅ Model loaded successfully from Hugging Face.\n",
44
+ "Exporting Fusion Model to temporary ONNX graph...\n",
45
+ "✅ Base ONNX export successful.\n",
46
+ "Compressing ONNX model to FP16 precision...\n",
47
+ "✅ ONNX graph compressed to FP16 successfully.\n",
48
+ "Uploading 'fusion_model.onnx' to Hugging Face repo: 'junaid17/best_fusion_model_fp16'...\n"
49
+ ]
50
+ },
51
+ {
52
+ "name": "stderr",
53
+ "output_type": "stream",
54
+ "text": [
55
+ "Processing Files (1 / 1): 100%|██████████| 142MB / 142MB, 680kB/s \n",
56
+ "New Data Upload: 100%|██████████| 142MB / 142MB, 680kB/s \n"
57
+ ]
58
+ },
59
+ {
60
+ "name": "stdout",
61
+ "output_type": "stream",
62
+ "text": [
63
+ "🚀 Successfully uploaded FP16 model to https://huggingface.co/junaid17/best_fusion_model_fp16/blob/main/fusion_model.onnx\n",
64
+ "Downloading 'fusion_model.onnx' from Hugging Face for evaluation...\n",
65
+ "📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
66
+ "\n",
67
+ "===========================================================================\n",
68
+ "IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
69
+ "===========================================================================\n",
70
+ "FB_10.jpg | 775.56 | 225.80 | ✅ YES \n",
71
+ "FB_11.jpg | 683.06 | 263.21 | ✅ YES \n",
72
+ "FB_13.jpg | 359.21 | 156.10 | ✅ YES \n",
73
+ "FB_15.jpg | 357.64 | 152.04 | ✅ YES \n",
74
+ "FB_16.jpg | 340.61 | 157.13 | ✅ YES \n",
75
+ "FB_19.jpg | 343.34 | 157.87 | ✅ YES \n",
76
+ "FB_20.jpg | 562.84 | 188.25 | ✅ YES \n",
77
+ "FB_4.jpg | 328.80 | 162.34 | ✅ YES \n",
78
+ "FB_5.jpg | 466.30 | 166.86 | ✅ YES \n",
79
+ "FB_9.jpg | 657.23 | 188.45 | ✅ YES \n",
80
+ "FC_1.jpg | 372.87 | 234.55 | ✅ YES \n",
81
+ "FC_10.jpg | 473.04 | 164.85 | ✅ YES \n",
82
+ "FC_11.jpg | 332.92 | 154.70 | ✅ YES \n",
83
+ "FC_12.jpg | 379.71 | 197.05 | ✅ YES \n",
84
+ "FC_3.jpg | 546.80 | 297.15 | ✅ YES \n",
85
+ "FC_4.jpg | 810.00 | 387.67 | ✅ YES \n",
86
+ "FC_6.jpg | 528.11 | 211.10 | ✅ YES \n",
87
+ "FC_7.jpg | 546.06 | 267.98 | ✅ YES \n",
88
+ "FC_8.jpg | 665.58 | 230.13 | ✅ YES \n",
89
+ "FC_9.jpg | 516.99 | 213.58 | ✅ YES \n",
90
+ "FN_1.jpg | 497.13 | 216.17 | ✅ YES \n",
91
+ "FN_10.jpg | 484.10 | 215.26 | ✅ YES \n",
92
+ "FN_11.jpg | 521.18 | 221.95 | ✅ YES \n",
93
+ "FN_12.jpg | 498.07 | 235.80 | ✅ YES \n",
94
+ "FN_3.jpg | 473.85 | 219.38 | ✅ YES \n",
95
+ "FN_4.jpg | 474.16 | 419.76 | ✅ YES \n",
96
+ "FN_5.jpg | 796.23 | 229.17 | ✅ YES \n",
97
+ "FN_7.jpg | 506.63 | 211.40 | ✅ YES \n",
98
+ "FN_8.jpg | 432.50 | 237.60 | ✅ YES \n",
99
+ "FN_9.jpg | 725.21 | 239.89 | ✅ YES \n",
100
+ "RB_10.jpg | 480.74 | 220.76 | ✅ YES \n",
101
+ "RB_11.jpg | 449.81 | 224.74 | ✅ YES \n",
102
+ "RB_13.jpg | 478.89 | 221.27 | ✅ YES \n",
103
+ "RB_14.jpg | 500.14 | 222.87 | ✅ YES \n",
104
+ "RB_2.jpg | 494.09 | 223.37 | ✅ YES \n",
105
+ "RB_3.jpg | 488.29 | 220.04 | ✅ YES \n",
106
+ "RB_4.jpg | 498.87 | 233.70 | ✅ YES \n",
107
+ "RB_6.jpg | 496.31 | 221.88 | ✅ YES \n",
108
+ "RB_7.jpg | 477.51 | 214.55 | ✅ YES \n",
109
+ "RB_8.jpg | 503.84 | 217.26 | ✅ YES \n",
110
+ "RC_10.jpg | 491.95 | 221.90 | ✅ YES \n",
111
+ "RC_11.jpg | 497.75 | 218.97 | ✅ YES \n",
112
+ "RC_2.jpg | 493.97 | 222.79 | ✅ YES \n",
113
+ "RC_3.jpg | 481.42 | 220.67 | ✅ YES \n",
114
+ "RC_4.jpg | 487.19 | 224.59 | ✅ YES \n",
115
+ "RC_5.jpg | 484.33 | 219.35 | ✅ YES \n",
116
+ "RC_6.jpg | 483.71 | 216.13 | ✅ YES \n",
117
+ "RC_7.jpg | 572.23 | 213.54 | ✅ YES \n",
118
+ "RC_8.jpg | 484.69 | 218.72 | ✅ YES \n",
119
+ "RC_9.jpg | 578.70 | 218.05 | ✅ YES \n",
120
+ "RN_1.jpg | 493.88 | 221.69 | ✅ YES \n",
121
+ "RN_10.jpg | 469.91 | 213.39 | ✅ YES \n",
122
+ "RN_11.jpg | 488.03 | 212.66 | ✅ YES \n",
123
+ "RN_12.jpg | 495.62 | 209.92 | ✅ YES \n",
124
+ "RN_2.jpg | 484.84 | 214.93 | ✅ YES \n",
125
+ "RN_3.jpg | 467.09 | 218.67 | ✅ YES \n",
126
+ "RN_4.jpg | 496.70 | 215.96 | ✅ YES \n",
127
+ "RN_7.jpg | 480.62 | 214.66 | ✅ YES \n",
128
+ "RN_8.jpg | 502.00 | 213.91 | ✅ YES \n",
129
+ "RN_9.jpg | 466.22 | 217.81 | ✅ YES \n",
130
+ "===========================================================================\n",
131
+ " BENCHMARK SUMMARY\n",
132
+ "===========================================================================\n",
133
+ "Total Images Evaluated : 60\n",
134
+ "Prediction Match Rate : 100.00% (60/60)\n",
135
+ "PyTorch Avg Latency : 503.75 ms / image\n",
136
+ "ONNX Runtime Avg Latency: 219.87 ms / image\n",
137
+ "Performance Gain : 56.35% faster with ONNX Runtime\n",
138
+ "===========================================================================\n",
139
+ "\n"
140
+ ]
141
+ }
142
+ ],
143
+ "source": [
144
+ "import os\n",
145
+ "import glob\n",
146
+ "import time\n",
147
+ "import tempfile\n",
148
+ "import warnings\n",
149
+ "import torch\n",
150
+ "import torchvision.transforms as transforms\n",
151
+ "import onnxruntime as ort\n",
152
+ "import numpy as np\n",
153
+ "import onnx\n",
154
+ "from onnxconverter_common import float16\n",
155
+ "from PIL import Image\n",
156
+ "from dotenv import load_dotenv\n",
157
+ "from transformers import ConvNextImageProcessor\n",
158
+ "from huggingface_hub import HfApi, hf_hub_download\n",
159
+ "\n",
160
+ "# Import model loader from base_fusion_model.py\n",
161
+ "from base_fusion_model import load_fusion_model_from_hf\n",
162
+ "\n",
163
+ "# Filter out PyTorch tracing and transformers load warnings for clean logs\n",
164
+ "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)\n",
165
+ "warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
166
+ "warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n",
167
+ "\n",
168
+ "# ==========================================\n",
169
+ "# 1. PREPROCESSING PIPELINE SETUP\n",
170
+ "# ==========================================\n",
171
+ "IMG_SIZE = 260\n",
172
+ "\n",
173
+ "val_transforms = transforms.Compose([\n",
174
+ " transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
175
+ "])\n",
176
+ "\n",
177
+ "eff_normalize = transforms.Compose([\n",
178
+ " transforms.ToTensor(),\n",
179
+ " transforms.Normalize(\n",
180
+ " mean=[0.485, 0.456, 0.406],\n",
181
+ " std=[0.229, 0.224, 0.225]\n",
182
+ " )\n",
183
+ "])\n",
184
+ "\n",
185
+ "\n",
186
+ "def preprocess_image(pil_image: Image.Image, processor: ConvNextImageProcessor):\n",
187
+ " resized_image = val_transforms(pil_image)\n",
188
+ " pixel_eff = eff_normalize(resized_image).unsqueeze(0)\n",
189
+ " enc = processor(images=resized_image, return_tensors='pt')\n",
190
+ " pixel_cnx = enc['pixel_values']\n",
191
+ "\n",
192
+ " return pixel_eff, pixel_cnx\n",
193
+ "\n",
194
+ "\n",
195
+ "# ==========================================\n",
196
+ "# 2. CONVERT & UPLOAD TO HUGGING FACE\n",
197
+ "# ==========================================\n",
198
+ "def convert_and_upload_onnx(\n",
199
+ " model: torch.nn.Module,\n",
200
+ " processor: ConvNextImageProcessor,\n",
201
+ " repo_id: str,\n",
202
+ " onnx_filename_in_repo: str = \"fusion_model.onnx\",\n",
203
+ " hf_token: str = None,\n",
204
+ "):\n",
205
+ " model.eval()\n",
206
+ "\n",
207
+ " dummy_pil = Image.new(\"RGB\", (IMG_SIZE, IMG_SIZE), color=\"white\")\n",
208
+ " dummy_eff, dummy_cnx = preprocess_image(dummy_pil, processor)\n",
209
+ "\n",
210
+ " with tempfile.TemporaryDirectory() as temp_dir:\n",
211
+ " temp_onnx_path = os.path.join(temp_dir, onnx_filename_in_repo)\n",
212
+ "\n",
213
+ " print(\"Exporting Fusion Model to temporary ONNX graph...\")\n",
214
+ " \n",
215
+ " with warnings.catch_warnings():\n",
216
+ " warnings.simplefilter(\"ignore\")\n",
217
+ " torch.onnx.export(\n",
218
+ " model,\n",
219
+ " (dummy_eff, dummy_cnx),\n",
220
+ " temp_onnx_path,\n",
221
+ " export_params=True,\n",
222
+ " opset_version=18,\n",
223
+ " do_constant_folding=True,\n",
224
+ " input_names=[\"pixel_values_eff\", \"pixel_values_cnx\"],\n",
225
+ " output_names=[\"logits\"],\n",
226
+ " dynamic_axes={\n",
227
+ " \"pixel_values_eff\": {0: \"batch_size\"},\n",
228
+ " \"pixel_values_cnx\": {0: \"batch_size\"},\n",
229
+ " \"logits\": {0: \"batch_size\"},\n",
230
+ " },\n",
231
+ " dynamo=False,\n",
232
+ " )\n",
233
+ " print(\"✅ Base ONNX export successful.\")\n",
234
+ "\n",
235
+ " # Convert ONNX graph weights to FP16 to keep file size ~142 MB\n",
236
+ " print(\"Compressing ONNX model to FP16 precision...\")\n",
237
+ " onnx_model = onnx.load(temp_onnx_path)\n",
238
+ " onnx_model_fp16 = float16.convert_float_to_float16(onnx_model, keep_io_types=True)\n",
239
+ " onnx.save(onnx_model_fp16, temp_onnx_path)\n",
240
+ " print(\"✅ ONNX graph compressed to FP16 successfully.\")\n",
241
+ "\n",
242
+ " # Upload directly to Hugging Face Hub using token from .env\n",
243
+ " api = HfApi(token=hf_token)\n",
244
+ " print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
245
+ " \n",
246
+ " api.upload_file(\n",
247
+ " path_or_fileobj=temp_onnx_path,\n",
248
+ " path_in_repo=onnx_filename_in_repo,\n",
249
+ " repo_id=repo_id,\n",
250
+ " repo_type=\"model\",\n",
251
+ " )\n",
252
+ " print(f\"🚀 Successfully uploaded FP16 model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
253
+ "\n",
254
+ "\n",
255
+ "# ==========================================\n",
256
+ "# 3. BENCHMARK ON REAL TEST IMAGES\n",
257
+ "# ==========================================\n",
258
+ "def benchmark_fusion_with_folder(\n",
259
+ " pytorch_model: torch.nn.Module,\n",
260
+ " processor: ConvNextImageProcessor,\n",
261
+ " repo_id: str,\n",
262
+ " onnx_filename_in_repo: str,\n",
263
+ " image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
264
+ " hf_token: str = None,\n",
265
+ "):\n",
266
+ " print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
267
+ " onnx_path = hf_hub_download(\n",
268
+ " repo_id=repo_id, \n",
269
+ " filename=onnx_filename_in_repo,\n",
270
+ " token=hf_token\n",
271
+ " )\n",
272
+ " session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
273
+ "\n",
274
+ " supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
275
+ " image_paths = []\n",
276
+ " for ext in supported_exts:\n",
277
+ " image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
278
+ "\n",
279
+ " if not image_paths:\n",
280
+ " raise FileNotFoundError(\n",
281
+ " f\"❌ No valid images found in folder '{image_folder}'. \"\n",
282
+ " f\"Please place your test images inside '{image_folder}'.\"\n",
283
+ " )\n",
284
+ "\n",
285
+ " print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
286
+ "\n",
287
+ " pytorch_latencies = []\n",
288
+ " onnx_latencies = []\n",
289
+ " matches = 0\n",
290
+ "\n",
291
+ " # Warmup\n",
292
+ " first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
293
+ " warmup_eff, warmup_cnx = preprocess_image(first_img, processor)\n",
294
+ " warmup_eff_np = warmup_eff.numpy()\n",
295
+ " warmup_cnx_np = warmup_cnx.numpy()\n",
296
+ "\n",
297
+ " pytorch_model.eval()\n",
298
+ " with torch.no_grad():\n",
299
+ " _ = pytorch_model(warmup_eff, warmup_cnx)\n",
300
+ " session.run(None, {\"pixel_values_eff\": warmup_eff_np, \"pixel_values_cnx\": warmup_cnx_np})\n",
301
+ "\n",
302
+ " # Benchmark loop\n",
303
+ " print(\"\\n\" + \"=\" * 75)\n",
304
+ " print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
305
+ " print(\"=\" * 75)\n",
306
+ "\n",
307
+ " for img_path in sorted(image_paths):\n",
308
+ " file_name = os.path.basename(img_path)\n",
309
+ " raw_img = Image.open(img_path).convert(\"RGB\")\n",
310
+ " \n",
311
+ " pixel_values_eff, pixel_values_cnx = preprocess_image(raw_img, processor)\n",
312
+ " np_eff = pixel_values_eff.numpy()\n",
313
+ " np_cnx = pixel_values_cnx.numpy()\n",
314
+ "\n",
315
+ " # PyTorch Inference\n",
316
+ " start_pt = time.perf_counter()\n",
317
+ " with torch.no_grad():\n",
318
+ " pt_logits = pytorch_model(pixel_values_eff, pixel_values_cnx)\n",
319
+ " pt_time = (time.perf_counter() - start_pt) * 1000\n",
320
+ " pytorch_latencies.append(pt_time)\n",
321
+ " pt_pred = int(torch.argmax(pt_logits, dim=1)[0])\n",
322
+ "\n",
323
+ " # ONNX Runtime Inference\n",
324
+ " start_onnx = time.perf_counter()\n",
325
+ " onnx_outputs = session.run(\n",
326
+ " None, \n",
327
+ " {\"pixel_values_eff\": np_eff, \"pixel_values_cnx\": np_cnx}\n",
328
+ " )\n",
329
+ " onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
330
+ " onnx_latencies.append(onnx_time)\n",
331
+ " onnx_pred = int(np.argmax(onnx_outputs[0], axis=1)[0])\n",
332
+ "\n",
333
+ " is_match = pt_pred == onnx_pred\n",
334
+ " if is_match:\n",
335
+ " matches += 1\n",
336
+ " match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
337
+ "\n",
338
+ " print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
339
+ "\n",
340
+ " avg_pt = np.mean(pytorch_latencies)\n",
341
+ " avg_onnx = np.mean(onnx_latencies)\n",
342
+ " speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
343
+ " accuracy_match_pct = (matches / len(image_paths)) * 100\n",
344
+ "\n",
345
+ " print(\"=\" * 75)\n",
346
+ " print(\" BENCHMARK SUMMARY\")\n",
347
+ " print(\"=\" * 75)\n",
348
+ " print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
349
+ " print(f\"Prediction Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
350
+ " print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
351
+ " print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
352
+ " print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
353
+ " print(\"=\" * 75 + \"\\n\")\n",
354
+ "\n",
355
+ "\n",
356
+ "# ==========================================\n",
357
+ "# MAIN EXECUTION FLOW\n",
358
+ "# ==========================================\n",
359
+ "if __name__ == \"__main__\":\n",
360
+ " # Load environment variables from .env file\n",
361
+ " load_dotenv()\n",
362
+ " HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
363
+ "\n",
364
+ " if not HF_TOKEN:\n",
365
+ " print(\"⚠️ Warning: HF_TOKEN not found in .env file. Pushing/pulling private repos may fail.\")\n",
366
+ "\n",
367
+ " NUM_CLASSES = 6\n",
368
+ " HF_REPO_ID = \"junaid17/best_fusion_model_fp16\"\n",
369
+ " ONNX_FILENAME = \"fusion_model.onnx\"\n",
370
+ " TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
371
+ " CONVNEXT_MODEL_NAME = \"facebook/convnext-small-224\"\n",
372
+ "\n",
373
+ " # Step 1: Initialize ConvNeXt Processor\n",
374
+ " processor = ConvNextImageProcessor.from_pretrained(CONVNEXT_MODEL_NAME)\n",
375
+ "\n",
376
+ " # Step 2: Load PyTorch model from Hugging Face\n",
377
+ " py_model = load_fusion_model_from_hf(\n",
378
+ " repo_id=HF_REPO_ID,\n",
379
+ " filename=\"best_fusion_model_fp16.pt\",\n",
380
+ " num_classes=NUM_CLASSES,\n",
381
+ " device=\"cpu\",\n",
382
+ " )\n",
383
+ "\n",
384
+ " # Step 3: Convert to FP16 ONNX and push directly to Hugging Face\n",
385
+ " convert_and_upload_onnx(\n",
386
+ " model=py_model,\n",
387
+ " processor=processor,\n",
388
+ " repo_id=HF_REPO_ID,\n",
389
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
390
+ " hf_token=HF_TOKEN,\n",
391
+ " )\n",
392
+ "\n",
393
+ " # Step 4: Run benchmarking on test image folder\n",
394
+ " benchmark_fusion_with_folder(\n",
395
+ " pytorch_model=py_model,\n",
396
+ " processor=processor,\n",
397
+ " repo_id=HF_REPO_ID,\n",
398
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
399
+ " image_folder=TEST_IMAGE_FOLDER,\n",
400
+ " hf_token=HF_TOKEN,\n",
401
+ " )"
402
+ ]
403
+ },
404
+ {
405
+ "cell_type": "markdown",
406
+ "id": "0e10195c",
407
+ "metadata": {},
408
+ "source": [
409
+ "# Resnet Model Conversion"
410
+ ]
411
+ },
412
+ {
413
+ "cell_type": "code",
414
+ "execution_count": 12,
415
+ "id": "000b4500",
416
+ "metadata": {},
417
+ "outputs": [
418
+ {
419
+ "name": "stdout",
420
+ "output_type": "stream",
421
+ "text": [
422
+ "Downloading checkpoint 'car-damage-classifier.pt' from Hugging Face repo 'junaid17/car-damage-classifier'...\n",
423
+ "✅ ResNet-18 model loaded successfully.\n",
424
+ "Exporting ResNet-18 Model to temporary FP32 ONNX graph...\n",
425
+ "✅ Base ONNX export successful.\n",
426
+ "Compressing ONNX model to FP16 precision...\n",
427
+ "✅ ONNX graph compressed to FP16 successfully.\n",
428
+ "Uploading 'car-damage-classifier.onnx' to Hugging Face repo: 'junaid17/car-damage-classifier'...\n"
429
+ ]
430
+ },
431
+ {
432
+ "name": "stderr",
433
+ "output_type": "stream",
434
+ "text": [
435
+ "Processing Files (1 / 1): 100%|██████████| 22.6MB / 22.6MB, 1.05MB/s \n",
436
+ "New Data Upload: 100%|██████████| 22.6MB / 22.6MB, 1.05MB/s \n"
437
+ ]
438
+ },
439
+ {
440
+ "name": "stdout",
441
+ "output_type": "stream",
442
+ "text": [
443
+ "🚀 Successfully uploaded FP16 ONNX model to https://huggingface.co/junaid17/car-damage-classifier/blob/main/car-damage-classifier.onnx\n",
444
+ "Downloading 'car-damage-classifier.onnx' from Hugging Face for evaluation...\n",
445
+ "📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
446
+ "\n",
447
+ "===========================================================================\n",
448
+ "IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
449
+ "===========================================================================\n",
450
+ "FB_10.jpg | 46.78 | 14.66 | ✅ YES \n",
451
+ "FB_11.jpg | 56.13 | 15.03 | ✅ YES \n",
452
+ "FB_13.jpg | 45.83 | 15.16 | ✅ YES \n",
453
+ "FB_15.jpg | 44.06 | 14.06 | ✅ YES \n",
454
+ "FB_16.jpg | 54.86 | 15.04 | ✅ YES \n",
455
+ "FB_19.jpg | 61.18 | 15.78 | ✅ YES \n",
456
+ "FB_20.jpg | 47.12 | 13.44 | ✅ YES \n",
457
+ "FB_4.jpg | 49.21 | 17.78 | ✅ YES \n",
458
+ "FB_5.jpg | 74.55 | 15.02 | ✅ YES \n",
459
+ "FB_9.jpg | 48.68 | 14.77 | ✅ YES \n",
460
+ "FC_1.jpg | 54.30 | 14.43 | ✅ YES \n",
461
+ "FC_10.jpg | 50.06 | 15.70 | ✅ YES \n",
462
+ "FC_11.jpg | 48.00 | 13.72 | ✅ YES \n",
463
+ "FC_12.jpg | 42.24 | 14.62 | ✅ YES \n",
464
+ "FC_3.jpg | 51.02 | 14.97 | ✅ YES \n",
465
+ "FC_4.jpg | 47.95 | 15.39 | ✅ YES \n",
466
+ "FC_6.jpg | 46.55 | 14.33 | ✅ YES \n",
467
+ "FC_7.jpg | 59.15 | 15.21 | ✅ YES \n",
468
+ "FC_8.jpg | 48.33 | 16.83 | ✅ YES \n",
469
+ "FC_9.jpg | 45.76 | 15.09 | ✅ YES \n",
470
+ "FN_1.jpg | 58.81 | 15.92 | ✅ YES \n",
471
+ "FN_10.jpg | 46.00 | 15.53 | ✅ YES \n",
472
+ "FN_11.jpg | 56.70 | 16.44 | ✅ YES \n",
473
+ "FN_12.jpg | 52.64 | 14.61 | ✅ YES \n",
474
+ "FN_3.jpg | 52.38 | 14.43 | ✅ YES \n",
475
+ "FN_4.jpg | 50.76 | 17.84 | ✅ YES \n",
476
+ "FN_5.jpg | 51.05 | 14.57 | ✅ YES \n",
477
+ "FN_7.jpg | 49.76 | 14.34 | ✅ YES \n",
478
+ "FN_8.jpg | 45.30 | 16.05 | ✅ YES \n",
479
+ "FN_9.jpg | 58.23 | 16.13 | ✅ YES \n",
480
+ "RB_10.jpg | 58.82 | 16.95 | ✅ YES \n",
481
+ "RB_11.jpg | 86.68 | 19.35 | ✅ YES \n",
482
+ "RB_13.jpg | 88.30 | 22.82 | ✅ YES \n",
483
+ "RB_14.jpg | 76.32 | 18.26 | ✅ YES \n",
484
+ "RB_2.jpg | 63.31 | 17.44 | ✅ YES \n",
485
+ "RB_3.jpg | 58.23 | 14.74 | ✅ YES \n",
486
+ "RB_4.jpg | 55.97 | 15.32 | ✅ YES \n",
487
+ "RB_6.jpg | 157.29 | 16.34 | ✅ YES \n",
488
+ "RB_7.jpg | 124.51 | 19.32 | ✅ YES \n",
489
+ "RB_8.jpg | 63.44 | 15.15 | ✅ YES \n",
490
+ "RC_10.jpg | 79.38 | 15.67 | ✅ YES \n",
491
+ "RC_11.jpg | 54.87 | 13.93 | ✅ YES \n",
492
+ "RC_2.jpg | 37.57 | 13.65 | ✅ YES \n",
493
+ "RC_3.jpg | 42.66 | 13.28 | ✅ YES \n",
494
+ "RC_4.jpg | 40.99 | 14.31 | ✅ YES \n",
495
+ "RC_5.jpg | 55.75 | 17.52 | ✅ YES \n",
496
+ "RC_6.jpg | 47.62 | 13.57 | ✅ YES \n",
497
+ "RC_7.jpg | 43.82 | 14.02 | ✅ YES \n",
498
+ "RC_8.jpg | 47.47 | 14.76 | ✅ YES \n",
499
+ "RC_9.jpg | 50.02 | 14.33 | ✅ YES \n",
500
+ "RN_1.jpg | 45.03 | 14.66 | ✅ YES \n",
501
+ "RN_10.jpg | 44.92 | 14.12 | ✅ YES \n",
502
+ "RN_11.jpg | 47.08 | 13.45 | ✅ YES \n",
503
+ "RN_12.jpg | 52.43 | 15.71 | ✅ YES \n",
504
+ "RN_2.jpg | 49.53 | 16.44 | ✅ YES \n",
505
+ "RN_3.jpg | 43.17 | 14.90 | ✅ YES \n",
506
+ "RN_4.jpg | 43.36 | 14.52 | ✅ YES \n",
507
+ "RN_7.jpg | 50.25 | 14.68 | ✅ YES \n",
508
+ "RN_8.jpg | 44.29 | 13.59 | ✅ YES \n",
509
+ "RN_9.jpg | 44.30 | 15.32 | ✅ YES \n",
510
+ "===========================================================================\n",
511
+ " BENCHMARK SUMMARY\n",
512
+ "===========================================================================\n",
513
+ "Total Images Evaluated : 60\n",
514
+ "Prediction Match Rate : 100.00% (60/60)\n",
515
+ "PyTorch Avg Latency : 55.68 ms / image\n",
516
+ "ONNX Runtime Avg Latency: 15.42 ms / image\n",
517
+ "Performance Gain : 72.31% faster with ONNX Runtime\n",
518
+ "===========================================================================\n",
519
+ "\n"
520
+ ]
521
+ }
522
+ ],
523
+ "source": [
524
+ "import os\n",
525
+ "import glob\n",
526
+ "import time\n",
527
+ "import tempfile\n",
528
+ "import warnings\n",
529
+ "import torch\n",
530
+ "import torchvision.transforms as transforms\n",
531
+ "import onnxruntime as ort\n",
532
+ "import numpy as np\n",
533
+ "import onnx\n",
534
+ "from onnxconverter_common import float16\n",
535
+ "from PIL import Image\n",
536
+ "from dotenv import load_dotenv\n",
537
+ "from huggingface_hub import HfApi, hf_hub_download\n",
538
+ "\n",
539
+ "# Import ResNet loader\n",
540
+ "from base_model_resnet import load_resnet_model_from_hf\n",
541
+ "\n",
542
+ "# Suppress PyTorch tracing and deprecation warnings\n",
543
+ "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)\n",
544
+ "warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
545
+ "warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n",
546
+ "\n",
547
+ "# ==========================================\n",
548
+ "# 1. PREPROCESSING PIPELINE\n",
549
+ "# ==========================================\n",
550
+ "test_transforms = transforms.Compose([\n",
551
+ " transforms.Resize((224, 224)),\n",
552
+ " transforms.ToTensor(),\n",
553
+ " transforms.Normalize(\n",
554
+ " mean=[0.485, 0.456, 0.406],\n",
555
+ " std=[0.229, 0.224, 0.225]\n",
556
+ " )\n",
557
+ "])\n",
558
+ "\n",
559
+ "\n",
560
+ "def preprocess_image(pil_image: Image.Image) -> torch.Tensor:\n",
561
+ " \"\"\"\n",
562
+ " Applies standard ResNet preprocessing and adds a batch dimension.\n",
563
+ " Shape returned: (1, 3, 224, 224)\n",
564
+ " \"\"\"\n",
565
+ " return test_transforms(pil_image).unsqueeze(0)\n",
566
+ "\n",
567
+ "\n",
568
+ "# ==========================================\n",
569
+ "# 2. CONVERT TO FP16 ONNX & UPLOAD\n",
570
+ "# ==========================================\n",
571
+ "def convert_and_upload_onnx_fp16(\n",
572
+ " model: torch.nn.Module,\n",
573
+ " repo_id: str,\n",
574
+ " onnx_filename_in_repo: str = \"car-damage-classifier.onnx\",\n",
575
+ " hf_token: str = None,\n",
576
+ "):\n",
577
+ " \"\"\"\n",
578
+ " Exports PyTorch ResNet model to ONNX, compresses it to FP16,\n",
579
+ " and pushes it directly to Hugging Face Hub.\n",
580
+ " \"\"\"\n",
581
+ " model.eval()\n",
582
+ "\n",
583
+ " # Create dummy tensor for graph tracing (1, 3, 224, 224)\n",
584
+ " dummy_input = torch.randn(1, 3, 224, 224)\n",
585
+ "\n",
586
+ " with tempfile.TemporaryDirectory() as temp_dir:\n",
587
+ " temp_onnx_path = os.path.join(temp_dir, onnx_filename_in_repo)\n",
588
+ "\n",
589
+ " print(\"Exporting ResNet-18 Model to temporary FP32 ONNX graph...\")\n",
590
+ " \n",
591
+ " with warnings.catch_warnings():\n",
592
+ " warnings.simplefilter(\"ignore\")\n",
593
+ " torch.onnx.export(\n",
594
+ " model,\n",
595
+ " dummy_input,\n",
596
+ " temp_onnx_path,\n",
597
+ " export_params=True,\n",
598
+ " opset_version=18,\n",
599
+ " do_constant_folding=True,\n",
600
+ " input_names=[\"pixel_values\"],\n",
601
+ " output_names=[\"logits\"],\n",
602
+ " dynamic_axes={\n",
603
+ " \"pixel_values\": {0: \"batch_size\"},\n",
604
+ " \"logits\": {0: \"batch_size\"},\n",
605
+ " },\n",
606
+ " dynamo=False,\n",
607
+ " )\n",
608
+ " print(\"✅ Base ONNX export successful.\")\n",
609
+ "\n",
610
+ " # Convert ONNX graph weights to FP16 to reduce file size\n",
611
+ " print(\"Compressing ONNX model to FP16 precision...\")\n",
612
+ " onnx_model = onnx.load(temp_onnx_path)\n",
613
+ " onnx_model_fp16 = float16.convert_float_to_float16(onnx_model, keep_io_types=True)\n",
614
+ " onnx.save(onnx_model_fp16, temp_onnx_path)\n",
615
+ " print(\"✅ ONNX graph compressed to FP16 successfully.\")\n",
616
+ "\n",
617
+ " # Upload directly to Hugging Face Hub\n",
618
+ " api = HfApi(token=hf_token)\n",
619
+ " print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
620
+ " \n",
621
+ " api.upload_file(\n",
622
+ " path_or_fileobj=temp_onnx_path,\n",
623
+ " path_in_repo=onnx_filename_in_repo,\n",
624
+ " repo_id=repo_id,\n",
625
+ " repo_type=\"model\",\n",
626
+ " )\n",
627
+ " print(f\"🚀 Successfully uploaded FP16 ONNX model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
628
+ "\n",
629
+ "\n",
630
+ "# ==========================================\n",
631
+ "# 3. BENCHMARK ON REAL TEST IMAGES\n",
632
+ "# ==========================================\n",
633
+ "def benchmark_resnet_with_folder(\n",
634
+ " pytorch_model: torch.nn.Module,\n",
635
+ " repo_id: str,\n",
636
+ " onnx_filename_in_repo: str,\n",
637
+ " image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
638
+ " hf_token: str = None,\n",
639
+ "):\n",
640
+ " \"\"\"\n",
641
+ " Downloads ONNX model from Hugging Face Hub and evaluates PyTorch vs ONNX Runtime performance.\n",
642
+ " \"\"\"\n",
643
+ " print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
644
+ " onnx_path = hf_hub_download(\n",
645
+ " repo_id=repo_id, \n",
646
+ " filename=onnx_filename_in_repo,\n",
647
+ " token=hf_token\n",
648
+ " )\n",
649
+ " session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
650
+ "\n",
651
+ " supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
652
+ " image_paths = []\n",
653
+ " for ext in supported_exts:\n",
654
+ " image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
655
+ "\n",
656
+ " if not image_paths:\n",
657
+ " raise FileNotFoundError(\n",
658
+ " f\"❌ No valid images found in folder '{image_folder}'. \"\n",
659
+ " f\"Please verify your path.\"\n",
660
+ " )\n",
661
+ "\n",
662
+ " print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
663
+ "\n",
664
+ " pytorch_latencies = []\n",
665
+ " onnx_latencies = []\n",
666
+ " matches = 0\n",
667
+ "\n",
668
+ " # Warmup run\n",
669
+ " first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
670
+ " warmup_tensor = preprocess_image(first_img)\n",
671
+ " warmup_np = warmup_tensor.numpy()\n",
672
+ "\n",
673
+ " pytorch_model.eval()\n",
674
+ " with torch.no_grad():\n",
675
+ " _ = pytorch_model(warmup_tensor)\n",
676
+ " session.run(None, {\"pixel_values\": warmup_np})\n",
677
+ "\n",
678
+ " # Benchmark Loop\n",
679
+ " print(\"\\n\" + \"=\" * 75)\n",
680
+ " print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
681
+ " print(\"=\" * 75)\n",
682
+ "\n",
683
+ " for img_path in sorted(image_paths):\n",
684
+ " file_name = os.path.basename(img_path)\n",
685
+ " raw_img = Image.open(img_path).convert(\"RGB\")\n",
686
+ " \n",
687
+ " img_tensor = preprocess_image(raw_img)\n",
688
+ " np_input = img_tensor.numpy()\n",
689
+ "\n",
690
+ " # --- PyTorch Inference ---\n",
691
+ " start_pt = time.perf_counter()\n",
692
+ " with torch.no_grad():\n",
693
+ " pt_logits = pytorch_model(img_tensor)\n",
694
+ " pt_time = (time.perf_counter() - start_pt) * 1000\n",
695
+ " pytorch_latencies.append(pt_time)\n",
696
+ " pt_pred = int(torch.argmax(pt_logits, dim=1)[0])\n",
697
+ "\n",
698
+ " # --- ONNX Runtime Inference ---\n",
699
+ " start_onnx = time.perf_counter()\n",
700
+ " onnx_outputs = session.run(None, {\"pixel_values\": np_input})\n",
701
+ " onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
702
+ " onnx_latencies.append(onnx_time)\n",
703
+ " onnx_pred = int(np.argmax(onnx_outputs[0], axis=1)[0])\n",
704
+ "\n",
705
+ " is_match = pt_pred == onnx_pred\n",
706
+ " if is_match:\n",
707
+ " matches += 1\n",
708
+ " match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
709
+ "\n",
710
+ " print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
711
+ "\n",
712
+ " avg_pt = np.mean(pytorch_latencies)\n",
713
+ " avg_onnx = np.mean(onnx_latencies)\n",
714
+ " speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
715
+ " accuracy_match_pct = (matches / len(image_paths)) * 100\n",
716
+ "\n",
717
+ " print(\"=\" * 75)\n",
718
+ " print(\" BENCHMARK SUMMARY\")\n",
719
+ " print(\"=\" * 75)\n",
720
+ " print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
721
+ " print(f\"Prediction Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
722
+ " print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
723
+ " print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
724
+ " print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
725
+ " print(\"=\" * 75 + \"\\n\")\n",
726
+ "\n",
727
+ "\n",
728
+ "# ==========================================\n",
729
+ "# MAIN EXECUTION FLOW\n",
730
+ "# ==========================================\n",
731
+ "if __name__ == \"__main__\":\n",
732
+ " # Load token from .env\n",
733
+ " load_dotenv()\n",
734
+ " HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
735
+ "\n",
736
+ " if not HF_TOKEN:\n",
737
+ " print(\"⚠️ Warning: HF_TOKEN not found in .env file. Pushing/pulling private repos may fail.\")\n",
738
+ "\n",
739
+ " NUM_CLASSES = 6\n",
740
+ " HF_REPO_ID = \"junaid17/car-damage-classifier\"\n",
741
+ " MODEL_FILENAME = \"car-damage-classifier.pt\"\n",
742
+ " ONNX_FILENAME = \"car-damage-classifier.onnx\"\n",
743
+ " TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
744
+ "\n",
745
+ " # Step 1: Load PyTorch ResNet Model from Hugging Face\n",
746
+ " py_model = load_resnet_model_from_hf(\n",
747
+ " repo_id=HF_REPO_ID,\n",
748
+ " filename=MODEL_FILENAME,\n",
749
+ " num_classes=NUM_CLASSES,\n",
750
+ " device=\"cpu\",\n",
751
+ " hf_token=HF_TOKEN,\n",
752
+ " )\n",
753
+ "\n",
754
+ " # Step 2: Convert to FP16 ONNX & Push directly to Hugging Face\n",
755
+ " convert_and_upload_onnx_fp16(\n",
756
+ " model=py_model,\n",
757
+ " repo_id=HF_REPO_ID,\n",
758
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
759
+ " hf_token=HF_TOKEN,\n",
760
+ " )\n",
761
+ "\n",
762
+ " # Step 3: Run benchmarking on test image folder\n",
763
+ " benchmark_resnet_with_folder(\n",
764
+ " pytorch_model=py_model,\n",
765
+ " repo_id=HF_REPO_ID,\n",
766
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
767
+ " image_folder=TEST_IMAGE_FOLDER,\n",
768
+ " hf_token=HF_TOKEN,\n",
769
+ " )"
770
+ ]
771
+ },
772
+ {
773
+ "cell_type": "markdown",
774
+ "id": "0c387aa2",
775
+ "metadata": {},
776
+ "source": [
777
+ "# YOLO Model Conversion"
778
+ ]
779
+ },
780
+ {
781
+ "cell_type": "code",
782
+ "execution_count": 11,
783
+ "id": "f0e5e525",
784
+ "metadata": {},
785
+ "outputs": [
786
+ {
787
+ "name": "stdout",
788
+ "output_type": "stream",
789
+ "text": [
790
+ "Downloading YOLO checkpoint 'damage_detector.pt' from Hugging Face repo 'junaid17/Yolo_Model'...\n",
791
+ "✅ YOLO model loaded successfully.\n",
792
+ "Exporting YOLO11 Model directly to FP16 ONNX format (imgsz=640)...\n",
793
+ "WARNING 'half' is deprecated and will be removed in the future. Use 'quantize' instead.\n",
794
+ "Ultralytics 8.4.116 Python-3.11.0 torch-2.13.0+cpu CPU (12th Gen Intel Core i3-1215U)\n",
795
+ "YOLO11l summary (fused): 190 layers, 25,280,083 parameters, 0 gradients, 86.8 GFLOPs\n",
796
+ "\n",
797
+ "\u001b[34m\u001b[1mPyTorch:\u001b[0m starting from 'C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.pt' with input shape (1, 3, 640, 640) BCHW and output shape(s) (1, 5, 8400) (48.8 MB)\n",
798
+ "\n",
799
+ "\u001b[34m\u001b[1mONNX:\u001b[0m starting export with onnx 1.22.0 opset 17...\n",
800
+ "\u001b[34m\u001b[1mONNX:\u001b[0m slimming with onnxslim 0.1.95...\n",
801
+ "\u001b[34m\u001b[1mONNX:\u001b[0m converting to FP16...\n",
802
+ "\u001b[34m\u001b[1mONNX:\u001b[0m export success 7.1s, saved as 'C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx' (48.5 MB)\n",
803
+ "\n",
804
+ "Export complete (11.6s)\n",
805
+ "Results saved to \u001b[1mC:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx\u001b[0m\n",
806
+ "Predict: yolo predict task=detect model=C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx imgsz=640 quantize=16\n",
807
+ "Validate: yolo val task=detect model=C:\\Users\\junai\\.cache\\huggingface\\hub\\models--junaid17--Yolo_Model\\snapshots\\0cbc6f112ebc6cb7df05fe93f3f0175cc847bec0\\damage_detector.onnx imgsz=640 data=/content/drive/MyDrive/custom_data/dataset_custom.yaml quantize=16 \n",
808
+ "Visualize: https://netron.app\n",
809
+ "✅ FP16 ONNX export successful.\n",
810
+ "📦 Model File Size: 48.50 MB\n",
811
+ "Uploading 'damage_detector.onnx' to Hugging Face repo: 'junaid17/Yolo_Model'...\n"
812
+ ]
813
+ },
814
+ {
815
+ "name": "stderr",
816
+ "output_type": "stream",
817
+ "text": [
818
+ "Processing Files (1 / 1): 100%|██████████| 50.9MB / 50.9MB, 2.79MB/s \n",
819
+ "New Data Upload: 100%|██████████| 8.84MB / 8.84MB, 568kB/s \n"
820
+ ]
821
+ },
822
+ {
823
+ "name": "stdout",
824
+ "output_type": "stream",
825
+ "text": [
826
+ "🚀 Successfully uploaded ONNX model to https://huggingface.co/junaid17/Yolo_Model/blob/main/damage_detector.onnx\n",
827
+ "Downloading 'damage_detector.onnx' from Hugging Face for evaluation...\n",
828
+ "📸 Found 60 images in 'D:\\DamageLensAI\\test_images'. Starting benchmark...\n",
829
+ "\n",
830
+ "===========================================================================\n",
831
+ "IMAGE NAME | PYTORCH (ms) | ONNX (ms) | MATCH? \n",
832
+ "===========================================================================\n",
833
+ "FB_10.jpg | 1961.16 | 933.21 | ✅ YES \n",
834
+ "FB_11.jpg | 1645.58 | 821.06 | ✅ YES \n",
835
+ "FB_13.jpg | 1973.07 | 884.90 | ✅ YES \n",
836
+ "FB_15.jpg | 2296.55 | 1064.76 | ✅ YES \n",
837
+ "FB_16.jpg | 1159.60 | 679.19 | ✅ YES \n",
838
+ "FB_19.jpg | 1310.77 | 729.79 | ✅ YES \n",
839
+ "FB_20.jpg | 1263.96 | 678.22 | ✅ YES \n",
840
+ "FB_4.jpg | 1385.82 | 658.67 | ✅ YES \n",
841
+ "FB_5.jpg | 1352.15 | 665.79 | ✅ YES \n",
842
+ "FB_9.jpg | 1281.14 | 654.91 | ✅ YES \n",
843
+ "FC_1.jpg | 1430.82 | 639.33 | ✅ YES \n",
844
+ "FC_10.jpg | 1223.63 | 772.76 | ✅ YES \n",
845
+ "FC_11.jpg | 1229.15 | 665.59 | ✅ YES \n",
846
+ "FC_12.jpg | 1442.46 | 654.07 | ✅ YES \n",
847
+ "FC_3.jpg | 1255.18 | 635.24 | ✅ YES \n",
848
+ "FC_4.jpg | 1381.86 | 646.25 | ✅ YES \n",
849
+ "FC_6.jpg | 1380.10 | 644.81 | ✅ YES \n",
850
+ "FC_7.jpg | 1211.48 | 644.08 | ✅ YES \n",
851
+ "FC_8.jpg | 1201.65 | 761.60 | ✅ YES \n",
852
+ "FC_9.jpg | 1429.97 | 644.52 | ✅ YES \n",
853
+ "FN_1.jpg | 1181.08 | 649.43 | ✅ YES \n",
854
+ "FN_10.jpg | 1190.17 | 637.96 | ✅ YES \n",
855
+ "FN_11.jpg | 1590.08 | 764.77 | ✅ YES \n",
856
+ "FN_12.jpg | 1300.76 | 601.92 | ✅ YES \n",
857
+ "FN_3.jpg | 1182.89 | 662.93 | ✅ YES \n",
858
+ "FN_4.jpg | 1638.14 | 650.37 | ✅ YES \n",
859
+ "FN_5.jpg | 1426.86 | 657.21 | ✅ YES \n",
860
+ "FN_7.jpg | 1440.60 | 717.18 | ✅ YES \n",
861
+ "FN_8.jpg | 1385.24 | 621.54 | ✅ YES \n",
862
+ "FN_9.jpg | 1377.56 | 643.62 | ✅ YES \n",
863
+ "RB_10.jpg | 1362.92 | 694.52 | ✅ YES \n",
864
+ "RB_11.jpg | 1352.90 | 630.82 | ✅ YES \n",
865
+ "RB_13.jpg | 1373.38 | 684.08 | ✅ YES \n",
866
+ "RB_14.jpg | 1395.17 | 693.95 | ✅ YES \n",
867
+ "RB_2.jpg | 1323.94 | 619.14 | ✅ YES \n",
868
+ "RB_3.jpg | 1204.51 | 657.64 | ✅ YES \n",
869
+ "RB_4.jpg | 1468.20 | 673.22 | ✅ YES \n",
870
+ "RB_6.jpg | 1317.30 | 635.59 | ✅ YES \n",
871
+ "RB_7.jpg | 1384.84 | 639.61 | ✅ YES \n",
872
+ "RB_8.jpg | 1476.26 | 668.84 | ✅ YES \n",
873
+ "RC_10.jpg | 1292.19 | 650.32 | ✅ YES \n",
874
+ "RC_11.jpg | 1374.14 | 667.15 | ✅ YES \n",
875
+ "RC_2.jpg | 1403.93 | 776.57 | ✅ YES \n",
876
+ "RC_3.jpg | 1314.82 | 644.49 | ✅ YES \n",
877
+ "RC_4.jpg | 1283.66 | 626.54 | ✅ YES \n",
878
+ "RC_5.jpg | 1343.69 | 682.90 | ✅ YES \n",
879
+ "RC_6.jpg | 1348.45 | 603.56 | ✅ YES \n",
880
+ "RC_7.jpg | 1229.81 | 623.58 | ✅ YES \n",
881
+ "RC_8.jpg | 1312.53 | 670.69 | ✅ YES \n",
882
+ "RC_9.jpg | 1196.52 | 713.91 | ✅ YES \n",
883
+ "RN_1.jpg | 1125.28 | 619.14 | ✅ YES \n",
884
+ "RN_10.jpg | 1177.20 | 745.49 | ✅ YES \n",
885
+ "RN_11.jpg | 1377.85 | 632.45 | ✅ YES \n",
886
+ "RN_12.jpg | 1271.99 | 611.52 | ✅ YES \n",
887
+ "RN_2.jpg | 1166.40 | 715.83 | ✅ YES \n",
888
+ "RN_3.jpg | 1209.96 | 602.54 | ✅ YES \n",
889
+ "RN_4.jpg | 1129.91 | 696.41 | ✅ YES \n",
890
+ "RN_7.jpg | 1165.09 | 642.48 | ✅ YES \n",
891
+ "RN_8.jpg | 1327.08 | 600.25 | ✅ YES \n",
892
+ "RN_9.jpg | 1123.79 | 609.49 | ✅ YES \n",
893
+ "===========================================================================\n",
894
+ " BENCHMARK SUMMARY\n",
895
+ "===========================================================================\n",
896
+ "Total Images Evaluated : 60\n",
897
+ "Detection Match Rate : 100.00% (60/60)\n",
898
+ "PyTorch Avg Latency : 1356.05 ms / image\n",
899
+ "ONNX Runtime Avg Latency: 680.31 ms / image\n",
900
+ "Performance Gain : 49.83% faster with ONNX Runtime\n",
901
+ "===========================================================================\n",
902
+ "\n"
903
+ ]
904
+ }
905
+ ],
906
+ "source": [
907
+ "import os\n",
908
+ "import glob\n",
909
+ "import time\n",
910
+ "import warnings\n",
911
+ "import torch\n",
912
+ "import torchvision.transforms as transforms\n",
913
+ "import onnxruntime as ort\n",
914
+ "import numpy as np\n",
915
+ "from PIL import Image\n",
916
+ "from dotenv import load_dotenv\n",
917
+ "from huggingface_hub import HfApi, hf_hub_download\n",
918
+ "from ultralytics import YOLO\n",
919
+ "\n",
920
+ "# Import YOLO loader\n",
921
+ "from base_model_yolo import load_yolo_model_from_hf\n",
922
+ "\n",
923
+ "# Suppress warnings\n",
924
+ "warnings.filterwarnings(\"ignore\")\n",
925
+ "\n",
926
+ "# ==========================================\n",
927
+ "# 1. PREPROCESSING PIPELINE\n",
928
+ "# ==========================================\n",
929
+ "IMG_SIZE = 640\n",
930
+ "\n",
931
+ "yolo_transforms = transforms.Compose([\n",
932
+ " transforms.Resize((IMG_SIZE, IMG_SIZE)),\n",
933
+ " transforms.ToTensor(),\n",
934
+ "])\n",
935
+ "\n",
936
+ "def preprocess_image(pil_image: Image.Image) -> torch.Tensor:\n",
937
+ " \"\"\"Preprocesses PIL image for YOLO11 (1, 3, 640, 640).\"\"\"\n",
938
+ " return yolo_transforms(pil_image).unsqueeze(0)\n",
939
+ "\n",
940
+ "\n",
941
+ "# ==========================================\n",
942
+ "# 2. CONVERT, OPTIMIZE TO FP16 & UPLOAD\n",
943
+ "# ==========================================\n",
944
+ "def convert_and_upload_yolo_onnx(\n",
945
+ " yolo_model: YOLO,\n",
946
+ " repo_id: str,\n",
947
+ " onnx_filename_in_repo: str = \"damage_detector.onnx\",\n",
948
+ " hf_token: str = None,\n",
949
+ " imgsz: int = 640,\n",
950
+ "):\n",
951
+ " print(f\"Exporting YOLO11 Model directly to FP16 ONNX format (imgsz={imgsz})...\")\n",
952
+ " \n",
953
+ " # Native Ultralytics FP16 export - lightning fast and avoids converter bugs\n",
954
+ " exported_onnx_path = yolo_model.export(\n",
955
+ " format=\"onnx\",\n",
956
+ " imgsz=imgsz,\n",
957
+ " half=True, # Shrinks the model to ~50MB natively\n",
958
+ " dynamic=False, # Disabling dynamic fixes the CPU type-mismatch errors\n",
959
+ " simplify=True,\n",
960
+ " opset=17\n",
961
+ " )\n",
962
+ " print(\"✅ FP16 ONNX export successful.\")\n",
963
+ "\n",
964
+ " file_size_mb = os.path.getsize(exported_onnx_path) / (1024 * 1024)\n",
965
+ " print(f\"📦 Model File Size: {file_size_mb:.2f} MB\")\n",
966
+ "\n",
967
+ " # Upload to Hugging Face Hub\n",
968
+ " api = HfApi(token=hf_token)\n",
969
+ " print(f\"Uploading '{onnx_filename_in_repo}' to Hugging Face repo: '{repo_id}'...\")\n",
970
+ " \n",
971
+ " api.upload_file(\n",
972
+ " path_or_fileobj=exported_onnx_path,\n",
973
+ " path_in_repo=onnx_filename_in_repo,\n",
974
+ " repo_id=repo_id,\n",
975
+ " repo_type=\"model\",\n",
976
+ " )\n",
977
+ " print(f\"🚀 Successfully uploaded ONNX model to https://huggingface.co/{repo_id}/blob/main/{onnx_filename_in_repo}\")\n",
978
+ "\n",
979
+ "\n",
980
+ "# ==========================================\n",
981
+ "# 3. BENCHMARK ON REAL TEST IMAGES\n",
982
+ "# ==========================================\n",
983
+ "def benchmark_yolo_with_folder(\n",
984
+ " yolo_model: YOLO,\n",
985
+ " repo_id: str,\n",
986
+ " onnx_filename_in_repo: str,\n",
987
+ " image_folder: str = r\"D:\\DamageLensAI\\test_images\",\n",
988
+ " hf_token: str = None,\n",
989
+ " conf_thresh: float = 0.25,\n",
990
+ "):\n",
991
+ " print(f\"Downloading '{onnx_filename_in_repo}' from Hugging Face for evaluation...\")\n",
992
+ " \n",
993
+ " # force_download=True ensures we grab the newly uploaded 50MB file\n",
994
+ " onnx_path = hf_hub_download(\n",
995
+ " repo_id=repo_id, \n",
996
+ " filename=onnx_filename_in_repo,\n",
997
+ " token=hf_token,\n",
998
+ " force_download=True \n",
999
+ " )\n",
1000
+ " \n",
1001
+ " session = ort.InferenceSession(onnx_path, providers=[\"CPUExecutionProvider\"])\n",
1002
+ " input_name = session.get_inputs()[0].name\n",
1003
+ "\n",
1004
+ " supported_exts = (\"*.jpg\", \"*.jpeg\", \"*.png\", \"*.bmp\", \"*.webp\")\n",
1005
+ " image_paths = []\n",
1006
+ " for ext in supported_exts:\n",
1007
+ " image_paths.extend(glob.glob(os.path.join(image_folder, ext)))\n",
1008
+ "\n",
1009
+ " if not image_paths:\n",
1010
+ " raise FileNotFoundError(f\"❌ No valid images found in folder '{image_folder}'.\")\n",
1011
+ "\n",
1012
+ " print(f\"📸 Found {len(image_paths)} images in '{image_folder}'. Starting benchmark...\")\n",
1013
+ "\n",
1014
+ " pytorch_latencies = []\n",
1015
+ " onnx_latencies = []\n",
1016
+ " matches = 0\n",
1017
+ "\n",
1018
+ " # Warmup\n",
1019
+ " first_img = Image.open(sorted(image_paths)[0]).convert(\"RGB\")\n",
1020
+ " warmup_tensor = preprocess_image(first_img)\n",
1021
+ " warmup_np = warmup_tensor.numpy()\n",
1022
+ "\n",
1023
+ " _ = yolo_model.model(warmup_tensor)\n",
1024
+ " session.run(None, {input_name: warmup_np})\n",
1025
+ "\n",
1026
+ " print(\"\\n\" + \"=\" * 75)\n",
1027
+ " print(f\"{'IMAGE NAME':<25} | {'PYTORCH (ms)':<14} | {'ONNX (ms)':<14} | {'MATCH?':<10}\")\n",
1028
+ " print(\"=\" * 75)\n",
1029
+ "\n",
1030
+ " for img_path in sorted(image_paths):\n",
1031
+ " file_name = os.path.basename(img_path)\n",
1032
+ " raw_img = Image.open(img_path).convert(\"RGB\")\n",
1033
+ " \n",
1034
+ " img_tensor = preprocess_image(raw_img)\n",
1035
+ " np_input = img_tensor.numpy()\n",
1036
+ "\n",
1037
+ " start_pt = time.perf_counter()\n",
1038
+ " with torch.no_grad():\n",
1039
+ " pt_out = yolo_model.model(img_tensor)\n",
1040
+ " pt_time = (time.perf_counter() - start_pt) * 1000\n",
1041
+ " pytorch_latencies.append(pt_time)\n",
1042
+ "\n",
1043
+ " start_onnx = time.perf_counter()\n",
1044
+ " onnx_out = session.run(None, {input_name: np_input})\n",
1045
+ " onnx_time = (time.perf_counter() - start_onnx) * 1000\n",
1046
+ " onnx_latencies.append(onnx_time)\n",
1047
+ "\n",
1048
+ " pt_preds = pt_out[0] if isinstance(pt_out, (list, tuple)) else pt_out\n",
1049
+ " pt_max_conf = float(torch.max(pt_preds[0, 4, :])) if pt_preds.ndim == 3 else 0.0\n",
1050
+ " \n",
1051
+ " onnx_preds = onnx_out[0]\n",
1052
+ " onnx_max_conf = float(np.max(onnx_preds[0, 4, :])) if onnx_preds.ndim == 3 else 0.0\n",
1053
+ "\n",
1054
+ " pt_detected = pt_max_conf >= conf_thresh\n",
1055
+ " onnx_detected = onnx_max_conf >= conf_thresh\n",
1056
+ " is_match = (pt_detected == onnx_detected)\n",
1057
+ "\n",
1058
+ " if is_match:\n",
1059
+ " matches += 1\n",
1060
+ " match_str = \"✅ YES\" if is_match else \"❌ NO\"\n",
1061
+ "\n",
1062
+ " print(f\"{file_name:<25} | {pt_time:<14.2f} | {onnx_time:<14.2f} | {match_str:<10}\")\n",
1063
+ "\n",
1064
+ " avg_pt = np.mean(pytorch_latencies)\n",
1065
+ " avg_onnx = np.mean(onnx_latencies)\n",
1066
+ " speedup = ((avg_pt - avg_onnx) / avg_pt) * 100\n",
1067
+ " accuracy_match_pct = (matches / len(image_paths)) * 100\n",
1068
+ "\n",
1069
+ " print(\"=\" * 75)\n",
1070
+ " print(\" BENCHMARK SUMMARY\")\n",
1071
+ " print(\"=\" * 75)\n",
1072
+ " print(f\"Total Images Evaluated : {len(image_paths)}\")\n",
1073
+ " print(f\"Detection Match Rate : {accuracy_match_pct:.2f}% ({matches}/{len(image_paths)})\")\n",
1074
+ " print(f\"PyTorch Avg Latency : {avg_pt:.2f} ms / image\")\n",
1075
+ " print(f\"ONNX Runtime Avg Latency: {avg_onnx:.2f} ms / image\")\n",
1076
+ " print(f\"Performance Gain : {speedup:.2f}% faster with ONNX Runtime\")\n",
1077
+ " print(\"=\" * 75 + \"\\n\")\n",
1078
+ "\n",
1079
+ "\n",
1080
+ "# ==========================================\n",
1081
+ "# MAIN EXECUTION FLOW\n",
1082
+ "# ==========================================\n",
1083
+ "if __name__ == \"__main__\":\n",
1084
+ " load_dotenv()\n",
1085
+ " HF_TOKEN = os.getenv(\"HF_TOKEN\")\n",
1086
+ "\n",
1087
+ " if not HF_TOKEN:\n",
1088
+ " print(\"⚠️ Warning: HF_TOKEN not found in .env file.\")\n",
1089
+ "\n",
1090
+ " HF_REPO_ID = \"junaid17/Yolo_Model\"\n",
1091
+ " MODEL_FILENAME = \"damage_detector.pt\"\n",
1092
+ " ONNX_FILENAME = \"damage_detector.onnx\"\n",
1093
+ " TEST_IMAGE_FOLDER = r\"D:\\DamageLensAI\\test_images\"\n",
1094
+ "\n",
1095
+ " yolo_model = load_yolo_model_from_hf(\n",
1096
+ " repo_id=HF_REPO_ID,\n",
1097
+ " filename=MODEL_FILENAME,\n",
1098
+ " hf_token=HF_TOKEN,\n",
1099
+ " )\n",
1100
+ "\n",
1101
+ " convert_and_upload_yolo_onnx(\n",
1102
+ " yolo_model=yolo_model,\n",
1103
+ " repo_id=HF_REPO_ID,\n",
1104
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
1105
+ " hf_token=HF_TOKEN,\n",
1106
+ " imgsz=IMG_SIZE,\n",
1107
+ " )\n",
1108
+ "\n",
1109
+ " benchmark_yolo_with_folder(\n",
1110
+ " yolo_model=yolo_model,\n",
1111
+ " repo_id=HF_REPO_ID,\n",
1112
+ " onnx_filename_in_repo=ONNX_FILENAME,\n",
1113
+ " image_folder=TEST_IMAGE_FOLDER,\n",
1114
+ " hf_token=HF_TOKEN,\n",
1115
+ " )"
1116
+ ]
1117
+ },
1118
+ {
1119
+ "cell_type": "code",
1120
+ "execution_count": null,
1121
+ "id": "5b257cbc",
1122
+ "metadata": {},
1123
+ "outputs": [],
1124
+ "source": []
1125
+ },
1126
+ {
1127
+ "cell_type": "code",
1128
+ "execution_count": null,
1129
+ "id": "0e9c3a35",
1130
+ "metadata": {},
1131
+ "outputs": [],
1132
+ "source": []
1133
+ },
1134
+ {
1135
+ "cell_type": "code",
1136
+ "execution_count": null,
1137
+ "id": "8bed307b",
1138
+ "metadata": {},
1139
+ "outputs": [],
1140
+ "source": []
1141
+ },
1142
+ {
1143
+ "cell_type": "code",
1144
+ "execution_count": null,
1145
+ "metadata": {},
1146
+ "outputs": [],
1147
+ "source": []
1148
+ }
1149
+ ],
1150
+ "metadata": {
1151
+ "kernelspec": {
1152
+ "display_name": "myvenv (3.11.0)",
1153
+ "language": "python",
1154
+ "name": "python3"
1155
+ },
1156
+ "language_info": {
1157
+ "codemirror_mode": {
1158
+ "name": "ipython",
1159
+ "version": 3
1160
+ },
1161
+ "file_extension": ".py",
1162
+ "mimetype": "text/x-python",
1163
+ "name": "python",
1164
+ "nbconvert_exporter": "python",
1165
+ "pygments_lexer": "ipython3",
1166
+ "version": "3.11.0"
1167
+ }
1168
+ },
1169
+ "nbformat": 4,
1170
+ "nbformat_minor": 5
1171
+ }
Notebooks/EfficientNet_ConvNext_Fusion.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Notebooks/Resnet18_fine_tuning_final.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
Notebooks/damage_detector_yolo.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
src/config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import torch
3
+
4
+ # ---------------- PATHS ----------------
5
+ BASE_DIR = Path(__file__).resolve().parents[1]
6
+
7
+ DATASET_DIR = BASE_DIR / "data" / "dataset"
8
+ CHECKPOINT_DIR = BASE_DIR / "checkpoints"
9
+ EXPORT_DIR = BASE_DIR / "exports"
10
+
11
+ CHECKPOINT_DIR.mkdir(exist_ok=True)
12
+ EXPORT_DIR.mkdir(exist_ok=True)
13
+
14
+ # ---------------- TRAINING ----------------
15
+ BATCH_SIZE = 16
16
+ NUM_WORKERS = 4
17
+ LEARNING_RATE = 1e-4
18
+ WEIGHT_DECAY = 1e-5
19
+ VALIDATION_SPLIT = 0.2
20
+ RANDOM_SEED = 42
21
+
22
+ # TEMP DEV SETTING
23
+ EPOCHS = 1
24
+
25
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
26
+
27
+ # ---------------- IMAGE SIZES ----------------
28
+ RESNET_IMAGE_SIZE = 224
29
+ FUSION_IMAGE_SIZE = 260
30
+ YOLO_IMAGE_SIZE = 640
31
+
32
+ # ---------------- YOLO ----------------
33
+ YOLO_BASE_MODEL = "yolo11m.pt"
34
+ YOLO_BATCH_SIZE = 10
35
+ YOLO_EPOCHS = 1
36
+ YOLO_CONFIDENCE_THRESHOLD = 0.05
37
+
38
+ # ---------------- CLASSES ----------------
39
+ CLASS_NAMES = [
40
+ "F_Breakage",
41
+ "F_Crushed",
42
+ "F_Normal",
43
+ "R_Breakage",
44
+ "R_Crushed",
45
+ "R_Normal"
46
+ ]
47
+
48
+
49
+
50
+ CLASS_MAP = {idx: cls for idx, cls in enumerate(CLASS_NAMES)}
51
+ CLASS_TO_IDX = {cls: idx for idx, cls in enumerate(CLASS_NAMES)}
52
+
53
+ NUM_CLASSES = len(CLASS_NAMES)
src/data/augmentation.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from torchvision import transforms
3
+
4
+ from src.config import RESNET_IMAGE_SIZE, FUSION_IMAGE_SIZE
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def get_resnet_train_transforms():
10
+ logger.info("Creating ResNet training transforms...")
11
+
12
+ return transforms.Compose([
13
+ transforms.Resize((RESNET_IMAGE_SIZE, RESNET_IMAGE_SIZE)),
14
+ transforms.RandomHorizontalFlip(),
15
+ transforms.RandomRotation(15),
16
+ transforms.ColorJitter(
17
+ brightness=0.2,
18
+ contrast=0.2,
19
+ saturation=0.2
20
+ ),
21
+ transforms.ToTensor(),
22
+ transforms.Normalize(
23
+ mean=[0.485, 0.456, 0.406],
24
+ std=[0.229, 0.224, 0.225]
25
+ )
26
+ ])
27
+
28
+
29
+ def get_resnet_val_transforms():
30
+ logger.info("Creating ResNet validation transforms...")
31
+
32
+ return transforms.Compose([
33
+ transforms.Resize((RESNET_IMAGE_SIZE, RESNET_IMAGE_SIZE)),
34
+ transforms.ToTensor(),
35
+ transforms.Normalize(
36
+ mean=[0.485, 0.456, 0.406],
37
+ std=[0.229, 0.224, 0.225]
38
+ )
39
+ ])
40
+
41
+
42
+ def get_fusion_train_transforms():
43
+ logger.info("Creating Fusion training transforms...")
44
+
45
+ return transforms.Compose([
46
+ transforms.Resize((FUSION_IMAGE_SIZE, FUSION_IMAGE_SIZE)),
47
+ transforms.RandomHorizontalFlip(),
48
+ transforms.RandomRotation(10),
49
+ transforms.ColorJitter(
50
+ brightness=0.15,
51
+ contrast=0.15,
52
+ saturation=0.15
53
+ ),
54
+ transforms.ToTensor(),
55
+ transforms.Normalize(
56
+ mean=[0.485, 0.456, 0.406],
57
+ std=[0.229, 0.224, 0.225]
58
+ )
59
+ ])
60
+
61
+
62
+ def get_fusion_val_transforms():
63
+ logger.info("Creating Fusion validation transforms...")
64
+
65
+ return transforms.Compose([
66
+ transforms.Resize((FUSION_IMAGE_SIZE, FUSION_IMAGE_SIZE)),
67
+ transforms.ToTensor(),
68
+ transforms.Normalize(
69
+ mean=[0.485, 0.456, 0.406],
70
+ std=[0.229, 0.224, 0.225]
71
+ )
72
+ ])
73
+
74
+
75
+ if __name__ == "__main__":
76
+ logging.basicConfig(
77
+ level=logging.INFO,
78
+ format="%(asctime)s - %(levelname)s - %(message)s"
79
+ )
80
+
81
+ resnet_train = get_resnet_train_transforms()
82
+ resnet_val = get_resnet_val_transforms()
83
+ fusion_train = get_fusion_train_transforms()
84
+ fusion_val = get_fusion_val_transforms()
85
+
86
+ print("\nTransforms created successfully:")
87
+ print("ResNet Train:", resnet_train)
88
+ print("ResNet Val:", resnet_val)
89
+ print("Fusion Train:", fusion_train)
90
+ print("Fusion Val:", fusion_val)
src/data/dataset.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from PIL import Image
3
+ from torch.utils.data import Dataset, DataLoader
4
+ from transformers import ConvNextImageProcessor
5
+
6
+ from src.config import (
7
+ BATCH_SIZE,
8
+ NUM_WORKERS
9
+ )
10
+
11
+ from src.data.ingestion import collect_image_paths
12
+ from src.data.preprocessing import split_dataset
13
+ from src.data.augmentation import (
14
+ get_resnet_train_transforms,
15
+ get_resnet_val_transforms,
16
+ get_fusion_train_transforms,
17
+ get_fusion_val_transforms
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ResNetDataset(Dataset):
24
+ def __init__(self, samples, transforms=None):
25
+ self.samples = samples
26
+ self.transforms = transforms
27
+
28
+ def __len__(self):
29
+ return len(self.samples)
30
+
31
+ def __getitem__(self, idx):
32
+ image_path, label = self.samples[idx]
33
+
34
+ image = Image.open(image_path).convert("RGB")
35
+
36
+ if self.transforms:
37
+ image = self.transforms(image)
38
+
39
+ return image, label
40
+
41
+
42
+ class FusionDataset(Dataset):
43
+ def __init__(
44
+ self,
45
+ samples,
46
+ transforms=None,
47
+ convnext_model_name="facebook/convnext-small-224"
48
+ ):
49
+ self.samples = samples
50
+ self.transforms = transforms
51
+
52
+ logger.info("Loading ConvNeXt processor...")
53
+
54
+ self.processor = ConvNextImageProcessor.from_pretrained(
55
+ convnext_model_name
56
+ )
57
+
58
+ def __len__(self):
59
+ return len(self.samples)
60
+
61
+ def __getitem__(self, idx):
62
+ image_path, label = self.samples[idx]
63
+
64
+ image = Image.open(image_path).convert("RGB")
65
+
66
+ if self.transforms:
67
+ eff_tensor = self.transforms(image)
68
+ else:
69
+ raise ValueError("Fusion transforms are required.")
70
+
71
+ convnext_inputs = self.processor(
72
+ images=image,
73
+ return_tensors="pt"
74
+ )
75
+
76
+ convnext_tensor = convnext_inputs["pixel_values"].squeeze(0)
77
+
78
+ return {
79
+ "pixel_values_eff": eff_tensor,
80
+ "pixel_values_cnx": convnext_tensor,
81
+ "labels": label
82
+ }
83
+
84
+
85
+ def create_resnet_dataloaders():
86
+ logger.info("Creating ResNet dataloaders...")
87
+
88
+ samples = collect_image_paths()
89
+ train_data, val_data = split_dataset(samples)
90
+
91
+ train_dataset = ResNetDataset(
92
+ train_data,
93
+ transforms=get_resnet_train_transforms()
94
+ )
95
+
96
+ val_dataset = ResNetDataset(
97
+ val_data,
98
+ transforms=get_resnet_val_transforms()
99
+ )
100
+
101
+ train_loader = DataLoader(
102
+ train_dataset,
103
+ batch_size=BATCH_SIZE,
104
+ shuffle=True,
105
+ num_workers=NUM_WORKERS
106
+ )
107
+
108
+ val_loader = DataLoader(
109
+ val_dataset,
110
+ batch_size=BATCH_SIZE,
111
+ shuffle=False,
112
+ num_workers=NUM_WORKERS
113
+ )
114
+
115
+ logger.info("ResNet dataloaders created successfully.")
116
+
117
+ return train_loader, val_loader
118
+
119
+
120
+ def create_fusion_dataloaders():
121
+ logger.info("Creating Fusion dataloaders...")
122
+
123
+ samples = collect_image_paths()
124
+ train_data, val_data = split_dataset(samples)
125
+
126
+ train_dataset = FusionDataset(
127
+ train_data,
128
+ transforms=get_fusion_train_transforms()
129
+ )
130
+
131
+ val_dataset = FusionDataset(
132
+ val_data,
133
+ transforms=get_fusion_val_transforms()
134
+ )
135
+
136
+ train_loader = DataLoader(
137
+ train_dataset,
138
+ batch_size=BATCH_SIZE,
139
+ shuffle=True,
140
+ num_workers=NUM_WORKERS
141
+ )
142
+
143
+ val_loader = DataLoader(
144
+ val_dataset,
145
+ batch_size=BATCH_SIZE,
146
+ shuffle=False,
147
+ num_workers=NUM_WORKERS
148
+ )
149
+
150
+ logger.info("Fusion dataloaders created successfully.")
151
+
152
+ return train_loader, val_loader
153
+
154
+
155
+ if __name__ == "__main__":
156
+ logging.basicConfig(
157
+ level=logging.INFO,
158
+ format="%(asctime)s - %(levelname)s - %(message)s"
159
+ )
160
+
161
+ print("\nTesting ResNet dataloaders...\n")
162
+
163
+ train_loader, val_loader = create_resnet_dataloaders()
164
+
165
+ images, labels = next(iter(train_loader))
166
+
167
+ print("ResNet batch shape:", images.shape)
168
+ print("ResNet labels shape:", labels.shape)
169
+
170
+ print("\nTesting Fusion dataloaders...\n")
171
+
172
+ train_loader, val_loader = create_fusion_dataloaders()
173
+
174
+ batch = next(iter(train_loader))
175
+
176
+ print(
177
+ "Fusion EfficientNet batch shape:",
178
+ batch["pixel_values_eff"].shape
179
+ )
180
+
181
+ print(
182
+ "Fusion ConvNeXt batch shape:",
183
+ batch["pixel_values_cnx"].shape
184
+ )
185
+
186
+ print(
187
+ "Fusion labels shape:",
188
+ batch["labels"].shape
189
+ )
src/data/ingestion.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from src.config import DATASET_DIR, CLASS_TO_IDX
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ VALID_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
9
+
10
+
11
+ def collect_image_paths():
12
+ logger.info("Starting dataset ingestion...")
13
+
14
+ if not DATASET_DIR.exists():
15
+ raise FileNotFoundError(f"Dataset directory not found: {DATASET_DIR}")
16
+
17
+ samples = []
18
+
19
+ for class_name, label in CLASS_TO_IDX.items():
20
+ class_dir = DATASET_DIR / class_name
21
+
22
+ if not class_dir.exists():
23
+ logger.warning(f"Missing class folder: {class_dir}")
24
+ continue
25
+
26
+ image_count = 0
27
+
28
+ for image_path in class_dir.iterdir():
29
+ if image_path.suffix.lower() in VALID_EXTENSIONS:
30
+ samples.append((str(image_path), label))
31
+ image_count += 1
32
+
33
+ logger.info(f"{class_name}: {image_count} images found")
34
+
35
+ if not samples:
36
+ raise ValueError("No valid images found in dataset.")
37
+
38
+ logger.info(f"Total images collected: {len(samples)}")
39
+
40
+ return samples
41
+
42
+
43
+ if __name__ == "__main__":
44
+ logging.basicConfig(
45
+ level=logging.INFO,
46
+ format="%(asctime)s - %(levelname)s - %(message)s"
47
+ )
48
+
49
+ data = collect_image_paths()
50
+
51
+ print(f"\nTotal samples: {len(data)}")
52
+ print("First 5 samples:")
53
+
54
+ for sample in data[:5]:
55
+ print(sample)
src/data/preprocessing.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from collections import Counter
3
+ from sklearn.model_selection import train_test_split
4
+
5
+ from src.config import VALIDATION_SPLIT, RANDOM_SEED
6
+ from src.data.ingestion import collect_image_paths
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def split_dataset(samples):
12
+ logger.info("Starting dataset preprocessing...")
13
+
14
+ if not samples:
15
+ raise ValueError("Empty dataset provided.")
16
+
17
+ image_paths = [sample[0] for sample in samples]
18
+ labels = [sample[1] for sample in samples]
19
+
20
+ logger.info(f"Total samples before split: {len(samples)}")
21
+
22
+ train_paths, val_paths, train_labels, val_labels = train_test_split(
23
+ image_paths,
24
+ labels,
25
+ test_size=VALIDATION_SPLIT,
26
+ stratify=labels,
27
+ random_state=RANDOM_SEED
28
+ )
29
+
30
+ train_data = list(zip(train_paths, train_labels))
31
+ val_data = list(zip(val_paths, val_labels))
32
+
33
+ logger.info(f"Training samples: {len(train_data)}")
34
+ logger.info(f"Validation samples: {len(val_data)}")
35
+
36
+ logger.info(f"Train distribution: {Counter(train_labels)}")
37
+ logger.info(f"Validation distribution: {Counter(val_labels)}")
38
+
39
+ return train_data, val_data
40
+
41
+
42
+ if __name__ == "__main__":
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format="%(asctime)s - %(levelname)s - %(message)s"
46
+ )
47
+
48
+ samples = collect_image_paths()
49
+
50
+ train_data, val_data = split_dataset(samples)
51
+
52
+ print("\nTrain sample preview:")
53
+ for sample in train_data[:5]:
54
+ print(sample)
55
+
56
+ print("\nValidation sample preview:")
57
+ for sample in val_data[:5]:
58
+ print(sample)
src/models/fusion_model.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ import torch.nn as nn
4
+ from torchvision import models
5
+ from transformers import ConvNextModel
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class FusionClassifier(nn.Module):
11
+ def __init__(
12
+ self,
13
+ num_classes,
14
+ convnext_model_name="facebook/convnext-small-224"
15
+ ):
16
+ super().__init__()
17
+
18
+ logger.info("Initializing Fusion model...")
19
+
20
+ # EfficientNet-V2-S
21
+ eff = models.efficientnet_v2_s(
22
+ weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1
23
+ )
24
+
25
+ for param in eff.parameters():
26
+ param.requires_grad = False
27
+
28
+ for param in eff.features[5].parameters():
29
+ param.requires_grad = True
30
+
31
+ for param in eff.features[6].parameters():
32
+ param.requires_grad = True
33
+
34
+ for param in eff.features[7].parameters():
35
+ param.requires_grad = True
36
+
37
+ self.eff_features = eff.features
38
+ self.eff_avgpool = eff.avgpool
39
+ self.eff_out_dim = eff.classifier[1].in_features
40
+
41
+ # ConvNeXt
42
+ cnx = ConvNextModel.from_pretrained(convnext_model_name)
43
+
44
+ for param in cnx.parameters():
45
+ param.requires_grad = False
46
+
47
+ for param in cnx.encoder.stages[2].parameters():
48
+ param.requires_grad = True
49
+
50
+ for param in cnx.encoder.stages[3].parameters():
51
+ param.requires_grad = True
52
+
53
+ for param in cnx.layernorm.parameters():
54
+ param.requires_grad = True
55
+
56
+ self.cnx_backbone = cnx
57
+ self.cnx_out_dim = 768
58
+
59
+ fused_dim = self.eff_out_dim + self.cnx_out_dim
60
+
61
+ self.fusion_head = nn.Sequential(
62
+ nn.Dropout(0.4),
63
+ nn.Linear(fused_dim, 512),
64
+ nn.LayerNorm(512),
65
+ nn.GELU(),
66
+
67
+ nn.Dropout(0.3),
68
+ nn.Linear(512, 256),
69
+ nn.LayerNorm(256),
70
+ nn.GELU(),
71
+
72
+ nn.Dropout(0.2),
73
+ nn.Linear(256, num_classes)
74
+ )
75
+
76
+ logger.info("Fusion model initialized successfully.")
77
+
78
+ def forward(self, pixel_values_eff, pixel_values_cnx):
79
+ x_eff = self.eff_features(pixel_values_eff)
80
+ x_eff = self.eff_avgpool(x_eff)
81
+ x_eff = torch.flatten(x_eff, 1)
82
+
83
+ cnx_out = self.cnx_backbone(
84
+ pixel_values=pixel_values_cnx,
85
+ return_dict=True
86
+ )
87
+
88
+ x_cnx = cnx_out.pooler_output
89
+
90
+ fused = torch.cat([x_eff, x_cnx], dim=1)
91
+
92
+ logits = self.fusion_head(fused)
93
+
94
+ return logits
95
+
96
+
97
+ if __name__ == "__main__":
98
+ import logging
99
+
100
+ logging.basicConfig(
101
+ level=logging.INFO,
102
+ format="%(asctime)s - %(levelname)s - %(message)s"
103
+ )
104
+
105
+ model = FusionClassifier(num_classes=6)
106
+
107
+ eff_dummy = torch.randn(2, 3, 260, 260)
108
+ cnx_dummy = torch.randn(2, 3, 224, 224)
109
+
110
+ output = model(eff_dummy, cnx_dummy)
111
+
112
+ print("Fusion output shape:", output.shape)
src/models/resnet_model.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ import torch.nn as nn
4
+ from torchvision import models
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class CarClassifierResNet(nn.Module):
10
+ def __init__(self, num_classes):
11
+ super().__init__()
12
+
13
+ logger.info("Initializing ResNet18 model...")
14
+
15
+ self.model = models.resnet18(weights="DEFAULT")
16
+
17
+ # Freeze everything
18
+ for param in self.model.parameters():
19
+ param.requires_grad = False
20
+
21
+ # Unfreeze last layers
22
+ for param in self.model.layer3.parameters():
23
+ param.requires_grad = True
24
+
25
+ for param in self.model.layer4.parameters():
26
+ param.requires_grad = True
27
+
28
+ # Custom classifier head
29
+ self.model.fc = nn.Sequential(
30
+ nn.Dropout(0.5),
31
+ nn.Linear(self.model.fc.in_features, 256),
32
+ nn.ReLU(),
33
+ nn.Dropout(0.3),
34
+ nn.Linear(256, num_classes)
35
+ )
36
+
37
+ logger.info("ResNet18 model initialized successfully.")
38
+
39
+ def forward(self, x):
40
+ return self.model(x)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ logging.basicConfig(
45
+ level=logging.INFO,
46
+ format="%(asctime)s - %(levelname)s - %(message)s"
47
+ )
48
+
49
+ model = CarClassifierResNet(num_classes=6)
50
+
51
+ dummy_input = torch.randn(2, 3, 128, 128)
52
+
53
+ output = model(dummy_input)
54
+
55
+ print("Output shape:", output.shape)
56
+
57
+ total_params = sum(p.numel() for p in model.parameters())
58
+ trainable_params = sum(
59
+ p.numel() for p in model.parameters()
60
+ if p.requires_grad
61
+ )
62
+
63
+ print("Total params:", total_params)
64
+ print("Trainable params:", trainable_params)
src/training/train_fusion.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch.nn as nn
3
+ from torch.optim import AdamW
4
+
5
+ from src.config import DEVICE, EPOCHS, NUM_CLASSES
6
+ from src.models.fusion_model import FusionClassifier
7
+ from src.data.dataset import create_fusion_dataloaders
8
+ from src.training.trainer import train_dual_input_model
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def run_fusion_training():
14
+ logger.info("Initializing Fusion training pipeline...")
15
+
16
+ train_loader, eval_loader = create_fusion_dataloaders()
17
+
18
+ model = FusionClassifier(
19
+ num_classes=NUM_CLASSES
20
+ ).to(DEVICE)
21
+
22
+ criterion = nn.CrossEntropyLoss(
23
+ label_smoothing=0.1
24
+ )
25
+
26
+ optimizer = AdamW([
27
+ # EfficientNet unfrozen blocks
28
+ {
29
+ "params": model.eff_features[5].parameters(),
30
+ "lr": 1e-5
31
+ },
32
+ {
33
+ "params": model.eff_features[6].parameters(),
34
+ "lr": 3e-5
35
+ },
36
+ {
37
+ "params": model.eff_features[7].parameters(),
38
+ "lr": 3e-5
39
+ },
40
+
41
+ # ConvNeXt unfrozen blocks
42
+ {
43
+ "params": model.cnx_backbone.encoder.stages[2].parameters(),
44
+ "lr": 3e-5
45
+ },
46
+ {
47
+ "params": model.cnx_backbone.encoder.stages[3].parameters(),
48
+ "lr": 3e-5
49
+ },
50
+ {
51
+ "params": model.cnx_backbone.layernorm.parameters(),
52
+ "lr": 3e-5
53
+ },
54
+
55
+ # Fusion head
56
+ {
57
+ "params": model.fusion_head.parameters(),
58
+ "lr": 1e-4
59
+ }
60
+
61
+ ], weight_decay=1e-4)
62
+
63
+ logger.info("Starting Fusion training...")
64
+
65
+ all_preds, all_labels = train_dual_input_model(
66
+ model=model,
67
+ train_loader=train_loader,
68
+ eval_loader=eval_loader,
69
+ optimizer=optimizer,
70
+ criterion=criterion,
71
+ device=DEVICE,
72
+ epochs=EPOCHS,
73
+ checkpoint_model_name="best_fusion_model",
74
+ patience=7
75
+ )
76
+
77
+ logger.info("Fusion training completed.")
78
+
79
+ return all_preds, all_labels
80
+
81
+
82
+ if __name__ == "__main__":
83
+ logging.basicConfig(
84
+ level=logging.INFO,
85
+ format="%(asctime)s - %(levelname)s - %(message)s"
86
+ )
87
+
88
+ preds, labels = run_fusion_training()
89
+
90
+ print("\nFusion training completed successfully.")
91
+ print("Prediction samples:", preds[:10])
92
+ print("Label samples:", labels[:10])
src/training/train_resnet.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch.nn as nn
3
+ from torch.optim import AdamW
4
+
5
+ from src.config import DEVICE, EPOCHS, NUM_CLASSES
6
+ from src.models.resnet_model import CarClassifierResNet
7
+ from src.data.dataset import create_resnet_dataloaders
8
+ from src.training.trainer import train_single_input_model
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def run_resnet_training():
14
+ logger.info("Initializing ResNet training pipeline...")
15
+
16
+ train_loader, eval_loader = create_resnet_dataloaders()
17
+
18
+ model = CarClassifierResNet(
19
+ num_classes=NUM_CLASSES
20
+ ).to(DEVICE)
21
+
22
+ criterion = nn.CrossEntropyLoss()
23
+
24
+ optimizer = AdamW([
25
+ {
26
+ "params": model.model.layer3.parameters(),
27
+ "lr": 1e-5
28
+ },
29
+ {
30
+ "params": model.model.layer4.parameters(),
31
+ "lr": 1e-5
32
+ },
33
+ {
34
+ "params": model.model.fc.parameters(),
35
+ "lr": 1e-4
36
+ }
37
+ ])
38
+
39
+ logger.info("Starting ResNet training...")
40
+
41
+ all_preds, all_labels = train_single_input_model(
42
+ model=model,
43
+ train_loader=train_loader,
44
+ eval_loader=eval_loader,
45
+ optimizer=optimizer,
46
+ criterion=criterion,
47
+ device=DEVICE,
48
+ epochs=EPOCHS,
49
+ checkpoint_model_name="best_resnet_model",
50
+ patience=7
51
+ )
52
+
53
+ logger.info("ResNet training completed.")
54
+
55
+ return all_preds, all_labels
56
+
57
+
58
+ if __name__ == "__main__":
59
+ logging.basicConfig(
60
+ level=logging.INFO,
61
+ format="%(asctime)s - %(levelname)s - %(message)s"
62
+ )
63
+
64
+ preds, labels = run_resnet_training()
65
+
66
+ print("\nTraining completed successfully.")
67
+ print("Prediction samples:", preds[:10])
68
+ print("Label samples:", labels[:10])
src/training/train_yolo.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from shutil import copy2, rmtree
3
+ from ultralytics import YOLO
4
+
5
+ from src.config import BASE_DIR, CHECKPOINT_DIR, DEVICE
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ YOLO_DATASET_CONFIG = BASE_DIR / "data" / "yolo" / "dataset_custom.yaml"
10
+ YOLO_BASE_MODEL = CHECKPOINT_DIR / "yolo11m.pt"
11
+
12
+
13
+ def run_yolo_training():
14
+ logger.info("Initializing YOLO training pipeline...")
15
+
16
+ if not YOLO_DATASET_CONFIG.exists():
17
+ raise FileNotFoundError(
18
+ f"YOLO dataset config not found: {YOLO_DATASET_CONFIG}"
19
+ )
20
+
21
+ if not YOLO_BASE_MODEL.exists():
22
+ raise FileNotFoundError(
23
+ f"YOLO base model not found: {YOLO_BASE_MODEL}"
24
+ )
25
+
26
+ yolo_device = 0 if DEVICE == "cuda" else "cpu"
27
+
28
+ checkpoint_root = CHECKPOINT_DIR.resolve()
29
+ temp_run_name = "temp_yolo_run"
30
+
31
+ logger.info("Loading YOLO base model...")
32
+
33
+ model = YOLO(str(YOLO_BASE_MODEL.resolve()))
34
+
35
+ logger.info("Starting YOLO training...")
36
+
37
+ model.train(
38
+ data=str(YOLO_DATASET_CONFIG.resolve()),
39
+ imgsz=416,
40
+ batch=4,
41
+ epochs=1,
42
+ device=yolo_device,
43
+ project=str(checkpoint_root),
44
+ name=temp_run_name,
45
+ exist_ok=True
46
+ )
47
+
48
+ best_model_path = (
49
+ checkpoint_root /
50
+ temp_run_name /
51
+ "weights" /
52
+ "best.pt"
53
+ )
54
+
55
+ if not best_model_path.exists():
56
+ raise FileNotFoundError(
57
+ f"YOLO best model not found: {best_model_path}"
58
+ )
59
+
60
+ final_model_path = checkpoint_root / "damage_detector.pt"
61
+
62
+ copy2(best_model_path, final_model_path)
63
+
64
+ logger.info(f"Final YOLO model saved at: {final_model_path}")
65
+
66
+ # cleanup temp training folder
67
+ temp_run_dir = checkpoint_root / temp_run_name
68
+
69
+ if temp_run_dir.exists():
70
+ rmtree(temp_run_dir)
71
+ logger.info("Temporary YOLO training artifacts deleted.")
72
+
73
+ return final_model_path
74
+
75
+
76
+ if __name__ == "__main__":
77
+ logging.basicConfig(
78
+ level=logging.INFO,
79
+ format="%(asctime)s - %(levelname)s - %(message)s"
80
+ )
81
+
82
+ model_path = run_yolo_training()
83
+
84
+ print("\nYOLO training completed successfully.")
85
+ print(f"Saved model: {model_path}")
src/training/trainer.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ from tqdm import tqdm
4
+ from transformers import get_cosine_schedule_with_warmup
5
+
6
+ from src.config import CHECKPOINT_DIR
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class EarlyStopping:
12
+ def __init__(self, patience=7, min_delta=0.001):
13
+ self.patience = patience
14
+ self.min_delta = min_delta
15
+ self.counter = 0
16
+ self.best_score = None
17
+ self.early_stop = False
18
+
19
+ def __call__(self, val_acc):
20
+ if self.best_score is None:
21
+ self.best_score = val_acc
22
+
23
+ elif val_acc < self.best_score + self.min_delta:
24
+ self.counter += 1
25
+
26
+ logger.info(
27
+ f"EarlyStopping counter: {self.counter}/{self.patience}"
28
+ )
29
+
30
+ if self.counter >= self.patience:
31
+ self.early_stop = True
32
+
33
+ else:
34
+ self.best_score = val_acc
35
+ self.counter = 0
36
+
37
+
38
+ def train_single_input_model(
39
+ model,
40
+ train_loader,
41
+ eval_loader,
42
+ optimizer,
43
+ criterion,
44
+ device,
45
+ epochs,
46
+ checkpoint_model_name,
47
+ patience=7
48
+ ):
49
+ logger.info("Starting single-input training...")
50
+
51
+ num_training_steps = epochs * len(train_loader)
52
+ num_warmup_steps = int(0.1 * num_training_steps)
53
+
54
+ scheduler = get_cosine_schedule_with_warmup(
55
+ optimizer=optimizer,
56
+ num_warmup_steps=num_warmup_steps,
57
+ num_training_steps=num_training_steps
58
+ )
59
+
60
+ early_stopping = EarlyStopping(patience=patience)
61
+
62
+ best_acc = 0.0
63
+ all_preds = []
64
+ all_labels = []
65
+
66
+ for epoch in range(epochs):
67
+ logger.info(f"Epoch {epoch + 1}/{epochs}")
68
+
69
+ model.train()
70
+
71
+ running_loss = 0
72
+ correct = 0
73
+ total = 0
74
+
75
+ for images, labels in tqdm(
76
+ train_loader,
77
+ desc=f"Epoch {epoch+1} Training"
78
+ ):
79
+ images = images.to(device)
80
+ labels = labels.to(device)
81
+
82
+ optimizer.zero_grad(set_to_none=True)
83
+
84
+ logits = model(images)
85
+
86
+ loss = criterion(logits, labels)
87
+
88
+ loss.backward()
89
+
90
+ optimizer.step()
91
+ scheduler.step()
92
+
93
+ running_loss += loss.item()
94
+
95
+ preds = torch.argmax(logits, dim=1)
96
+
97
+ correct += (preds == labels).sum().item()
98
+ total += labels.size(0)
99
+
100
+ train_loss = running_loss / len(train_loader)
101
+ train_acc = 100 * correct / total
102
+
103
+ model.eval()
104
+
105
+ val_running_loss = 0
106
+ val_correct = 0
107
+ val_total = 0
108
+
109
+ all_preds = []
110
+ all_labels = []
111
+
112
+ with torch.no_grad():
113
+ for images, labels in tqdm(
114
+ eval_loader,
115
+ desc=f"Epoch {epoch+1} Validation"
116
+ ):
117
+ images = images.to(device)
118
+ labels = labels.to(device)
119
+
120
+ logits = model(images)
121
+
122
+ loss = criterion(logits, labels)
123
+
124
+ val_running_loss += loss.item()
125
+
126
+ preds = torch.argmax(logits, dim=1)
127
+
128
+ val_correct += (preds == labels).sum().item()
129
+ val_total += labels.size(0)
130
+
131
+ all_preds.extend(preds.cpu().numpy())
132
+ all_labels.extend(labels.cpu().numpy())
133
+
134
+ val_loss = val_running_loss / len(eval_loader)
135
+ val_acc = 100 * val_correct / val_total
136
+
137
+ logger.info(
138
+ f"Train Loss: {train_loss:.4f} | "
139
+ f"Train Acc: {train_acc:.2f}% || "
140
+ f"Val Loss: {val_loss:.4f} | "
141
+ f"Val Acc: {val_acc:.2f}%"
142
+ )
143
+
144
+ if val_acc > best_acc:
145
+ best_acc = val_acc
146
+
147
+ checkpoint_path = CHECKPOINT_DIR / f"{checkpoint_model_name}.pt"
148
+
149
+ torch.save(
150
+ {
151
+ "model_state_dict": model.state_dict(),
152
+ "optimizer_state_dict": optimizer.state_dict(),
153
+ "epoch": epoch,
154
+ "val_acc": val_acc
155
+ },
156
+ checkpoint_path
157
+ )
158
+
159
+ logger.info(f"Best checkpoint saved at: {checkpoint_path}")
160
+
161
+ early_stopping(val_acc)
162
+
163
+ if early_stopping.early_stop:
164
+ logger.info("Early stopping triggered.")
165
+ break
166
+
167
+ return all_preds, all_labels
168
+
169
+
170
+ def train_dual_input_model(
171
+ model,
172
+ train_loader,
173
+ eval_loader,
174
+ optimizer,
175
+ criterion,
176
+ device,
177
+ epochs,
178
+ checkpoint_model_name,
179
+ patience=7
180
+ ):
181
+ logger.info("Starting dual-input training...")
182
+
183
+ num_training_steps = epochs * len(train_loader)
184
+ num_warmup_steps = int(0.1 * num_training_steps)
185
+
186
+ scheduler = get_cosine_schedule_with_warmup(
187
+ optimizer=optimizer,
188
+ num_warmup_steps=num_warmup_steps,
189
+ num_training_steps=num_training_steps
190
+ )
191
+
192
+ early_stopping = EarlyStopping(patience=patience)
193
+
194
+ best_acc = 0.0
195
+ all_preds = []
196
+ all_labels = []
197
+
198
+ for epoch in range(epochs):
199
+ logger.info(f"Epoch {epoch + 1}/{epochs}")
200
+
201
+ model.train()
202
+
203
+ running_loss = 0
204
+ correct = 0
205
+ total = 0
206
+
207
+ for batch in tqdm(
208
+ train_loader,
209
+ desc=f"Epoch {epoch+1} Training"
210
+ ):
211
+ images_eff = batch["pixel_values_eff"].to(device)
212
+ images_cnx = batch["pixel_values_cnx"].to(device)
213
+ labels = batch["labels"].to(device)
214
+
215
+ optimizer.zero_grad(set_to_none=True)
216
+
217
+ logits = model(images_eff, images_cnx)
218
+
219
+ loss = criterion(logits, labels)
220
+
221
+ loss.backward()
222
+
223
+ optimizer.step()
224
+ scheduler.step()
225
+
226
+ running_loss += loss.item()
227
+
228
+ preds = torch.argmax(logits, dim=1)
229
+
230
+ correct += (preds == labels).sum().item()
231
+ total += labels.size(0)
232
+
233
+ train_loss = running_loss / len(train_loader)
234
+ train_acc = 100 * correct / total
235
+
236
+ model.eval()
237
+
238
+ val_running_loss = 0
239
+ val_correct = 0
240
+ val_total = 0
241
+
242
+ all_preds = []
243
+ all_labels = []
244
+
245
+ with torch.no_grad():
246
+ for batch in tqdm(
247
+ eval_loader,
248
+ desc=f"Epoch {epoch+1} Validation"
249
+ ):
250
+ images_eff = batch["pixel_values_eff"].to(device)
251
+ images_cnx = batch["pixel_values_cnx"].to(device)
252
+ labels = batch["labels"].to(device)
253
+
254
+ logits = model(images_eff, images_cnx)
255
+
256
+ loss = criterion(logits, labels)
257
+
258
+ val_running_loss += loss.item()
259
+
260
+ preds = torch.argmax(logits, dim=1)
261
+
262
+ val_correct += (preds == labels).sum().item()
263
+ val_total += labels.size(0)
264
+
265
+ all_preds.extend(preds.cpu().numpy())
266
+ all_labels.extend(labels.cpu().numpy())
267
+
268
+ val_loss = val_running_loss / len(eval_loader)
269
+ val_acc = 100 * val_correct / val_total
270
+
271
+ logger.info(
272
+ f"Train Loss: {train_loss:.4f} | "
273
+ f"Train Acc: {train_acc:.2f}% || "
274
+ f"Val Loss: {val_loss:.4f} | "
275
+ f"Val Acc: {val_acc:.2f}%"
276
+ )
277
+
278
+ if val_acc > best_acc:
279
+ best_acc = val_acc
280
+
281
+ checkpoint_path = CHECKPOINT_DIR / f"{checkpoint_model_name}.pt"
282
+
283
+ torch.save(
284
+ {
285
+ "model_state_dict": model.state_dict(),
286
+ "optimizer_state_dict": optimizer.state_dict(),
287
+ "epoch": epoch,
288
+ "val_acc": val_acc
289
+ },
290
+ checkpoint_path
291
+ )
292
+
293
+ logger.info(f"Best checkpoint saved at: {checkpoint_path}")
294
+
295
+ early_stopping(val_acc)
296
+
297
+ if early_stopping.early_stop:
298
+ logger.info("Early stopping triggered.")
299
+ break
300
+
301
+ return all_preds, all_labels
302
+
303
+
304
+ if __name__ == "__main__":
305
+ print("Trainer utilities ready.")
test/test_augmentation.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from PIL import Image
3
+
4
+ from src.data.augmentation import (
5
+ get_resnet_train_transforms,
6
+ get_fusion_train_transforms
7
+ )
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def test_augmentation():
13
+ logger.info("Testing augmentation pipelines...")
14
+
15
+ dummy_image = Image.new("RGB", (300, 300))
16
+
17
+ resnet_transform = get_resnet_train_transforms()
18
+ fusion_transform = get_fusion_train_transforms()
19
+
20
+ resnet_tensor = resnet_transform(dummy_image)
21
+ fusion_tensor = fusion_transform(dummy_image)
22
+
23
+ assert resnet_tensor.shape == (3, 128, 128), \
24
+ f"Unexpected ResNet shape: {resnet_tensor.shape}"
25
+
26
+ assert fusion_tensor.shape == (3, 260, 260), \
27
+ f"Unexpected Fusion shape: {fusion_tensor.shape}"
28
+
29
+ logger.info("Augmentation test passed.")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format="%(asctime)s - %(levelname)s - %(message)s"
36
+ )
37
+
38
+ test_augmentation()
39
+
40
+ print("Augmentation test completed successfully.")
test/test_config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.config import (
4
+ BASE_DIR,
5
+ CHECKPOINT_DIR,
6
+ DEVICE,
7
+ BATCH_SIZE,
8
+ EPOCHS,
9
+ NUM_CLASSES
10
+ )
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def test_config():
16
+ logger.info("Testing config settings...")
17
+
18
+ assert BASE_DIR.exists(), "BASE_DIR missing"
19
+ assert CHECKPOINT_DIR.exists(), "CHECKPOINT_DIR missing"
20
+
21
+ assert DEVICE in ["cpu", "cuda"], "Invalid device"
22
+ assert BATCH_SIZE > 0, "Invalid batch size"
23
+ assert EPOCHS > 0, "Invalid epochs"
24
+ assert NUM_CLASSES == 6, "NUM_CLASSES mismatch"
25
+
26
+ logger.info("Config test passed.")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format="%(asctime)s - %(levelname)s - %(message)s"
33
+ )
34
+
35
+ test_config()
36
+
37
+ print("Config test completed successfully.")
test/test_dataset.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.data.dataset import (
4
+ create_resnet_dataloaders,
5
+ create_fusion_dataloaders
6
+ )
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def test_dataset():
12
+ logger.info("Testing dataset loaders...")
13
+
14
+ # ---------------- ResNet ----------------
15
+ resnet_loader, _ = create_resnet_dataloaders()
16
+
17
+ images, labels = next(iter(resnet_loader))
18
+
19
+ assert images.shape[1:] == (3, 128, 128), \
20
+ f"Unexpected ResNet image shape: {images.shape}"
21
+
22
+ assert len(labels.shape) == 1, \
23
+ f"Unexpected ResNet labels shape: {labels.shape}"
24
+
25
+ logger.info("ResNet dataloader test passed.")
26
+
27
+ # ---------------- Fusion ----------------
28
+ fusion_loader, _ = create_fusion_dataloaders()
29
+
30
+ batch = next(iter(fusion_loader))
31
+
32
+ assert "pixel_values_eff" in batch, "Missing EfficientNet input"
33
+ assert "pixel_values_cnx" in batch, "Missing ConvNeXt input"
34
+ assert "labels" in batch, "Missing labels"
35
+
36
+ assert batch["pixel_values_eff"].shape[1:] == (3, 260, 260), \
37
+ f"Unexpected Fusion EfficientNet shape: {batch['pixel_values_eff'].shape}"
38
+
39
+ assert batch["pixel_values_cnx"].shape[1:] == (3, 224, 224), \
40
+ f"Unexpected Fusion ConvNeXt shape: {batch['pixel_values_cnx'].shape}"
41
+
42
+ assert len(batch["labels"].shape) == 1, \
43
+ f"Unexpected Fusion labels shape: {batch['labels'].shape}"
44
+
45
+ logger.info("Fusion dataloader test passed.")
46
+ logger.info("Dataset test passed successfully.")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ logging.basicConfig(
51
+ level=logging.INFO,
52
+ format="%(asctime)s - %(levelname)s - %(message)s"
53
+ )
54
+
55
+ test_dataset()
56
+
57
+ print("Dataset test completed successfully.")
test/test_fusion_model.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+
4
+ from src.models.fusion_model import FusionClassifier
5
+ from src.config import NUM_CLASSES
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def test_fusion_model():
11
+ logger.info("Testing Fusion model architecture...")
12
+
13
+ model = FusionClassifier(
14
+ num_classes=NUM_CLASSES
15
+ )
16
+
17
+ model.eval()
18
+
19
+ eff_dummy = torch.randn(2, 3, 260, 260)
20
+ cnx_dummy = torch.randn(2, 3, 224, 224)
21
+
22
+ with torch.no_grad():
23
+ output = model(
24
+ eff_dummy,
25
+ cnx_dummy
26
+ )
27
+
28
+ assert output.shape == (2, NUM_CLASSES), \
29
+ f"Unexpected output shape: {output.shape}"
30
+
31
+ logger.info("Fusion model test passed.")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ logging.basicConfig(
36
+ level=logging.INFO,
37
+ format="%(asctime)s - %(levelname)s - %(message)s"
38
+ )
39
+
40
+ test_fusion_model()
41
+
42
+ print("Fusion model test completed successfully.")
test/test_ingestion.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
+ from src.data.ingestion import collect_image_paths
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def test_ingestion():
10
+ logger.info("Testing ingestion...")
11
+
12
+ samples = collect_image_paths()
13
+
14
+ assert len(samples) > 0, "No samples found"
15
+
16
+ image_path, label = samples[0]
17
+
18
+ assert os.path.exists(image_path), "Image path missing"
19
+ assert isinstance(label, int), "Label invalid"
20
+
21
+ logger.info("Ingestion test passed.")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format="%(asctime)s - %(levelname)s - %(message)s"
28
+ )
29
+
30
+ test_ingestion()
31
+
32
+ print("Ingestion test completed successfully.")
test/test_preprocessing.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.data.ingestion import collect_image_paths
4
+ from src.data.preprocessing import split_dataset
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def test_preprocessing():
10
+ logger.info("Testing preprocessing...")
11
+
12
+ samples = collect_image_paths()
13
+
14
+ train_data, val_data = split_dataset(samples)
15
+
16
+ assert len(train_data) > 0, "Train split is empty"
17
+ assert len(val_data) > 0, "Validation split is empty"
18
+
19
+ train_paths = set(x[0] for x in train_data)
20
+ val_paths = set(x[0] for x in val_data)
21
+
22
+ overlap = train_paths.intersection(val_paths)
23
+
24
+ assert len(overlap) == 0, "Train and validation overlap found"
25
+
26
+ logger.info("Preprocessing test passed.")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format="%(asctime)s - %(levelname)s - %(message)s"
33
+ )
34
+
35
+ test_preprocessing()
36
+
37
+ print("Preprocessing test completed successfully.")
test/test_resnet_model.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+
4
+ from src.models.resnet_model import CarClassifierResNet
5
+ from src.config import NUM_CLASSES
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def test_resnet_model():
11
+ logger.info("Testing ResNet model architecture...")
12
+
13
+ model = CarClassifierResNet(
14
+ num_classes=NUM_CLASSES
15
+ )
16
+
17
+ model.eval()
18
+
19
+ dummy_input = torch.randn(2, 3, 128, 128)
20
+
21
+ with torch.no_grad():
22
+ output = model(dummy_input)
23
+
24
+ assert output.shape == (2, NUM_CLASSES), \
25
+ f"Unexpected output shape: {output.shape}"
26
+
27
+ logger.info("ResNet model test passed.")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format="%(asctime)s - %(levelname)s - %(message)s"
34
+ )
35
+
36
+ test_resnet_model()
37
+
38
+ print("ResNet model test completed successfully.")
test/test_train_fusion.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.training.train_fusion import run_fusion_training
4
+ from src.config import CHECKPOINT_DIR
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def test_train_fusion():
10
+ logger.info("Testing Fusion training pipeline...")
11
+
12
+ checkpoint_path = CHECKPOINT_DIR / "best_fusion_model.pt"
13
+
14
+ if checkpoint_path.exists():
15
+ checkpoint_path.unlink()
16
+
17
+ preds, labels = run_fusion_training()
18
+
19
+ assert checkpoint_path.exists(), \
20
+ "Fusion checkpoint was not created"
21
+
22
+ assert len(preds) > 0, \
23
+ "No predictions returned"
24
+
25
+ assert len(labels) > 0, \
26
+ "No labels returned"
27
+
28
+ logger.info("Fusion training test passed.")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ logging.basicConfig(
33
+ level=logging.INFO,
34
+ format="%(asctime)s - %(levelname)s - %(message)s"
35
+ )
36
+
37
+ test_train_fusion()
38
+
39
+ print("Fusion training test completed successfully.")
test/test_train_resnet.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.training.train_resnet import run_resnet_training
4
+ from src.config import CHECKPOINT_DIR
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def test_train_resnet():
10
+ logger.info("Testing ResNet training pipeline...")
11
+
12
+ checkpoint_path = CHECKPOINT_DIR / "best_resnet_model.pt"
13
+
14
+ if checkpoint_path.exists():
15
+ checkpoint_path.unlink()
16
+
17
+ preds, labels = run_resnet_training()
18
+
19
+ assert checkpoint_path.exists(), \
20
+ "ResNet checkpoint was not created"
21
+
22
+ assert len(preds) > 0, \
23
+ "No predictions returned"
24
+
25
+ assert len(labels) > 0, \
26
+ "No labels returned"
27
+
28
+ logger.info("ResNet training test passed.")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ logging.basicConfig(
33
+ level=logging.INFO,
34
+ format="%(asctime)s - %(levelname)s - %(message)s"
35
+ )
36
+
37
+ test_train_resnet()
38
+
39
+ print("ResNet training test completed successfully.")
test/test_train_yolo.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from src.training.train_yolo import run_yolo_training
4
+ from src.config import CHECKPOINT_DIR
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def test_train_yolo():
10
+ logger.info("Testing YOLO training pipeline...")
11
+
12
+ checkpoint_path = CHECKPOINT_DIR / "damage_detector.pt"
13
+
14
+ if checkpoint_path.exists():
15
+ checkpoint_path.unlink()
16
+
17
+ output_path = run_yolo_training()
18
+
19
+ assert checkpoint_path.exists(), \
20
+ "YOLO checkpoint was not created"
21
+
22
+ assert output_path.exists(), \
23
+ "Returned YOLO model path invalid"
24
+
25
+ logger.info("YOLO training test passed.")
26
+
27
+
28
+ if __name__ == "__main__":
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format="%(asctime)s - %(levelname)s - %(message)s"
32
+ )
33
+
34
+ test_train_yolo()
35
+
36
+ print("YOLO training test completed successfully.")