{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Çekicin Hasar AI v3: GPU retraining on the official CarDD split, logged to Weights & Biases\n**Why:** CPU-only post-hoc training (learned rescorer, tiled inference) adds at most +0.2 AP on top of v2.1. See the paper draft, Sec. 5.\nThe deployed weights were also trained on a *re-split* of CarDD, so their official-test numbers may be optimistic.\nThis notebook trains **from generic COCO weights on the official CarDD train split only**, chooses a checkpoint on val, and reports official test once.\n\n| Reference: CarDD official test, box COCO | AP | AP50 |\n|---|---|---|\n| Deployed YOLO11m int8 (fast) | 59.9 | 75.7 |\n| Hasar AI 2.1 careful (flip + WBF) | 62.2 | 78.0 |\n| CarDD paper best, DCN+ R101 | 60.6 | 78.8 |\n\n**Setup:**\n- Runtime: **GPU** (T4 or better).\n- Colab ▸ 🔑 Secrets: add `WANDB_API_KEY` (and optionally `HF_TOKEN`).\n- Everything is logged to W&B `elasa2next-sosa-/cekicin-hasar-ai`: training curves, configs, test metrics and the ONNX model as an artifact.\n\n**Licence:** CarDD is non-commercial research data, so this is a benchmark run. Option A (RF-DETR) has Apache-2.0 code and weights. Option B (Ultralytics) is AGPL-3.0."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "!nvidia-smi -L\n!pip -q install rfdetr ultralytics pycocotools huggingface_hub onnxruntime wandb"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import os, wandb\ntry:\n    from google.colab import userdata; os.environ['WANDB_API_KEY'] = userdata.get('WANDB_API_KEY')\nexcept Exception: pass   # or: os.environ['WANDB_API_KEY'] = '...'\nos.environ['WANDB_ENTITY'] = 'elasa2next-sosa-'; os.environ['WANDB_PROJECT'] = 'cekicin-hasar-ai'\nwandb.login()"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# 1) CarDD official split -> COCO layout (RF-DETR) and YOLO layout (Ultralytics)\nimport json, shutil\nfrom huggingface_hub import snapshot_download\nsrc = snapshot_download('shawnmichael/CarDD', repo_type='dataset', allow_patterns=['annotations/instances_*2017.json', 'images/*'], max_workers=4)\nroot = '/content/cardd_rf'; yroot = '/content/cardd_yolo'\nfor split, out in [('train', 'train'), ('val', 'valid'), ('test', 'test')]:\n    d = f'{root}/{out}'; os.makedirs(d, exist_ok=True); os.makedirs(f'{yroot}/images/{out}', exist_ok=True); os.makedirs(f'{yroot}/labels/{out}', exist_ok=True)\n    ann = json.load(open(f'{src}/annotations/instances_{split}2017.json')); ims = {im['id']: im for im in ann['images']}; lab = {i: [] for i in ims}\n    for im in ann['images']:\n        p = f'{src}/images/{split}/{im[\"file_name\"]}'\n        for q in (f'{d}/{im[\"file_name\"]}', f'{yroot}/images/{out}/{im[\"file_name\"]}'):\n            if not os.path.exists(q): os.symlink(p, q)\n    for a in ann['annotations']:\n        a.pop('segmentation', None); im = ims[a['image_id']]; x, y, w, h = a['bbox']\n        lab[a['image_id']].append(f'{a[\"category_id\"] - 1} {(x + w / 2) / im[\"width\"]:.6f} {(y + h / 2) / im[\"height\"]:.6f} {w / im[\"width\"]:.6f} {h / im[\"height\"]:.6f}')\n    for i, L in lab.items(): open(f'{yroot}/labels/{out}/{os.path.splitext(ims[i][\"file_name\"])[0]}.txt', 'w').write('\\n'.join(L))\n    json.dump(ann, open(f'{d}/_annotations.coco.json', 'w')); print(out, len(ann['images']), 'images', len(ann['annotations']), 'boxes')\nCATS = json.load(open(f'{root}/train/_annotations.coco.json'))['categories']; NAMES = [c['name'] for c in CATS]\nopen(f'{yroot}/data.yaml', 'w').write(f'path: {yroot}\\ntrain: images/train\\nval: images/valid\\ntest: images/test\\nnames: {dict(enumerate(NAMES))}\\n')"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Option A: RF-DETR-M (Apache-2.0). Native W&B logging"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "EPOCHS = 50   # 12 for a smoke test\nfrom rfdetr import RFDETRMedium\nrf = RFDETRMedium()\nrf.train(dataset_dir=root, epochs=EPOCHS, batch_size=8, grad_accum_steps=2, lr=1e-4, output_dir='/content/rfdetr_cardd',\n         wandb=True, project='cekicin-hasar-ai', run='v3-rfdetr-m-cardd-official')"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Option B: YOLO11m from COCO weights, multi-scale (DCN+-style). Ultralytics → W&B"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "from ultralytics import YOLO, settings\nsettings.update({'wandb': True})\ny = YOLO('yolo11m.pt')   # generic COCO weights, NOT the CarDD-trained checkpoint (avoids leakage)\ny.train(data=f'{yroot}/data.yaml', epochs=EPOCHS, imgsz=640, batch=16, multi_scale=True, cos_lr=True, close_mosaic=5, project='cekicin-hasar-ai', name='v3-yolo11m-cardd-official-ms')"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Evaluation on the official test split: same COCO protocol and flip+WBF as the paper"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import numpy as np\nfrom PIL import Image, ImageOps\nfrom pycocotools.coco import COCO\nfrom pycocotools.cocoeval import COCOeval\ngt = COCO(f'{root}/test/_annotations.coco.json'); cat_ids = [c['id'] for c in CATS]\ndef iou(a, b):\n    x1, y1, x2, y2 = max(a[0], b[0]), max(a[1], b[1]), min(a[2], b[2]), min(a[3], b[3]); i = max(0, x2 - x1) * max(0, y2 - y1)\n    return i / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - i + 1e-9)\ndef wbf(lists, thr=0.55):\n    allb = sorted([d for L in lists for d in L], key=lambda d: -d[1]); cl = []\n    for d in allb:\n        c = next((c for c in cl if c['c'] == d[0] and iou(c['b'], d[2:]) > thr), None)\n        if c is None: cl.append({'c': d[0], 'm': [d], 'b': list(d[2:])}); continue\n        c['m'].append(d); w = sum(x[1] for x in c['m']); c['b'] = [sum(x[2 + k] * x[1] for x in c['m']) / w for k in range(4)]\n    return [[c['c'], sum(x[1] for x in c['m']) / len(c['m']) * min(len(c['m']), len(lists)) / len(lists)] + c['b'] for c in cl]\ndef coco_eval(dets):\n    res = [{'image_id': i, 'category_id': cat_ids[c], 'score': s, 'bbox': [x1, y1, x2 - x1, y2 - y1]} for i, L in dets.items() for c, s, x1, y1, x2, y2 in sorted(L, key=lambda d: -d[1])[:100] if 0 <= c < len(cat_ids)]\n    E = COCOeval(gt, gt.loadRes(res), 'bbox'); E.evaluate(); E.accumulate(); E.summarize(); return [round(100 * v, 2) for v in E.stats]\ndef predict_fn(name):\n    if name == 'rfdetr':\n        m = RFDETRMedium(pretrain_weights='/content/rfdetr_cardd/checkpoint_best_total.pth')\n        def f(img):\n            r = m.predict(img, threshold=0.001); ids = np.asarray(r.class_id)\n            off = 1 if ids.size and ids.min() >= 1 and ids.max() >= len(cat_ids) else 0   # RF-DETR versions differ (COCO ids vs 0-based)\n            return [[int(c) - off, float(s)] + list(map(float, x)) for x, s, c in zip(r.xyxy, r.confidence, ids)]\n    else:\n        m = YOLO(sorted(__import__('glob').glob('/content/cekicin-hasar-ai/v3-yolo11m-cardd-official-ms*/weights/best.pt'))[-1])\n        def f(img):\n            r = m.predict(img, conf=0.001, iou=0.45, max_det=300, verbose=False)[0].boxes\n            return [[int(c), float(s)] + list(map(float, x)) for x, s, c in zip(r.xyxy.cpu().numpy(), r.conf.cpu().numpy(), r.cls.cpu().numpy())]\n    return f\ndef evaluate(name):\n    f = predict_fn(name); A, B = {}, {}\n    for im in gt.dataset['images']:\n        img = Image.open(f'{root}/test/{im[\"file_name\"]}').convert('RGB'); W = img.width\n        A[im['id']] = f(img); B[im['id']] = [[c, s, W - x2, y1, W - x1, y2] for c, s, x1, y1, x2, y2 in f(ImageOps.mirror(img))]\n    plain = coco_eval(A); fused = coco_eval({i: wbf([A[i], B[i]]) for i in A})\n    run = wandb.init(project='cekicin-hasar-ai', name=f'v3-{name}-official-test', job_type='eval', tags=['v3', name, 'test'])\n    run.summary.update({'test/AP': plain[0], 'test/AP50': plain[1], 'test/APs': plain[3], 'test/AP_flip_wbf': fused[0], 'test/AP50_flip_wbf': fused[1], 'test/APs_flip_wbf': fused[3]}); run.finish()\n    print(name, 'plain AP/AP50/APs', plain[0], plain[1], plain[3], '| flip+WBF', fused[0], fused[1], fused[3]); return plain, fused\nresults = {}\nfor name in ['rfdetr', 'yolo11m']:\n    try: results[name] = evaluate(name)\n    except Exception as e: print(name, 'skipped:', e)\nprint('Reference: deployed 59.9 / 75.7 · Hasar AI 2.1 careful 62.2 / 78.0 · DCN+ R101 60.6 / 78.8')"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Export to ONNX for the browser and save it as a W&B model artifact"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "import glob, time, onnxruntime as ort\nrun = wandb.init(project='cekicin-hasar-ai', name='v3-export', job_type='export', tags=['v3', 'onnx'])\nif 'rfdetr' in results:\n    p = RFDETRMedium(pretrain_weights='/content/rfdetr_cardd/checkpoint_best_total.pth').export(output_dir='/content/rfdetr_onnx', format='onnx')\n    a = wandb.Artifact('hasar-ai-v3-rfdetr-m', type='model', metadata=dict(test_AP=results['rfdetr'][0][0], test_AP50=results['rfdetr'][0][1], licence='Apache-2.0 code/weights; trained on CarDD (research only)'))\n    a.add_dir('/content/rfdetr_onnx'); run.log_artifact(a, aliases=['candidate'])\nif 'yolo11m' in results:\n    best = sorted(glob.glob('/content/cekicin-hasar-ai/v3-yolo11m-cardd-official-ms*/weights/best.pt'))[-1]; onnx = YOLO(best).export(format='onnx', imgsz=640, opset=17)\n    a = wandb.Artifact('hasar-ai-v3-yolo11m-ms', type='model', metadata=dict(test_AP=results['yolo11m'][0][0], test_AP50=results['yolo11m'][0][1], licence='AGPL-3.0; trained on CarDD (research only)'))\n    a.add_file(onnx); a.add_file(best); run.log_artifact(a, aliases=['candidate'])\nrun.finish()"
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}