{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":127283,"databundleVersionId":15634477,"isSourceIdPinned":false}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ACCIDENT @ CVPR — Zero-Shot Accident Detection Baseline\n\n### What is this competition?\n\nTraffic accidents are rare but high-impact events. Fixed CCTV cameras record\nthousands of hours of footage, but manually reviewing it is slow and expensive.\nThis competition asks you to build a system that, given a short CCTV clip,\npredicts three things:\n\n| Prediction | Description | Score component |\n|---|---|---|\n| **When** | Accident timestamp in seconds | Temporal (𝒯) |\n| **Where** | Impact point as normalised (x, y) | Spatial (𝒮) |\n| **What type** | Collision class (rear-end, head-on, …) | Classification (𝒞) |\n\nThe final leaderboard score is the **harmonic mean** of 𝒯, 𝒮, 𝒞. The harmonic\nmean is harsh — if any one component is near zero, the overall score tanks.\nYou cannot ignore any sub-task.\n\n### Why is it hard?\n\n- **No labeled real training data.** The test set is real CCTV, but there is\n  no real training split. You get synthetic CARLA data for development — but\n  the domain gap (game engine vs real cameras) is significant.\n- **Variable quality.** Real clips range from 384p to 4K, day and night,\n  rain and snow, with compression artefacts and occlusions.\n- **Accidents are rare.** Most frames in a clip show normal traffic. The model\n  must distinguish the ~1 second of crash from ~29 seconds of driving.\n\n### Our approach\n\nWe fine-tune **RF-DETR Large** (v1.6.4, DINOv2 backbone, 128M parameters) on\nframes extracted from synthetic CARLA videos. The model learns to detect\n\"accident regions\" classified by collision type. At test time we scan each\nvideo frame-by-frame — the highest-confidence detection gives us the time,\nlocation, and type.\n\n### Why RF-DETR?\n\nRF-DETR is a transformer-based detector with a DINOv2 backbone pretrained on\nlarge-scale image data. Its domain adaptability (measured on RF100-VL) makes\nit well-suited for transferring from synthetic to real data. Since v1.6 it\nuses PyTorch Lightning internally, giving us CSVLogger, COCO evaluation,\nEMA, and checkpointing for free.\n\n### Multi-GPU (2× T4) — known limitation\n\nRF-DETR 1.6's `ddp_notebook` strategy enables spawn-based DDP in Jupyter.\nOn Kaggle's 2× T4 (PCIe, no NVLink) we observed NCCL collective timeouts\nduring COCO mAP evaluation — the metric logging doesn't use `sync_dist=True`\nfor all metrics, causing rank divergence. We use single-GPU with gradient\naccumulation instead, which gives the same effective batch size (16) and\nreliably completes training.\n\nThe `%%writefile` pattern below is still useful for other DDP setups\n(e.g. multi-node with proper interconnect). It writes our DataModule\nto an importable `.py` file so spawn can pickle it — the standard fix for\nthe \"`Can't get attribute on __main__`\" error.","metadata":{"_uuid":"72a6bfe3-ee39-48ae-84f4-32c05126352b","_cell_guid":"0b359ec1-5d63-4dd2-b6be-344fdd0c38f4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"---\n## 0 — Environment Setup\n\nWe install `rfdetr[train]` which pulls in PyTorch Lightning, Albumentations,\ntorchmetrics, and all training dependencies. The `[train]` extra is required\n— without it only inference works. `tqdm` comes with the install.","metadata":{"_uuid":"2924e4e5-a072-4ed4-8415-2c685a5bc61c","_cell_guid":"e7c6a1e8-2b6b-4275-a581-322b48e6d8d3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"!pip install -q \"rfdetr[train]\" supervision opencv-python-headless pandas matplotlib seaborn albumentations","metadata":{"_uuid":"008d5721-c5bf-430f-97b7-1a0801585596","_cell_guid":"69a3a983-765a-42c7-a750-87e146cdf3aa","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:15:36.809463Z","iopub.execute_input":"2026-04-13T08:15:36.809913Z","iopub.status.idle":"2026-04-13T08:16:03.060434Z","shell.execute_reply.started":"2026-04-13T08:15:36.809885Z","shell.execute_reply":"2026-04-13T08:16:03.059365Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%matplotlib inline\n\nimport os\nimport shutil\nimport sys\nfrom pathlib import Path\n\nimport cv2\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport torch","metadata":{"_uuid":"94da81f7-ff37-4ab7-b3ff-2da68fdcdbe0","_cell_guid":"637de13a-4147-45c1-80bb-ed8e31b86800","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:03.062285Z","iopub.execute_input":"2026-04-13T08:16:03.062567Z","iopub.status.idle":"2026-04-13T08:16:10.851743Z","shell.execute_reply.started":"2026-04-13T08:16:03.062538Z","shell.execute_reply":"2026-04-13T08:16:10.851007Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 0.1 — Write DataModule to disk for DDP compatibility\n\n### Why do we need this?\n\nWhen you use multi-GPU training with spawn-based DDP, Python creates child\nprocesses by `import`-ing the main module. In a notebook, `__main__` is the\nIPython kernel — not a `.py` file — so child processes fail with:\n\n> `AttributeError: Can't get attribute 'AccidentDataModule' on <module '__main__'>`\n\nRF-DETR's own source code documents this (line 54 of `trainer.py`):\n> *\"Classes MUST live at module level so that Python's pickle can serialise\n> them for the spawned child processes.\"*\n\n### The fix: `%%writefile`\n\n`%%writefile` is a built-in Jupyter/IPython cell magic. It writes the entire\ncell contents to a file on disk. The code below is normal, syntax-highlighted,\neditable Python — but it also creates `accident_data.py` in the working\ndirectory. After running this cell, any process can `import accident_data`.\n\n### What does the DataModule do?\n\n**`SyntheticAccidentDataset`** reads frames directly from CARLA video files\nusing `cv2.VideoCapture` + seek. No COCO JSON, no YOLO `.txt` files, no disk\nconversion. Each sample is either:\n- A **positive frame** at `accident_time` with a bounding box and class label\n- A **negative frame** (empty annotations) sampled away from the crash\n\nNegatives teach the model that normal traffic ≠ accident. Without them the\ndetector fires on every frame, destroying temporal localisation.\n\n**`AccidentDataModule`** is a Lightning `LightningDataModule` that builds the\nframe index in `setup()` (cheap — only reads video headers for fps/frame count)\nand reads actual pixels lazily in `__getitem__` (expensive, per-sample). It\nprovides `class_names` so that `BestModelCallback` can embed collision-type\nlabels into saved checkpoints.\n\nAugmentations (training only, all bbox-aware via Albumentations):\n- `HorizontalFlip` — traffic scenes are roughly symmetric\n- `ColorJitter` — simulate different cameras and lighting\n- `GaussianBlur` — mimic poor CCTV quality\n- `ImageCompression` — simulate JPEG compression artefacts","metadata":{"_uuid":"fbca98b1-9705-479a-9de2-ad4b87f526c2","_cell_guid":"4a8fb440-9319-46f9-a44c-9bdd2fd4a0a0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"%%writefile accident_data.py\n\"\"\"Accident detection DataModule — importable for spawn-based DDP.\"\"\"\n\nfrom pathlib import Path\n\nimport albumentations as A\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom pytorch_lightning import LightningDataModule\nfrom torch.utils.data import DataLoader, Dataset\nfrom tqdm.auto import tqdm\n\n\nclass SyntheticAccidentDataset(Dataset):\n    \"\"\"Reads accident frames directly from CARLA video files.\n\n    No COCO/YOLO conversion — the labels CSV is the dataset index.\n    \"\"\"\n\n    MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)\n    STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)\n\n    def __init__(self, records, syn_root, resolution, augment=False):\n        self.records = records\n        self.syn_root = Path(syn_root)\n        self.resolution = resolution\n\n        tfms = []\n        if augment:\n            tfms.extend([\n                A.HorizontalFlip(p=0.5),\n                A.ColorJitter(\n                    brightness=0.3, contrast=0.3,\n                    saturation=0.2, hue=0.05, p=0.6,\n                ),\n                A.GaussianBlur(blur_limit=(3, 5), p=0.2),\n                A.ImageCompression(quality_range=(40, 95), p=0.3),\n            ])\n        tfms.append(A.Resize(resolution, resolution))\n        self._tfm = A.Compose(\n            tfms,\n            bbox_params=A.BboxParams(\n                format=\"yolo\",\n                label_fields=[\"cls\"],\n                min_visibility=0.2,\n            ),\n        )\n\n    def __len__(self):\n        return len(self.records)\n\n    def __getitem__(self, idx):\n        rec = self.records[idx]\n        cap = cv2.VideoCapture(str(self.syn_root / rec[\"vpath\"]))\n        cap.set(cv2.CAP_PROP_POS_FRAMES, rec[\"frame\"])\n        ok, bgr = cap.read()\n        cap.release()\n\n        if ok:\n            img = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n        else:\n            img = np.zeros(\n                (self.resolution, self.resolution, 3), dtype=np.uint8\n            )\n\n        h0, w0 = img.shape[:2]\n        out = self._tfm(\n            image=img, bboxes=rec[\"bboxes\"], cls=rec[\"cls\"]\n        )\n        img = out[\"image\"]\n\n        t = torch.from_numpy(img).permute(2, 0, 1).float().div_(255.0)\n        t.sub_(torch.tensor(self.MEAN).view(3, 1, 1))\n        t.div_(torch.tensor(self.STD).view(3, 1, 1))\n\n        bboxes = out[\"bboxes\"]\n        cls = out[\"cls\"]\n        if bboxes:\n            boxes = torch.tensor(bboxes, dtype=torch.float32)\n            labels = torch.tensor(cls, dtype=torch.long)\n        else:\n            boxes = torch.zeros((0, 4), dtype=torch.float32)\n            labels = torch.zeros(0, dtype=torch.long)\n\n        return t, {\n            \"boxes\": boxes,\n            \"labels\": labels,\n            \"image_id\": torch.tensor([idx]),\n            \"orig_size\": torch.tensor([h0, w0]),\n            \"size\": torch.tensor([self.resolution, self.resolution]),\n        }\n\n\nclass AccidentDataModule(LightningDataModule):\n    \"\"\"Drop-in for RF-DETR's RFDETRDataModule.\n\n    Reads frames directly from video files using the labels CSV.\n    No COCO/YOLO disk conversion. Provides class_names for\n    BestModelCallback checkpoint embedding.\n    \"\"\"\n\n    def __init__(\n        self, syn_df, syn_root, resolution, collision_types,\n        batch_size=4, val_split=0.1, neg_per_vid=2, neg_margin=2.0,\n        num_workers=2,\n    ):\n        super().__init__()\n        self.syn_df = syn_df\n        self.syn_root = Path(syn_root)\n        self.resolution = resolution\n        self.class_names = list(collision_types)\n        self.type_to_id = {\n            t: i for i, t in enumerate(collision_types)\n        }\n        self.batch_size = batch_size\n        self.val_split = val_split\n        self.neg_per_vid = neg_per_vid\n        self.neg_margin = neg_margin\n        self.num_workers = num_workers\n\n    def setup(self, stage=None):\n        shuffled = self.syn_df.sample(\n            frac=1, random_state=42\n        ).reset_index(drop=True)\n        n_val = max(1, int(len(shuffled) * self.val_split))\n        train_recs, val_recs = [], []\n\n        for idx, row in tqdm(\n            shuffled.iterrows(),\n            total=len(shuffled),\n            desc=\"Building index\",\n        ):\n            recs = val_recs if idx < n_val else train_recs\n            vpath = row[\"rgb_path\"]\n            cap = cv2.VideoCapture(str(self.syn_root / vpath))\n            if not cap.isOpened():\n                continue\n            fps = cap.get(cv2.CAP_PROP_FPS) or 25.0\n            total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n            cap.release()\n\n            acc_f = min(\n                int(row[\"accident_time\"] * fps), total - 1\n            )\n            cx = (row[\"x1\"] + row[\"x2\"]) / 2.0\n            cy = (row[\"y1\"] + row[\"y2\"]) / 2.0\n            bw = max(row[\"x2\"] - row[\"x1\"], 0.01)\n            bh = max(row[\"y2\"] - row[\"y1\"], 0.01)\n            cid = self.type_to_id.get(row[\"type\"], 0)\n\n            recs.append({\n                \"vpath\": vpath, \"frame\": acc_f,\n                \"bboxes\": [[cx, cy, bw, bh]], \"cls\": [cid],\n            })\n\n            margin = int(self.neg_margin * fps)\n            negs = []\n            if acc_f - margin > 5:\n                negs.append((acc_f - margin) // 2)\n            if acc_f + margin < total - 5:\n                negs.append((acc_f + margin + total) // 2)\n            while len(negs) < self.neg_per_vid:\n                negs.append(min(3, total - 1))\n            for nf in negs[:self.neg_per_vid]:\n                recs.append({\n                    \"vpath\": vpath, \"frame\": nf,\n                    \"bboxes\": [], \"cls\": [],\n                })\n\n        self.train_ds = SyntheticAccidentDataset(\n            train_recs, self.syn_root, self.resolution,\n            augment=True,\n        )\n        self.val_ds = SyntheticAccidentDataset(\n            val_recs, self.syn_root, self.resolution,\n            augment=False,\n        )\n        print(\n            f\"  DataModule — \"\n            f\"train: {len(self.train_ds)}  val: {len(self.val_ds)}\"\n        )\n\n    @staticmethod\n    def _collate(batch):\n        from rfdetr.utilities import nested_tensor_from_tensor_list\n        imgs, tgts = zip(*batch)\n        return (\n            nested_tensor_from_tensor_list(list(imgs)),\n            list(tgts),\n        )\n\n    def train_dataloader(self):\n        return DataLoader(\n            self.train_ds, batch_size=self.batch_size,\n            shuffle=True, num_workers=self.num_workers,\n            collate_fn=self._collate, pin_memory=True,\n            drop_last=True,\n        )\n\n    def val_dataloader(self):\n        return DataLoader(\n            self.val_ds, batch_size=self.batch_size,\n            shuffle=False, num_workers=self.num_workers,\n            collate_fn=self._collate, pin_memory=True,\n        )","metadata":{"_uuid":"d99a8423-22d6-4034-82f4-82bae132b05b","_cell_guid":"e4c95454-6408-4ed1-a24d-05f1a50a3319","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:10.852646Z","iopub.execute_input":"2026-04-13T08:16:10.852995Z","iopub.status.idle":"2026-04-13T08:16:10.860833Z","shell.execute_reply.started":"2026-04-13T08:16:10.852964Z","shell.execute_reply":"2026-04-13T08:16:10.860262Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Import the module we just wrote\n\nAfter `%%writefile` creates the file, we import from it. The print confirms\nthe class comes from `accident_data` (not `__main__`), which means DDP\nspawn can pickle it.","metadata":{"_uuid":"76e3fdf1-4fc5-42c6-a717-261de2b1766a","_cell_guid":"26cf06bf-8c57-4052-a8ce-b0d528404c34","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"from accident_data import AccidentDataModule\n\nprint(f\"AccidentDataModule from: {AccidentDataModule.__module__}\")","metadata":{"_uuid":"ed1b0188-1db6-44cf-bed1-7f4ff0163f7d","_cell_guid":"59662f99-7d64-45c1-8813-0356f8849f45","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:10.862458Z","iopub.execute_input":"2026-04-13T08:16:10.863229Z","iopub.status.idle":"2026-04-13T08:16:34.390735Z","shell.execute_reply.started":"2026-04-13T08:16:10.863204Z","shell.execute_reply":"2026-04-13T08:16:34.390147Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Paths & hyperparameters\n\nAll tuneable constants in one place. The paths assume standard Kaggle layout\n(`/kaggle/input/` for competition data, `/kaggle/working/` for outputs).","metadata":{"_uuid":"64c9dc9b-3207-4770-a3f2-e2e6781edc97","_cell_guid":"c4aa15ac-0ffc-486f-a908-5d1181ba30e5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"COMP_DIR = Path(\"/kaggle/input/competitions/accident\")\nif not COMP_DIR.exists():\n    COMP_DIR = Path(\".\")\n\nOUTPUT_DIR = (\n    Path(\"/kaggle/working\")\n    if Path(\"/kaggle/working\").exists()\n    else Path(\".\")\n)\n\nTEST_META_PATH = COMP_DIR / \"test_metadata.csv\"\nSYN_LABELS_PATH = COMP_DIR / \"sim_dataset\" / \"labels.csv\"\nSYN_VIDEO_DIR = COMP_DIR / \"sim_dataset\"\nCKPT_DIR = OUTPUT_DIR / \"rfdetr_accident_ckpt\"\n\nassert TEST_META_PATH.is_file(), f\"Missing {TEST_META_PATH}\"\nassert SYN_LABELS_PATH.is_file(), f\"Missing {SYN_LABELS_PATH}\"\n\nTRAIN_EPOCHS = 10\nTRAIN_BATCH_SIZE = 4\nTRAIN_LR = 1e-4\nVAL_SPLIT = 0.1\nDET_THRESHOLD = 0.25\nMAX_FRAMES_PER_VIDEO = 60","metadata":{"_uuid":"c7b5f2fd-4789-4027-a12f-0a407cacc909","_cell_guid":"29843515-e593-4bb7-b528-e9ba8e84c46d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:34.391671Z","iopub.execute_input":"2026-04-13T08:16:34.392160Z","iopub.status.idle":"2026-04-13T08:16:34.408906Z","shell.execute_reply.started":"2026-04-13T08:16:34.392130Z","shell.execute_reply":"2026-04-13T08:16:34.408352Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 1 — Exploratory Data Analysis\n\nBefore building any model we need to understand what we're working with.\nTwo datasets matter:\n\n- **Test set** (`test_metadata.csv`): ~2000 real CCTV clips with metadata\n  (duration, resolution, weather, scene layout) but **no labels**.\n- **Synthetic set** (`sim_dataset/labels.csv`): CARLA-generated clips with\n  full annotations (accident time, impact point, bbox, collision type).\n\n### 1.1 Test metadata\n\nKey questions we need answered for pipeline design:\n- How long are the clips? (determines how many frames to sample)\n- What resolutions? (affects whether we need multi-scale inference)\n- Day vs night ratio? (determines augmentation strategy)\n- Weather conditions? (rain/snow need specific augmentations)","metadata":{"_uuid":"45003d2a-bd9e-4e44-9d68-8b2faf243da4","_cell_guid":"3e8c5b8d-1b8f-4200-8a09-78759137e876","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"test_meta = pd.read_csv(TEST_META_PATH)\nprint(f\"Test set: {len(test_meta)} clips\")\ntest_meta.head(10)","metadata":{"_uuid":"ce0147a4-0d9f-4f72-8925-7b7d482fa3b8","_cell_guid":"97dfc7f7-dc48-4db4-b9cb-f7ec958876a1","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:34.409847Z","iopub.execute_input":"2026-04-13T08:16:34.410155Z","iopub.status.idle":"2026-04-13T08:16:34.470856Z","shell.execute_reply.started":"2026-04-13T08:16:34.410127Z","shell.execute_reply":"2026-04-13T08:16:34.470188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(6, 1, figsize=(10, 24))\nfig.suptitle(\"Test-Set Distributions\", fontsize=14, y=1.005)\n\naxes[0].hist(\n    test_meta[\"duration\"], bins=40, color=\"#3b82f6\", edgecolor=\"white\"\n)\naxes[0].set_xlabel(\"Duration (s)\")\naxes[0].set_title(\"Clip duration — most are 25-30s\")\n\ntest_meta[\"height\"].value_counts().sort_index().plot.bar(\n    ax=axes[1], color=\"#8b5cf6\", edgecolor=\"white\"\n)\naxes[1].set_title(\"Frame height — 720p dominates\")\naxes[1].tick_params(axis=\"x\", rotation=45)\n\ntest_meta[\"day_time\"].value_counts().plot.pie(\n    ax=axes[2], autopct=\"%1.0f%%\", colors=[\"#fbbf24\", \"#1e3a5f\"]\n)\naxes[2].set_ylabel(\"\")\naxes[2].set_title(\"Day vs Night — 32% night means we need low-light robustness\")\n\ntest_meta[\"weather\"].value_counts().head(6).plot.barh(\n    ax=axes[3], color=\"#10b981\", edgecolor=\"white\"\n)\naxes[3].set_title(\"Weather — overwhelmingly normal, some rain/snow\")\n\ntest_meta[\"scene_layout\"].value_counts().head(8).plot.barh(\n    ax=axes[4], color=\"#f97316\", edgecolor=\"white\"\n)\naxes[4].set_title(\"Scene layout — highway + signalised intersections = 80%\")\n\ntest_meta[\"quality\"].value_counts().reindex(\n    [\"Very_Poor\", \"Poor\", \"Fine\", \"Good\", \"Very_Good\"]\n).dropna().plot.bar(ax=axes[5], color=\"#ef4444\", edgecolor=\"white\")\naxes[5].set_title(\"Subjective quality — mostly Poor/Fine, typical CCTV\")\naxes[5].tick_params(axis=\"x\", rotation=45)\n\nfor ax in axes.flat:\n    ax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(\n    OUTPUT_DIR / \"eda_test_distributions.png\",\n    dpi=150,\n    bbox_inches=\"tight\",\n)\nplt.show()","metadata":{"_uuid":"7c16d8d5-0dd7-4d07-b753-883aa21ad498","_cell_guid":"a337d6ee-a6e1-4fbd-a7b7-d53d5440df01","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:34.472355Z","iopub.execute_input":"2026-04-13T08:16:34.472638Z","iopub.status.idle":"2026-04-13T08:16:36.431144Z","shell.execute_reply.started":"2026-04-13T08:16:34.472607Z","shell.execute_reply":"2026-04-13T08:16:36.430351Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.2 Synthetic labels (CARLA)\n\nThis is our **only source of supervision**. Each row describes one synthetic\nvideo with:\n- `accident_time` — when the crash happens (seconds)\n- `center_x`, `center_y` — impact point in normalised [0, 1] coordinates\n- `x1, y1, x2, y2` — bounding box around crash participants (normalised)\n- `type` — collision type (the class label for our detector)\n\nWe need to understand:\n- **Class balance** — are some collision types rare? (affects training)\n- **Temporal distribution** — when in the clip do crashes happen? (fallback prior)\n- **Spatial distribution** — where in the frame? (fallback prior)","metadata":{"_uuid":"571a8fdb-497b-4cdf-a18b-1d307dc3cb79","_cell_guid":"a6b27097-a6ce-412e-bf48-9241993db6ef","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"syn = pd.read_csv(SYN_LABELS_PATH)\nprint(f\"Synthetic set: {len(syn)} clips\")\ndisplay(syn.head())","metadata":{"_uuid":"28128b38-96b9-47ae-b910-af58826034d7","_cell_guid":"6de370c8-0dc5-4f14-afe9-73e64a196cd8","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:36.432186Z","iopub.execute_input":"2026-04-13T08:16:36.432504Z","iopub.status.idle":"2026-04-13T08:16:36.478590Z","shell.execute_reply.started":"2026-04-13T08:16:36.432474Z","shell.execute_reply":"2026-04-13T08:16:36.477869Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(3, 1, figsize=(10, 12))\nfig.suptitle(\"Synthetic Set — Label Distributions\", fontsize=13)\n\nsyn[\"type\"].value_counts().plot.bar(\n    ax=axes[0], color=\"#6366f1\", edgecolor=\"white\"\n)\naxes[0].set_title(\"Collision type — check for class imbalance\")\naxes[0].tick_params(axis=\"x\", rotation=45)\n\nsyn[\"accident_time\"].hist(\n    bins=30, ax=axes[1], color=\"#ec4899\", edgecolor=\"white\"\n)\naxes[1].set_title(\"Accident time — when in the clip crashes occur\")\naxes[1].set_xlabel(\"Seconds\")\n\naxes[2].scatter(\n    syn[\"center_x\"], syn[\"center_y\"], s=4, alpha=0.3, c=\"#14b8a6\"\n)\naxes[2].set_xlim(0, 1)\naxes[2].set_ylim(1, 0)\naxes[2].set_title(\n    \"Impact location — crashes cluster near road centre\"\n    \" (useful fallback prior)\"\n)\naxes[2].set_xlabel(\"center_x\")\naxes[2].set_ylabel(\"center_y\")\naxes[2].set_aspect(\"equal\")\n\nfor ax in axes.flat:\n    ax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(\n    OUTPUT_DIR / \"eda_synthetic_distributions.png\",\n    dpi=150,\n    bbox_inches=\"tight\",\n)\nplt.show()","metadata":{"_uuid":"b86622f6-bf9b-4fa1-bf68-6e8be1d53efb","_cell_guid":"30dcf4ce-48ea-41db-be95-c6fa260a637f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:36.479633Z","iopub.execute_input":"2026-04-13T08:16:36.479911Z","iopub.status.idle":"2026-04-13T08:16:37.687906Z","shell.execute_reply.started":"2026-04-13T08:16:36.479885Z","shell.execute_reply":"2026-04-13T08:16:37.687319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"COLLISION_TYPES = sorted(syn[\"type\"].unique().tolist())\nNUM_CLASSES = len(COLLISION_TYPES)\nFALLBACK_TYPE = syn[\"type\"].value_counts().index[0]\nprint(f\"Classes ({NUM_CLASSES}): {COLLISION_TYPES}\")\nprint(f\"Most common (fallback): {FALLBACK_TYPE}\")","metadata":{"_uuid":"69670dbc-386b-4a24-8323-f219670c17a3","_cell_guid":"c1b406b2-6d9b-4551-9410-b652f2a5e01d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:37.690178Z","iopub.execute_input":"2026-04-13T08:16:37.690430Z","iopub.status.idle":"2026-04-13T08:16:37.697286Z","shell.execute_reply.started":"2026-04-13T08:16:37.690404Z","shell.execute_reply":"2026-04-13T08:16:37.696485Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 2 — Build RF-DETR Model + Trainer\n\n### What is `RFDETRModelModule`?\n\nSince RF-DETR 1.6, training is fully PyTorch Lightning. `RFDETRModelModule`\nis a `LightningModule` that internally:\n\n1. **Builds the DETR model** — transformer encoder-decoder with a DINOv2\n   backbone, deformable attention, and learned object queries.\n2. **Loads COCO-pretrained weights** — the backbone retains strong visual\n   features from DINOv2 pretraining, and the detection head has already\n   learned generic object detection on COCO.\n3. **Adapts the classification head** — since our `num_classes` differs from\n   COCO's 90, the class embedding layer is reinitialised (detected via\n   pydantic field tracking).\n4. **Configures optimizers** — AdamW with separate learning rate groups\n   (lower for the frozen DINOv2 backbone, higher for the detection head)\n   + cosine or step LR scheduler.\n5. **Manages EMA** — exponential moving average of model weights for more\n   stable evaluation.\n\n### What is `build_trainer`?\n\nRF-DETR's trainer factory creates a Lightning `Trainer` with:\n- **`CSVLogger`** — writes `metrics.csv` for plotting training curves\n- **`COCOEvalCallback`** — computes `val/mAP_50_95` from our validation data\n- **`BestModelCallback`** — saves `checkpoint_best_total.pth` when mAP improves\n- **Gradient clipping** (max norm 0.1) for stable transformer training\n- **Auto precision** — bf16 on Ampere+, fp16 on T4, fp32 on CPU\n\n### Training configuration\n\nWe use a single T4 GPU with `grad_accum_steps=4` to reach an effective\nbatch size of 16 (4 micro-batch × 4 accumulation). This matches what\nyou'd get with 2× T4 DDP (`batch_size=4 × 2 GPUs × 2 accum`), without\nthe NCCL timeout risks on Kaggle's PCIe interconnect.","metadata":{"_uuid":"12fcbc9d-9d24-4f31-8461-611b34164336","_cell_guid":"e8b30e86-3438-4434-9d88-e67cf617ae2b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"from rfdetr import RFDETRLarge\nfrom rfdetr.training import RFDETRModelModule, build_trainer\nfrom rfdetr.config import TrainConfig\n\nrfdetr = RFDETRLarge()\nmodel_config = rfdetr.model_config\nmodel_config.num_classes = NUM_CLASSES\nRESOLUTION = model_config.resolution\nprint(f\"Resolution: {RESOLUTION}px  Classes: {NUM_CLASSES}\")","metadata":{"_uuid":"7caa875f-95e4-4a52-aaa3-43b6f0b70acd","_cell_guid":"ac7dd678-7691-4e18-933a-e8f5b9aad56f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:37.698251Z","iopub.execute_input":"2026-04-13T08:16:37.698720Z","iopub.status.idle":"2026-04-13T08:16:42.202147Z","shell.execute_reply.started":"2026-04-13T08:16:37.698691Z","shell.execute_reply":"2026-04-13T08:16:42.201374Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Multi-GPU note\n\nRF-DETR 1.6+ supports `strategy=\"ddp_notebook\"` with `devices=2` for\n2× T4 on Kaggle. However, Kaggle's T4s are PCIe-connected (no NVLink),\nand the `COCOEvalCallback`'s heavy `all_gather` operations combined with\nour video-seeking DataModule cause NCCL collective timeouts due to\nrank divergence. Until RF-DETR adds `sync_dist=True` to all logged\nmetrics, single-GPU with gradient accumulation is more reliable and\ngives the same effective batch size.","metadata":{"_uuid":"7e6c9633-f901-46cf-b0d2-32193e278446","_cell_guid":"90e362dc-53f0-419a-82ed-5437db0f7cae","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"train_config = TrainConfig(\n    dataset_dir=str(SYN_VIDEO_DIR),\n    epochs=TRAIN_EPOCHS,\n    batch_size=TRAIN_BATCH_SIZE,\n    lr=TRAIN_LR,\n    output_dir=str(CKPT_DIR),\n    class_names=COLLISION_TYPES,\n    lr_scheduler=\"cosine\",\n    checkpoint_interval=5,\n    compute_val_loss=True,\n    progress_bar=\"tqdm\",\n    devices=1,\n    grad_accum_steps=4,\n)","metadata":{"_uuid":"9a979997-1a59-4725-b579-2ea80cecb3c5","_cell_guid":"0904a065-fb1e-461e-a077-020e797e826a","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:42.203242Z","iopub.execute_input":"2026-04-13T08:16:42.203585Z","iopub.status.idle":"2026-04-13T08:16:42.208155Z","shell.execute_reply.started":"2026-04-13T08:16:42.203542Z","shell.execute_reply":"2026-04-13T08:16:42.207598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if CKPT_DIR.exists():\n    shutil.rmtree(CKPT_DIR)\n\nmodule = RFDETRModelModule(model_config, train_config)\n\ndatamodule = AccidentDataModule(\n    syn, SYN_VIDEO_DIR, RESOLUTION, COLLISION_TYPES,\n    batch_size=TRAIN_BATCH_SIZE,\n)\n\ntrainer = build_trainer(train_config, model_config)\n\nprint(f\"Module : {type(module).__name__}\")\nprint(\n    f\"Data   : {datamodule.__class__.__module__}\"\n    f\".{type(datamodule).__name__}\"\n)\nprint(\n    f\"Trainer: epochs={trainer.max_epochs}\"\n    f\"  precision={trainer.precision}\"\n)","metadata":{"_uuid":"6317eab7-dbe3-474f-a99e-925e6fd96d42","_cell_guid":"bb1f1aaf-8dce-4ffc-9211-f9c8bf8faa63","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:42.209057Z","iopub.execute_input":"2026-04-13T08:16:42.209471Z","iopub.status.idle":"2026-04-13T08:16:43.992635Z","shell.execute_reply.started":"2026-04-13T08:16:42.209429Z","shell.execute_reply":"2026-04-13T08:16:43.992117Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.1 Train\n\nStandard Lightning `trainer.fit()` call. The trainer handles everything:\nmixed-precision casting, gradient accumulation and clipping, distributed\ncommunication (if DDP), COCO metric evaluation, and checkpointing.\n\n**Expected time:** ~15–20 min for 30 epochs with ~6000 training frames on T4.","metadata":{"_uuid":"297e09e1-9258-414f-a348-cb2983f93e14","_cell_guid":"603df201-581e-4fb2-94b6-ce1b24ae3cb0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"trainer.fit(module, datamodule)\nprint(\"Training complete!\")","metadata":{"_uuid":"d5f03e68-df61-432e-a758-bed4051db4ea","_cell_guid":"748875d8-641e-4f6f-88d4-8d9ca5b44a4d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-04-13T08:16:43.993577Z","iopub.execute_input":"2026-04-13T08:16:43.994018Z","execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 3 — Training Curves\n\nLightning's `CSVLogger` writes all logged metrics to `metrics.csv`. We look\nfor three groups:\n\n- **Loss** — should decrease steadily. If val loss diverges from train loss\n  the model is overfitting (try more augmentation or fewer epochs).\n- **mAP** — should increase. `COCOEvalCallback` computes `val/mAP_50_95`\n  on our validation data using the torchmetrics implementation.\n- **Learning rate** — should follow the cosine schedule from `TRAIN_LR`\n  down toward zero.","metadata":{"_uuid":"0615e428-356a-4275-a312-cec2bb7c34b5","_cell_guid":"affcdb89-7f21-41af-bf66-ea1b2125904e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"metrics_files = sorted(Path(CKPT_DIR).glob(\"**/metrics.csv\"))\nassert metrics_files, f\"No metrics.csv under {CKPT_DIR}\"\nmdf = pd.read_csv(metrics_files[-1])\nprint(f\"Metrics: {metrics_files[-1]} ({mdf.shape})\")","metadata":{"_uuid":"aab3b2e9-0854-43de-bba7-c4e64bd03831","_cell_guid":"d4f05480-64a7-4da0-8e9b-11c84a08a70b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"loss_cols = [\n    c for c in mdf.columns\n    if \"loss\" in c.lower() and mdf[c].notna().sum() > 2\n]\nlr_cols = [\n    c for c in mdf.columns\n    if \"lr\" in c.lower() and mdf[c].notna().sum() > 2\n]\nmap_cols = [\n    c for c in mdf.columns\n    if (\"map\" in c.lower() or \"ap\" in c.lower())\n    and mdf[c].notna().sum() > 2\n]\n\ngroups = [\n    (c, t) for c, t in [\n        (loss_cols, \"Loss Curves\"),\n        (map_cols, \"mAP / AP Metrics\"),\n        (lr_cols, \"Learning Rate Schedule\"),\n    ] if c\n]\nn = len(groups)\n\nif n == 0:\n    print(\"No plottable metrics. Columns:\", mdf.columns.tolist())\nelse:\n    fig, axes = plt.subplots(n, 1, figsize=(10, 5 * n))\n    if n == 1:\n        axes = [axes]\n    for ax, (cols, title) in zip(axes, groups):\n        for col in cols:\n            s = mdf[col].dropna()\n            ax.plot(s.index, s.values, label=col, linewidth=1.5)\n        ax.set_xlabel(\"Logged step\")\n        ax.set_title(title)\n        ax.legend(fontsize=8)\n        ax.grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.savefig(\n        OUTPUT_DIR / \"training_curves.png\",\n        dpi=150,\n        bbox_inches=\"tight\",\n    )\n    plt.show()","metadata":{"_uuid":"b4c9d78a-df0a-4e07-8b2a-eeebfc0abaed","_cell_guid":"ee4a91ea-b7c8-44ed-b14f-22d1e2464997","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 4 — Load Best Checkpoint\n\n### How RF-DETR saves checkpoints\n\n`BestModelCallback` monitors `val/mAP_50_95` (computed by `COCOEvalCallback`\non our validation frames) and saves the best model as\n`checkpoint_best_total.pth` — a stripped dict containing only\n`{model: state_dict, args, epoch}`, no optimizer or scheduler state.\n\nThe checkpoint may come in two formats:\n- **RF-DETR `.pth`** — keys are plain (`backbone.xxx`, `class_embed.xxx`)\n- **Lightning `.ckpt`** — keys are prefixed with `model.` (the attribute\n  name on the `LightningModule`)\n\nOur loading code handles both by detecting and stripping the prefix.\n\nAfter loading, we sync the weights into the `RFDETRLarge` wrapper so we\ncan use its `predict()` method, which handles image resizing, normalisation,\nforward pass, and postprocessing (NMS, box decoding) automatically.","metadata":{"_uuid":"bac4f40e-1277-41dd-8dc4-057760a0550a","_cell_guid":"4ebde62c-f055-47d3-8578-4449a434bf7c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def find_best_checkpoint(ckpt_dir=CKPT_DIR):\n    \"\"\"Locate the best checkpoint with fallbacks.\n\n    Search order:\n    1. checkpoint_best_total.pth (best of regular vs EMA)\n    2. checkpoint_best_regular.pth\n    3. Any .pth (periodic checkpoints)\n    4. Any .ckpt (Lightning full checkpoints)\n    \"\"\"\n    for name in [\n        \"checkpoint_best_total.pth\",\n        \"checkpoint_best_regular.pth\",\n    ]:\n        p = ckpt_dir / name\n        if p.is_file():\n            return p\n    pths = sorted(ckpt_dir.glob(\"*.pth\"))\n    if pths:\n        return pths[-1]\n    ckpts = sorted(ckpt_dir.glob(\"**/*.ckpt\"))\n    assert ckpts, f\"No checkpoint in {ckpt_dir}\"\n    return ckpts[-1]\n\n\nbest_ckpt = find_best_checkpoint()\nprint(f\"Best checkpoint: {best_ckpt}\")\n\nckpt_data = torch.load(\n    best_ckpt, map_location=\"cpu\", weights_only=False\n)\nstate_dict = ckpt_data.get(\n    \"model\", ckpt_data.get(\"state_dict\", ckpt_data)\n)\n\n# Strip Lightning \"model.\" prefix if present\nif any(k.startswith(\"model.\") for k in state_dict):\n    state_dict = {\n        k.replace(\"model.\", \"\", 1): v\n        for k, v in state_dict.items()\n        if k.startswith(\"model.\")\n    }\n\nn_loaded = sum(\n    1 for k in state_dict if k in module.model.state_dict()\n)\nmodule.model.load_state_dict(state_dict, strict=False)\nprint(f\"Loaded {n_loaded}/{len(module.model.state_dict())} params\")\n\n# Sync into RFDETRLarge wrapper for predict()\nrfdetr.model.model = module.model\nrfdetr.model.class_names = COLLISION_TYPES\nprint(f\"class_names: {rfdetr.class_names}\")","metadata":{"_uuid":"f882d29d-7d9d-4246-81ef-5a37878641f0","_cell_guid":"574aec58-370e-4a33-9d7e-4bbb2a97b883","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 5 — Inference on Test Videos → Submission\n\n### Strategy\n\nFor each test video we:\n\n1. **Sample ~60 frames** uniformly across the clip. With a 30-fps, 30-second\n   clip this gives roughly 1 frame every 15 frames — enough temporal\n   resolution to catch the accident moment.\n2. **Run `rfdetr.predict()`** on each frame. This handles resizing to the\n   model's resolution (704px), ImageNet normalisation, forward pass through\n   the transformer, and postprocessing (top-K selection, box decoding).\n3. **Pick the frame with the highest-confidence detection.** This gives us:\n   - `accident_time` = that frame's timestamp\n   - `center_x, center_y` = centre of the detected bbox (normalised by frame size)\n   - `type` = predicted class of that detection\n4. **Fallback** if no detection fires above the threshold: clip midpoint for\n   time, frame centre (0.5, 0.5) for location, most common collision type.\n\n### Why max-confidence?\n\nFor a baseline, picking the single strongest detection across all frames is\nsimple and effective. The model learns that \"accident frames look different\nfrom normal traffic\", so high confidence correlates with the actual crash\nmoment. A future improvement would be temporal smoothing (Gaussian kernel\nover the per-frame confidence signal).","metadata":{"_uuid":"ec61ceca-edb5-420b-bcd4-978a36af720c","_cell_guid":"5155cfef-4584-4d94-95c6-3ead68243a64","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def extract_frames(video_path, max_frames=MAX_FRAMES_PER_VIDEO):\n    \"\"\"Sample frames uniformly from a video.\"\"\"\n    cap = cv2.VideoCapture(video_path)\n    if not cap.isOpened():\n        return []\n    total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    fps = cap.get(cv2.CAP_PROP_FPS) or 25.0\n    stride = max(1, total // max_frames)\n    frames = []\n    idx = 0\n    while True:\n        ok, frm = cap.read()\n        if not ok:\n            break\n        if idx % stride == 0:\n            frames.append((idx, idx / fps, frm))\n        idx += 1\n    cap.release()\n    return frames\n\n\ndef run_inference(\n    test_df, model, class_names=COLLISION_TYPES,\n    max_frames=MAX_FRAMES_PER_VIDEO, threshold=DET_THRESHOLD,\n    fallback=FALLBACK_TYPE,\n):\n    \"\"\"Run fine-tuned RF-DETR on test videos → submission DataFrame.\"\"\"\n    from PIL import Image\n    from tqdm.auto import tqdm\n\n    rows = []\n\n    for i, row in tqdm(\n        test_df.iterrows(), total=len(test_df), desc=\"Inference\"\n    ):\n        vpath = str(COMP_DIR / row[\"path\"])\n        if not os.path.isfile(vpath):\n            rows.append({\n                \"path\": row[\"path\"],\n                \"accident_time\": row[\"duration\"] / 2,\n                \"center_x\": 0.5,\n                \"center_y\": 0.5,\n                \"type\": fallback,\n            })\n            continue\n\n        frames = extract_frames(vpath, max_frames)\n        if not frames:\n            rows.append({\n                \"path\": row[\"path\"],\n                \"accident_time\": row[\"duration\"] / 2,\n                \"center_x\": 0.5,\n                \"center_y\": 0.5,\n                \"type\": fallback,\n            })\n            continue\n\n        h, w = frames[0][2].shape[:2]\n        best_conf = 0.0\n        best_ts = row[\"duration\"] / 2\n        best_cx = 0.5\n        best_cy = 0.5\n        best_type = fallback\n\n        for fidx, ts, bgr in frames:\n            pil = Image.fromarray(\n                cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n            )\n            dets = model.predict(pil, threshold=threshold)\n\n            if (\n                len(dets) == 0\n                or dets.confidence is None\n                or len(dets.confidence) == 0\n            ):\n                continue\n\n            top = int(np.argmax(dets.confidence))\n            conf = float(dets.confidence[top])\n            if conf > best_conf:\n                best_conf = conf\n                best_ts = ts\n                box = dets.xyxy[top]\n                best_cx = float(\n                    np.clip((box[0] + box[2]) / 2.0 / w, 0, 1)\n                )\n                best_cy = float(\n                    np.clip((box[1] + box[3]) / 2.0 / h, 0, 1)\n                )\n                if dets.class_id is not None:\n                    cid = int(dets.class_id[top])\n                    if cid < len(class_names):\n                        best_type = class_names[cid]\n\n        rows.append({\n            \"path\": row[\"path\"],\n            \"accident_time\": round(best_ts, 2),\n            \"center_x\": round(best_cx, 3),\n            \"center_y\": round(best_cy, 3),\n            \"type\": best_type,\n        })\n\n    return pd.DataFrame(rows)","metadata":{"_uuid":"e9bd04ce-c875-44ad-a92e-43cdd4cf23d9","_cell_guid":"4c332dcb-89d2-45d1-8731-3d32879af4a4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Running inference on test set...\")\nsubmission = run_inference(test_meta, rfdetr)\nprint(f\"Submission: {submission.shape}\")\nsubmission.head(10)","metadata":{"_uuid":"d6a34249-4dee-42f6-8790-308325fa3848","_cell_guid":"479dc681-12ea-44ca-a0dc-934c76d1b4fa","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.1 Sanity checks\n\nWe verify that predictions are spread across the full range of times,\nlocations, and types. If everything is `(0.5, 0.5, \"rear-end\")` it means\nthe model didn't learn anything and fell through to the fallback.","metadata":{"_uuid":"8186f83d-f4dd-45ee-b6f4-1e65b6edd6b6","_cell_guid":"0bffcb81-40ff-4b34-a2e7-1f3bb8821877","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"print(f\"Rows: {len(submission)}\")\nprint(f\"\\n{submission['type'].value_counts().to_string()}\")","metadata":{"_uuid":"1710e296-4a8d-4bfb-9ab0-6abb14a2bfe9","_cell_guid":"ce5c75bb-9083-4e16-ace0-effffb2afab6","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(3, 1, figsize=(10, 12))\nfig.suptitle(\"Submission Predictions\", fontsize=13)\n\nsubmission[\"accident_time\"].hist(\n    bins=30, ax=axes[0], color=\"#3b82f6\", edgecolor=\"white\"\n)\naxes[0].set_title(\"Predicted accident time (s)\")\n\naxes[1].scatter(\n    submission[\"center_x\"], submission[\"center_y\"],\n    s=3, alpha=0.3, c=\"#ef4444\",\n)\naxes[1].set_xlim(0, 1)\naxes[1].set_ylim(1, 0)\naxes[1].set_title(\"Predicted impact location\")\naxes[1].set_aspect(\"equal\")\n\nsubmission[\"type\"].value_counts().plot.bar(\n    ax=axes[2], color=\"#8b5cf6\", edgecolor=\"white\"\n)\naxes[2].set_title(\"Predicted collision type\")\naxes[2].tick_params(axis=\"x\", rotation=45)\n\nfor ax in axes:\n    ax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig(\n    OUTPUT_DIR / \"submission_distributions.png\",\n    dpi=150,\n    bbox_inches=\"tight\",\n)\nplt.show()","metadata":{"_uuid":"342e414e-b3f0-4ddc-8fda-eee9099c97d9","_cell_guid":"a98dd406-229b-4697-9cc3-f51a0ffe6948","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 6 — Save Submission","metadata":{"_uuid":"c8949662-7833-4c97-9fae-88be68f7183a","_cell_guid":"602416cd-c973-46f6-b26b-9dfb897b6c8a","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"sub_path = OUTPUT_DIR / \"submission.csv\"\nsubmission.to_csv(sub_path, index=False)\nprint(f\"Saved to {sub_path}\")","metadata":{"_uuid":"c4d223ce-a6b8-400e-9184-41f5c6c3bbba","_cell_guid":"fbc994d8-fb59-41cd-8e7e-8db584945d92","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 7 — Evaluate on Synthetic Val Split\n\n### Why evaluate on synthetic data?\n\nWe can't compute the real leaderboard score locally (no ground truth for\ntest clips). But we can get an approximate estimate on the held-out\nsynthetic validation split. This uses the same random seed (42) and split\nratio as the DataModule, so these clips were never seen during training.\n\n### Competition metric\n\nEach component produces a score in [0, 1]:\n\n- **Temporal (𝒯)**: Gaussian similarity — small time errors → ~1, large → ~0.\n  A 2-second error gives ~0.6, a 5-second error gives ~0.04.\n- **Spatial (𝒮)**: Gaussian similarity on Euclidean distance between the\n  predicted and true impact points. 10% of frame width off gives ~0.6.\n- **Classification (𝒞)**: Top-1 accuracy — 1 if the type matches, 0 otherwise.\n- **Final score**: Harmonic mean of 𝒯, 𝒮, 𝒞. If any one is 0.1, the final\n  score is at most ~0.26 no matter how good the others are.","metadata":{"_uuid":"48ef1a51-b0fa-4753-aa03-dcdfb01e77f1","_cell_guid":"1dc85585-4036-449f-b51a-610ce7e2d270","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def gaussian_score(pred, gt, sigma):\n    return float(np.exp(-0.5 * ((pred - gt) / sigma) ** 2))\n\n\ndef evaluate(pred_df, gt_df, sigma_t=2.0, sigma_s=0.1):\n    \"\"\"Compute approximate competition metric.\"\"\"\n    merged = pred_df.merge(\n        gt_df, on=\"path\", suffixes=(\"_pred\", \"_gt\")\n    )\n    t_s, s_s, c_s = [], [], []\n    for _, r in merged.iterrows():\n        t_s.append(gaussian_score(\n            r[\"accident_time_pred\"],\n            r[\"accident_time_gt\"],\n            sigma_t,\n        ))\n        dist = np.hypot(\n            r[\"center_x_pred\"] - r[\"center_x_gt\"],\n            r[\"center_y_pred\"] - r[\"center_y_gt\"],\n        )\n        s_s.append(gaussian_score(dist, 0.0, sigma_s))\n        c_s.append(\n            1.0 if r[\"type_pred\"] == r[\"type_gt\"] else 0.0\n        )\n    T, S, C = np.mean(t_s), np.mean(s_s), np.mean(c_s)\n    H = 3.0 / (\n        1 / max(T, 1e-9)\n        + 1 / max(S, 1e-9)\n        + 1 / max(C, 1e-9)\n    )\n    return {\"T\": T, \"S\": S, \"C\": C, \"H\": H, \"n\": len(merged)}","metadata":{"_uuid":"66a4b469-0058-45f3-b3cc-d44243653579","_cell_guid":"6e512581-10d9-4a0e-a85c-de465e874502","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.066Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"syn_shuffled = syn.sample(\n    frac=1, random_state=42\n).reset_index(drop=True)\nn_val = max(1, int(len(syn_shuffled) * VAL_SPLIT))\nsyn_val = syn_shuffled.head(n_val).rename(\n    columns={\"rgb_path\": \"path\"}\n)\nprint(f\"Evaluating on {len(syn_val)} synthetic val clips...\")\n\nsyn_preds = run_inference(syn_val, rfdetr)\ngt = syn_val[\n    [\"path\", \"accident_time\", \"center_x\", \"center_y\", \"type\"]\n]\nmetrics = evaluate(syn_preds, gt)\n\nprint(f\"\\n{'=' * 40}\")\nprint(f\"  Temporal  (T) : {metrics['T']:.3f}\")\nprint(f\"  Spatial   (S) : {metrics['S']:.3f}\")\nprint(f\"  Class     (C) : {metrics['C']:.3f}\")\nprint(f\"  Harmonic  (H) : {metrics['H']:.3f}\")\nprint(f\"  n             : {metrics['n']}\")\nprint(f\"{'=' * 40}\")","metadata":{"_uuid":"fadac6b1-1cfb-4faa-89a8-99ca741c1cb0","_cell_guid":"d7f29683-fa4d-4d8d-8843-22935ebffe29","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"execution_failed":"2026-04-13T08:31:47.066Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n## 8 — Next Steps & Improvement Ideas\n\n### Quick wins (try these first)\n- **More hard negatives**: extract pre-accident and post-accident frames too\n- **Multi-frame augmentation**: extract ±2 frames around `accident_time`\n- **Tune `DET_THRESHOLD`** on synthetic val (try 0.1, 0.15, 0.2, 0.25, 0.3)\n- **Train longer**: 50–100 epochs with the cosine schedule\n\n### Temporal localization (𝒯)\n- Sliding-window confidence smoothing (Gaussian kernel before picking peak)\n- Optical flow spike detection (RAFT / Farneback) as complementary signal\n- Frame-to-frame DINOv2 feature distance — sudden change = potential crash\n\n### Spatial localization (𝒮)\n- Weighted centroid across top-K detections in the peak time window\n- Multi-scale inference: tile wide-angle CCTV frames for distant vehicles\n\n### Classification (𝒞)\n- CLIP zero-shot: embed collision-type descriptions, match to frame crops\n- Train classifier head on DINOv2 features from synthetic bbox crops\n\n### Domain adaptation (synthetic → real)\n- Aggressive augmentation: JPEG compression, Gaussian noise, resolution\n  downsampling to mimic real CCTV quality\n- Pseudo-labels from COCO model on unlabeled test frames\n- Use synthetic segmentation annotations for richer supervision\n\n### Advanced\n- Ensemble with a VLM (Qwen2-VL, InternVL) for video-level QA\n- Test-time augmentation with horizontal flip + multi-resolution\n- Vehicle tracking (ByteTrack) + trajectory anomaly detection","metadata":{"_uuid":"42646ba6-4454-4e3a-aa26-2f739a622e58","_cell_guid":"d2a5e299-12ce-4a6e-9639-61c5ff348d12","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}}]}