{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"none","dataSources":[{"sourceType":"competition","sourceId":127283,"databundleVersionId":15634477}],"dockerImageVersionId":31259,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Accident Detection Baseline: Optical Flow Motion Analysis\n\n## Competition Overview\n\nThis notebook provides a **baseline solution** for an accident detection competition. The goal is to detect three key aspects of traffic accidents in video footage:\n\n1. **When** did the accident happen? (temporal detection - time in seconds)\n2. **Where** did it happen? (spatial detection - location coordinates within the frame)\n3. **What type** of accident was it? (classification - e.g., t-bone, rear-end, single vehicle, etc.)\n\n## Baseline Approach: Optical Flow Motion Analysis\n\n### The Core Idea\n\nWhen vehicles collide, the motion patterns in the video change dramatically:\n- **Relative velocities spike** at the moment of contact\n- **Debris and vehicle rebound** create sudden motion bursts\n- **Motion field changes** are visible even in low-quality footage\n- These motion changes are **class-agnostic** (work for all accident types)\n\nOptical flow measures how pixels move between consecutive frames, capturing these motion changes directly from the video without needing object detection.\n\n### The Method\n\nOur baseline solution addresses the temporal detection requirement through motion analysis:\n\n1. **Compute optical flow** between consecutive frames using OpenCV's Farneback algorithm\n   - This creates a 2D motion field showing how each pixel moves between frames\n2. **Summarize motion per frame** by calculating the mean magnitude: $M_t = \\frac{1}{HW}\\sum_{x,y} \\|\\mathbf{u}_t(x,y)\\|_2$\n   - This converts the 2D flow field into a single scalar value representing overall motion intensity\n   - Creates a time series of motion scores\n3. **Find the change point** using Kernel Change-Point Detection (KernelCPD)\n   - Identifies when the motion pattern changes most dramatically\n   - The earliest significant change point is predicted as the accident moment\n\n**Note**: This baseline only addresses temporal detection (when). For spatial detection (where) and accident type classification (what), we use simple defaults:\n- **Spatial location**: Defaults to frame center (0.5, 0.5)\n- **Accident type**: Defaults to \"single\" (most common type)","metadata":{}},{"cell_type":"markdown","source":"### Imports & constants","metadata":{}},{"cell_type":"code","source":"!pip install ruptures","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:12:33.51225Z","iopub.execute_input":"2026-02-07T21:12:33.512582Z","iopub.status.idle":"2026-02-07T21:12:39.342669Z","shell.execute_reply.started":"2026-02-07T21:12:33.512555Z","shell.execute_reply":"2026-02-07T21:12:39.341277Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport pickle\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\nimport numpy as np\nimport ruptures as rpt\n\nfrom joblib import Parallel, delayed\n\n# Define paths and load video files\nOPTICAL_FLOW_PATH = Path(\"./optical_flow.pkl\")\nDATASET_PATH = Path(\"/kaggle/input/accident\")\nvideo_paths = list((DATASET_PATH / \"videos\").iterdir())\nmetadata_df = pd.read_csv(DATASET_PATH/\"test_metadata.csv\", index_col=\"path\")","metadata":{"execution":{"iopub.status.busy":"2026-02-07T21:12:39.345611Z","iopub.execute_input":"2026-02-07T21:12:39.346962Z","iopub.status.idle":"2026-02-07T21:12:41.083193Z","shell.execute_reply.started":"2026-02-07T21:12:39.346789Z","shell.execute_reply":"2026-02-07T21:12:41.082125Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Testing Configuration\n\n**For Development/Testing**: By default, this notebook processes a tiny subset of data (recommended for initial runs). You can tweak the `take = 2` line to set the subset size. The full (test) dataset runs for 8-12+ hours.\n\n**For Full Submission**: Comment out or remove the `take = 2` line to process all videos in the dataset.","metadata":{}},{"cell_type":"code","source":"# Limit the number of videos processed for testing purposes\ntake = None\ntake = 5 # comment out this line to run on all\nvideo_paths = video_paths[:(take or len(video_paths))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:12:41.084554Z","iopub.execute_input":"2026-02-07T21:12:41.084995Z","iopub.status.idle":"2026-02-07T21:12:41.09033Z","shell.execute_reply.started":"2026-02-07T21:12:41.08496Z","shell.execute_reply":"2026-02-07T21:12:41.089379Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 1: Computing Optical Flow Scores\n\n### What We're Doing\n\nIn this step, we compute **optical flow** between consecutive frames to measure motion intensity. Optical flow tracks how pixels move between frames, creating a motion field that captures vehicle movements, debris, and collision dynamics.\n\n### How It Works\n\n1. **Frame sampling**: If the video has higher FPS than `TARGET_FPS` (5 fps), we skip frames to reduce computation time while maintaining sufficient temporal resolution\n    - **TARGET_FPS**: 5 frames per second - controls the temporal resolution. Lower values process faster but may miss brief events\n2. **Optical flow calculation**: For each consecutive frame pair, we use OpenCV's `calcOpticalFlowFarneback` algorithm to compute a 2D motion field\n3. **Motion magnitude**: We calculate the magnitude (speed) of motion at each pixel and compute the mean magnitude across the entire frame\n4. **Motion score**: This mean magnitude becomes our motion score for that frame pair\n\n### Output\n\nFor each video, we generate a pickle file containing:\n- **Scores**: Array of mean optical flow magnitudes (one per frame pair)\n- **Frames**: Frame indices corresponding to each score\n- **Path**: Relative path to the video file, acting as a video id\n\n**💡 AI Improvement Suggestions**:\n- Use directional flow information (not just magnitude) to detect specific collision patterns\n- Apply spatial filtering to focus on regions with high motion\n- Combine with object detection to track motion of specific vehicles\n- Use multi-scale optical flow for better robustness","metadata":{}},{"cell_type":"code","source":"# Algorithm parameters:\nTARGET_FPS = 5  # Target frames per second for processing. Lower FPS = faster computation but less temporal resolution\n\ndef compute_optical_flow_score_on_frame(current_frame: cv2.typing.MatLike, previous_frame: cv2.typing.MatLike, motion_threshold: float = 0.0) -> float | None:\n    \"\"\"\n    Computes the degree of motion between the current and previous frames.\n\n    Args:\n        current_frame (np.ndarray): Current frame in grayscale.\n        previous_frame (np.ndarray): Previous frame in grayscale.\n        motion_threshold (float): Threshold to filter out low-motion noise.\n\n    Returns:\n        float: A single value representing the degree of motion in the frame.\n    \"\"\"\n    # Calculate optical flow\n    flow = cv2.calcOpticalFlowFarneback(\n        prev=previous_frame,\n        next=current_frame,\n        flow=None,\n        pyr_scale=0.5,\n        levels=3,\n        winsize=15,\n        iterations=3,\n        poly_n=5,\n        poly_sigma=1.2,\n        flags=0,\n    )\n\n    # Compute magnitude and angle of flow vectors\n    magnitude, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1], angleInDegrees=True)\n    magnitude = np.clip(magnitude, 0, 1e19)\n\n    # Filter out small magnitudes (noise) using a threshold\n    magnitude = magnitude[magnitude > motion_threshold]\n\n    # Return the mean magnitude\n    return magnitude.mean() if magnitude.size > 0 else 0.0\n\ndef compute_optical_flow_scores_on_video(video_path: Path) -> dict:\n    \"\"\"\n    Process a single video file to compute optical flow at the target FPS.\n    \n    Args:\n        video_path: source video file\n    Returns:\n        Dictionary containing video filename, fps, and optical flow results with their corresponding frames.\n        Note there is one more frame than the optical flow scores as the optical flow is computed between two frames.\n    \"\"\"\n    assert video_path.exists()\n\n    cap = cv2.VideoCapture(video_path)\n    original_fps = cap.get(cv2.CAP_PROP_FPS)\n    \n    # will use every `frame_skip`-th frame of the video\n    frame_skip = int(original_fps/min(original_fps, TARGET_FPS) )\n\n    previous_frame = None\n    scores = []\n    frames = []\n    current_frame_idx = 0\n\n    while cap.isOpened():\n        ret, frame = cap.read()\n        if not ret:\n            break\n\n        if current_frame_idx % frame_skip == 0:\n            current_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n            frames.append(current_frame_idx)\n            if previous_frame is not None:\n                scores.append(\n                    compute_optical_flow_score_on_frame(current_frame, previous_frame)\n                )\n            previous_frame = current_frame\n        current_frame_idx += 1\n\n    cap.release()\n    return {\n        \"path\": str(video_path.relative_to(DATASET_PATH).as_posix()), \n        \"scores\": np.array(scores), \n        \"frames\": np.array(frames)\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:12:41.091635Z","iopub.execute_input":"2026-02-07T21:12:41.092083Z","iopub.status.idle":"2026-02-07T21:12:41.10705Z","shell.execute_reply.started":"2026-02-07T21:12:41.092049Z","shell.execute_reply":"2026-02-07T21:12:41.10594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if OPTICAL_FLOW_PATH.exists():\n    print(f\"Optical flow scores already computed and saved at {OPTICAL_FLOW_PATH}.\")\nelse:\n    optical_flow = Parallel(n_jobs=-1)(\n        delayed(compute_optical_flow_scores_on_video)(filename) \n        for filename in tqdm(video_paths)\n    )\n\n    with open(OPTICAL_FLOW_PATH, \"wb\") as f:\n\t    pickle.dump(optical_flow, f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:12:41.109233Z","iopub.execute_input":"2026-02-07T21:12:41.109532Z","iopub.status.idle":"2026-02-07T21:17:34.48003Z","shell.execute_reply.started":"2026-02-07T21:12:41.109506Z","shell.execute_reply":"2026-02-07T21:17:34.479043Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 2: Temporal Detection - When Did the Accident Happen?\n\n### The Problem\n\nWe need to identify the exact moment (frame number and time) when the accident occurred in each video.\n\n### Our Approach: Motion Change Detection\n\nThe temporal detection process works as follows:\n\n1. **Normalize motion scores**: Convert the optical flow scores to proportions (0-1) to make the method scale-invariant\n2. **Apply Kernel Change-Point Detection**: Use the `ruptures` library's KernelCPD algorithm to find when the motion pattern changes most dramatically\n3. **Return the change point**: The frame where the biggest change occurred is our predicted accident moment\n\n### How It Works\n\n**KernelCPD (Kernel Change-Point Detection)**: This algorithm looks for points where the statistical distribution of the data changes. In our case, it finds when the bounding box area pattern shifts (indicating collision).\nWe use a Radial Basis Function (RBF) kernel, which is good at detecting smooth changes in patterns.\n\n### Output\n\nFor each video, we get:\n- `accident_time`: The predicted time in seconds where the accident occurred\n\n### Spatial and Type Defaults\n\nThis baseline only addresses temporal detection. For the other two competition requirements:\n- **Spatial detection (where)**: Defaults to frame center (0.5, 0.5) - a placeholder that needs improvement\n- **Accident type (what)**: Defaults to \"single\" (most common type) - a placeholder that needs improvement\n\n**💡 AI Improvement Suggestions**:\n- Use optical flow direction to identify collision regions for spatial detection\n- Analyze motion patterns (direction, magnitude, spatial distribution) to classify accident types\n- Combine with object detection to locate specific vehicles involved\n- Use temporal models (LSTM, Transformer) to learn accident patterns from motion sequences","metadata":{}},{"cell_type":"code","source":"with open(OPTICAL_FLOW_PATH, \"rb\") as f:\n    optical_flow = pickle.load(f)\n\nresults:dict[Path, dict] = {}\nfor info in optical_flow[:take or len(optical_flow)]:\n    metadata = metadata_df.loc[info[\"path\"]]\n    # normalize the scores\n    scores = info[\"scores\"] / np.linalg.norm(info[\"scores\"])\n\n    # search for the biggest changes in the optical flow and get its frame\n    changes = rpt.KernelCPD(kernel=\"rbf\").fit_predict(scores, n_bkps=1)\n    assert changes and len(changes) >= 1\n    # take the middle frame/frame fraction as the optical flow is a change between two frames\n    frame = (info[\"frames\"][changes[0]] + info[\"frames\"][changes[0]+1]) / 2\n    fps = metadata[\"no_frames\"] / metadata[\"duration\"]\n    results[info[\"path\"]] = {\"accident_time\": float(frame / fps)}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:17:34.481122Z","iopub.execute_input":"2026-02-07T21:17:34.481388Z","iopub.status.idle":"2026-02-07T21:17:34.503073Z","shell.execute_reply.started":"2026-02-07T21:17:34.481363Z","shell.execute_reply":"2026-02-07T21:17:34.502098Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 3. Export `submission.csv`\n\nFinally, its submission time. For each test video, write one row to `submission.csv` containing the predicted `accident_time` in seconds\n\nThe remaining required fields are filled with fixed default values:\n\n- `center_x` / `center_y`: `0.5` / `0.5` (image center)\n- `type: \"single\"`\n\nThis ensures the submission format is valid while development focuses on predicting accident timing.\n\n**💡 This is a major area for improvement!**","metadata":{}},{"cell_type":"code","source":"# Put default values for center_x, center_y, and type\nfor path, item in results.items():\n    item[\"center_x\"] = 0.5\n    item[\"center_y\"] = 0.5\n    item[\"type\"] = \"single\"\n\nresults_df = pd.DataFrame([{\"path\": str(path), **i } for path, i in results.items()])\nresults_df.to_csv(\"submission.csv\", index=False)\n\nresults_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:17:34.504197Z","iopub.execute_input":"2026-02-07T21:17:34.504507Z","iopub.status.idle":"2026-02-07T21:17:34.544475Z","shell.execute_reply.started":"2026-02-07T21:17:34.50448Z","shell.execute_reply":"2026-02-07T21:17:34.54345Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Bonus step: Visualization 📈\n\nAs a final step, we include a simple qualitative visualization to help interpret the baseline predictions.\n\nWhat the visualization shows\n\n- **Left panel**: the video frame at the predicted accident time, with the predicted accident location highlighted.\n\n- **Right panel**: the evolution of the normalized overal optical flow over time, with a vertical line marking the predicted accident moment.","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef show_video_card(\n    video_path,\n    values_per_frame,\n    frames,\n    fps,\n    accident_time,\n    center_x=None,\n    center_y=None,\n    title=None,\n):\n    # ---- Load accident frame ----\n    cap = cv2.VideoCapture(video_path)\n    frame_idx = int(accident_time * fps)\n    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)\n\n    ret, frame = cap.read()\n    cap.release()\n\n    if not ret:\n        raise RuntimeError(f\"Could not read frame from {video_path}\")\n\n    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n\n    # Optional: draw accident location\n    if center_x is not None and center_y is not None:\n        h, w, _ = frame.shape\n        px = int(center_x * w)\n        py = int(center_y * h)\n        cv2.circle(frame, (px, py), 10, (255, 0, 0), -1)\n\n    # ---- Build card ----\n    fig, (ax_img, ax_plot) = plt.subplots(\n        1, 2, figsize=(14, 5), gridspec_kw={\"width_ratios\": [1, 1.3]}\n    )\n\n    # Image panel\n    ax_img.imshow(frame)\n    ax_img.set_title(f\"Predicted accident at {accident_time:.2f}s\")\n    ax_img.axis(\"off\")\n\n    # Signal panel\n    ax_plot.plot([x/fps for x in frames[1:]], values_per_frame)\n    ax_plot.axvline(accident_time)\n    ax_plot.set_xlabel(\"Time (seconds)\")\n    ax_plot.set_ylabel(\"Overall optical flow (normalized)\")\n    ax_plot.set_title(\"Optical flow\")\n\n    if title:\n        fig.suptitle(title)\n\n    plt.tight_layout()\n    plt.show()\n\n\nfor _, row in results_df.iterrows():\n    metadata = metadata_df.loc[info[\"path\"]]\n    \n    of = next(x for x in optical_flow if x[\"path\"] == row[\"path\"])\n    show_video_card(\n        video_path=DATASET_PATH / row[\"path\"],\n        values_per_frame=of[\"scores\"] / np.linalg.norm(of[\"scores\"]),\n        frames = of[\"frames\"],\n        fps = (metadata[\"no_frames\"] / metadata[\"duration\"]),\n        accident_time=row[\"accident_time\"],\n        center_x=row[\"center_x\"],\n        center_y=row[\"center_y\"],\n        title=f\"{row['path']} predicted as {row['type']}\",\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:17:34.545576Z","iopub.execute_input":"2026-02-07T21:17:34.545846Z","iopub.status.idle":"2026-02-07T21:17:38.309373Z","shell.execute_reply.started":"2026-02-07T21:17:34.54582Z","shell.execute_reply":"2026-02-07T21:17:38.308366Z"}},"outputs":[],"execution_count":null}]}