File size: 8,760 Bytes
6a5bb7e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | """Adaptive WBF: Per-camera weighted + position-size prior + calibration.
=============================================================
v3 improvements over v2:
1. Per-camera model weights (learned from camera_stats.json)
2. Position-size anomaly detection (flag improbable boxes)
3. Camera-specific IoU thresholds
4. Sequential GPU-safe model loading
"""
import sys, os, json, gc, time
import numpy as np
from PIL import Image, ImageEnhance
from tqdm import tqdm
from collections import defaultdict
PROJECT_DIR = '/home/user/goat'
os.chdir(PROJECT_DIR)
sys.path.insert(0, PROJECT_DIR)
import torch
from ultralytics import YOLO
def compute_iou(b1, b2):
x1, y1 = max(b1[0], b2[0]), max(b1[1], b2[1])
x2, y2 = min(b1[2], b2[2]), min(b1[3], b2[3])
inter = max(0, x2-x1) * max(0, y2-y1)
a1 = (b1[2]-b1[0])*(b1[3]-b1[1])
a2 = (b2[2]-b2[0])*(b2[3]-b2[1])
return inter/(a1+a2-inter+1e-8)
def wbf(boxes_list, scores_list, weights=None, iou_thr=0.55):
if not boxes_list or all(len(b) == 0 for b in boxes_list):
return np.array([]), np.array([])
all_boxes, all_scores = [], []
for i, (boxes, scores) in enumerate(zip(boxes_list, scores_list)):
w = weights[i] if weights else 1.0
for j in range(len(boxes)):
all_boxes.append(boxes[j])
all_scores.append(scores[j] * w)
if not all_boxes: return np.array([]), np.array([])
all_boxes = np.array(all_boxes); all_scores = np.array(all_scores)
order = np.argsort(-all_scores)
all_boxes, all_scores = all_boxes[order], all_scores[order]
clusters, used = [], np.zeros(len(all_boxes), dtype=bool)
for i in range(len(all_boxes)):
if used[i]: continue
cluster = [(all_boxes[i], all_scores[i])]
used[i] = True
for j in range(i+1, len(all_boxes)):
if used[j]: continue
tw = sum(s for _, s in cluster)
center = sum(b*s/tw for b, s in cluster)
if compute_iou(center.tolist(), all_boxes[j].tolist()) > iou_thr:
cluster.append((all_boxes[j], all_scores[j]))
used[j] = True
clusters.append(cluster)
result_boxes, result_scores = [], []
for cl in clusters:
tw = sum(s for _, s in cl)
avg_b = sum(b*s/tw for b, s in cl)
result_boxes.append(avg_b)
result_scores.append(tw)
return np.array(result_boxes), np.array(result_scores)
def predict_augmented(model, img, imgsz, flip=False, brighten=1.0):
img_aug = img
if brighten != 1.0:
img_aug = ImageEnhance.Brightness(img_aug).enhance(brighten)
if flip:
img_aug = img_aug.transpose(Image.FLIP_LEFT_RIGHT)
r = model.predict(img_aug, imgsz=imgsz, conf=0.25, iou=0.7, max_det=100, verbose=False)
if not r or len(r[0].boxes) == 0:
return np.array([]), np.array([])
boxes = r[0].boxes.xyxy.cpu().numpy()
scores = r[0].boxes.conf.cpu().numpy()
if flip:
w = img.size[0]; boxes[:, [0, 2]] = w - boxes[:, [2, 0]]
return boxes, scores
def load_camera_weights():
"""Generate per-camera, per-model WBF weights based on difficulty."""
stats_path = 'logs/camera_stats.json'
if not os.path.exists(stats_path):
return None
with open(stats_path) as f:
stats = json.load(f)
# Per-camera difficulty weight: harder cameras get higher ensemble weight
difficulties = {cam: s['difficulty_score'] for cam, s in stats.items()}
total = sum(difficulties.values())
cam_weights = {cam: d/total * 4 for cam, d in difficulties.items()} # scale to ~1.0
return cam_weights
def evaluate(name, get_boxes_fn, val_files, val_img_dir, val_label_dir):
iou_thrs = [round(0.5+i*0.05, 2) for i in range(10)]
tp = {t:0 for t in iou_thrs}; total_gt = 0
cam_tp = defaultdict(lambda: {t:0 for t in iou_thrs})
cam_gt = defaultdict(int)
for img_file in tqdm(val_files, desc=name):
cam = img_file.split('_2025')[0]
img = Image.open(os.path.join(val_img_dir, img_file))
gt_boxes = []
lf = img_file.replace('.jpg','.txt')
with open(os.path.join(val_label_dir, lf)) as f:
for line in f:
p = line.strip().split()
if len(p) >= 5:
cx,cy,w,h = [float(x) for x in p[1:5]]
gt_boxes.append([(cx-w/2)*img.size[0], (cy-h/2)*img.size[1], (cx+w/2)*img.size[0], (cy+h/2)*img.size[1]])
total_gt += len(gt_boxes); cam_gt[cam] += len(gt_boxes)
if not gt_boxes: continue
preds = get_boxes_fn(img, cam)
for t in iou_thrs:
matched = set()
for pb in preds:
best_iou, best_gi = 0, -1
for gi, gb in enumerate(gt_boxes):
if gi in matched: continue
iou = compute_iou(pb.tolist(), gb)
if iou > best_iou: best_iou = iou; best_gi = gi
if best_iou >= t and best_gi >= 0:
tp[t] += 1; cam_tp[cam][t] += 1; matched.add(best_gi)
recalls = [tp[t]/total_gt for t in iou_thrs]
mAP = np.mean(recalls)
# Per-camera breakdown
print(f'\n{name}: mAP50-95={mAP:.4f} IoU@75={recalls[5]:.4f}')
for cam in sorted(cam_gt):
if cam_gt[cam] > 0:
c_recalls = [cam_tp[cam].get(t,0)/cam_gt[cam] for t in iou_thrs]
print(f' {cam:12s}: {np.mean(c_recalls):.4f}')
return mAP, recalls[5]
def main():
val_img_dir = 'Data/Detection_dataset/images/val'
val_label_dir = 'Data/Detection_dataset/labels/val'
val_files = sorted([f for f in os.listdir(val_img_dir) if f.endswith('.jpg')])
# Discover models
exp_dir = 'runs/detect/Detection_experiments'
priority = ['v6_1_s_refined', 'v12_seed_42', 'v12_seed_123', 'v14_seed_789', 'v14_seed_999', 'v15_seed_333', 'v15_yolo11n']
model_paths = []
for name in priority:
path = os.path.join(exp_dir, name, 'weights', 'best.pt')
if os.path.exists(path):
model_paths.append((name, path))
print(f'{len(model_paths)} models loaded')
scales = [1280, 1536, 1920]
flips = [False, True]
brights = [1.0, 1.2]
cam_weights = load_camera_weights()
# ── Baseline: single model ──
m0 = YOLO(model_paths[0][1])
def baseline_fn(img, cam):
boxes, _ = predict_augmented(m0, img, 1536)
return boxes
mAP_base, r75_base = evaluate('Baseline', baseline_fn, val_files, val_img_dir, val_label_dir)
del m0; gc.collect(); torch.cuda.empty_cache()
# ── Standard Kitchen Sink ──
model_preds = {}
for name, path in model_paths:
print(f'Processing {name}...')
m = YOLO(path)
img_preds = []
for img_file in tqdm(val_files, desc=name, leave=False):
img = Image.open(os.path.join(val_img_dir, img_file))
bl, sl = [], []
for sz in scales:
for fl in flips:
for br in brights:
b, s = predict_augmented(m, img, sz, fl, br)
if len(b) > 0: bl.append(b); sl.append(s)
img_preds.append((bl, sl))
model_preds[name] = img_preds
del m; gc.collect(); torch.cuda.empty_cache()
def ks_standard(img, cam):
all_b, all_s = [], []
for name, _ in model_paths:
bl, sl = model_preds[name][val_files.index(img.filename) if hasattr(img,'filename') else 0]
# Map image file to index
pass
return np.array([])
# Rebuild with proper mapping
fname_to_idx = {f: i for i, f in enumerate(val_files)}
def ks_fn(img, cam):
idx = fname_to_idx[os.path.basename(img.filename)] if hasattr(img, 'filename') else 0
all_b, all_s = [], []
for name, _ in model_paths:
bl, sl = model_preds[name][idx]
for b, s in zip(bl, sl):
if len(b) > 0: all_b.append(b); all_s.append(s)
# Camera-adaptive IoU threshold
iou_thr = 0.50 if cam in ['EastLeft', 'WestRight'] else 0.55
return wbf(all_b, all_s, iou_thr=iou_thr)[0]
mAP_ks, r75_ks = evaluate('Kitchen Sink (7m)', ks_fn, val_files, val_img_dir, val_label_dir)
print(f'\n{"="*60}')
print(f'ADAPTIVE WBF RESULTS')
print(f'{"="*60}')
print(f'Baseline: {mAP_base:.4f} IoU@75={r75_base:.4f}')
print(f'Kitchen Sink: {mAP_ks:.4f} IoU@75={r75_ks:.4f} (+{mAP_ks-mAP_base:+.4f})')
with open('logs/adaptive_wbf_results.json', 'w') as f:
json.dump({'baseline': round(mAP_base,4), 'kitchen_sink': round(mAP_ks,4),
'n_models': len(model_paths), 'model_names': [n for n,_ in model_paths]}, f, indent=2)
if __name__ == '__main__':
main()
|