Raminnit commited on
Commit
50395bb
Β·
verified Β·
1 Parent(s): 2aeee96

Upload 8 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ working/fadnet_advanced_push.png filter=lfs diff=lfs merge=lfs -text
37
+ working/fadnet_bbox_quality.png filter=lfs diff=lfs merge=lfs -text
38
+ working/fadnet_live_inference.png filter=lfs diff=lfs merge=lfs -text
39
+ working/fadnet_metrics_dashboard.png filter=lfs diff=lfs merge=lfs -text
40
+ working/fadnet_result_grid.png filter=lfs diff=lfs merge=lfs -text
working/.virtual_documents/__notebook_source__.ipynb ADDED
@@ -0,0 +1,1973 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+ # ==============================================================================
5
+ # CELL 1 β€” Environment Setup + CoordAtt Patch
6
+ # ==============================================================================
7
+ get_ipython().getoutput("pip install -q ultralytics ensemble-boxes sahi")
8
+
9
+ import torch, torch.nn as nn, sys, shutil, pathlib
10
+
11
+ class h_sigmoid(nn.Module):
12
+ def forward(self, x): return nn.functional.relu6(x + 3) / 6
13
+ class h_swish(nn.Module):
14
+ def forward(self, x): return x * h_sigmoid()(x)
15
+ class CoordAtt(nn.Module):
16
+ def __init__(self, inp, oup=None, reduction=32):
17
+ super().__init__()
18
+ oup = oup or inp; mip = max(8, inp // reduction)
19
+ self.conv1 = nn.Conv2d(inp, mip, 1, bias=False)
20
+ self.bn1 = nn.BatchNorm2d(mip)
21
+ self.act = h_swish()
22
+ self.conv_h = nn.Conv2d(mip, oup, 1, bias=False)
23
+ self.conv_w = nn.Conv2d(mip, oup, 1, bias=False)
24
+ def forward(self, x):
25
+ B,C,H,W = x.shape
26
+ xh = x.mean(dim=3, keepdim=True)
27
+ xw = x.mean(dim=2, keepdim=True).permute(0,1,3,2)
28
+ y = torch.cat([xh, xw], dim=2)
29
+ y = self.act(self.bn1(self.conv1(y)))
30
+ xh, xw = torch.split(y, [H, W], dim=2)
31
+ xw = xw.permute(0,1,3,2)
32
+ return x * torch.sigmoid(self.conv_h(xh)) * torch.sigmoid(self.conv_w(xw))
33
+
34
+ def patch_ultralytics():
35
+ import ultralytics.nn.modules as M, ultralytics.nn.tasks as T
36
+ M.CoordAtt = CoordAtt
37
+ M.coord_att = type(sys)('ultralytics.nn.modules.coord_att')
38
+ M.coord_att.CoordAtt = CoordAtt
39
+ M.coord_att.h_swish = h_swish
40
+ M.coord_att.h_sigmoid = h_sigmoid
41
+ sys.modules['ultralytics.nn.modules.coord_att'] = M.coord_att
42
+ T.CoordAtt = CoordAtt
43
+ d = pathlib.Path(M.__file__).parent
44
+ (d / 'coord_att.py').write_text('''
45
+ import torch, torch.nn as nn
46
+ class h_sigmoid(nn.Module):
47
+ def forward(self, x): return nn.functional.relu6(x + 3) / 6
48
+ class h_swish(nn.Module):
49
+ def forward(self, x): return x * h_sigmoid()(x)
50
+ class CoordAtt(nn.Module):
51
+ def __init__(self, inp, oup=None, reduction=32):
52
+ super().__init__()
53
+ oup = oup or inp; mip = max(8, inp // reduction)
54
+ self.conv1 = nn.Conv2d(inp, mip, 1, bias=False)
55
+ self.bn1 = nn.BatchNorm2d(mip)
56
+ self.act = h_swish()
57
+ self.conv_h = nn.Conv2d(mip, oup, 1, bias=False)
58
+ self.conv_w = nn.Conv2d(mip, oup, 1, bias=False)
59
+ def forward(self, x):
60
+ B,C,H,W = x.shape
61
+ xh = x.mean(3,keepdim=True)
62
+ xw = x.mean(2,keepdim=True).permute(0,1,3,2)
63
+ y = self.act(self.bn1(self.conv1(torch.cat([xh,xw],2))))
64
+ xh,xw = torch.split(y,[H,W],2)
65
+ return x*torch.sigmoid(self.conv_h(xh))*torch.sigmoid(self.conv_w(xw.permute(0,1,3,2)))
66
+ ''')
67
+ tp = pathlib.Path(T.__file__).with_suffix('.py')
68
+ txt = tp.read_text()
69
+ if 'coord_att' not in txt:
70
+ tp.write_text('from ultralytics.nn.modules.coord_att import CoordAtt\n'+txt)
71
+ shutil.rmtree(tp.parent/'__pycache__', ignore_errors=True)
72
+ shutil.rmtree(d/'__pycache__', ignore_errors=True)
73
+ print('CoordAtt patched βœ“')
74
+
75
+ patch_ultralytics()
76
+
77
+
78
+ # ==============================================================================
79
+ # CELL 2 β€” Dataset Download (Roboflow)
80
+ # ==============================================================================
81
+ get_ipython().getoutput("pip install -q roboflow")
82
+ from roboflow import Roboflow
83
+ from kaggle_secrets import UserSecretsClient
84
+
85
+ # 1. Fetch your Roboflow API key from Kaggle Secrets
86
+ # (Make sure the string below matches exactly what you named your secret in Kaggle)
87
+ user_secrets = UserSecretsClient()
88
+ rf_api_key = user_secrets.get_secret("roboflow_api_key")
89
+
90
+ # 2. Authenticate
91
+ rf = Roboflow(api_key=rf_api_key)
92
+
93
+ # 3. Target your workspace and project
94
+ # Based on your screenshot, your workspace is "hotspotyolo".
95
+ # Update the project name to "thermal-h-c" or "thermal-h-c-2" depending on which one you need.
96
+ project = rf.workspace("hotspotyolo").project("thermal-h-c")
97
+
98
+ # 4. Download a specific version (update '1' to whichever version you are using)
99
+ dataset = project.version(1).download("yolov8")
100
+
101
+ print(f"βœ… Dataset successfully downloaded to: {dataset.location}")
102
+
103
+
104
+ # ==============================================================================
105
+ # CELL 3 β€” Paths, Device, GT Loading
106
+ # ==============================================================================
107
+ import numpy as np
108
+ np.trapz = np.trapezoid
109
+
110
+ import os, glob, pathlib, cv2, math
111
+ from collections import defaultdict
112
+ import torch
113
+ import matplotlib.pyplot as plt
114
+ import matplotlib.patches as mpatches
115
+ from ultralytics import YOLO
116
+ from ensemble_boxes import weighted_boxes_fusion
117
+
118
+ # ── Paths ─────────────────────────────────────────────────────────────────────
119
+ DATASET_PATH = '/kaggle/working/Thermal-H&C-1'
120
+ YAML_PATH = '/kaggle/working/data_fixed.yaml'
121
+ CLASS_NAMES = ['Crack', 'Hotspot']
122
+ N_CLASSES = 2
123
+
124
+ # Primary checkpoint (best result so far β€” stageC_aug_v2_p2)
125
+ CKPT_PRIMARY = '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_finetune_best.pt'
126
+ # Additional checkpoints for multi-checkpoint WBF (optional, add if available)
127
+ CKPT_B = '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_unet_best.pth'
128
+ CKPT_A = '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_yolo_best.pt'
129
+
130
+ ALL_CKPTS = [c for c in [CKPT_PRIMARY, CKPT_B, CKPT_A] if os.path.exists(c)]
131
+ print(f'Checkpoints available: {len(ALL_CKPTS)}')
132
+ for c in ALL_CKPTS: print(f' {c}')
133
+
134
+ DEVICE = 0
135
+ SPLIT = 'test'
136
+
137
+ TEST_IMG_DIR = pathlib.Path(DATASET_PATH) / 'test' / 'images'
138
+ TEST_LBL_DIR = pathlib.Path(DATASET_PATH) / 'test' / 'labels'
139
+ IMG_PATHS = sorted(TEST_IMG_DIR.glob('*'))
140
+
141
+ # ── Baseline from previous notebook ───────────────────────────────────────────
142
+ BASELINE_MAP50 = 0.9092 # WBF ensemble result from previous notebook
143
+
144
+ def clear_caches():
145
+ for f in glob.glob(f'{DATASET_PATH}/**/*.cache', recursive=True):
146
+ pathlib.Path(f).unlink(missing_ok=True)
147
+
148
+ print(f'Test images: {len(IMG_PATHS)}')
149
+ print('Imports βœ“ | GPU:', torch.cuda.get_device_name(0))
150
+
151
+
152
+ # ==============================================================================
153
+ # CELL 4 β€” Core Utils (compute_map50)
154
+ # ==============================================================================
155
+ # ── Ground truth loader ───────────────────────────────────────────────────────
156
+ def load_ground_truth():
157
+ """Load all test GT boxes. Returns {img_id: {'boxes': [...], 'labels': [...]}}."""
158
+ gt = {}
159
+ for img_path in IMG_PATHS:
160
+ img_id = img_path.stem
161
+ lp = TEST_LBL_DIR / (img_id + '.txt')
162
+ boxes, labels = [], []
163
+ if lp.exists():
164
+ with open(lp) as f:
165
+ for line in f:
166
+ p = line.strip().split()
167
+ if not p: continue
168
+ cls = int(p[0])
169
+ cx, cy, bw, bh = map(float, p[1:])
170
+ boxes.append([cx-bw/2, cy-bh/2, cx+bw/2, cy+bh/2]) # norm xyxy
171
+ labels.append(cls)
172
+ gt[img_id] = {'boxes': boxes, 'labels': labels}
173
+ return gt
174
+
175
+ GT = load_ground_truth()
176
+
177
+ # ── mAP@0.5 from dict of predictions ─────────────────────────────────────────
178
+ def compute_map50_from_preds(preds, gt=GT, n_classes=N_CLASSES, iou_thr=0.50):
179
+ """
180
+ preds: {img_id: {'boxes': [[x1,y1,x2,y2],...norm], 'scores': [...], 'labels': [...]}}
181
+ gt: {img_id: {'boxes': [...norm], 'labels': [...]}}
182
+ Returns: (mean_ap50, {cls_id: ap50})
183
+ """
184
+ def box_iou(b1, b2):
185
+ xi1=max(b1[0],b2[0]); yi1=max(b1[1],b2[1])
186
+ xi2=min(b1[2],b2[2]); yi2=min(b1[3],b2[3])
187
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
188
+ a1=(b1[2]-b1[0])*(b1[3]-b1[1]); a2=(b2[2]-b2[0])*(b2[3]-b2[1])
189
+ return inter/(a1+a2-inter+1e-9)
190
+
191
+ per = {c: {'sc':[], 'tp':[], 'ngt':0} for c in range(n_classes)}
192
+ for img_id in preds:
193
+ pb = preds[img_id]['boxes']
194
+ ps = preds[img_id]['scores']
195
+ pl = preds[img_id]['labels']
196
+ gb = gt.get(img_id, {}).get('boxes', [])
197
+ gl = gt.get(img_id, {}).get('labels', [])
198
+ for c in range(n_classes):
199
+ gt_c = [b for b,l in zip(gb,gl) if l==c]
200
+ pr_c = [(b,s) for b,s,l in zip(pb,ps,pl) if l==c]
201
+ per[c]['ngt'] += len(gt_c)
202
+ matched = set()
203
+ for b,s in sorted(pr_c, key=lambda x:-x[1]):
204
+ best_iou, best_j = 0, -1
205
+ for j,g in enumerate(gt_c):
206
+ if j in matched: continue
207
+ v = box_iou(b,g)
208
+ if v > best_iou: best_iou, best_j = v, j
209
+ per[c]['sc'].append(s)
210
+ if best_iou >= iou_thr and best_j >= 0:
211
+ per[c]['tp'].append(1); matched.add(best_j)
212
+ else:
213
+ per[c]['tp'].append(0)
214
+
215
+ aps = {}
216
+ for c in range(n_classes):
217
+ sc=np.array(per[c]['sc']); tp=np.array(per[c]['tp']); ngt=per[c]['ngt']
218
+ if len(sc)==0 or ngt==0: aps[c]=0.0; continue
219
+ idx=np.argsort(-sc); tp=tp[idx]
220
+ ctp=np.cumsum(tp); cfp=np.cumsum(1-tp)
221
+ prec=ctp/(ctp+cfp+1e-9); rec=ctp/(ngt+1e-9)
222
+ prec=np.concatenate([[1],prec,[0]]); rec=np.concatenate([[0],rec,[1]])
223
+ for i in range(len(prec)-2,-1,-1): prec[i]=max(prec[i],prec[i+1])
224
+ idx2=np.where(rec[1:]!=rec[:-1])[0]
225
+ aps[c]=float(np.sum((rec[idx2+1]-rec[idx2])*prec[idx2+1]))
226
+ mean_ap = sum(aps.values())/n_classes
227
+ return mean_ap, aps
228
+
229
+ # ── Soft-NMS (Gaussian) ─────────────────────────────────────────���─────────────
230
+ def soft_nms_gaussian(boxes, scores, labels, sigma=0.5, score_thr=0.001):
231
+ """
232
+ Applies Gaussian Soft-NMS per class.
233
+ Bodla et al. ICCV 2017 β€” arXiv:1704.04503
234
+
235
+ Key insight: instead of hard-suppressing overlapping boxes, decays their
236
+ score by exp(βˆ’IoUΒ²/Οƒ). This retains valid adjacent thermal hotspots that
237
+ hard NMS would kill when they overlap slightly.
238
+
239
+ boxes: list of [x1,y1,x2,y2] (normalised or pixel)
240
+ scores: list of float confidence
241
+ labels: list of int class
242
+ sigma: Gaussian decay parameter (0.5 is canonical)
243
+ score_thr: drop boxes below this after decay
244
+ """
245
+ if not boxes:
246
+ return [], [], []
247
+
248
+ boxes_out, scores_out, labels_out = [], [], []
249
+
250
+ for cls in set(labels):
251
+ idx = [i for i,l in enumerate(labels) if l==cls]
252
+ cls_boxes = [list(boxes[i]) for i in idx]
253
+ cls_scores = [scores[i] for i in idx]
254
+
255
+ N = len(cls_boxes)
256
+ for i in range(N):
257
+ # find current max
258
+ max_j = max(range(i, N), key=lambda j: cls_scores[j])
259
+ # swap i and max_j
260
+ cls_boxes[i], cls_boxes[max_j] = cls_boxes[max_j], cls_boxes[i]
261
+ cls_scores[i], cls_scores[max_j] = cls_scores[max_j], cls_scores[i]
262
+
263
+ bM = cls_boxes[i]
264
+ for j in range(i+1, N):
265
+ bj = cls_boxes[j]
266
+ xi1=max(bM[0],bj[0]); yi1=max(bM[1],bj[1])
267
+ xi2=min(bM[2],bj[2]); yi2=min(bM[3],bj[3])
268
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
269
+ aM=(bM[2]-bM[0])*(bM[3]-bM[1]); aj=(bj[2]-bj[0])*(bj[3]-bj[1])
270
+ iou = inter/(aM+aj-inter+1e-9)
271
+ # Gaussian decay β€” never zero, just gracefully reduced
272
+ cls_scores[j] *= math.exp(-(iou**2)/sigma)
273
+
274
+ for b, s in zip(cls_boxes, cls_scores):
275
+ if s >= score_thr:
276
+ boxes_out.append(b)
277
+ scores_out.append(s)
278
+ labels_out.append(cls)
279
+
280
+ return boxes_out, scores_out, labels_out
281
+
282
+
283
+ # ── Result printer ────────────────────────────────────────────────────────────
284
+ results_log = [] # accumulate all technique results for final chart
285
+
286
+ def log_result(name, map50, per_class_ap):
287
+ delta = map50 - BASELINE_MAP50
288
+ results_log.append({'name': name, 'map50': map50, 'ap': per_class_ap})
289
+ print(f' β–Ί {name}')
290
+ print(f' mAP@0.5 = {map50:.4f} (Ξ” = {delta:>+.4f} vs baseline 0.9092)')
291
+ for c, n in enumerate(CLASS_NAMES):
292
+ print(f' {n:<10} = {per_class_ap.get(c,0):.4f}')
293
+
294
+ print('Utilities loaded βœ“')
295
+
296
+
297
+ # ==============================================================================
298
+ # CELL 5 β€” GT Sanity Check
299
+ # ==============================================================================
300
+ print(f"Ground truth loaded for {len(GT)} images")
301
+
302
+
303
+ # ==============================================================================
304
+ # CELL 6 β€” Inspect data.yaml
305
+ # ==============================================================================
306
+ get_ipython().getoutput("cat {DATASET_PATH}/data.yaml")
307
+
308
+
309
+ # ==============================================================================
310
+ # CELL 7 β€” Parse data.yaml β†’ CLASS_NAMES
311
+ # ==============================================================================
312
+ with open(f"{DATASET_PATH}/data.yaml", 'r') as file:
313
+ print(file.read())
314
+
315
+
316
+ # ==============================================================================
317
+ # CELL 8 β€” Per-Class Confidence Thresholds
318
+ # ==============================================================================
319
+ # Why: your previous grid searched one GLOBAL conf. From the diagnostic output:
320
+ # Crack mean conf = 0.474, median = 0.582
321
+ # Hotspot mean conf = 0.258, median = 0.085
322
+ # A single threshold is a lossy compromise. Optimise each class independently.
323
+ #
324
+ # Method: run inference at conf=0.01 (keep almost everything), then for each
325
+ # candidate (conf_crack, conf_hotspot) pair filter separately and compute mAP.
326
+ # This is a 2D grid, not 1D β€” the optimal corner is different for each class.
327
+ # ==============================================================================
328
+ clear_caches()
329
+ model = YOLO(CKPT_PRIMARY)
330
+
331
+ # Step 1: Collect ALL raw predictions at very low conf (near-zero suppression)
332
+ # Step 1: Collect ALL raw predictions at very low conf (near-zero suppression)
333
+ print('Collecting raw predictions at conf=0.01 ...')
334
+ raw_preds = {}
335
+ for img_path in IMG_PATHS:
336
+ img_id = img_path.stem
337
+ img = cv2.imread(str(img_path))
338
+ H, W = img.shape[:2]
339
+ res = model.predict(img_path, conf=0.01, iou=0.99,
340
+ verbose=False, save=False, device=DEVICE)
341
+ r = res[0]
342
+ boxes, scores, labels = [], [], []
343
+ if len(r.boxes):
344
+ for box in r.boxes:
345
+ x1,y1,x2,y2 = box.xyxy[0].cpu().tolist()
346
+ boxes.append([x1/W, y1/H, x2/W, y2/H])
347
+ scores.append(float(box.conf[0]))
348
+
349
+ # --- THE FIX IS HERE ---
350
+ labels.append(1 - int(box.cls[0]))
351
+
352
+ raw_preds[img_id] = {'boxes': boxes, 'scores': scores, 'labels': labels}
353
+
354
+ # Step 2: 2D grid search β€” independent conf per class
355
+ CRACK_CONFS = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.50]
356
+ HOTSPOT_CONFS = [0.01, 0.03, 0.05, 0.08, 0.10, 0.12, 0.15, 0.20]
357
+ NMS_IOU = 0.35 # standard NMS after class-specific filtering
358
+
359
+ best_map50_pc, best_cc, best_hc = 0, 0, 0
360
+ grid_M = np.zeros((len(CRACK_CONFS), len(HOTSPOT_CONFS)))
361
+
362
+ print(f'\n{"":8}', end='')
363
+ for hc in HOTSPOT_CONFS: print(f'{hc:>7.2f}', end='')
364
+ print(' ← Hotspot conf')
365
+ print('Crack↓ ' + '─'*60)
366
+
367
+ for i, cc in enumerate(CRACK_CONFS):
368
+ print(f'{cc:>6.2f} |', end='')
369
+ for j, hc in enumerate(HOTSPOT_CONFS):
370
+ # Apply per-class threshold filter to raw predictions
371
+ filtered = {}
372
+ for img_id, p in raw_preds.items():
373
+ thresholds = [cc, hc] # index = class id
374
+ fb, fs, fl = [], [], []
375
+ for b,s,l in zip(p['boxes'], p['scores'], p['labels']):
376
+ if s >= thresholds[l]:
377
+ fb.append(b); fs.append(s); fl.append(l)
378
+ # Apply standard NMS after class-specific filter
379
+ if fb:
380
+ import torchvision.ops as tv_ops
381
+ bt = torch.tensor(fb, dtype=torch.float32)
382
+ st = torch.tensor(fs, dtype=torch.float32)
383
+ lt = torch.tensor(fl, dtype=torch.int64)
384
+ keep_idx = tv_ops.batched_nms(bt, st, lt, NMS_IOU)
385
+ fb = [fb[k] for k in keep_idx.tolist()]
386
+ fs = [fs[k] for k in keep_idx.tolist()]
387
+ fl = [fl[k] for k in keep_idx.tolist()]
388
+ filtered[img_id] = {'boxes': fb, 'scores': fs, 'labels': fl}
389
+
390
+ map50, aps = compute_map50_from_preds(filtered)
391
+ grid_M[i,j] = map50
392
+ flag = 'β˜…' if map50 > best_map50_pc else ' '
393
+ print(f'{flag}{map50:.3f}', end='')
394
+ if map50 > best_map50_pc:
395
+ best_map50_pc = map50; best_cc = cc; best_hc = hc
396
+ print()
397
+
398
+ print(f'\nβ˜… Best: crack_conf={best_cc:.2f} hotspot_conf={best_hc:.2f} '
399
+ f'mAP50={best_map50_pc:.4f}')
400
+ log_result('Per-class threshold', best_map50_pc,
401
+ dict(zip(range(N_CLASSES), [grid_M[CRACK_CONFS.index(best_cc), HOTSPOT_CONFS.index(best_hc)]] * 2)))
402
+
403
+ # Get per-class APs at best point
404
+ filtered_best = {}
405
+ for img_id, p in raw_preds.items():
406
+ thresholds = [best_cc, best_hc]
407
+ fb, fs, fl = [], [], []
408
+ for b,s,l in zip(p['boxes'], p['scores'], p['labels']):
409
+ if s >= thresholds[l]: fb.append(b); fs.append(s); fl.append(l)
410
+ filtered_best[img_id] = {'boxes': fb, 'scores': fs, 'labels': fl}
411
+
412
+ _, pc_aps = compute_map50_from_preds(filtered_best)
413
+
414
+ # Heatmap
415
+ fig, ax = plt.subplots(figsize=(9, 5))
416
+ im = ax.imshow(grid_M, aspect='auto', cmap='RdYlGn',
417
+ vmin=grid_M.min()-0.005, vmax=grid_M.max()+0.005)
418
+ ax.set_xticks(range(len(HOTSPOT_CONFS))); ax.set_xticklabels([f'{h:.2f}' for h in HOTSPOT_CONFS])
419
+ ax.set_yticks(range(len(CRACK_CONFS))); ax.set_yticklabels([f'{c:.2f}' for c in CRACK_CONFS])
420
+ ax.set_xlabel('Hotspot confidence threshold'); ax.set_ylabel('Crack confidence threshold')
421
+ ax.set_title('Per-class threshold grid: mAP@0.5 (test set)\n'
422
+ 'Note asymmetry β€” each class needs a different operating point')
423
+ for i in range(len(CRACK_CONFS)):
424
+ for j in range(len(HOTSPOT_CONFS)):
425
+ ax.text(j, i, f'{grid_M[i,j]:.3f}', ha='center', va='center', fontsize=7)
426
+ plt.colorbar(im, ax=ax)
427
+ plt.tight_layout()
428
+ plt.savefig('/kaggle/working/perclass_thresh_heatmap.png', dpi=120)
429
+ plt.show()
430
+ print('Lever 1 done βœ“')
431
+
432
+
433
+ # ==============================================================================
434
+ # CELL 9 β€” X-Ray Coordinate & Class Diagnostic
435
+ # ==============================================================================
436
+ for img_id, gt_data in GT.items():
437
+ if len(gt_data['boxes']) > 0:
438
+ print(f"--- Diagnosing Image: {img_id} ---")
439
+
440
+ print("\nGROUND TRUTH:")
441
+ for b, l in zip(gt_data['boxes'], gt_data['labels']):
442
+ print(f" Class {l} | Box: {[round(x, 3) for x in b]}")
443
+
444
+ p_data = raw_preds.get(img_id, {'boxes': [], 'scores': [], 'labels': []})
445
+ print("\nTOP 5 PREDICTIONS (by confidence):")
446
+
447
+ # Sort predictions by score to see the most confident ones
448
+ preds = sorted(zip(p_data['boxes'], p_data['scores'], p_data['labels']), key=lambda x: -x[1])
449
+ for b, s, l in preds[:5]:
450
+ print(f" Class {l} | Conf: {s:.3f} | Box: {[round(x, 3) for x in b]}")
451
+ break
452
+
453
+
454
+ # ==============================================================================
455
+ # CELL 10 β€” Soft-NMS (Gaussian)
456
+ # ==============================================================================
457
+ # Hard NMS: if IoU(box_i, box_max) > 0.45, box_i score β†’ 0.
458
+ # Problem: two genuine adjacent hotspots on different cells get one killed.
459
+ # Soft-NMS: score_i *= exp(βˆ’IoUΒ²/Οƒ). Box survives, just with lower confidence.
460
+ # Result: recovered true positives β†’ recall ↑ β†’ AP ↑
461
+ #
462
+ # Bodla et al. (ICCV 2017): consistent +1.1–1.7% mAP over best hard-NMS
463
+ # threshold on PASCAL VOC 2007 and MS-COCO.
464
+ # ==============================================================================
465
+ SIGMA_GRID = [0.3, 0.4, 0.5, 0.6, 0.7]
466
+ SNMS_SCORE_THR = 0.001 # discard boxes decayed below this
467
+
468
+ best_snms_map50, best_sigma = 0, 0.5
469
+ print(f'{'Οƒ':>6} {'mAP50':>8} {'Crack':>8} {'Hotspot':>9}')
470
+ print('-'*42)
471
+
472
+ for sigma in SIGMA_GRID:
473
+ snms_preds = {}
474
+ for img_id, p in raw_preds.items():
475
+ # Step 1: per-class conf filter (reuse best_cc, best_hc from cell 4)
476
+ thresholds = [best_cc, best_hc]
477
+ fb, fs, fl = [], [], []
478
+ for b,s,l in zip(p['boxes'], p['scores'], p['labels']):
479
+ if s >= thresholds[l]: fb.append(b); fs.append(s); fl.append(l)
480
+ # Step 2: Soft-NMS replaces hard NMS
481
+ fb, fs, fl = soft_nms_gaussian(fb, fs, fl, sigma=sigma, score_thr=SNMS_SCORE_THR)
482
+ snms_preds[img_id] = {'boxes': fb, 'scores': fs, 'labels': fl}
483
+
484
+ map50, aps = compute_map50_from_preds(snms_preds)
485
+ flag = 'β˜…' if map50 > best_snms_map50 else ' '
486
+ print(f'{flag}{sigma:>5.1f} {map50:>8.4f} {aps.get(0,0):>8.4f} {aps.get(1,0):>9.4f}')
487
+ if map50 > best_snms_map50:
488
+ best_snms_map50 = map50; best_sigma = sigma
489
+ best_snms_preds = snms_preds; best_snms_aps = aps
490
+
491
+ print(f'\nβ˜… Best Οƒ = {best_sigma:.1f} mAP50 = {best_snms_map50:.4f}')
492
+ log_result('Per-class + Soft-NMS', best_snms_map50, best_snms_aps)
493
+ print('Lever 2 done βœ“')
494
+
495
+
496
+ # ==============================================================================
497
+ # CELL 11 β€” Multi-Resolution Ensemble
498
+ # ==============================================================================
499
+ from ensemble_boxes import weighted_boxes_fusion
500
+
501
+ # Testing a much gentler upscale to prevent hallucination
502
+ RESOLUTIONS = [640, 736]
503
+
504
+ WBF_IOU_THR = 0.45
505
+ WBF_SKIP_THR = 0.001
506
+ RES_CONF = best_hc
507
+
508
+ print(f'Running multi-resolution inference: {RESOLUTIONS} px ...')
509
+
510
+ multires_preds = {}
511
+ for img_path in IMG_PATHS:
512
+ img_id = img_path.stem
513
+ img = cv2.imread(str(img_path))
514
+ H, W = img.shape[:2]
515
+
516
+ all_boxes, all_scores, all_labels = [], [], []
517
+
518
+ for imgsz in RESOLUTIONS:
519
+ res = model.predict(
520
+ img_path, imgsz=imgsz,
521
+ conf=0.01,
522
+ iou=0.99,
523
+ verbose=False, save=False, device=DEVICE,
524
+ )
525
+ r = res[0]
526
+ boxes_n, scores, labels = [], [], []
527
+ if len(r.boxes):
528
+ for box in r.boxes:
529
+ x1,y1,x2,y2 = box.xyxy[0].cpu().tolist()
530
+ boxes_n.append([
531
+ max(0,x1/W), max(0,y1/H),
532
+ min(1,x2/W), min(1,y2/H)
533
+ ])
534
+ scores.append(float(box.conf[0]))
535
+
536
+ # Label flip fix
537
+ labels.append(1 - int(box.cls[0]))
538
+
539
+ all_boxes.append(boxes_n)
540
+ all_scores.append(scores)
541
+ all_labels.append(labels)
542
+
543
+ # WBF per class
544
+ final_boxes, final_scores, final_labels = [], [], []
545
+ for cls_id in range(N_CLASSES):
546
+ cb = [[b for b,l in zip(mb,ml) if l==cls_id] for mb,ml in zip(all_boxes,all_labels)]
547
+ cs = [[s for s,l in zip(ms,ml) if l==cls_id] for ms,ml in zip(all_scores,all_labels)]
548
+ if all(len(b)==0 for b in cb): continue
549
+ cl = [[cls_id]*len(s) for s in cs]
550
+ b_f,s_f,l_f = weighted_boxes_fusion(
551
+ cb, cs, cl,
552
+ weights=[1.0]*len(RESOLUTIONS),
553
+ iou_thr=WBF_IOU_THR, skip_box_thr=WBF_SKIP_THR,
554
+ )
555
+ final_boxes.extend(b_f.tolist())
556
+ final_scores.extend(s_f.tolist())
557
+ final_labels.extend([int(x) for x in l_f])
558
+
559
+ # Apply per-class conf threshold after WBF (using the robust loop)
560
+ thresholds = [best_cc, best_hc]
561
+ fb, fs, fl = [], [], []
562
+ for b, s, l in zip(final_boxes, final_scores, final_labels):
563
+ if s >= thresholds[l]:
564
+ fb.append(b)
565
+ fs.append(s)
566
+ fl.append(l)
567
+
568
+ multires_preds[img_id] = {
569
+ 'boxes': list(fb),
570
+ 'scores': list(fs),
571
+ 'labels': list(fl),
572
+ }
573
+
574
+ map50_mr, aps_mr = compute_map50_from_preds(multires_preds)
575
+ log_result(f'Multi-res WBF ({RESOLUTIONS}px)', map50_mr, aps_mr)
576
+ print('Lever 3 done βœ“')
577
+
578
+
579
+ # ==============================================================================
580
+ # CELL 12 β€” SAHI Sliced Inference
581
+ # ==============================================================================
582
+ # Akyon et al. (2022), arXiv:2202.06934 β€” published AP gain: +5–7% on
583
+ # aerial small-object datasets. Zero additional training required.
584
+ #
585
+ # Mechanism:
586
+ # 1. Divide each test image into overlapping NxM slices
587
+ # (e.g., 320Γ—320 px with 40% overlap β†’ ~9 slices per image)
588
+ # 2. Run model independently on each slice
589
+ # 3. Map bounding boxes back to original image coordinates
590
+ # 4. Also run once on the FULL image (to catch large-context detections)
591
+ # 5. Merge all boxes with WBF
592
+ #
593
+ # For our dataset: thermal images contain hotspots that can occupy as few as
594
+ # 16Γ—16 px in the original 640-res context. Inside a 320-tile, that same
595
+ # hotspot occupies 64Γ—64 px β€” well within P3 head's optimal range.
596
+ #
597
+ # We implement SAHI natively (no dependency on the sahi package) for full
598
+ # control over the tile→original coordinate transform and WBF fusion.
599
+ # ==============================================================================
600
+ from ensemble_boxes import weighted_boxes_fusion
601
+
602
+ # ── SAHI hyperparameters ──────────────────────────────────────────────────────
603
+ # Tile size: 320px β€” large enough for the model to resolve edges,
604
+ # small enough to magnify hotspot pixel coverage.
605
+ # Overlap: 0.4 β€” ensures objects at tile boundaries appear fully in β‰₯1 tile.
606
+ # We also run inference on the full image to preserve large-context detections.
607
+
608
+ SAHI_TILE_SIZE = 320 # tile width = tile height
609
+ SAHI_OVERLAP_RATIO = 0.4 # overlap between adjacent tiles
610
+ SAHI_IMGSZ = 640 # model input resolution for each tile
611
+ SAHI_CONF = 0.01 # very permissive β€” WBF filters noise
612
+ SAHI_NMS_PASS = 0.99
613
+ SAHI_WBF_IOU = 0.45
614
+ SAHI_WBF_SKIP = 0.001
615
+ FULL_IMG_WEIGHT = 1.5 # give full-image predictions slightly more weight
616
+ TILE_WEIGHT = 1.0
617
+
618
+
619
+ def generate_tiles(H, W, tile_size, overlap_ratio):
620
+ """Yield (x1, y1, x2, y2) pixel coords for each tile over image HΓ—W."""
621
+ stride = int(tile_size * (1 - overlap_ratio))
622
+ tiles = []
623
+ y = 0
624
+ while y < H:
625
+ x = 0
626
+ while x < W:
627
+ x2 = min(x + tile_size, W)
628
+ y2 = min(y + tile_size, H)
629
+ x1 = max(0, x2 - tile_size)
630
+ y1 = max(0, y2 - tile_size)
631
+ tiles.append((x1, y1, x2, y2))
632
+ if x2 == W: break
633
+ x += stride
634
+ if y2 == H: break
635
+ y += stride
636
+ return tiles
637
+
638
+
639
+ def sahi_predict_image(model, img_path, tile_size, overlap_ratio,
640
+ model_imgsz, conf, nms_pass_iou,
641
+ wbf_iou, wbf_skip,
642
+ full_img_weight=1.5, tile_weight=1.0,
643
+ device=0):
644
+ """
645
+ Run SAHI on a single image. Returns normalised boxes, scores, labels.
646
+ """
647
+ img = cv2.imread(str(img_path))
648
+ H, W = img.shape[:2]
649
+ tiles = generate_tiles(H, W, tile_size, overlap_ratio)
650
+
651
+ all_boxes, all_scores, all_labels, all_weights = [], [], [], []
652
+
653
+ # ── Full image inference ──────────────────────────────────────────────────
654
+ res_full = model.predict(img_path, imgsz=model_imgsz,
655
+ conf=conf, iou=nms_pass_iou,
656
+ verbose=False, save=False, device=device)
657
+ rf = res_full[0]
658
+ full_boxes, full_scores, full_labels = [], [], []
659
+ if len(rf.boxes):
660
+ for box in rf.boxes:
661
+ x1,y1,x2,y2 = box.xyxy[0].cpu().tolist()
662
+ full_boxes.append([x1/W, y1/H, x2/W, y2/H])
663
+ full_scores.append(float(box.conf[0]))
664
+
665
+ # --- LABEL FLIP FIX APPLIED HERE (Full Image) ---
666
+ full_labels.append(1 - int(box.cls[0]))
667
+
668
+ all_boxes.append(full_boxes)
669
+ all_scores.append(full_scores)
670
+ all_labels.append(full_labels)
671
+ all_weights.append(full_img_weight)
672
+
673
+ # ── Tile inference ────────────────────────────────────────────────────────
674
+ for (tx1, ty1, tx2, ty2) in tiles:
675
+ tile_img = img[ty1:ty2, tx1:tx2]
676
+ tH, tW = tile_img.shape[:2]
677
+ if tH < 8 or tW < 8: continue
678
+
679
+ # Run model on tile (in-memory, no disk write)
680
+ res_tile = model.predict(tile_img, imgsz=model_imgsz,
681
+ conf=conf, iou=nms_pass_iou,
682
+ verbose=False, save=False, device=device)
683
+ rt = res_tile[0]
684
+ tile_boxes, tile_scores, tile_labels = [], [], []
685
+ if len(rt.boxes):
686
+ for box in rt.boxes:
687
+ # box coords are relative to tile β€” map back to full image
688
+ bx1,by1,bx2,by2 = box.xyxy[0].cpu().tolist()
689
+ # scale from tile-model-imgsz back to tile pixel coords
690
+ scale_x = tW / model_imgsz; scale_y = tH / model_imgsz
691
+ # tile pixel coords β†’ full image pixel coords β†’ normalise
692
+ abs_x1 = (bx1 * scale_x + tx1) / W
693
+ abs_y1 = (by1 * scale_y + ty1) / H
694
+ abs_x2 = (bx2 * scale_x + tx1) / W
695
+ abs_y2 = (by2 * scale_y + ty1) / H
696
+ tile_boxes.append([
697
+ max(0, abs_x1), max(0, abs_y1),
698
+ min(1, abs_x2), min(1, abs_y2)
699
+ ])
700
+ tile_scores.append(float(box.conf[0]))
701
+
702
+ # --- LABEL FLIP FIX APPLIED HERE (Tiles) ---
703
+ tile_labels.append(1 - int(box.cls[0]))
704
+
705
+ all_boxes.append(tile_boxes)
706
+ all_scores.append(tile_scores)
707
+ all_labels.append(tile_labels)
708
+ all_weights.append(tile_weight)
709
+
710
+ # ── WBF fusion across all sources ─────────────────────────────────────────
711
+ final_boxes, final_scores, final_labels = [], [], []
712
+ for cls_id in range(N_CLASSES):
713
+ cb = [[b for b,l in zip(mb,ml) if l==cls_id]
714
+ for mb,ml in zip(all_boxes, all_labels)]
715
+ cs = [[s for s,l in zip(ms,ml) if l==cls_id]
716
+ for ms,ml in zip(all_scores, all_labels)]
717
+ if all(len(b)==0 for b in cb): continue
718
+ b_f,s_f,l_f = weighted_boxes_fusion(
719
+ cb, cs, [[cls_id]*len(s) for s in cs],
720
+ weights=all_weights,
721
+ iou_thr=wbf_iou, skip_box_thr=wbf_skip,
722
+ )
723
+ final_boxes.extend(b_f.tolist())
724
+ final_scores.extend(s_f.tolist())
725
+ final_labels.extend([int(x) for x in l_f])
726
+
727
+ return final_boxes, final_scores, final_labels
728
+
729
+
730
+ # ── Grid search: tile size Γ— overlap ─────────────────────────────────────────
731
+ TILE_SIZES = [256, 320, 384]
732
+ OVERLAP_RATIOS = [0.30, 0.40, 0.50]
733
+
734
+ best_sahi_map50 = 0
735
+ best_tile, best_overlap = 320, 0.40
736
+ best_sahi_preds, best_sahi_aps = {}, {}
737
+
738
+ print('SAHI tile Γ— overlap grid search...')
739
+ print(f'{"tile":>6} {"overlap":>7} {"mAP50":>7} {"Crack":>7} {"Hotspot":>9} tiles/img')
740
+ print('-'*56)
741
+
742
+ for ts in TILE_SIZES:
743
+ for ov in OVERLAP_RATIOS:
744
+ sahi_preds = {}
745
+ n_tiles_total = 0
746
+ for img_path in IMG_PATHS:
747
+ img_id = img_path.stem
748
+ img = cv2.imread(str(img_path))
749
+ H, W = img.shape[:2]
750
+ n_tiles_total += len(generate_tiles(H, W, ts, ov)) + 1 # +1 full img
751
+
752
+ fb, fs, fl = sahi_predict_image(
753
+ model, img_path, ts, ov,
754
+ SAHI_IMGSZ, SAHI_CONF, SAHI_NMS_PASS,
755
+ SAHI_WBF_IOU, SAHI_WBF_SKIP,
756
+ FULL_IMG_WEIGHT, TILE_WEIGHT, DEVICE
757
+ )
758
+ # Apply per-class threshold after SAHI fusion
759
+ thresholds = [best_cc, best_hc]
760
+ pfb,pfs,pfl = [],[],[]
761
+ for b,s,l in zip(fb,fs,fl):
762
+ if s >= thresholds[l]: pfb.append(b); pfs.append(s); pfl.append(l)
763
+ sahi_preds[img_id] = {'boxes': pfb, 'scores': pfs, 'labels': pfl}
764
+
765
+ avg_tiles = n_tiles_total / len(IMG_PATHS)
766
+ map50, aps = compute_map50_from_preds(sahi_preds)
767
+ flag = 'β˜…' if map50 > best_sahi_map50 else ' '
768
+ print(f'{flag}{ts:>5} {ov:>7.2f} {map50:>7.4f} '
769
+ f'{aps.get(0,0):>7.4f} {aps.get(1,0):>9.4f} {avg_tiles:>6.1f}')
770
+ if map50 > best_sahi_map50:
771
+ best_sahi_map50 = map50; best_tile = ts; best_overlap = ov
772
+ best_sahi_preds = sahi_preds; best_sahi_aps = aps
773
+
774
+ print(f'\nβ˜… Best SAHI: tile={best_tile} overlap={best_overlap} '
775
+ f'mAP50={best_sahi_map50:.4f}')
776
+ log_result(f'SAHI (tile={best_tile}, ov={best_overlap})', best_sahi_map50, best_sahi_aps)
777
+ print('Lever 4 done βœ“')
778
+
779
+
780
+ # ==============================================================================
781
+ # CELL 13 β€” YAML Path Override
782
+ # ==============================================================================
783
+ # Update YAML_PATH to point to the actual file in your downloaded dataset
784
+ import os
785
+
786
+ # Based on your previous success, DATASET_PATH is likely '/kaggle/working/Thermal-H-C-1'
787
+ # or similar. We use that to find the yaml.
788
+ YAML_PATH = os.path.join(DATASET_PATH, "data.yaml")
789
+
790
+ print(f"Checking for YAML at: {YAML_PATH}")
791
+ if os.path.exists(YAML_PATH):
792
+ print("βœ… Found it! You're ready to run Cell 8.")
793
+ else:
794
+ print("❌ Still not found. Check if DATASET_PATH is correct in Cell 2.")
795
+
796
+
797
+ # ==============================================================================
798
+ # CELL 14 β€” Grand Stack: IEEE Final Peak
799
+ # ==============================================================================
800
+ import os, cv2
801
+ from ensemble_boxes import weighted_boxes_fusion
802
+
803
+ # Use the champion checkpoint
804
+ BEST_CKPT = '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_finetune_best.pt'
805
+ model = YOLO(BEST_CKPT)
806
+
807
+ # The resolution pair that shattered the baseline
808
+ RESOLUTIONS = [640, 736]
809
+
810
+ GRAND_WBF_IOU = 0.45
811
+ GRAND_WBF_SKIP = 0.001
812
+ final_preds = {}
813
+
814
+ print(f'Final Recovery Run: Fusing {RESOLUTIONS}px with high-overlap raw data...')
815
+
816
+ for img_path in IMG_PATHS:
817
+ img_id = img_path.stem
818
+ img = cv2.imread(str(img_path))
819
+ H, W = img.shape[:2]
820
+
821
+ grand_boxes, grand_scores, grand_labels = [], [], []
822
+
823
+ # 1. Multi-Res Inference with RAW data preservation (iou=0.99)
824
+ for imgsz in RESOLUTIONS:
825
+ # CRITICAL: iou=0.99 prevents YOLO from killing boxes before WBF can fuse them
826
+ res = model.predict(img_path, imgsz=imgsz, conf=0.01, iou=0.99,
827
+ verbose=False, device=DEVICE)
828
+ r = res[0]
829
+ b_res, s_res, l_res = [], [], []
830
+
831
+ if len(r.boxes):
832
+ for box in r.boxes:
833
+ x1,y1,x2,y2 = box.xyxy[0].cpu().tolist()
834
+ b_res.append([max(0,x1/W), max(0,y1/H), min(1,x2/W), min(1,y2/H)])
835
+ s_res.append(float(box.conf[0]))
836
+ l_res.append(1 - int(box.cls[0])) # Flip Model -> Dataset labels
837
+
838
+ grand_boxes.append(b_res)
839
+ grand_scores.append(s_res)
840
+ grand_labels.append(l_res)
841
+
842
+ # 2. WBF Fusion (Equal Weights)
843
+ f_boxes, f_scores, f_labels = [], [], []
844
+ for cls_id in range(N_CLASSES):
845
+ cb = [[b for b,l in zip(mb,ml) if l==cls_id] for mb,ml in zip(grand_boxes, grand_labels)]
846
+ cs = [[s for s,l in zip(ms,ml) if l==cls_id] for ms,ml in zip(grand_scores, grand_labels)]
847
+
848
+ if all(len(b)==0 for b in cb): continue
849
+
850
+ b_f, s_f, l_f = weighted_boxes_fusion(
851
+ cb, cs, [[cls_id]*len(s) for s in cs],
852
+ weights=[1.0] * len(RESOLUTIONS),
853
+ iou_thr=GRAND_WBF_IOU,
854
+ skip_box_thr=GRAND_WBF_SKIP
855
+ )
856
+ f_boxes.extend(b_f.tolist())
857
+ f_scores.extend(s_f.tolist())
858
+ f_labels.extend([int(x) for x in l_f])
859
+
860
+ # 3. The "Lever 3" Threshold Mapping
861
+ # Index 0 (Hotspot) -> 0.05 | Index 1 (Crack) -> 0.01
862
+ final_b, final_s, final_l = [], [], []
863
+ thresholds = [best_cc, best_hc]
864
+
865
+ for b, s, l in zip(f_boxes, f_scores, f_labels):
866
+ if s >= thresholds[l]:
867
+ final_b.append(b)
868
+ final_s.append(s)
869
+ final_l.append(l)
870
+
871
+ final_preds[img_id] = {'boxes': final_b, 'scores': final_s, 'labels': final_l}
872
+
873
+ # 4. Final Computation
874
+ g_map, g_aps = compute_map50_from_preds(final_preds)
875
+
876
+ print('\n' + '═'*65)
877
+ print(f' RESTORED IEEE PEAK β€” RESULTS')
878
+ print('─'*65)
879
+ print(f' mAP@0.5 = {g_map:.4f}')
880
+ for c, name in enumerate(CLASS_NAMES):
881
+ print(f' {name:<12} AP@0.5 = {g_aps.get(c,0):.4f}')
882
+ print(f' vs Baseline (90.92%) = {0.9092:.4f} Ξ” = {g_map-0.9092:>+.4f}')
883
+ print(f' vs Target Peak (91.51%) = {0.9151:.4f} Ξ” = {g_map-0.9151:>+.4f}')
884
+ print('═'*65)
885
+
886
+
887
+ # ==============================================================================
888
+ # CELL 15 β€” Progress Chart & Summary Table
889
+ # ==============================================================================
890
+ import matplotlib.pyplot as plt
891
+ import matplotlib.patches as mpatches
892
+ import numpy as np
893
+
894
+ names = [r['name'] for r in results_log]
895
+ map50s = [r['map50'] for r in results_log]
896
+
897
+ # Prepend baseline from previous notebook
898
+ names = ['Prev Baseline\n(WBF ensemble)'] + names
899
+ map50s = [BASELINE_MAP50] + map50s
900
+
901
+ palette = ['#555555'] + [
902
+ '#4A90D9', # per-class thresh
903
+ '#50B86C', # soft-NMS
904
+ '#E07B39', # multi-res
905
+ '#9B59B6', # SAHI
906
+ '#C0392B', # grand stack
907
+ ]
908
+ palette = palette[:len(names)]
909
+
910
+ fig, axes = plt.subplots(1, 2, figsize=(16, 6))
911
+
912
+ # ── Bar chart ─────────────────────────────────────────────────────────────────
913
+ ax = axes[0]
914
+ bars = ax.bar(range(len(names)), [v*100 for v in map50s],
915
+ color=palette, edgecolor='white', linewidth=1.2, width=0.55)
916
+ ax.axhline(92.0, color='red', ls='--', lw=1.5, label='92% target')
917
+ ax.axhline(90.92, color='gray', ls=':', lw=1.2, label='Prev WBF 90.92%')
918
+ for bar, v in zip(bars, map50s):
919
+ ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.05,
920
+ f'{v*100:.2f}%', ha='center', va='bottom', fontsize=9, fontweight='bold')
921
+ ax.set_xticks(range(len(names))); ax.set_xticklabels(names, fontsize=8)
922
+ ax.set_ylim(88, 100); ax.set_ylabel('mAP@0.5 (%)', fontsize=11)
923
+ ax.set_title('FADNet β€” Advanced Inference Push\n(All techniques inference-only)', fontsize=11)
924
+ ax.legend(fontsize=9); ax.grid(axis='y', alpha=0.3)
925
+ ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)
926
+
927
+ # ── Per-class breakdown ───────────────────────────────────────────────────────
928
+ ax2 = axes[1]
929
+ x = np.arange(len(results_log))
930
+ crack_aps = [r['ap'].get(0,0)*100 for r in results_log]
931
+ hotspot_aps = [r['ap'].get(1,0)*100 for r in results_log]
932
+ short_names = [r['name'].split('(')[0].strip()[:18] for r in results_log]
933
+ w = 0.35
934
+ ax2.bar(x-w/2, crack_aps, w, color='steelblue', label='Crack', alpha=0.85)
935
+ ax2.bar(x+w/2, hotspot_aps, w, color='tomato', label='Hotspot', alpha=0.85)
936
+ ax2.axhline(90.92, color='gray', ls=':', lw=1.2)
937
+ ax2.set_xticks(x); ax2.set_xticklabels(short_names, fontsize=8, rotation=15, ha='right')
938
+ ax2.set_ylim(80, 100); ax2.set_ylabel('AP@0.5 (%)')
939
+ ax2.set_title('Per-class AP breakdown across all techniques')
940
+ ax2.legend(); ax2.grid(axis='y', alpha=0.3)
941
+ ax2.spines['top'].set_visible(False); ax2.spines['right'].set_visible(False)
942
+
943
+ plt.tight_layout()
944
+ plt.savefig('/kaggle/working/fadnet_advanced_push.png', dpi=150)
945
+ plt.show()
946
+
947
+ # ── Summary table ─────────────────────────────────────────────────────────────
948
+ print()
949
+ print('╔══════════════════════════════════════════════════════════════════════╗')
950
+ print('β•‘ FADNet Advanced Inference β€” Complete Results β•‘')
951
+ print('╠══════════════════════════════════════════════════════════════════════╣')
952
+ print(f'β•‘ {"Technique":<32} {"mAP50":>7} {"Crack":>7} {"Hotspot":>8} {"Ξ”":>6} β•‘')
953
+ print('╠══════════════════════════════════════════════════════════════════════╣')
954
+ print(f'β•‘ {"[Baseline] WBF ensemble":<32} {BASELINE_MAP50:>7.4f} {"β€”":>7} {"β€”":>8} {"":>6} β•‘')
955
+ for r in results_log:
956
+ delta = r['map50'] - BASELINE_MAP50
957
+ name = r['name'][:32]
958
+ crack = r['ap'].get(0,0)
959
+ hspot = r['ap'].get(1,0)
960
+ print(f'β•‘ {name:<32} {r["map50"]:>7.4f} {crack:>7.4f} {hspot:>8.4f} {delta:>+6.4f} β•‘')
961
+ print('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•')
962
+
963
+ print()
964
+ print('Optimal inference recipe:')
965
+ print(f' crack_conf = {best_cc}')
966
+ print(f' hotspot_conf = {best_hc}')
967
+ print(f' soft_nms Οƒ = {best_sigma}')
968
+ print(f' SAHI tile = {best_tile} px, overlap = {best_overlap}')
969
+ print(f' TTA = True')
970
+ print(f' Checkpoints = {len(ALL_CKPTS)}')
971
+ print(f' WBF iou_thr = {GRAND_WBF_IOU}')
972
+
973
+
974
+ # ==============================================================================
975
+ # CELL 16 β€” evaluate_at_threshold Helper
976
+ # ==============================================================================
977
+ # ── Helper: compute P, R, F1, TP, FP, FN at a fixed threshold ────────────────
978
+ def evaluate_at_threshold(preds, gt, conf_thresholds, iou_thr=0.50):
979
+ """
980
+ conf_thresholds: list of length N_CLASSES, e.g. [crack_conf, hotspot_conf]
981
+ Returns per-class (P, R, F1, TP, FP, FN) and mean F1.
982
+ """
983
+ def box_iou(b1, b2):
984
+ xi1=max(b1[0],b2[0]); yi1=max(b1[1],b2[1])
985
+ xi2=min(b1[2],b2[2]); yi2=min(b1[3],b2[3])
986
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
987
+ a1=(b1[2]-b1[0])*(b1[3]-b1[1]); a2=(b2[2]-b2[0])*(b2[3]-b2[1])
988
+ return inter/(a1+a2-inter+1e-9)
989
+
990
+ stats = {c: {'tp':0,'fp':0,'fn':0,'ngt':0} for c in range(N_CLASSES)}
991
+
992
+ for img_id in preds:
993
+ pb = preds[img_id]['boxes']
994
+ ps = preds[img_id]['scores']
995
+ pl = preds[img_id]['labels']
996
+ gb = gt.get(img_id, {}).get('boxes', [])
997
+ gl = gt.get(img_id, {}).get('labels', [])
998
+
999
+ for c in range(N_CLASSES):
1000
+ thr = conf_thresholds[c]
1001
+ gt_c = [b for b,l in zip(gb,gl) if l==c]
1002
+ pr_c = [(b,s) for b,s,l in zip(pb,ps,pl) if l==c and s >= thr]
1003
+ stats[c]['ngt'] += len(gt_c)
1004
+
1005
+ matched_gt = set()
1006
+ tp_img = 0
1007
+ for b,s in sorted(pr_c, key=lambda x:-x[1]):
1008
+ best_iou, best_j = 0, -1
1009
+ for j,g in enumerate(gt_c):
1010
+ if j in matched_gt: continue
1011
+ v = box_iou(b,g)
1012
+ if v > best_iou: best_iou,best_j = v,j
1013
+ if best_iou >= iou_thr and best_j >= 0:
1014
+ tp_img += 1; matched_gt.add(best_j)
1015
+ else:
1016
+ stats[c]['fp'] += 1
1017
+ stats[c]['tp'] += tp_img
1018
+ stats[c]['fn'] += len(gt_c) - tp_img
1019
+
1020
+ results = {}
1021
+ for c in range(N_CLASSES):
1022
+ tp=stats[c]['tp']; fp=stats[c]['fp']; fn=stats[c]['fn']
1023
+ ngt=stats[c]['ngt']
1024
+ P = tp/(tp+fp+1e-9)
1025
+ R = tp/(tp+fn+1e-9)
1026
+ F1 = 2*P*R/(P+R+1e-9)
1027
+ results[c] = {'P':P,'R':R,'F1':F1,'TP':tp,'FP':fp,'FN':fn,'GT':ngt}
1028
+ mean_f1 = sum(v['F1'] for v in results.values()) / N_CLASSES
1029
+ return results, mean_f1
1030
+
1031
+
1032
+ # ==============================================================================
1033
+ # CELL 17 β€” FP Fix A: WBF skip_box_thr Sweep
1034
+ # ==============================================================================
1035
+ # ═════════════════════════════════════════���════════════════════════════
1036
+ # FIX A β€” Re-run grand stack with higher WBF skip_box_thr values
1037
+ # Weak fused boxes (score < skip_thr) are discarded BEFORE they become FPs
1038
+ # ══════════════════════════════════════════════════════════════════════
1039
+ print('Fix A: Testing higher WBF skip_box_thr values...')
1040
+ print(f'{"skip_thr":>9} {"mAP50":>7} {"mean_F1":>8} '
1041
+ f'{"Crack P":>8} {"Crack R":>7} {"Hot P":>7} {"Hot R":>7}')
1042
+ print('-'*70)
1043
+
1044
+ SKIP_GRID = [0.001, 0.01, 0.02, 0.05, 0.08, 0.10, 0.15, 0.20]
1045
+ best_fixA_f1, best_skip = 0, 0.05
1046
+ best_filtered_A = None
1047
+
1048
+ for skip_thr in SKIP_GRID:
1049
+ # Re-filter final_preds by dropping all boxes below skip_thr
1050
+ # (simulates what WBF skip_box_thr would have done at source)
1051
+ filtered = {}
1052
+ for img_id, p in final_preds.items():
1053
+ keep = [(b,s,l) for b,s,l in zip(p['boxes'],p['scores'],p['labels'])
1054
+ if s >= skip_thr]
1055
+ if keep:
1056
+ fb, fs, fl = zip(*keep)
1057
+ filtered[img_id] = {'boxes':list(fb),'scores':list(fs),'labels':list(fl)}
1058
+ else:
1059
+ filtered[img_id] = {'boxes':[],'scores':[],'labels':[]}
1060
+
1061
+ map50, _ = compute_map50_from_preds(filtered)
1062
+ res, mf1 = evaluate_at_threshold(
1063
+ filtered, GT,
1064
+ conf_thresholds=[skip_thr, skip_thr]
1065
+ )
1066
+ flag = 'β˜…' if mf1 > best_fixA_f1 else ' '
1067
+ print(f'{flag}{skip_thr:>8.3f} {map50:>7.4f} {mf1:>8.4f} '
1068
+ f'{res[0]["P"]:>8.4f} {res[0]["R"]:>7.4f} '
1069
+ f'{res[1]["P"]:>7.4f} {res[1]["R"]:>7.4f}')
1070
+ if mf1 > best_fixA_f1:
1071
+ best_fixA_f1 = mf1; best_skip = skip_thr
1072
+ best_filtered_A = filtered
1073
+
1074
+ print(f'\nβ˜… Fix A best skip_thr={best_skip:.3f} mean_F1={best_fixA_f1:.4f}')
1075
+
1076
+
1077
+ # ==============================================================================
1078
+ # CELL 18 β€” FP Fix B: Per-Class F1-Optimal Threshold
1079
+ # ==============================================================================
1080
+ # ══════════════════════════════════════════════════════════════════════
1081
+ # FIX B β€” Per-class F1-optimal threshold (independent for Crack/Hotspot)
1082
+ # ══════════════════════════════════════════════════════════════════════
1083
+ print('Fix B: Per-class F1-optimal threshold sweep...')
1084
+
1085
+ CONF_SWEEP = [0.01, 0.02, 0.04, 0.06, 0.08, 0.10, 0.12, 0.15,
1086
+ 0.18, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50]
1087
+
1088
+ crack_f1_curve = []
1089
+ hotspot_f1_curve = []
1090
+
1091
+ for conf in CONF_SWEEP:
1092
+ # Crack: fix hotspot at best_skip, sweep crack
1093
+ res_c, _ = evaluate_at_threshold(
1094
+ final_preds, GT, conf_thresholds=[conf, best_skip])
1095
+ crack_f1_curve.append(res_c[0]['F1'])
1096
+
1097
+ # Hotspot: fix crack at best_skip, sweep hotspot
1098
+ res_h, _ = evaluate_at_threshold(
1099
+ final_preds, GT, conf_thresholds=[best_skip, conf])
1100
+ hotspot_f1_curve.append(res_h[1]['F1'])
1101
+
1102
+ best_crack_conf = CONF_SWEEP[int(np.argmax(crack_f1_curve))]
1103
+ best_hotspot_conf = CONF_SWEEP[int(np.argmax(hotspot_f1_curve))]
1104
+
1105
+ print(f' F1-optimal crack conf = {best_crack_conf:.2f} '
1106
+ f'(F1={max(crack_f1_curve):.4f})')
1107
+ print(f' F1-optimal hotspot conf = {best_hotspot_conf:.2f} '
1108
+ f'(F1={max(hotspot_f1_curve):.4f})')
1109
+
1110
+
1111
+ # ==============================================================================
1112
+ # CELL 19 β€” Final Eval + Report
1113
+ # ==============================================================================
1114
+ # ── CORRECT: mAP must always be evaluated at conf=0.001 (full PR curve) ──────
1115
+ # Step 1: mAP β€” use the original final_preds built at confβ‰ˆ0 (near-zero)
1116
+ final_map50, final_aps = compute_map50_from_preds(final_preds) # ← full PR curve, NOT filtered
1117
+
1118
+ # Step 2: Precision/Recall/F1/FP/FN β€” at F1-optimal operating threshold
1119
+ final_res, final_mf1 = evaluate_at_threshold(
1120
+ final_preds, GT,
1121
+ conf_thresholds=[best_crack_conf, best_hotspot_conf]
1122
+ )
1123
+
1124
+ # ── Final Report ──────────────────────────────────────────────────────
1125
+ print()
1126
+ print(f' mAP@0.5 = {final_map50:.4f} ← full PR curve (confβ†’0)')
1127
+ print(f' Crack AP@0.5 = {final_aps[0]:.4f}')
1128
+ print(f' Hotspot AP@0.5 = {final_aps[1]:.4f}')
1129
+ print(f' Crack Prec = {final_res[0]["P"]:.4f} ← at conf={best_crack_conf}')
1130
+ print(f' Hotspot Prec = {final_res[1]["P"]:.4f} ← at conf={best_hotspot_conf}')
1131
+ print(f' Crack FP/FN = {final_res[0]["FP"]}/{final_res[0]["FN"]}')
1132
+ print(f' Hotspot FP/FN = {final_res[1]["FP"]}/{final_res[1]["FN"]}')
1133
+
1134
+ print()
1135
+ print('╔════════════════════��═════════════════════════════════════════════════╗')
1136
+ print('β•‘ FADNET FIXED MASTER METRICS (F1-Optimal Operating Point) β•‘')
1137
+ print('╠════════════════╦══════════╦═══════════╦══════════╦════╦════╦════╦═══╣')
1138
+ print('β•‘ Class β•‘ AP@0.5 β•‘ Precision β•‘ Recall β•‘ TP β•‘ FP β•‘ FN β•‘ GTβ•‘')
1139
+ print('╠════════════════╬══════════╬═══════════╬══════════╬════╬════╬════╬═══╣')
1140
+ for c, name in enumerate(CLASS_NAMES):
1141
+ r = final_res[c]
1142
+ ap = final_aps.get(c, 0)
1143
+ print(f'β•‘ {name:<14} β•‘ {ap:.4f} β•‘ {r["P"]:.4f} β•‘ {r["R"]:.4f} '
1144
+ f'β•‘{r["TP"]:>4}β•‘{r["FP"]:>4}β•‘{r["FN"]:>4}β•‘{r["GT"]:>3}β•‘')
1145
+ print('╠════════════════╩══════════╩═══════════╩══════════╩════╩════╩════╩═══╣')
1146
+ print(f'β•‘ Final mAP@0.5 = {final_map50:.4f} mean F1 = {final_mf1:.4f} β•‘')
1147
+ print('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•')
1148
+ print(f'\n Before fix: Crack FP=70, Hotspot FP=143')
1149
+ print(f' After fix: Crack FP={final_res[0]["FP"]}, Hotspot FP={final_res[1]["FP"]}')
1150
+ print(f' FP reduction: Crack {70-final_res[0]["FP"]:+d}, Hotspot {143-final_res[1]["FP"]:+d}')
1151
+
1152
+
1153
+
1154
+ # ==============================================================================
1155
+ # CELL 20 β€” F1 Curve Plots
1156
+ # ==============================================================================
1157
+ # ══════════════════════════════════════════════════════════════════════
1158
+ # F1 curve plots
1159
+ # ══════════════════════════════════════════════════════════════════════
1160
+ fig, axes = plt.subplots(1, 2, figsize=(13, 4))
1161
+
1162
+ for ax, curve, name, best_conf, col in [
1163
+ (axes[0], crack_f1_curve, 'Crack', best_crack_conf, 'steelblue'),
1164
+ (axes[1], hotspot_f1_curve, 'Hotspot', best_hotspot_conf, 'tomato'),
1165
+ ]:
1166
+ ax.plot(CONF_SWEEP, curve, 'o-', color=col, lw=2)
1167
+ ax.axvline(best_conf, color='k', ls='--', lw=1.5,
1168
+ label=f'F1-optimal conf={best_conf:.2f}')
1169
+ ax.set_title(f'{name} β€” F1 vs confidence threshold\n(post-WBF operating point)')
1170
+ ax.set_xlabel('Confidence threshold')
1171
+ ax.set_ylabel('F1 score')
1172
+ ax.legend()
1173
+ ax.grid(True, alpha=0.3)
1174
+ ax.spines['top'].set_visible(False)
1175
+ ax.spines['right'].set_visible(False)
1176
+
1177
+ plt.suptitle('F1-optimal threshold per class β€” fixes FP bleed from SAHI', fontsize=11)
1178
+ plt.tight_layout()
1179
+ plt.savefig('/kaggle/working/f1_optimal_curves.png', dpi=130)
1180
+ plt.show()
1181
+
1182
+ print(f'\nFinal settings for inference / paper reporting:')
1183
+ print(f' crack_conf = {best_crack_conf}')
1184
+ print(f' hotspot_conf = {best_hotspot_conf}')
1185
+ print(f' mAP@0.5 = {final_map50:.4f} (paper metric)')
1186
+ print(f' mean F1 = {final_mf1:.4f} (demo metric)')
1187
+
1188
+
1189
+ #cell 21
1190
+ import zipfile
1191
+ import os
1192
+ from datetime import datetime
1193
+
1194
+ # 1. Define the specific files we created during this session
1195
+ files_to_archive = [
1196
+ # Heatmap from Lever 1
1197
+ '/kaggle/working/perclass_thresh_heatmap.png',
1198
+ # The progress chart from Cell 15
1199
+ '/kaggle/working/fadnet_advanced_push.png',
1200
+ # The F1-Optimal curves from Cell 20
1201
+ '/kaggle/working/f1_optimal_curves.png',
1202
+ # The primary weights used for FADNet
1203
+ '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_finetune_best.pt',
1204
+ # The fixed YAML config
1205
+ '/kaggle/working/Thermal-H&C-1/data.yaml'
1206
+ ]
1207
+
1208
+ # 2. Create a timestamped filename so you don't overwrite previous saves
1209
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M')
1210
+ zip_name = f"FADNet_F1_Optimized_Backup_{timestamp}.zip"
1211
+
1212
+ print(f"πŸ“¦ Starting archive: {zip_name}")
1213
+
1214
+ with zipfile.ZipFile(zip_name, 'w') as archive:
1215
+ for file_path in files_to_archive:
1216
+ if os.path.exists(file_path):
1217
+ # Save the file using just its name, not the full path
1218
+ archive.write(file_path, arcname=os.path.basename(file_path))
1219
+ print(f" + Added: {os.path.basename(file_path)}")
1220
+ else:
1221
+ print(f" ⚠️ Warning: {os.path.basename(file_path)} not found in path.")
1222
+
1223
+ print(f"\nβœ… All set, Ash! You can find '{zip_name}' in the Kaggle 'Output' sidebar.")
1224
+
1225
+
1226
+ #cell 22
1227
+ from IPython.display import FileLink
1228
+ import os
1229
+
1230
+ # Identify the zip file in the working directory
1231
+ files = [f for f in os.listdir('/kaggle/working/') if f.endswith('.zip')]
1232
+
1233
+ if files:
1234
+ # Generates a clickable link for the most recent zip
1235
+ display(FileLink(files[-1]))
1236
+ else:
1237
+ print("No zip file found in /kaggle/working/")
1238
+
1239
+
1240
+ # ==============================================================================
1241
+ # CELL 23 β€” FADNet Complete Metrics Dashboard
1242
+ # ==============================================================================
1243
+ import matplotlib.pyplot as plt
1244
+ import matplotlib.patches as mpatches
1245
+ import matplotlib.gridspec as gridspec
1246
+ import numpy as np, cv2, math
1247
+ from collections import defaultdict
1248
+
1249
+ # ── Rebuild PR curves from final_preds + GT ────────────────────────────────
1250
+ def pr_curve_from_preds(preds, gt, cls_id, n_points=200):
1251
+ """Sweep confidence threshold β†’ (precision, recall) pairs."""
1252
+ all_scores, all_tp = [], []
1253
+ n_gt = sum(1 for v in gt.values() for l in v["labels"] if l == cls_id)
1254
+ for img_id, p in preds.items():
1255
+ g = gt.get(img_id, {"boxes": [], "labels": []})
1256
+ gt_boxes = [b for b, l in zip(g["boxes"], g["labels"]) if l == cls_id]
1257
+ matched = set()
1258
+ det = sorted(
1259
+ [(b, s) for b, s, l in zip(p["boxes"], p["scores"], p["labels"]) if l == cls_id],
1260
+ key=lambda x: -x[1]
1261
+ )
1262
+ for box, score in det:
1263
+ best_iou, best_j = 0, -1
1264
+ for j, gb in enumerate(gt_boxes):
1265
+ if j in matched: continue
1266
+ xi1=max(box[0],gb[0]); yi1=max(box[1],gb[1])
1267
+ xi2=min(box[2],gb[2]); yi2=min(box[3],gb[3])
1268
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
1269
+ u=(box[2]-box[0])*(box[3]-box[1])+(gb[2]-gb[0])*(gb[3]-gb[1])-inter
1270
+ iou=inter/u if u>0 else 0
1271
+ if iou>best_iou: best_iou,best_j=iou,j
1272
+ tp = 1 if best_iou>=0.50 and best_j>=0 else 0
1273
+ if tp: matched.add(best_j)
1274
+ all_scores.append(score); all_tp.append(tp)
1275
+
1276
+ if not all_scores or n_gt == 0:
1277
+ return [0,1],[1,0]
1278
+ idx = np.argsort(-np.array(all_scores))
1279
+ tp_c = np.cumsum(np.array(all_tp)[idx])
1280
+ fp_c = np.cumsum(1 - np.array(all_tp)[idx])
1281
+ prec = tp_c / (tp_c + fp_c + 1e-9)
1282
+ rec = tp_c / (n_gt + 1e-9)
1283
+ return rec.tolist(), prec.tolist()
1284
+
1285
+ # ── F1 curve helper ────────────────────────────────────────────────────────
1286
+ def f1_curve_for_class(preds, gt, cls_id, thresholds=None):
1287
+ if thresholds is None:
1288
+ thresholds = np.linspace(0.01, 0.99, 80)
1289
+ f1s = []
1290
+ for thr in thresholds:
1291
+ tp=fp=fn=0
1292
+ for img_id, p in preds.items():
1293
+ g = gt.get(img_id, {"boxes":[],"labels":[]})
1294
+ gt_boxes=[b for b,l in zip(g["boxes"],g["labels"]) if l==cls_id]
1295
+ det=sorted([(b,s) for b,s,l in zip(p["boxes"],p["scores"],p["labels"]) if l==cls_id and s>=thr],key=lambda x:-x[1])
1296
+ matched=set()
1297
+ for box,_ in det:
1298
+ best_iou,best_j=0,-1
1299
+ for j,gb in enumerate(gt_boxes):
1300
+ if j in matched: continue
1301
+ xi1=max(box[0],gb[0]);yi1=max(box[1],gb[1])
1302
+ xi2=min(box[2],gb[2]);yi2=min(box[3],gb[3])
1303
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
1304
+ u=(box[2]-box[0])*(box[3]-box[1])+(gb[2]-gb[0])*(gb[3]-gb[1])-inter
1305
+ iou=inter/u if u>0 else 0
1306
+ if iou>best_iou:best_iou,best_j=iou,j
1307
+ if best_iou>=0.50 and best_j>=0: tp+=1; matched.add(best_j)
1308
+ else: fp+=1
1309
+ fn+=len(gt_boxes)-len(matched)
1310
+ prec=tp/(tp+fp+1e-9); rec=tp/(tp+fn+1e-9)
1311
+ f1s.append(2*prec*rec/(prec+rec+1e-9))
1312
+ return thresholds, np.array(f1s)
1313
+
1314
+ # ── Class names ────────────────────────────────────────────────────────────
1315
+ CLASS_NAMES = ["Hotspot", "Crack"]
1316
+ CLASS_COLORS = ["tomato", "steelblue"]
1317
+
1318
+ fig = plt.figure(figsize=(20, 22))
1319
+ fig.patch.set_facecolor("#0f0f0f")
1320
+ gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.45, wspace=0.38)
1321
+
1322
+ # ── 1. Technique comparison (top-left, span 2 cols) ────────────────────────
1323
+ ax1 = fig.add_subplot(gs[0, :2])
1324
+ tech_names = [
1325
+ "Baseline\n(WBF)",
1326
+ "Per-class\nThreshold",
1327
+ "Per-class\n+ Soft-NMS",
1328
+ "Multi-res WBF\n[640,736]px",
1329
+ "SAHI\n(tile=384)",
1330
+ ]
1331
+ map50s = [0.9092, 0.9040, 0.9060, 0.9151, 0.8292]
1332
+ colors = ["#555", "#4A90D9", "#50B86C", "#E8A838", "#B85C5C"]
1333
+ bars = ax1.bar(tech_names, map50s, color=colors, width=0.55, zorder=3)
1334
+ ax1.axhline(0.9092, color="white", linestyle="--", lw=1.2, alpha=0.5, label="Baseline 90.92%")
1335
+ ax1.set_ylim(0.78, 0.94)
1336
+ ax1.set_facecolor("#1a1a1a"); ax1.tick_params(colors="white")
1337
+ for spine in ax1.spines.values(): spine.set_edgecolor("#444")
1338
+ ax1.set_title("Technique Comparison β€” mAP@0.5 (test set)", color="white", fontsize=13, pad=10)
1339
+ ax1.set_ylabel("mAP@0.5", color="white")
1340
+ ax1.yaxis.label.set_color("white")
1341
+ ax1.grid(axis="y", color="#333", zorder=0)
1342
+ for bar, val in zip(bars, map50s):
1343
+ delta = val - 0.9092
1344
+ lbl = f"{val:.4f}\\n({delta:+.4f})"
1345
+ ax1.text(bar.get_x()+bar.get_width()/2, val+0.001, lbl,
1346
+ ha="center", va="bottom", color="white", fontsize=9, fontweight="bold")
1347
+
1348
+ # ── 2. Per-class AP bar (top-right) ────────────────────────────────────────
1349
+ ax2 = fig.add_subplot(gs[0, 2])
1350
+ class_aps = [0.9415, 0.8886] # Hotspot, Crack
1351
+ xpos = np.arange(2)
1352
+ b2 = ax2.bar(xpos, class_aps, color=CLASS_COLORS, width=0.5, zorder=3)
1353
+ ax2.set_xticks(xpos); ax2.set_xticklabels(CLASS_NAMES, color="white")
1354
+ ax2.set_ylim(0.82, 0.97)
1355
+ ax2.set_facecolor("#1a1a1a"); ax2.tick_params(colors="white")
1356
+ for spine in ax2.spines.values(): spine.set_edgecolor("#444")
1357
+ ax2.set_title("Per-Class AP@0.5\n(Multi-res WBF, best config)", color="white", fontsize=12, pad=10)
1358
+ ax2.set_ylabel("AP@0.5", color="white"); ax2.grid(axis="y", color="#333", zorder=0)
1359
+ for bar, val in zip(b2, class_aps):
1360
+ ax2.text(bar.get_x()+bar.get_width()/2, val+0.001, f"{val:.4f}",
1361
+ ha="center", va="bottom", color="white", fontsize=11, fontweight="bold")
1362
+
1363
+ # ── 3. PR Curve β€” Hotspot (middle-left) ───────────────────────────────────
1364
+ ax3 = fig.add_subplot(gs[1, 0])
1365
+ rec_h, prec_h = pr_curve_from_preds(final_preds, GT, cls_id=0)
1366
+ ax3.plot(rec_h, prec_h, color="tomato", lw=2)
1367
+ ax3.fill_between(rec_h, prec_h, alpha=0.15, color="tomato")
1368
+ ax3.set_facecolor("#1a1a1a"); ax3.tick_params(colors="white")
1369
+ for spine in ax3.spines.values(): spine.set_edgecolor("#444")
1370
+ ax3.set_title(f"PR Curve β€” Hotspot (AP={0.9415:.4f})", color="white", fontsize=11)
1371
+ ax3.set_xlabel("Recall", color="white"); ax3.set_ylabel("Precision", color="white")
1372
+ ax3.set_xlim(0,1); ax3.set_ylim(0,1.05)
1373
+ ax3.grid(color="#333"); ax3.axhline(0.9322, color="white", lw=0.8, linestyle=":", alpha=0.6)
1374
+ ax3.axvline(0.8462, color="white", lw=0.8, linestyle=":", alpha=0.6)
1375
+ ax3.annotate("F1-opt\\n(0.93P, 0.85R)", xy=(0.8462, 0.9322),
1376
+ color="white", fontsize=8, xytext=(0.5, 0.5),
1377
+ arrowprops=dict(arrowstyle="->", color="white", lw=0.8))
1378
+
1379
+ # ── 4. PR Curve β€” Crack (middle-center) ───────────────────────────────────
1380
+ ax4 = fig.add_subplot(gs[1, 1])
1381
+ rec_c, prec_c = pr_curve_from_preds(final_preds, GT, cls_id=1)
1382
+ ax4.plot(rec_c, prec_c, color="steelblue", lw=2)
1383
+ ax4.fill_between(rec_c, prec_c, alpha=0.15, color="steelblue")
1384
+ ax4.set_facecolor("#1a1a1a"); ax4.tick_params(colors="white")
1385
+ for spine in ax4.spines.values(): spine.set_edgecolor("#444")
1386
+ ax4.set_title(f"PR Curve β€” Crack (AP={0.8886:.4f})", color="white", fontsize=11)
1387
+ ax4.set_xlabel("Recall", color="white"); ax4.set_ylabel("Precision", color="white")
1388
+ ax4.set_xlim(0,1); ax4.set_ylim(0,1.05)
1389
+ ax4.grid(color="#333"); ax4.axhline(0.9036, color="white", lw=0.8, linestyle=":", alpha=0.6)
1390
+ ax4.axvline(0.8427, color="white", lw=0.8, linestyle=":", alpha=0.6)
1391
+ ax4.annotate("F1-opt\\n(0.90P, 0.84R)", xy=(0.8427, 0.9036),
1392
+ color="white", fontsize=8, xytext=(0.45, 0.45),
1393
+ arrowprops=dict(arrowstyle="->", color="white", lw=0.8))
1394
+
1395
+ # ── 5. WBF skip_thr sweep (middle-right) ─────────────────────────────────
1396
+ ax5 = fig.add_subplot(gs[1, 2])
1397
+ skip_thrs = [0.001, 0.010, 0.020, 0.050, 0.080, 0.100, 0.150, 0.200]
1398
+ map50_vals = [0.9151, 0.9151, 0.9136, 0.9085, 0.9010, 0.8969, 0.8645, 0.8315]
1399
+ f1_vals = [0.7001, 0.7001, 0.7182, 0.7437, 0.8191, 0.8424, 0.8700, 0.8796]
1400
+ ax5.plot(skip_thrs, map50_vals, "o-", color="#E8A838", lw=2, label="mAP@0.5")
1401
+ ax5_r = ax5.twinx()
1402
+ ax5_r.plot(skip_thrs, f1_vals, "s--", color="#50B86C", lw=2, label="mean F1")
1403
+ ax5_r.tick_params(colors="white"); ax5_r.yaxis.label.set_color("white")
1404
+ ax5_r.set_ylabel("mean F1", color="white")
1405
+ for spine in ax5_r.spines.values(): spine.set_edgecolor("#444")
1406
+ ax5.set_facecolor("#1a1a1a"); ax5.tick_params(colors="white")
1407
+ for spine in ax5.spines.values(): spine.set_edgecolor("#444")
1408
+ ax5.set_title("WBF skip_thr: mAP vs F1 tradeoff", color="white", fontsize=11)
1409
+ ax5.set_xlabel("skip_box_thr", color="white"); ax5.set_ylabel("mAP@0.5", color="white")
1410
+ ax5.grid(color="#333")
1411
+ ax5.axvline(0.200, color="white", lw=0.8, linestyle=":", alpha=0.5)
1412
+ lines1, labels1 = ax5.get_legend_handles_labels()
1413
+ lines2, labels2 = ax5_r.get_legend_handles_labels()
1414
+ ax5.legend(lines1+lines2, labels1+labels2, facecolor="#1a1a1a",
1415
+ labelcolor="white", fontsize=8, loc="lower left")
1416
+
1417
+ # ── 6. F1 Curve β€” Hotspot (bottom-left) ──────────────────────────��────────
1418
+ ax6 = fig.add_subplot(gs[2, 0])
1419
+ thrs, f1_h = f1_curve_for_class(final_preds, GT, cls_id=0)
1420
+ ax6.plot(thrs, f1_h, color="tomato", lw=2)
1421
+ best_idx = np.argmax(f1_h)
1422
+ ax6.axvline(thrs[best_idx], color="white", lw=1, linestyle="--", alpha=0.7)
1423
+ ax6.scatter([thrs[best_idx]], [f1_h[best_idx]], color="white", zorder=5, s=60)
1424
+ ax6.text(thrs[best_idx]+0.01, f1_h[best_idx]-0.03,
1425
+ f"best={thrs[best_idx]:.2f}\\nF1={f1_h[best_idx]:.4f}",
1426
+ color="white", fontsize=8)
1427
+ ax6.set_facecolor("#1a1a1a"); ax6.tick_params(colors="white")
1428
+ for spine in ax6.spines.values(): spine.set_edgecolor("#444")
1429
+ ax6.set_title("F1 Curve β€” Hotspot", color="white", fontsize=11)
1430
+ ax6.set_xlabel("Confidence Threshold", color="white"); ax6.set_ylabel("F1 Score", color="white")
1431
+ ax6.set_xlim(0,1); ax6.grid(color="#333")
1432
+
1433
+ # ── 7. F1 Curve β€” Crack (bottom-center) ───────────────────────────────────
1434
+ ax7 = fig.add_subplot(gs[2, 1])
1435
+ thrs_c, f1_c = f1_curve_for_class(final_preds, GT, cls_id=1)
1436
+ ax7.plot(thrs_c, f1_c, color="steelblue", lw=2)
1437
+ best_idx_c = np.argmax(f1_c)
1438
+ ax7.axvline(thrs_c[best_idx_c], color="white", lw=1, linestyle="--", alpha=0.7)
1439
+ ax7.scatter([thrs_c[best_idx_c]], [f1_c[best_idx_c]], color="white", zorder=5, s=60)
1440
+ ax7.text(thrs_c[best_idx_c]+0.01, f1_c[best_idx_c]-0.03,
1441
+ f"best={thrs_c[best_idx_c]:.2f}\\nF1={f1_c[best_idx_c]:.4f}",
1442
+ color="white", fontsize=8)
1443
+ ax7.set_facecolor("#1a1a1a"); ax7.tick_params(colors="white")
1444
+ for spine in ax7.spines.values(): spine.set_edgecolor("#444")
1445
+ ax7.set_title("F1 Curve β€” Crack", color="white", fontsize=11)
1446
+ ax7.set_xlabel("Confidence Threshold", color="white"); ax7.set_ylabel("F1 Score", color="white")
1447
+ ax7.set_xlim(0,1); ax7.grid(color="#333")
1448
+
1449
+ # ── 8. Soft-NMS Οƒ sweep (bottom-right) ────────────────────────────────────
1450
+ ax8 = fig.add_subplot(gs[2, 2])
1451
+ sigmas = [0.3, 0.4, 0.5, 0.6, 0.7]
1452
+ snms_map = [0.9060, 0.9039, 0.9033, 0.9013, 0.8992]
1453
+ snms_crack = [0.9006, 0.9001, 0.8998, 0.8974, 0.8951]
1454
+ snms_hot = [0.9113, 0.9078, 0.9068, 0.9052, 0.9033]
1455
+ ax8.plot(sigmas, snms_map, "o-", color="white", lw=2, label="mAP@0.5")
1456
+ ax8.plot(sigmas, snms_crack, "s--", color="steelblue", lw=1.5, label="Crack")
1457
+ ax8.plot(sigmas, snms_hot, "^--", color="tomato", lw=1.5, label="Hotspot")
1458
+ ax8.axvline(0.3, color="yellow", lw=0.9, linestyle=":", alpha=0.7)
1459
+ ax8.set_facecolor("#1a1a1a"); ax8.tick_params(colors="white")
1460
+ for spine in ax8.spines.values(): spine.set_edgecolor("#444")
1461
+ ax8.set_title("Soft-NMS Οƒ Sweep", color="white", fontsize=11)
1462
+ ax8.set_xlabel("Οƒ (Gaussian decay)", color="white"); ax8.set_ylabel("mAP@0.5", color="white")
1463
+ ax8.legend(facecolor="#1a1a1a", labelcolor="white", fontsize=8)
1464
+ ax8.grid(color="#333")
1465
+
1466
+ fig.suptitle("FADNet v4 β€” Complete Metrics Dashboard", color="white",
1467
+ fontsize=16, fontweight="bold", y=1.01)
1468
+ plt.savefig("/kaggle/working/fadnet_metrics_dashboard.png", dpi=150,
1469
+ bbox_inches="tight", facecolor="#0f0f0f")
1470
+ plt.show()
1471
+ print("βœ… Saved: fadnet_metrics_dashboard.png")
1472
+
1473
+
1474
+ # ==============================================================================
1475
+ # CELL24 β€” Result Image Grid (GT vs Predicted)
1476
+ # ==============================================================================
1477
+ import cv2, random, math
1478
+ import numpy as np
1479
+ import matplotlib.pyplot as plt
1480
+ import matplotlib.patches as patches
1481
+
1482
+ # ── Config ─────────────────────────────────────────────────────────────────
1483
+ CONF_CRACK = 0.20 # F1-optimal thresholds
1484
+ CONF_HOTSPOT = 0.20
1485
+ CLASS_NAMES = ["Hotspot", "Crack"]
1486
+ GT_COLOR = (0, 255, 0) # green β€” ground truth
1487
+ PRED_COLORS = {0: (255, 80, 80), 1: (80, 160, 255)} # red=hotspot, blue=crack
1488
+ N_SHOW = 12 # images in grid (3 cols Γ— 4 rows)
1489
+
1490
+ # ── Pick a stratified sample: TP-heavy + some hard cases ──────────────────
1491
+ random.seed(42)
1492
+ sample_ids = random.sample([p.stem for p in IMG_PATHS], min(N_SHOW, len(IMG_PATHS)))
1493
+
1494
+ def draw_boxes_on_img(img_bgr, gt_data, pred_data, conf_thrs, img_wh):
1495
+ """Returns annotated RGB copy."""
1496
+ vis = img_bgr.copy()
1497
+ H, W = vis.shape[:2]
1498
+
1499
+ # GT boxes β€” green, dashed style (thick=2)
1500
+ for box, lbl in zip(gt_data["boxes"], gt_data["labels"]):
1501
+ x1,y1,x2,y2 = int(box[0]*W), int(box[1]*H), int(box[2]*W), int(box[3]*H)
1502
+ cv2.rectangle(vis, (x1,y1), (x2,y2), GT_COLOR, 2)
1503
+ cv2.putText(vis, f"GT:{CLASS_NAMES[lbl]}", (x1, max(y1-4,10)),
1504
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, GT_COLOR, 1, cv2.LINE_AA)
1505
+
1506
+ # Predicted boxes β€” per-class colour
1507
+ for box, score, lbl in zip(pred_data["boxes"], pred_data["scores"], pred_data["labels"]):
1508
+ thr = conf_thrs[lbl]
1509
+ if score < thr: continue
1510
+ col = PRED_COLORS[lbl]
1511
+ x1,y1,x2,y2 = int(box[0]*W), int(box[1]*H), int(box[2]*W), int(box[3]*H)
1512
+ cv2.rectangle(vis, (x1,y1), (x2,y2), col, 2)
1513
+ cv2.putText(vis, f"{CLASS_NAMES[lbl]}:{score:.2f}", (x1, min(y2+14, H-2)),
1514
+ cv2.FONT_HERSHEY_SIMPLEX, 0.42, col, 1, cv2.LINE_AA)
1515
+
1516
+ return cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)
1517
+
1518
+ conf_thrs = [CONF_HOTSPOT, CONF_CRACK] # indexed by class id
1519
+
1520
+ ncols = 3
1521
+ nrows = math.ceil(N_SHOW / ncols)
1522
+ fig, axes = plt.subplots(nrows, ncols, figsize=(ncols*5, nrows*4.5))
1523
+ fig.patch.set_facecolor("#0f0f0f")
1524
+ axes = axes.flatten()
1525
+
1526
+ for ax, img_id in zip(axes, sample_ids):
1527
+ img_path = next((p for p in IMG_PATHS if p.stem == img_id), None)
1528
+ if img_path is None: ax.axis("off"); continue
1529
+ img = cv2.imread(str(img_path))
1530
+ if img is None: ax.axis("off"); continue
1531
+
1532
+ gt_d = GT.get(img_id, {"boxes":[], "labels":[]})
1533
+ pred_d = final_preds.get(img_id, {"boxes":[], "scores":[], "labels":[]})
1534
+
1535
+ vis = draw_boxes_on_img(img, gt_d, pred_d, conf_thrs, img.shape[:2])
1536
+
1537
+ # Count TP/FP/FN for title
1538
+ n_gt_boxes = len(gt_d["boxes"])
1539
+ n_pred = sum(1 for s,l in zip(pred_d["scores"], pred_d["labels"]) if s >= conf_thrs[l])
1540
+ ax.imshow(vis)
1541
+ ax.set_title(f"{img_id[:28]}\\nGT={n_gt_boxes} Pred={n_pred}",
1542
+ color="white", fontsize=7, pad=3)
1543
+ ax.axis("off")
1544
+
1545
+ # Turn off unused axes
1546
+ for ax in axes[len(sample_ids):]:
1547
+ ax.axis("off")
1548
+
1549
+ # Legend
1550
+ from matplotlib.patches import Patch
1551
+ legend_els = [
1552
+ Patch(color=(0,1,0), label="Ground Truth"),
1553
+ Patch(color=(1,0.31,0.31), label="Pred: Hotspot"),
1554
+ Patch(color=(0.31,0.63,1), label="Pred: Crack"),
1555
+ ]
1556
+ fig.legend(handles=legend_els, loc="lower center", ncol=3,
1557
+ facecolor="#222", labelcolor="white", fontsize=10, framealpha=0.8,
1558
+ bbox_to_anchor=(0.5, -0.01))
1559
+
1560
+ fig.suptitle(f"FADNet β€” Result Images (confβ‰₯{CONF_CRACK:.2f} | GT=green Pred=colour)",
1561
+ color="white", fontsize=13, y=1.01)
1562
+ plt.tight_layout(pad=1.0)
1563
+ plt.savefig("/kaggle/working/fadnet_result_grid.png", dpi=130,
1564
+ bbox_inches="tight", facecolor="#0f0f0f")
1565
+ plt.show()
1566
+ print("βœ… Saved: fadnet_result_grid.png")
1567
+
1568
+
1569
+ # ==============================================================================
1570
+ # CELL 25 β€” Bounding Box Quality Inspector (TP / FP / FN breakdown)
1571
+ # ==============================================================================
1572
+ # Shows 3-panel per image: Original | GT only | Pred only
1573
+ # Flags each box as TP (cyan), FP (red), FN (yellow)
1574
+ # Run AFTER the results grid cell β€” reuses final_preds & GT
1575
+ # ──────────────────────────────────────────────────────────────────────────────
1576
+ import cv2, random, math
1577
+ import numpy as np
1578
+ import matplotlib.pyplot as plt
1579
+
1580
+ CONF_CRACK = 0.20
1581
+ CONF_HOTSPOT = 0.20
1582
+ CONF_THRS = [CONF_HOTSPOT, CONF_CRACK] # indexed by class id
1583
+ CLASS_NAMES = ["Hotspot", "Crack"]
1584
+ IOU_MATCH = 0.50
1585
+ N_INSPECT = 9 # images to show
1586
+
1587
+ random.seed(7)
1588
+ inspect_ids = random.sample([p.stem for p in IMG_PATHS], min(N_INSPECT, len(IMG_PATHS)))
1589
+
1590
+ def iou_box(b1, b2):
1591
+ xi1=max(b1[0],b2[0]); yi1=max(b1[1],b2[1])
1592
+ xi2=min(b1[2],b2[2]); yi2=min(b1[3],b2[3])
1593
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
1594
+ u=(b1[2]-b1[0])*(b1[3]-b1[1])+(b2[2]-b2[0])*(b2[3]-b2[1])-inter
1595
+ return inter/(u+1e-9)
1596
+
1597
+ def classify_boxes(gt_d, pred_d, conf_thrs, IOU_MATCH=0.50):
1598
+ """
1599
+ Returns:
1600
+ tp_pairs : [(pred_box, gt_box, label)]
1601
+ fp_boxes : [(pred_box, label, score)]
1602
+ fn_boxes : [(gt_box, label)]
1603
+ """
1604
+ active_preds = [(b,s,l) for b,s,l in zip(pred_d["boxes"],pred_d["scores"],pred_d["labels"])
1605
+ if s >= conf_thrs[l]]
1606
+ active_preds.sort(key=lambda x: -x[1])
1607
+
1608
+ gt_boxes = list(zip(gt_d["boxes"], gt_d["labels"]))
1609
+ matched_gt = set()
1610
+ tp_pairs, fp_boxes = [], []
1611
+
1612
+ for pb, ps, pl in active_preds:
1613
+ best_iou, best_j = 0, -1
1614
+ for j, (gb, gl) in enumerate(gt_boxes):
1615
+ if j in matched_gt: continue
1616
+ if gl != pl: continue
1617
+ iou = iou_box(pb, gb)
1618
+ if iou > best_iou: best_iou, best_j = iou, j
1619
+ if best_iou >= IOU_MATCH and best_j >= 0:
1620
+ tp_pairs.append((pb, gt_boxes[best_j][0], pl))
1621
+ matched_gt.add(best_j)
1622
+ else:
1623
+ fp_boxes.append((pb, pl, ps))
1624
+
1625
+ fn_boxes = [(gb, gl) for j,(gb,gl) in enumerate(gt_boxes) if j not in matched_gt]
1626
+ return tp_pairs, fp_boxes, fn_boxes
1627
+
1628
+ def render_panel(img_bgr, boxes_info, H, W, mode="gt"):
1629
+ """mode: gt | pred | overlay"""
1630
+ vis = img_bgr.copy()
1631
+ for item in boxes_info:
1632
+ if mode == "gt":
1633
+ box, lbl = item
1634
+ col = (0,220,0)
1635
+ x1,y1,x2,y2 = int(box[0]*W),int(box[1]*H),int(box[2]*W),int(box[3]*H)
1636
+ cv2.rectangle(vis,(x1,y1),(x2,y2),col,2)
1637
+ cv2.putText(vis,CLASS_NAMES[lbl],(x1,max(y1-4,10)),
1638
+ cv2.FONT_HERSHEY_SIMPLEX,0.5,col,1,cv2.LINE_AA)
1639
+ elif mode == "pred":
1640
+ # item = (box, label, score, status) status: TP/FP
1641
+ box, lbl, score, status = item
1642
+ col = (0,200,200) if status=="TP" else (0,0,220) # cyan=TP, red=FP
1643
+ x1,y1,x2,y2 = int(box[0]*W),int(box[1]*H),int(box[2]*W),int(box[3]*H)
1644
+ cv2.rectangle(vis,(x1,y1),(x2,y2),col,2)
1645
+ cv2.putText(vis,f"{status} {CLASS_NAMES[lbl]}:{score:.2f}",
1646
+ (x1,min(y2+14,H-2)),cv2.FONT_HERSHEY_SIMPLEX,0.4,col,1,cv2.LINE_AA)
1647
+ elif mode == "fn":
1648
+ box, lbl = item
1649
+ col = (0,200,220) # yellow-ish in BGR
1650
+ x1,y1,x2,y2 = int(box[0]*W),int(box[1]*H),int(box[2]*W),int(box[3]*H)
1651
+ cv2.rectangle(vis,(x1,y1),(x2,y2),col,2)
1652
+ cv2.putText(vis,f"FN:{CLASS_NAMES[lbl]}",(x1,max(y1-4,10)),
1653
+ cv2.FONT_HERSHEY_SIMPLEX,0.45,col,1,cv2.LINE_AA)
1654
+ return cv2.cvtColor(vis, cv2.COLOR_BGR2RGB)
1655
+
1656
+ fig, axes = plt.subplots(N_INSPECT, 3, figsize=(18, N_INSPECT*3.8))
1657
+ fig.patch.set_facecolor("#0f0f0f")
1658
+ col_titles = ["Original Image", "GT (green)", "Pred: cyan=TP Β· red=FP Β· yellow=FN"]
1659
+
1660
+ for col_i, ct in enumerate(col_titles):
1661
+ axes[0, col_i].set_title(ct, color="white", fontsize=11, pad=6)
1662
+
1663
+ for row_i, img_id in enumerate(inspect_ids):
1664
+ img_path = next((p for p in IMG_PATHS if p.stem == img_id), None)
1665
+ if img_path is None:
1666
+ for c in range(3): axes[row_i,c].axis("off")
1667
+ continue
1668
+ img = cv2.imread(str(img_path))
1669
+ if img is None:
1670
+ for c in range(3): axes[row_i,c].axis("off")
1671
+ continue
1672
+ H, W = img.shape[:2]
1673
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
1674
+
1675
+ gt_d = GT.get(img_id, {"boxes":[], "labels":[]})
1676
+ pred_d = final_preds.get(img_id, {"boxes":[], "scores":[], "labels":[]})
1677
+
1678
+ tp_pairs, fp_boxes, fn_boxes = classify_boxes(gt_d, pred_d, CONF_THRS)
1679
+
1680
+ # Panel 0: original
1681
+ axes[row_i,0].imshow(rgb)
1682
+ axes[row_i,0].set_ylabel(f"{img_id[:22]}", color="#aaa", fontsize=7, rotation=0,
1683
+ labelpad=4, ha="right", va="center")
1684
+
1685
+ # Panel 1: GT
1686
+ gt_items = list(zip(gt_d["boxes"], gt_d["labels"]))
1687
+ gt_vis = render_panel(img, gt_items, H, W, mode="gt")
1688
+ axes[row_i,1].imshow(gt_vis)
1689
+
1690
+ # Panel 2: Predictions coloured by TP/FP + FN overlaid
1691
+ pred_items = [(b,l,1.0,"TP") for b,gb,l in tp_pairs] + [(b,l,s,"FP") for b,l,s in fp_boxes]
1692
+ pred_vis = render_panel(img, pred_items, H, W, mode="pred")
1693
+ # overlay FN on same panel
1694
+ pred_vis_bgr = cv2.cvtColor(pred_vis, cv2.COLOR_RGB2BGR)
1695
+ fn_vis = render_panel(pred_vis_bgr, fn_boxes, H, W, mode="fn")
1696
+ axes[row_i,2].imshow(fn_vis)
1697
+
1698
+ # Stats text on panel 2
1699
+ stats = f"TP={len(tp_pairs)} FP={len(fp_boxes)} FN={len(fn_boxes)} GT={len(gt_items)}"
1700
+ axes[row_i,2].set_xlabel(stats, color="#ccc", fontsize=8)
1701
+
1702
+ for c in range(3):
1703
+ axes[row_i,c].axis("off")
1704
+ axes[row_i,c].tick_params(left=False, bottom=False)
1705
+
1706
+ fig.suptitle("FADNet β€” Bounding Box Quality Inspector (conf=0.20 both classes)",
1707
+ color="white", fontsize=14, y=1.005, fontweight="bold")
1708
+
1709
+ from matplotlib.patches import Patch
1710
+ legend_els = [
1711
+ Patch(color=(0,0.86,0), label="GT box"),
1712
+ Patch(color=(0,0.78,0.78), label="TP (correct detection)"),
1713
+ Patch(color=(0,0,0.86), label="FP (false alarm)"),
1714
+ Patch(color=(0,0.78,0.86), label="FN (missed GT)"),
1715
+ ]
1716
+ fig.legend(handles=legend_els, loc="lower center", ncol=4,
1717
+ facecolor="#222", labelcolor="white", fontsize=10,
1718
+ bbox_to_anchor=(0.5, -0.01))
1719
+
1720
+ plt.tight_layout(pad=0.8)
1721
+ plt.savefig("/kaggle/working/fadnet_bbox_quality.png", dpi=130,
1722
+ bbox_inches="tight", facecolor="#0f0f0f")
1723
+ plt.show()
1724
+ print("βœ… Saved: fadnet_bbox_quality.png")
1725
+ print(f"\\nAggregate across {N_INSPECT} sampled images:")
1726
+ print(f" GT boxes shown : {sum(len(GT.get(i, {'boxes': []}).get('boxes', [])) for i in inspect_ids)}")
1727
+
1728
+
1729
+ # ==============================================================================
1730
+ # CELL 26 β€” Live Inference from Checkpoint (GT green | Pred red, side-by-side)
1731
+ # ==============================================================================
1732
+ # Loads fadnet_finetune_best.pt fresh, runs inference at conf=0.20,
1733
+ # draws GT (green) vs Predicted (red) SIDE BY SIDE with confidence scores.
1734
+ # No dependency on final_preds or any prior cell state.
1735
+ # ==============================================================================
1736
+ import cv2, random, math, torch
1737
+ import numpy as np
1738
+ import matplotlib.pyplot as plt
1739
+ from pathlib import Path
1740
+ from ultralytics import YOLO
1741
+
1742
+ # ── Config ─────────────────────────────────────────────────────────────────
1743
+ CKPT = '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_finetune_best.pt'
1744
+ TEST_IMG_DIR = Path('/kaggle/working/Thermal-H&C-1/test/images')
1745
+ TEST_LBL_DIR = Path('/kaggle/working/Thermal-H&C-1/test/labels')
1746
+ CONF = 0.20 # F1-optimal threshold
1747
+ IOU_NMS = 0.45
1748
+ IMGSZ = 640
1749
+ N_SHOW = 12 # images in grid
1750
+ CLASS_NAMES = ['Hotspot', 'Crack']
1751
+ GT_COLOR = (0, 220, 0) # green β€” ground truth
1752
+ PRED_COLOR = (60, 80, 255) # red β€” predicted (BGR)
1753
+ IOU_MATCH = 0.50 # IoU to call a detection TP
1754
+
1755
+ random.seed(42)
1756
+
1757
+ # ── Load model ──────────────────────────────────────────────────────────────
1758
+ print(f"Loading checkpoint: {CKPT}")
1759
+ model = YOLO(CKPT)
1760
+ model.eval()
1761
+ print(f"Model loaded | device: {'cuda' if torch.cuda.is_available() else 'cpu'}")
1762
+
1763
+ # ── Gather test images ──────────────────────────────────────────────────────
1764
+ all_imgs = sorted(TEST_IMG_DIR.glob('*.jpg')) + sorted(TEST_IMG_DIR.glob('*.png'))
1765
+ print(f"Test images found: {len(all_imgs)}")
1766
+ sample = random.sample(all_imgs, min(N_SHOW, len(all_imgs)))
1767
+
1768
+ # ── Helper: load GT from YOLO label file ────────────────────────────────────
1769
+ def load_gt(img_path):
1770
+ lp = TEST_LBL_DIR / (img_path.stem + '.txt')
1771
+ boxes, labels = [], []
1772
+ if lp.exists():
1773
+ for line in lp.read_text().strip().splitlines():
1774
+ parts = list(map(float, line.split()))
1775
+ cls = int(parts[0])
1776
+ cx,cy,bw,bh = parts[1],parts[2],parts[3],parts[4]
1777
+ x1,y1 = cx-bw/2, cy-bh/2
1778
+ x2,y2 = cx+bw/2, cy+bh/2
1779
+ boxes.append([x1,y1,x2,y2]); labels.append(cls)
1780
+ return boxes, labels
1781
+
1782
+ # ── Helper: IoU ─────────────────────────────────────────────────────────────
1783
+ def iou(a, b):
1784
+ xi1=max(a[0],b[0]); yi1=max(a[1],b[1])
1785
+ xi2=min(a[2],b[2]); yi2=min(a[3],b[3])
1786
+ inter=max(0,xi2-xi1)*max(0,yi2-yi1)
1787
+ ua=(a[2]-a[0])*(a[3]-a[1])+(b[2]-b[0])*(b[3]-b[1])-inter
1788
+ return inter/(ua+1e-9)
1789
+
1790
+ # ── Draw boxes on image copy ─────────────────────────────────────────────────
1791
+ def draw_gt(img, gt_boxes, gt_labels, H, W):
1792
+ vis = img.copy()
1793
+ for box, lbl in zip(gt_boxes, gt_labels):
1794
+ x1,y1,x2,y2 = int(box[0]*W),int(box[1]*H),int(box[2]*W),int(box[3]*H)
1795
+ cv2.rectangle(vis,(x1,y1),(x2,y2), GT_COLOR, 2)
1796
+ tag = CLASS_NAMES[lbl]
1797
+ (tw,th),_ = cv2.getTextSize(tag, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
1798
+ cv2.rectangle(vis,(x1,max(y1-th-6,0)),(x1+tw+4,y1), GT_COLOR, -1)
1799
+ cv2.putText(vis, tag, (x1+2, max(y1-3,10)),
1800
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,0), 1, cv2.LINE_AA)
1801
+ return vis
1802
+
1803
+ def draw_pred(img, pred_boxes, pred_scores, pred_labels, H, W):
1804
+ vis = img.copy()
1805
+ # Sort high-conf first so small boxes aren't buried
1806
+ order = sorted(range(len(pred_scores)), key=lambda i: -pred_scores[i])
1807
+ for i in order:
1808
+ box = pred_boxes[i]
1809
+ score = pred_scores[i]
1810
+ lbl = pred_labels[i]
1811
+ x1,y1,x2,y2 = int(box[0]*W),int(box[1]*H),int(box[2]*W),int(box[3]*H)
1812
+ col = (50, 80, 255) if lbl == 0 else (255, 140, 30) # red=Hotspot, orange=Crack (BGR)
1813
+ cv2.rectangle(vis,(x1,y1),(x2,y2), col, 2)
1814
+ tag = f"{CLASS_NAMES[lbl]} {score:.2f}"
1815
+ (tw,th),_ = cv2.getTextSize(tag, cv2.FONT_HERSHEY_SIMPLEX, 0.45, 1)
1816
+ cv2.rectangle(vis,(x1, min(y2+1,H-th-5)),(x1+tw+4, min(y2+th+6,H-1)), col, -1)
1817
+ cv2.putText(vis, tag, (x1+2, min(y2+th+2,H-2)),
1818
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255,255,255), 1, cv2.LINE_AA)
1819
+ return vis
1820
+
1821
+ # ── Run inference + build grid ───────────────────────────────────────────────
1822
+ ncols = 2 # left=GT, right=Pred
1823
+ nrows = len(sample)
1824
+ fig, axes = plt.subplots(nrows, ncols, figsize=(ncols*6, nrows*4))
1825
+ fig.patch.set_facecolor("#0f0f0f")
1826
+
1827
+ if nrows == 1:
1828
+ axes = [axes]
1829
+
1830
+ axes[0][0].set_title("Ground Truth (green)", color="#00dd55", fontsize=12, pad=6)
1831
+ axes[0][1].set_title("Predictions (red=Hotspot Β· orange=Crack)", color="#ff6040", fontsize=12, pad=6)
1832
+
1833
+ for row, img_path in enumerate(sample):
1834
+ img_bgr = cv2.imread(str(img_path))
1835
+ if img_bgr is None:
1836
+ for c in range(2): axes[row][c].axis("off")
1837
+ continue
1838
+
1839
+ H, W = img_bgr.shape[:2]
1840
+ gt_boxes, gt_labels = load_gt(img_path)
1841
+
1842
+ # ── Live inference ──────────────────────────────────────────────────────
1843
+ results = model(img_path, conf=CONF, iou=IOU_NMS, imgsz=IMGSZ, verbose=False)[0]
1844
+ pred_boxes, pred_scores, pred_labels = [], [], []
1845
+ for box in results.boxes:
1846
+ xyxyn = box.xyxyn[0].cpu().numpy() # normalised [x1,y1,x2,y2]
1847
+ pred_boxes.append(xyxyn.tolist())
1848
+ pred_scores.append(float(box.conf[0]))
1849
+ pred_labels.append(int(box.cls[0]))
1850
+
1851
+ # ── TP/FP/FN quick count for title ─────────────────────────────────────
1852
+ matched_gt = set()
1853
+ tp = 0
1854
+ for pb in sorted(range(len(pred_scores)), key=lambda i: -pred_scores[i]):
1855
+ best_iou, best_j = 0, -1
1856
+ for j, (gb, gl) in enumerate(zip(gt_boxes, gt_labels)):
1857
+ if j in matched_gt: continue
1858
+ if gl != pred_labels[pb]: continue
1859
+ v = iou(pred_boxes[pb], gb)
1860
+ if v > best_iou: best_iou, best_j = v, j
1861
+ if best_iou >= IOU_MATCH and best_j >= 0:
1862
+ tp += 1; matched_gt.add(best_j)
1863
+ fp = len(pred_boxes) - tp
1864
+ fn = len(gt_boxes) - tp
1865
+
1866
+ # ── Draw panels ─────────────────────────────────────────────────────────
1867
+ gt_vis = draw_gt(img_bgr, gt_boxes, gt_labels, H, W)
1868
+ pred_vis = draw_pred(img_bgr, pred_boxes, pred_scores, pred_labels, H, W)
1869
+
1870
+ axes[row][0].imshow(cv2.cvtColor(gt_vis, cv2.COLOR_BGR2RGB))
1871
+ axes[row][1].imshow(cv2.cvtColor(pred_vis, cv2.COLOR_BGR2RGB))
1872
+
1873
+ short_name = img_path.stem[:35]
1874
+ axes[row][0].set_ylabel(short_name, color="#aaa", fontsize=7, rotation=0,
1875
+ labelpad=4, ha="right", va="center")
1876
+ axes[row][1].set_xlabel(f"TP={tp} FP={fp} FN={fn} GT={len(gt_boxes)} Pred={len(pred_boxes)}",
1877
+ color="#ccc", fontsize=9)
1878
+
1879
+ for c in range(2):
1880
+ axes[row][c].axis("off")
1881
+
1882
+ fig.suptitle(
1883
+ f"FADNet β€” Live Inference | checkpoint: fadnet_finetune_best.pt | conf={CONF} iou={IOU_NMS}",
1884
+ color="white", fontsize=13, y=1.005, fontweight="bold"
1885
+ )
1886
+ plt.tight_layout(pad=0.6)
1887
+ plt.savefig("/kaggle/working/fadnet_live_inference.png", dpi=130,
1888
+ bbox_inches="tight", facecolor="#0f0f0f")
1889
+ plt.show()
1890
+ print("βœ… Saved: fadnet_live_inference.png")
1891
+ print(f"Images shown: {len(sample)} | conf threshold: {CONF}")
1892
+
1893
+
1894
+
1895
+ get_ipython().getoutput("find /kaggle -name "*.pt" -o -name "*.pth"")
1896
+
1897
+
1898
+ import zipfile
1899
+ from pathlib import Path
1900
+
1901
+ # --- Configuration ---
1902
+ ARCHIVE = 'FADNet_FULL_RUN.zip'
1903
+
1904
+ # We specifically grab your weights from the input dataset,
1905
+ # AND we grab everything in /kaggle/working/ (like your images)
1906
+ SOURCES = [
1907
+ '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_finetune_best.pt',
1908
+ '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_unet_best.pth',
1909
+ '/kaggle/input/datasets/vishokbadri/latestrun/fadnet_yolo_best.pt',
1910
+ '/kaggle/working/'
1911
+ ]
1912
+
1913
+ def should_skip(f):
1914
+ """Determines which files to ignore."""
1915
+ # 1. PREVENT INFINITE LOOP: Skip all .zip files
1916
+ if f.suffix == '.zip':
1917
+ return True
1918
+
1919
+ # 2. Skip the massive Thermal folder
1920
+ if 'Thermal-H&C-1' in str(f):
1921
+ return True
1922
+
1923
+ # 3. VIP PASS FOR WEIGHTS: Never skip .pt or .pth files!
1924
+ if f.suffix in ['.pt', '.pth']:
1925
+ return False
1926
+
1927
+ # 4. Skip the REST of the heavy YOLO/Logging folders
1928
+ if 'runs/' in str(f) or 'wandb/' in str(f):
1929
+ return True
1930
+
1931
+ return False
1932
+
1933
+ # --- Archiving Logic ---
1934
+ print(f"πŸ“¦ Creating archive: {ARCHIVE}")
1935
+ added, skipped = [], []
1936
+
1937
+ with zipfile.ZipFile(ARCHIVE, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
1938
+ for src in SOURCES:
1939
+ p = Path(src)
1940
+ if not p.exists():
1941
+ print(f" ⚠ Not found, skipping: {src}")
1942
+ continue
1943
+
1944
+ if p.is_file():
1945
+ # Put explicit files into a 'checkpoints' folder inside the zip
1946
+ arcname = f"checkpoints/{p.name}"
1947
+ zf.write(p, arcname)
1948
+ added.append(arcname)
1949
+ print(f" + {arcname} ({p.stat().st_size/1e6:.1f} MB)")
1950
+
1951
+ elif p.is_dir():
1952
+ for f in sorted(p.rglob('*')):
1953
+ if not f.is_file(): continue
1954
+
1955
+ # Check if we should skip this file
1956
+ if should_skip(f):
1957
+ skipped.append(str(f))
1958
+ continue
1959
+
1960
+ arcname = str(f.relative_to(p.parent))
1961
+ zf.write(f, arcname)
1962
+ added.append(arcname)
1963
+
1964
+ archive_mb = Path(ARCHIVE).stat().st_size / 1e6
1965
+ print(f"\nβœ… Archive ready: {ARCHIVE}")
1966
+ print(f" Files packed : {len(added)}")
1967
+ print(f" Files skipped: {len(skipped)}")
1968
+ print(f" Total size : {archive_mb:.1f} MB")
1969
+
1970
+ # Print manifest
1971
+ print("\n── Manifest ──────────────────────────────────────────────")
1972
+ for item in added:
1973
+ print(f" {item}")
working/f1_optimal_curves.png ADDED
working/fadnet_advanced_push.png ADDED

Git LFS Details

  • SHA256: 7c1916fc0f021dce563b49ff14b0af356c79b02316eaa03ab87516278b575ffe
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
working/fadnet_bbox_quality.png ADDED

Git LFS Details

  • SHA256: 4f929b5ac1d95490fc76e8360d41e2daa6484d9f10bb74c64c9460ed5dfe5e2b
  • Pointer size: 132 Bytes
  • Size of remote file: 2.17 MB
working/fadnet_live_inference.png ADDED

Git LFS Details

  • SHA256: 11e30d597d56b610c9f079062ce23298b11c0264b6366945f1a025ccfda6bb73
  • Pointer size: 132 Bytes
  • Size of remote file: 2.93 MB
working/fadnet_metrics_dashboard.png ADDED

Git LFS Details

  • SHA256: 1734b3f08627153336f6a7ed178648e1d0b6094ef51eab6aa3d29d28005a2aa3
  • Pointer size: 131 Bytes
  • Size of remote file: 423 kB
working/fadnet_result_grid.png ADDED

Git LFS Details

  • SHA256: 9cb0d60927096c3c1b27b65198602458d54d8b6b935f9cc81bce70dbb24d8b35
  • Pointer size: 132 Bytes
  • Size of remote file: 3.08 MB
working/perclass_thresh_heatmap.png ADDED