{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4b9c8591",
   "metadata": {},
   "source": [
    "# Spin **detection**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "a56667d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import cv2\n",
    "import pandas as pd\n",
    "from matplotlib import pyplot as plt\n",
    "import time\n",
    "from IPython.display import clear_output, display"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "01b481bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# --- Configurations ---\n",
    "VIDEO_NUMBER = \"5\"\n",
    "PROJECT_ROOT = Path().resolve().parent.parent\n",
    "INPUT_VIDEO_PATH = str(\n",
    "    PROJECT_ROOT\n",
    "    / \"data\"\n",
    "    / f\"recording_{VIDEO_NUMBER}\"\n",
    "    / f\"Recording_{VIDEO_NUMBER}.mp4\"\n",
    ")\n",
    "INPUT_CSV_PATH = str(\n",
    "    PROJECT_ROOT\n",
    "    / \"data\"\n",
    "    / \"auxiliary_data\"\n",
    "    / \"circle_positions\"\n",
    "    / f\"Adjusted_positions_new_{VIDEO_NUMBER}.csv\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "48a18635",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9AAAAH6CAYAAADvBqSRAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAR/5JREFUeJzt3Xm0XnV99/3v3vsaznxOThJykgBJCCTakCqDTAZCBBKiVEAFxVplKvZBXY93F3Wht2jFqq21j3cduO1SC6UCVhEHpuIEKFQolkEQqEQykZCc5MzTNey9f88fLHme3CD5fPFsksD7tZZrVXjz43fta0/fcyhGIYRgAAAAAADgBcV7egMAAAAAAOwLGKABAAAAABAwQAMAAAAAIGCABgAAAABAwAANAAAAAICAARoAAAAAAAEDNAAAAAAAAgZoAAAAAAAEDNAAAAAAAAgYoAEAAAAAEDBAA7/HVVddZVEUPe9/Lr300j29vWkxPj5uH//4x+3UU0+13t5ei6LIrrrqqudtv/rVr9rKlSttzpw5Vq1WbdGiRXbeeefZhg0bdule6LhFUWTXXHNN8R8MAPCKwfN6V+rz2sxsZGTEPvShD9khhxxira2ttmDBArvgggts06ZNxX4gYB9W2tMbAPZ2l19+uS1atGiXP3booYfuod1Mr507d9rll19uBx54oL3mNa+xO+644/e2DzzwgC1atMje/OY324wZM2z9+vX21a9+1W666SZ76KGHbN68eWZmdsIJJ9i//uu/Puev//znP28PPfSQnXTSSUV9HADAKxjP62eoz+s8z+2UU06xRx991C6++GJbsmSJrVu3zq644gq77bbb7LHHHrPOzs6X6BMC+w4GaGA31q5da0ceeaTU1mo1q1QqFsf7xj/cMXfuXHv66aetr6/PfvnLX9rrXve639teccUVz/ljZ5xxhh155JF29dVXP/tT/oMOOsgOOuigXbqpqSm7+OKL7Q1veIP19fVN74cAAMB4Xv+O+ry+55577L777rMvfelL9r73ve/ZdunSpXb++efbj3/8YzvzzDOn/8MA+7h9464B7IXuuOMOi6LIvvnNb9pHP/pRmz9/vrW1tdno6KgNDg7aJZdcYsuXL7eOjg7r6uqytWvX2kMPPfS8a3zrW9+yT3ziEzZ//nzr7Oy0t73tbTYyMmL1et0++MEP2n777WcdHR123nnnWb1ef85evvGNb9gRRxxhra2t1tvba+94xzts8+bNu/0M1Wr1DxpoFy5caGZmw8PDL9jdeOONNjY2Zn/6p3/6ov9eAAC8GDyvn/95PTo6amZmc+bM2aWdO3eumZm1tra+6L8f8HLGb6CB3RgZGbGdO3fu8sdmzZr17P/9yU9+0iqVil1yySVWr9etUqnYo48+at/73vfsrLPOskWLFtn27dvtn/7pn2zlypX26KOPPvuPT/3OZz7zGWttbbVLL73U1q1bZ1/84hetXC5bHMc2NDRkf/3Xf2333HOPXXXVVbZo0SL72Mc+9uxf+6lPfcouu+wyO/vss+3CCy+0HTt22Be/+EU74YQT7IEHHrCenp5pPR4DAwOWZZlt2rTJLr/8cjOz3f5j2ddcc421trbaW97ylmndCwAAv8Pzele7e14feeSR1t7ebpdddpn19vba0qVLbd26dfahD33IXve619nJJ588rfsBXjYCgOd15ZVXBjN73v+EEMLtt98ezCwcdNBBYXJycpe/tlarhSzLdvlj69evD9VqNVx++eXP/rHfrXHooYeGRqPx7B8/55xzQhRFYe3atbusceyxx4YFCxY8+983bNgQkiQJn/rUp3bpHn744VAqlZ7zx1/IfffdF8wsXHnllS/YVavVZ4/DzJkzwxe+8IUX7AcGBkKlUglnn322vBcAAFQ8r5+f8ry+6aabwty5c3c5ZmvWrAljY2PyfoBXGn4DDezGl7/8ZVuyZMnv/fPvec97nvOPOVWr1Wf/7yzLbHh42Do6Omzp0qV2//33P2eNd7/73VYul5/970cffbRdd911dv755+/SHX300faFL3zB0jS1UqlkN9xwg+V5bmefffYuP3Xv6+uzQw45xG6//Xb7yEc+4v7ML+TWW2+1Wq1mjz32mH3jG9+wiYmJF+yvv/56azQa/OPbAIBC8bzelfK8nj17th122GH2/ve/35YtW2YPPvigffazn7XzzjvPvv3tb0/rfoCXCwZoYDeOOuqoF/yXkvyf/8ZPs2f+zZb/+I//aFdccYWtX7/esix79s/NnDnzOf2BBx64y3/v7u42M7MDDjjgOX88z3MbGRmxmTNn2hNPPGEhBDvkkEOed2///4f8dFm1apWZPfMvazn99NPt0EMPtY6ODnv/+9//vP0111xjvb29tnbt2mnfCwAAv8Pzele7e14/+eSTtmrVKrv66qvtrW99q5mZnX766bZw4UI799xz7dZbb+XZDTwPBmjgD/R8/5KNT3/603bZZZfZ+eefb5/85Cett7fX4ji2D37wg5bn+XP6JEmed+3f98dDCGb2zIM/iiK79dZbn7ft6OjwfBS3xYsX22GHHWbXXHPN8w7QmzZtsp///Od20UUXFfJyAACAiuf1rs/rq666ymq1mp122mm7tG9+85vNzOzuu+9mgAaeBwM0UIDrr7/eVq1aZV//+td3+ePDw8O7/AtN/lCLFy+2EIItWrToBf+xtSJNTU09779p1MzsuuuusxAC//g2AGCv9Ep+Xm/fvt1CCLv81t3MrNlsmplZmqYv6f6AfQX/M1ZAAZIkefanzr/z7W9/27Zs2TKtf5+3vOUtliSJfeITn3jO3y+EYAMDA9Py90nT1IaGhp7zx//zP//THn744d/7j8xde+21duCBB9qKFSumZR8AAEynV/LzesmSJRZCsG9961u7tNddd52ZmR122GHTsifg5YbfQAMFOO200+zyyy+38847z4477jh7+OGH7ZprrrGDDjpoWv8+ixcvtr/5m7+xD3/4w7ZhwwY744wzrLOz09avX2/f/e537aKLLrJLLrnkBdf40pe+ZMPDw7Z161Yze+Z/s/mpp54yM7MPfOAD1t3dbePj43bAAQfY29/+dlu2bJm1t7fbww8/bFdeeaV1d3fbZZdd9px1H3nkEfvVr35ll156qUVRNK2fGwCA6fBKfl6fe+659rnPfc7e+9732gMPPGDLli2z+++/3772ta/ZsmXL7Mwzz5zWYwC8XDBAAwX4yEc+YhMTE3bttdfav/3bv9nhhx9uN998s1166aXT/ve69NJLbcmSJfb5z3/ePvGJT5jZM/8yk9WrVz/7/8f0Qj73uc/Zxo0bn/3vN9xwg91www1mZvaud73Luru7ra2tzS688EK7/fbb7frrr7epqSmbN2+enXPOOfbRj37UFi5c+Jx1r7nmGjMze+c73zkNnxIAgOn3Sn5ez5w50375y1/axz72MbvxxhvtK1/5is2cOdPOP/98+/SnP22VSmV6DwDwMhGF//OfIwEAAAAAAM/B/w80AAAAAAACBmgAAAAAAAQM0AAAAAAACBigAQAAAAAQMEADAAAAACBggAYAAAAAQMAADQAAAACAoKSGjdCUF42j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",
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Failed to read frame 155.\n",
      "Failed to read frame 155.\n",
      "Failed to read frame 156.\n",
      "Failed to read frame 157.\n",
      "Failed to read frame 158.\n",
      "Failed to read frame 159.\n",
      "Failed to read frame 160.\n",
      "Failed to read frame 161.\n",
      "Failed to read frame 162.\n",
      "Failed to read frame 163.\n",
      "Failed to read frame 164.\n",
      "Failed to read frame 165.\n",
      "Failed to read frame 166.\n",
      "Failed to read frame 167.\n",
      "Failed to read frame 168.\n",
      "Failed to read frame 169.\n",
      "Failed to read frame 170.\n",
      "Failed to read frame 171.\n",
      "Failed to read frame 172.\n",
      "Failed to read frame 173.\n",
      "Failed to read frame 174.\n",
      "Failed to read frame 175.\n",
      "Failed to read frame 176.\n",
      "Failed to read frame 177.\n",
      "Failed to read frame 178.\n",
      "Failed to read frame 179.\n",
      "Failed to read frame 180.\n",
      "Failed to read frame 181.\n",
      "Failed to read frame 182.\n",
      "Failed to read frame 183.\n",
      "Failed to read frame 184.\n",
      "Failed to read frame 185.\n",
      "Failed to read frame 186.\n",
      "Failed to read frame 187.\n",
      "Failed to read frame 188.\n",
      "Failed to read frame 189.\n",
      "Failed to read frame 190.\n",
      "Failed to read frame 191.\n",
      "Failed to read frame 192.\n",
      "Failed to read frame 193.\n",
      "Failed to read frame 194.\n",
      "Failed to read frame 195.\n",
      "Failed to read frame 196.\n",
      "Failed to read frame 197.\n",
      "Failed to read frame 198.\n",
      "Failed to read frame 199.\n",
      "Failed to read frame 200.\n",
      "Failed to read frame 201.\n",
      "Failed to read frame 202.\n",
      "Failed to read frame 203.\n",
      "Failed to read frame 204.\n",
      "Failed to read frame 205.\n",
      "Failed to read frame 206.\n",
      "Failed to read frame 207.\n",
      "Failed to read frame 208.\n",
      "Failed to read frame 209.\n",
      "Failed to read frame 210.\n",
      "Failed to read frame 211.\n",
      "Failed to read frame 212.\n",
      "Failed to read frame 213.\n",
      "Failed to read frame 214.\n",
      "Failed to read frame 215.\n",
      "Failed to read frame 216.\n",
      "Failed to read frame 217.\n",
      "Failed to read frame 218.\n",
      "Failed to read frame 219.\n",
      "Failed to read frame 220.\n",
      "Failed to read frame 221.\n",
      "Failed to read frame 222.\n",
      "Failed to read frame 223.\n",
      "Failed to read frame 224.\n",
      "Failed to read frame 225.\n",
      "Failed to read frame 226.\n",
      "Failed to read frame 227.\n",
      "Failed to read frame 228.\n",
      "Failed to read frame 229.\n",
      "Failed to read frame 230.\n",
      "Failed to read frame 231.\n",
      "Failed to read frame 232.\n",
      "Failed to read frame 233.\n",
      "Failed to read frame 234.\n",
      "Failed to read frame 235.\n",
      "Failed to read frame 236.\n",
      "Failed to read frame 237.\n",
      "Failed to read frame 238.\n",
      "Failed to read frame 239.\n",
      "Failed to read frame 240.\n",
      "Failed to read frame 241.\n",
      "Failed to read frame 242.\n",
      "Failed to read frame 243.\n",
      "Failed to read frame 244.\n",
      "Failed to read frame 245.\n",
      "Failed to read frame 246.\n",
      "Failed to read frame 247.\n",
      "Failed to read frame 248.\n",
      "Failed to read frame 249.\n",
      "Failed to read frame 250.\n",
      "Failed to read frame 251.\n",
      "Failed to read frame 252.\n",
      "Failed to read frame 253.\n",
      "Failed to read frame 254.\n",
      "Failed to read frame 255.\n",
      "Failed to read frame 256.\n"
     ]
    }
   ],
   "source": [
    "# --- Load video and CSV ---\n",
    "cap = cv2.VideoCapture(INPUT_VIDEO_PATH)\n",
    "fps = cap.get(cv2.CAP_PROP_FPS)\n",
    "\n",
    "if not cap.isOpened():\n",
    "    raise IOError(\"Error: Could not open video.\")\n",
    "\n",
    "df = pd.read_csv(INPUT_CSV_PATH)\n",
    "if df.empty:\n",
    "    raise ValueError(\"Error: The CSV file is empty.\")\n",
    "\n",
    "frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n",
    "offset = 2\n",
    "output_data = []\n",
    "\n",
    "\n",
    "# --- Process frame pairs ---\n",
    "for frame_number in range(frame_count - 1):\n",
    "    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)\n",
    "    ret, frame1 = cap.read()\n",
    "    if not ret:\n",
    "        print(f\"Failed to read frame {frame_number}.\")\n",
    "        continue\n",
    "\n",
    "    ret, frame2 = cap.read()\n",
    "    if not ret:\n",
    "        print(f\"Failed to read frame {frame_number + 1}.\")\n",
    "        continue\n",
    "\n",
    "    # --- Get CSV data for current frame pair ---\n",
    "    try:\n",
    "        df_row1 = df.iloc[frame_number]\n",
    "        df_row2 = df.iloc[frame_number + 1]\n",
    "\n",
    "        if (\n",
    "            df_row1[[\"x\", \"y\", \"radius\"]].isnull().any()\n",
    "            or df_row2[[\"x\", \"y\", \"radius\"]].isnull().any()\n",
    "        ):\n",
    "            continue\n",
    "    except IndexError:\n",
    "        print(f\"No CSV data for frame {frame_number}.\")\n",
    "        continue\n",
    "\n",
    "    # --- Compute ball centers and ROI bounds ---\n",
    "    ball_center1 = np.array([int(df_row1[\"x\"]), int(df_row1[\"y\"])])\n",
    "    ball_center2 = np.array([int(df_row2[\"x\"]), int(df_row2[\"y\"])])\n",
    "    ball_radius = int(df_row1[\"radius\"])\n",
    "\n",
    "    def roi_bounds(center, radius, frame_shape):\n",
    "        x_min = max(center[0] - radius - offset, 0)\n",
    "        x_max = min(center[0] + radius + offset, frame_shape[1])\n",
    "        y_min = max(center[1] - radius - offset, 0)\n",
    "        y_max = min(center[1] + radius + offset, frame_shape[0])\n",
    "        return x_min, x_max, y_min, y_max\n",
    "\n",
    "    x_min1, x_max1, y_min1, y_max1 = roi_bounds(ball_center1, ball_radius, frame1.shape)\n",
    "    x_min2, x_max2, y_min2, y_max2 = roi_bounds(ball_center2, ball_radius, frame2.shape)\n",
    "\n",
    "    roi1 = frame1[y_min1:y_max1, x_min1:x_max1]\n",
    "    roi2 = frame2[y_min2:y_max2, x_min2:x_max2]\n",
    "    gray1 = cv2.cvtColor(roi1, cv2.COLOR_BGR2GRAY)\n",
    "    gray2 = cv2.cvtColor(roi2, cv2.COLOR_BGR2GRAY)\n",
    "\n",
    "    center_roi1 = ball_center1 - [x_min1, y_min1]\n",
    "    center_roi2 = ball_center2 - [x_min2, y_min2]\n",
    "\n",
    "    # --- Mask for feature detection ---\n",
    "    mask = np.zeros_like(gray1)\n",
    "    cv2.circle(mask, tuple(center_roi1.astype(int)), int(ball_radius * 0.8), 255, -1)\n",
    "\n",
    "    feature_params = dict(\n",
    "        maxCorners=100, qualityLevel=0.001, minDistance=0, blockSize=3\n",
    "    )\n",
    "\n",
    "    p0 = cv2.goodFeaturesToTrack(gray1, mask=mask, **feature_params)\n",
    "    if p0 is None:\n",
    "        raise ValueError(\"No features detected. Try relaxing parameters.\")\n",
    "\n",
    "    # --- Optical flow tracking (forward + backward for validation) ---\n",
    "    lk_params = dict(\n",
    "        winSize=(21, 21),\n",
    "        maxLevel=3,\n",
    "        criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 15, 0.01),\n",
    "    )\n",
    "\n",
    "    # Forward tracking\n",
    "    p1, status_forward, _ = cv2.calcOpticalFlowPyrLK(\n",
    "        gray1, gray2, p0, None, **lk_params\n",
    "    )\n",
    "    # Backward tracking\n",
    "    p0r, status_backward, _ = cv2.calcOpticalFlowPyrLK(\n",
    "        gray2, gray1, p1, None, **lk_params\n",
    "    )\n",
    "\n",
    "    # Calculate forward-backward error\n",
    "    fb_error = np.linalg.norm(p0 - p0r, axis=2)\n",
    "\n",
    "    # Filtering criteria\n",
    "    fb_threshold = 10.0  # Pixel threshold for forward-backward error\n",
    "    movement_threshold = 100.0  # Reject points moving too far (could be tuned)\n",
    "\n",
    "    good_pts = []\n",
    "    old3d, new3d = [], []\n",
    "\n",
    "    for i, (old_pt, new_pt, back_pt, s1, s2, err) in enumerate(\n",
    "        zip(\n",
    "            p0.reshape(-1, 2),\n",
    "            p1.reshape(-1, 2),\n",
    "            p0r.reshape(-1, 2),\n",
    "            status_forward.ravel(),\n",
    "            status_backward.ravel(),\n",
    "            fb_error.ravel(),\n",
    "        )\n",
    "    ):\n",
    "        if s1 and s2 and err < fb_threshold:\n",
    "            displacement = np.linalg.norm(new_pt - old_pt)\n",
    "            if displacement < movement_threshold:\n",
    "                ox, oy = old_pt - center_roi1\n",
    "                nx, ny = new_pt - center_roi2\n",
    "                oz = np.sqrt(max(ball_radius**2 - ox**2 - oy**2, 0))\n",
    "                nz = np.sqrt(max(ball_radius**2 - nx**2 - ny**2, 0))\n",
    "                old3d.append([ox, oy, oz])\n",
    "                new3d.append([nx, ny, nz])\n",
    "                good_pts.append((old_pt, new_pt))  # Save for visualization\n",
    "\n",
    "    # --- At this point, old3d and new3d are already filtered; just convert them to arrays ---\n",
    "    old3d = np.array(old3d)\n",
    "    new3d = np.array(new3d)\n",
    "\n",
    "    if old3d.shape[0] < 3:\n",
    "        print(\n",
    "            f\"Frame {frame_number}: only {old3d.shape[0]} points after filtering, skipping rotation\"\n",
    "        )\n",
    "        continue\n",
    "\n",
    "    # --- Kabsch algorithm for rotation ---\n",
    "    P = old3d - old3d.mean(axis=0)\n",
    "    Q = new3d - new3d.mean(axis=0)\n",
    "    H = P.T @ Q\n",
    "    U, _, Vt = np.linalg.svd(H)\n",
    "    R = Vt.T @ U.T\n",
    "    if np.linalg.det(R) < 0:\n",
    "        Vt[-1, :] *= -1\n",
    "        R = Vt.T @ U.T\n",
    "\n",
    "    cos_theta = (np.trace(R) - 1) / 2\n",
    "    cos_theta = np.clip(cos_theta, -1.0, 1.0)\n",
    "    theta = np.arccos(cos_theta)\n",
    "    if np.isclose(theta, 0):\n",
    "        axis = np.array([0.0, 0.0, 1.0])  # default/fallback axis\n",
    "    else:\n",
    "        axis = np.array([R[2, 1] - R[1, 2], R[0, 2] - R[2, 0], R[1, 0] - R[0, 1]]) / (\n",
    "            2 * np.sin(theta)\n",
    "        )\n",
    "        axis /= np.linalg.norm(axis)\n",
    "\n",
    "    print(f\"θ = {theta:.3f} rad, which is {theta * 180 / np.pi:.1f}°\")\n",
    "\n",
    "    # Save data for output\n",
    "    output_data.append(\n",
    "        {\n",
    "            \"frame\": int(df_row1[\"frame\"]),\n",
    "            \"x\": df_row1[\"x\"],\n",
    "            \"y\": df_row1[\"y\"],\n",
    "            \"radius\": df_row1[\"radius\"],\n",
    "            \"x_axis\": axis[0],\n",
    "            \"y_axis\": axis[1],\n",
    "            \"z_axis\": axis[2],\n",
    "            \"angle\": theta,  # in radians; multiply by 180/np.pi if degrees preferred\n",
    "        }\n",
    "    )\n",
    "\n",
    "    # --- Compute rotation poles ---\n",
    "    pole3d = axis * ball_radius\n",
    "    opp_pole3d = -pole3d\n",
    "\n",
    "    pole2d = center_roi1[:2] - pole3d[:2]\n",
    "    opp_pole2d = center_roi1[:2] - opp_pole3d[:2]\n",
    "\n",
    "    # --- Visualization overlay ---\n",
    "    mask = np.zeros_like(roi1)\n",
    "    mask2 = np.zeros_like(roi2)\n",
    "\n",
    "    for old_pt, new_pt in good_pts:\n",
    "        c = tuple(old_pt.astype(int))\n",
    "        a = tuple(new_pt.astype(int))\n",
    "        cv2.circle(mask, c, 1, (0, 0, 255), -1)\n",
    "        cv2.circle(mask2, a, 1, (255, 0, 0), -1)\n",
    "        cv2.line(mask2, c, a, (0, 255, 0), 1)\n",
    "\n",
    "    output_roi1 = cv2.addWeighted(roi1, 1, mask, 1, 0)\n",
    "    output_rgb1 = cv2.cvtColor(output_roi1, cv2.COLOR_BGR2RGB)\n",
    "\n",
    "    output_roi2 = cv2.addWeighted(roi2, 1, mask2, 1, 0)\n",
    "    output_rgb2 = cv2.cvtColor(output_roi2, cv2.COLOR_BGR2RGB)\n",
    "\n",
    "    cv2.circle(output_rgb1, tuple(center_roi1), ball_radius, (0, 255, 0), 1)\n",
    "    cv2.circle(output_rgb2, tuple(center_roi2), ball_radius, (0, 255, 0), 1)\n",
    "\n",
    "    cv2.circle(output_rgb1, tuple(pole2d.astype(int)), 2, (0, 0, 255), -1)\n",
    "    # cv2.circle(output_rgb1, tuple(opp_pole2d.astype(int)), 2, (0, 0, 255), -1)\n",
    "\n",
    "    print(f\"Frame {frame_number}: {len(good_pts)} points used\")\n",
    "\n",
    "    # --- Display results ---\n",
    "    clear_output(wait=True)\n",
    "    fig, axes = plt.subplots(1, 2, figsize=(10, 5))\n",
    "    axes[0].imshow(output_rgb1)\n",
    "    axes[0].axis(\"off\")\n",
    "    axes[0].set_title(f\"Frame {frame_number}\")\n",
    "    axes[1].imshow(output_rgb2)\n",
    "    axes[1].axis(\"off\")\n",
    "    axes[1].set_title(f\"Frame {frame_number + 1}\")\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    display(plt.gcf())\n",
    "    plt.close()\n",
    "    time.sleep(1 / fps)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "b984be6a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved rotation data to /home/davic/projects/IACV_project/bowling-analysis/notebook/spin/intermediate_data/Rotation_data_TEST_5.csv\n"
     ]
    }
   ],
   "source": [
    "output_df = pd.DataFrame(output_data)\n",
    "OUTPUT_CSV_PATH = str(\n",
    "    PROJECT_ROOT\n",
    "    / \"notebook\"\n",
    "    / \"spin\"\n",
    "    / \"intermediate_data\"\n",
    "    / f\"Rotation_data_TEST_{VIDEO_NUMBER}.csv\"\n",
    ")\n",
    "output_df.to_csv(OUTPUT_CSV_PATH, index=False)\n",
    "print(f\"Saved rotation data to {OUTPUT_CSV_PATH}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
