| """Master Pipeline β runs everything automatically when GPU frees up. |
| =================================================================== |
| Step 1: Wait for GPU > 18GB free |
| Step 2: Analyze cameras (CPU) |
| Step 3: Cross-validation (GPU) |
| Step 4: Adaptive WBF evaluation (GPU, sequential) |
| Step 5: Kitchen Sink Ultimate v3 |
| Step 6: Save all results |
| """ |
| import sys, os, time, json, gc, subprocess |
| import numpy as np |
| from PIL import Image, ImageEnhance |
| from tqdm import tqdm |
| from collections import defaultdict |
| from datetime import datetime |
|
|
| PROJECT_DIR = '/home/user/goat' |
| os.chdir(PROJECT_DIR) |
| sys.path.insert(0, PROJECT_DIR) |
|
|
| import torch |
| from ultralytics import YOLO |
|
|
|
|
| def gpu_free_gb(): |
| r = subprocess.run(['nvidia-smi', '--query-gpu=memory.free', '--format=csv,noheader'], |
| capture_output=True, text=True) |
| return int(r.stdout.strip().split()[0]) / 1024 |
|
|
|
|
| def log(msg): |
| ts = datetime.now().strftime('%H:%M:%S') |
| line = f'[{ts}] {msg}' |
| print(line) |
| with open('logs/pipeline_master.log', 'a') as f: f.write(line + '\n') |
|
|
|
|
| |
| 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, 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 boxes, scores in zip(boxes_list, scores_list): |
| all_boxes.extend(boxes); all_scores.extend(scores) |
| 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 evaluate(name, preds_per_img, 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 idx, img_file in enumerate(val_files): |
| 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 = preds_per_img[idx] |
| for t in iou_thrs: |
| matched = set() |
| for pb in preds: |
| if len(pb)==0: continue |
| 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_cam = {} |
| for cam in sorted(cam_gt): |
| if cam_gt[cam] > 0: |
| per_cam[cam] = float(np.mean([cam_tp[cam].get(t,0)/cam_gt[cam] for t in iou_thrs])) |
| return mAP, recalls[5], per_cam |
|
|
|
|
| def main(): |
| os.makedirs('logs', exist_ok=True) |
| log('PIPELINE MASTER START') |
|
|
| |
| log('Waiting for GPU > 18GB free...') |
| while True: |
| free = gpu_free_gb() |
| if free > 18: break |
| if int(time.time()) % 300 < 2: |
| log(f' GPU free: {free:.0f}GB β still waiting') |
| time.sleep(60) |
| log(f'GPU free: {gpu_free_gb():.0f}GB β STARTING!') |
|
|
| 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')]) |
|
|
| |
| 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)) |
| log(f'Models: {len(model_paths)} β {[n for n,_ in model_paths]}') |
|
|
| scales = [1280, 1536, 1920] |
| flips = [False, True] |
| brights = [1.0, 1.2] |
|
|
| |
| log('Running baseline...') |
| m0 = YOLO(model_paths[0][1]) |
| base_preds = [] |
| for img_file in tqdm(val_files, desc='Baseline'): |
| img = Image.open(os.path.join(val_img_dir, img_file)) |
| b, _ = predict_augmented(m0, img, 1536); base_preds.append(b) |
| mAP_base, r75_base, cams_base = evaluate('Baseline', base_preds, val_files, val_img_dir, val_label_dir) |
| log(f'Baseline: mAP50-95={mAP_base:.4f} IoU@75={r75_base:.4f}') |
| del m0; gc.collect(); torch.cuda.empty_cache() |
|
|
| |
| log('Running multi-model predictions...') |
| model_preds = {} |
| for name, path in model_paths: |
| log(f' Model: {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() |
|
|
| |
| log('Kitchen Sink standard...') |
| ks_preds = [] |
| for idx in range(len(val_files)): |
| 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) |
| ks_preds.append(wbf(all_b, all_s)[0]) |
| mAP_ks, r75_ks, cams_ks = evaluate('Kitchen Sink', ks_preds, val_files, val_img_dir, val_label_dir) |
| log(f'Kitchen Sink: mAP50-95={mAP_ks:.4f} IoU@75={r75_ks:.4f} (+{mAP_ks-mAP_base:+.4f})') |
|
|
| |
| log('Camera-adaptive WBF...') |
| |
| if os.path.exists('logs/camera_stats.json'): |
| with open('logs/camera_stats.json') as f: cam_stats = json.load(f) |
| else: |
| cam_stats = {} |
|
|
| cam_thresholds = {} |
| for cam in ['EastLeft','EastRight','WestLeft','WestRight']: |
| if cam in cam_stats: |
| diff = cam_stats[cam].get('difficulty_score', 0.5) |
| cam_thresholds[cam] = 0.50 if diff > 0.6 else 0.55 |
| else: |
| cam_thresholds[cam] = 0.55 |
|
|
| adapt_preds = [] |
| for idx in range(len(val_files)): |
| cam = val_files[idx].split('_2025')[0] |
| iou_thr = cam_thresholds.get(cam, 0.55) |
| 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) |
| adapt_preds.append(wbf(all_b, all_s, iou_thr=iou_thr)[0]) |
| mAP_adapt, r75_adapt, cams_adapt = evaluate('Adaptive WBF', adapt_preds, val_files, val_img_dir, val_label_dir) |
| log(f'Adaptive WBF: mAP50-95={mAP_adapt:.4f} IoU@75={r75_adapt:.4f} (+{mAP_adapt-mAP_base:+.4f})') |
|
|
| |
| all_results = { |
| 'timestamp': datetime.now().isoformat(), |
| 'baseline': {'mAP50-95': round(mAP_base,4), 'IoU@75': round(r75_base,4), 'per_camera': cams_base}, |
| 'kitchen_sink': {'mAP50-95': round(mAP_ks,4), 'IoU@75': round(r75_ks,4), 'delta': round(mAP_ks-mAP_base,4), 'per_camera': cams_ks}, |
| 'adaptive_wbf': {'mAP50-95': round(mAP_adapt,4), 'IoU@75': round(r75_adapt,4), 'delta': round(mAP_adapt-mAP_base,4), 'per_camera': cams_adapt}, |
| 'n_models': len(model_paths), |
| 'model_names': [n for n,_ in model_paths], |
| 'n_sources_per_model': len(scales)*len(flips)*len(brights), |
| } |
| with open('logs/pipeline_master_results.json', 'w') as f: |
| json.dump(all_results, f, indent=2) |
|
|
| log('='*60) |
| log('PIPELINE MASTER COMPLETE') |
| log(f' Baseline: {mAP_base:.4f}') |
| log(f' Kitchen Sink: {mAP_ks:.4f} (+{mAP_ks-mAP_base:+.4f})') |
| log(f' Adaptive WBF: {mAP_adapt:.4f} (+{mAP_adapt-mAP_base:+.4f})') |
| log(f' Best: {max(mAP_ks, mAP_adapt):.4f}') |
| log(f' Results saved to logs/pipeline_master_results.json') |
| log('='*60) |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|