{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "bSu3U-SNu8v6", "outputId": "b5c5357f-9389-4210-ee84-7d97e6fca753" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Requirement already satisfied: huggingface_hub in /usr/local/lib/python3.12/dist-packages (0.36.0)\n", "Requirement already satisfied: filelock in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (3.20.0)\n", "Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (2025.3.0)\n", "Requirement already satisfied: packaging>=20.9 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (25.0)\n", "Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (6.0.3)\n", "Requirement already satisfied: requests in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (2.32.4)\n", "Requirement already satisfied: tqdm>=4.42.1 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (4.67.1)\n", "Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (4.15.0)\n", "Requirement already satisfied: hf-xet<2.0.0,>=1.1.3 in /usr/local/lib/python3.12/dist-packages (from huggingface_hub) (1.2.0)\n", "Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests->huggingface_hub) (3.4.4)\n", "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests->huggingface_hub) (3.11)\n", "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests->huggingface_hub) (2.5.0)\n", "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests->huggingface_hub) (2025.11.12)\n", "Requirement already satisfied: opencv-python in /usr/local/lib/python3.12/dist-packages (4.12.0.88)\n", "Requirement already satisfied: torch in /usr/local/lib/python3.12/dist-packages (2.9.0+cu126)\n", "Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)\n", "Requirement already satisfied: torchvision in /usr/local/lib/python3.12/dist-packages (0.24.0+cu126)\n", "Requirement already satisfied: tqdm in /usr/local/lib/python3.12/dist-packages (4.67.1)\n", "Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)\n", "Requirement already satisfied: ipywidgets in /usr/local/lib/python3.12/dist-packages (7.7.1)\n", "Requirement already satisfied: filelock in /usr/local/lib/python3.12/dist-packages (from torch) (3.20.0)\n", "Requirement already satisfied: typing-extensions>=4.10.0 in /usr/local/lib/python3.12/dist-packages (from torch) (4.15.0)\n", "Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from torch) (75.2.0)\n", "Requirement already satisfied: sympy>=1.13.3 in /usr/local/lib/python3.12/dist-packages (from torch) (1.14.0)\n", "Requirement already satisfied: networkx>=2.5.1 in /usr/local/lib/python3.12/dist-packages (from torch) (3.6.1)\n", "Requirement already satisfied: jinja2 in /usr/local/lib/python3.12/dist-packages (from torch) (3.1.6)\n", "Requirement already satisfied: fsspec>=0.8.5 in /usr/local/lib/python3.12/dist-packages (from torch) (2025.3.0)\n", "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.77)\n", "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.77)\n", "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.6.80 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.80)\n", "Requirement already satisfied: nvidia-cudnn-cu12==9.10.2.21 in /usr/local/lib/python3.12/dist-packages (from torch) (9.10.2.21)\n", "Requirement already satisfied: nvidia-cublas-cu12==12.6.4.1 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.4.1)\n", "Requirement already satisfied: nvidia-cufft-cu12==11.3.0.4 in /usr/local/lib/python3.12/dist-packages (from torch) (11.3.0.4)\n", "Requirement already satisfied: nvidia-curand-cu12==10.3.7.77 in /usr/local/lib/python3.12/dist-packages (from torch) (10.3.7.77)\n", "Requirement already satisfied: nvidia-cusolver-cu12==11.7.1.2 in /usr/local/lib/python3.12/dist-packages (from torch) (11.7.1.2)\n", "Requirement already satisfied: nvidia-cusparse-cu12==12.5.4.2 in /usr/local/lib/python3.12/dist-packages (from torch) (12.5.4.2)\n", "Requirement already satisfied: nvidia-cusparselt-cu12==0.7.1 in /usr/local/lib/python3.12/dist-packages (from torch) (0.7.1)\n", "Requirement already satisfied: nvidia-nccl-cu12==2.27.5 in /usr/local/lib/python3.12/dist-packages (from torch) (2.27.5)\n", "Requirement already satisfied: nvidia-nvshmem-cu12==3.3.20 in /usr/local/lib/python3.12/dist-packages (from torch) (3.3.20)\n", "Requirement already satisfied: nvidia-nvtx-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.77)\n", "Requirement already satisfied: nvidia-nvjitlink-cu12==12.6.85 in /usr/local/lib/python3.12/dist-packages (from torch) (12.6.85)\n", "Requirement already satisfied: nvidia-cufile-cu12==1.11.1.6 in /usr/local/lib/python3.12/dist-packages (from torch) (1.11.1.6)\n", "Requirement already satisfied: triton==3.5.0 in /usr/local/lib/python3.12/dist-packages (from torch) (3.5.0)\n", "Requirement already satisfied: pillow!=8.3.*,>=5.3.0 in /usr/local/lib/python3.12/dist-packages (from torchvision) (11.3.0)\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)\n", "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)\n", "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.3)\n", "Requirement already satisfied: ipykernel>=4.5.1 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (6.17.1)\n", "Requirement already satisfied: ipython-genutils~=0.2.0 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (0.2.0)\n", "Requirement already satisfied: traitlets>=4.3.1 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (5.7.1)\n", "Requirement already satisfied: widgetsnbextension~=3.6.0 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (3.6.10)\n", "Requirement already satisfied: ipython>=4.0.0 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (7.34.0)\n", "Requirement already satisfied: jupyterlab-widgets>=1.0.0 in /usr/local/lib/python3.12/dist-packages (from ipywidgets) (3.0.16)\n", "Requirement already satisfied: debugpy>=1.0 in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (1.8.15)\n", "Requirement already satisfied: jupyter-client>=6.1.12 in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (7.4.9)\n", "Requirement already satisfied: matplotlib-inline>=0.1 in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (0.2.1)\n", "Requirement already satisfied: nest-asyncio in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (1.6.0)\n", "Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (25.0)\n", "Requirement already satisfied: psutil in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (5.9.5)\n", "Requirement already satisfied: pyzmq>=17 in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (26.2.1)\n", "Requirement already satisfied: tornado>=6.1 in /usr/local/lib/python3.12/dist-packages (from ipykernel>=4.5.1->ipywidgets) (6.5.1)\n", "Requirement already satisfied: jedi>=0.16 in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (0.19.2)\n", "Requirement already satisfied: decorator in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (4.4.2)\n", "Requirement already satisfied: pickleshare in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (0.7.5)\n", "Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (3.0.52)\n", "Requirement already satisfied: pygments in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (2.19.2)\n", "Requirement already satisfied: backcall in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (0.2.0)\n", "Requirement already satisfied: pexpect>4.3 in /usr/local/lib/python3.12/dist-packages (from ipython>=4.0.0->ipywidgets) (4.9.0)\n", "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n", "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from sympy>=1.13.3->torch) (1.3.0)\n", "Requirement already satisfied: notebook>=4.4.1 in /usr/local/lib/python3.12/dist-packages (from widgetsnbextension~=3.6.0->ipywidgets) (6.5.7)\n", "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/dist-packages (from jinja2->torch) (3.0.3)\n", "Requirement already satisfied: parso<0.9.0,>=0.8.4 in /usr/local/lib/python3.12/dist-packages (from jedi>=0.16->ipython>=4.0.0->ipywidgets) (0.8.5)\n", "Requirement already satisfied: entrypoints in /usr/local/lib/python3.12/dist-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets) (0.4)\n", "Requirement already satisfied: jupyter-core>=4.9.2 in /usr/local/lib/python3.12/dist-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets) (5.9.1)\n", "Requirement already satisfied: argon2-cffi in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (25.1.0)\n", "Requirement already satisfied: nbformat in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (5.10.4)\n", "Requirement already satisfied: nbconvert>=5 in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (7.16.6)\n", "Requirement already satisfied: Send2Trash>=1.8.0 in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.8.3)\n", "Requirement already satisfied: terminado>=0.8.3 in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.18.1)\n", "Requirement already satisfied: prometheus-client in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.23.1)\n", "Requirement already satisfied: nbclassic>=0.4.7 in /usr/local/lib/python3.12/dist-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.3.3)\n", "Requirement already satisfied: ptyprocess>=0.5 in /usr/local/lib/python3.12/dist-packages (from pexpect>4.3->ipython>=4.0.0->ipywidgets) (0.7.0)\n", "Requirement already satisfied: wcwidth in /usr/local/lib/python3.12/dist-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=4.0.0->ipywidgets) (0.2.14)\n", "Requirement already satisfied: platformdirs>=2.5 in /usr/local/lib/python3.12/dist-packages (from jupyter-core>=4.9.2->jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets) (4.5.1)\n", "Requirement already satisfied: notebook-shim>=0.2.3 in /usr/local/lib/python3.12/dist-packages (from nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.2.4)\n", "Requirement already satisfied: beautifulsoup4 in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (4.13.5)\n", "Requirement already satisfied: bleach!=5.0.0 in /usr/local/lib/python3.12/dist-packages (from bleach[css]!=5.0.0->nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (6.3.0)\n", "Requirement already satisfied: defusedxml in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.7.1)\n", "Requirement already satisfied: jupyterlab-pygments in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.3.0)\n", "Requirement already satisfied: mistune<4,>=2.0.3 in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (3.1.4)\n", "Requirement already satisfied: nbclient>=0.5.0 in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.10.2)\n", "Requirement already satisfied: pandocfilters>=1.4.1 in /usr/local/lib/python3.12/dist-packages (from nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.5.1)\n", "Requirement already satisfied: fastjsonschema>=2.15 in /usr/local/lib/python3.12/dist-packages (from nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2.21.2)\n", "Requirement already satisfied: jsonschema>=2.6 in /usr/local/lib/python3.12/dist-packages (from nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (4.25.1)\n", "Requirement already satisfied: argon2-cffi-bindings in /usr/local/lib/python3.12/dist-packages (from argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (25.1.0)\n", "Requirement already satisfied: webencodings in /usr/local/lib/python3.12/dist-packages (from bleach!=5.0.0->bleach[css]!=5.0.0->nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.5.1)\n", "Requirement already satisfied: tinycss2<1.5,>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from bleach[css]!=5.0.0->nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.4.0)\n", "Requirement already satisfied: attrs>=22.2.0 in /usr/local/lib/python3.12/dist-packages (from jsonschema>=2.6->nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (25.4.0)\n", "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /usr/local/lib/python3.12/dist-packages (from jsonschema>=2.6->nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2025.9.1)\n", "Requirement already satisfied: referencing>=0.28.4 in /usr/local/lib/python3.12/dist-packages (from jsonschema>=2.6->nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.37.0)\n", "Requirement already satisfied: rpds-py>=0.7.1 in /usr/local/lib/python3.12/dist-packages (from jsonschema>=2.6->nbformat->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.30.0)\n", "Requirement already satisfied: jupyter-server<3,>=1.8 in /usr/local/lib/python3.12/dist-packages (from notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2.14.0)\n", "Requirement already satisfied: cffi>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from argon2-cffi-bindings->argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2.0.0)\n", "Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.12/dist-packages (from beautifulsoup4->nbconvert>=5->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2.8)\n", "Requirement already satisfied: pycparser in /usr/local/lib/python3.12/dist-packages (from cffi>=1.0.1->argon2-cffi-bindings->argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (2.23)\n", "Requirement already satisfied: anyio>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (4.12.0)\n", "Requirement already satisfied: jupyter-events>=0.9.0 in /usr/local/lib/python3.12/dist-packages (from jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.12.0)\n", "Requirement already satisfied: jupyter-server-terminals>=0.4.4 in /usr/local/lib/python3.12/dist-packages (from jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.5.3)\n", "Requirement already satisfied: overrides>=5.0 in /usr/local/lib/python3.12/dist-packages (from jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (7.7.0)\n", "Requirement already satisfied: websocket-client>=1.7 in /usr/local/lib/python3.12/dist-packages (from jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.9.0)\n", "Requirement already satisfied: idna>=2.8 in /usr/local/lib/python3.12/dist-packages (from anyio>=3.1.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (3.11)\n", "Requirement already satisfied: python-json-logger>=2.0.4 in /usr/local/lib/python3.12/dist-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (4.0.0)\n", "Requirement already satisfied: pyyaml>=5.3 in /usr/local/lib/python3.12/dist-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (6.0.3)\n", "Requirement already satisfied: rfc3339-validator in /usr/local/lib/python3.12/dist-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.1.4)\n", "Requirement already satisfied: rfc3986-validator>=0.1.1 in /usr/local/lib/python3.12/dist-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (0.1.1)\n", "Requirement already satisfied: fqdn in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.5.1)\n", "Requirement already satisfied: isoduration in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (20.11.0)\n", "Requirement already satisfied: jsonpointer>1.13 in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (3.0.0)\n", "Requirement already satisfied: rfc3987-syntax>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.1.0)\n", "Requirement already satisfied: uri-template in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.3.0)\n", "Requirement already satisfied: webcolors>=24.6.0 in /usr/local/lib/python3.12/dist-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (25.10.0)\n", "Requirement already satisfied: lark>=1.2.2 in /usr/local/lib/python3.12/dist-packages (from rfc3987-syntax>=1.1.0->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.3.1)\n", "Requirement already satisfied: arrow>=0.15.0 in /usr/local/lib/python3.12/dist-packages (from isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=1.8->notebook-shim>=0.2.3->nbclassic>=0.4.7->notebook>=4.4.1->widgetsnbextension~=3.6.0->ipywidgets) (1.4.0)\n", "Requirement already satisfied: lap in /usr/local/lib/python3.12/dist-packages (0.5.12)\n", "Requirement already satisfied: numpy>=1.21.6 in /usr/local/lib/python3.12/dist-packages (from lap) (2.0.2)\n", "Requirement already satisfied: ultralytics in /usr/local/lib/python3.12/dist-packages (8.3.248)\n", "Requirement already satisfied: numpy>=1.23.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (2.0.2)\n", "Requirement already satisfied: matplotlib>=3.3.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (3.10.0)\n", "Requirement already satisfied: opencv-python>=4.6.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (4.12.0.88)\n", "Requirement already satisfied: pillow>=7.1.2 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (11.3.0)\n", "Requirement already satisfied: pyyaml>=5.3.1 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (6.0.3)\n", "Requirement already satisfied: requests>=2.23.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (2.32.4)\n", "Requirement already satisfied: scipy>=1.4.1 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (1.16.3)\n", "Requirement already satisfied: torch>=1.8.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (2.9.0+cu126)\n", "Requirement already satisfied: torchvision>=0.9.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (0.24.0+cu126)\n", "Requirement already satisfied: psutil>=5.8.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (5.9.5)\n", "Requirement already satisfied: polars>=0.20.0 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (1.31.0)\n", "Requirement already satisfied: ultralytics-thop>=2.0.18 in /usr/local/lib/python3.12/dist-packages (from ultralytics) (2.0.18)\n", "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (1.3.3)\n", "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (0.12.1)\n", "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (4.61.1)\n", "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (1.4.9)\n", "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (25.0)\n", "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (3.2.5)\n", "Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.12/dist-packages (from matplotlib>=3.3.0->ultralytics) (2.9.0.post0)\n", "Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests>=2.23.0->ultralytics) (3.4.4)\n", "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests>=2.23.0->ultralytics) (3.11)\n", "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests>=2.23.0->ultralytics) (2.5.0)\n", "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests>=2.23.0->ultralytics) (2025.11.12)\n", "Requirement already satisfied: filelock in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (3.20.0)\n", "Requirement already satisfied: typing-extensions>=4.10.0 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (4.15.0)\n", "Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (75.2.0)\n", "Requirement already satisfied: sympy>=1.13.3 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (1.14.0)\n", "Requirement already satisfied: networkx>=2.5.1 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (3.6.1)\n", "Requirement already satisfied: jinja2 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (3.1.6)\n", "Requirement already satisfied: fsspec>=0.8.5 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (2025.3.0)\n", "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.77)\n", "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.77)\n", "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.6.80 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.80)\n", "Requirement already satisfied: nvidia-cudnn-cu12==9.10.2.21 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (9.10.2.21)\n", "Requirement already satisfied: nvidia-cublas-cu12==12.6.4.1 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.4.1)\n", "Requirement already satisfied: nvidia-cufft-cu12==11.3.0.4 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (11.3.0.4)\n", "Requirement already satisfied: nvidia-curand-cu12==10.3.7.77 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (10.3.7.77)\n", "Requirement already satisfied: nvidia-cusolver-cu12==11.7.1.2 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (11.7.1.2)\n", "Requirement already satisfied: nvidia-cusparse-cu12==12.5.4.2 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.5.4.2)\n", "Requirement already satisfied: nvidia-cusparselt-cu12==0.7.1 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (0.7.1)\n", "Requirement already satisfied: nvidia-nccl-cu12==2.27.5 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (2.27.5)\n", "Requirement already satisfied: nvidia-nvshmem-cu12==3.3.20 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (3.3.20)\n", "Requirement already satisfied: nvidia-nvtx-cu12==12.6.77 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.77)\n", "Requirement already satisfied: nvidia-nvjitlink-cu12==12.6.85 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (12.6.85)\n", "Requirement already satisfied: nvidia-cufile-cu12==1.11.1.6 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (1.11.1.6)\n", "Requirement already satisfied: triton==3.5.0 in /usr/local/lib/python3.12/dist-packages (from torch>=1.8.0->ultralytics) (3.5.0)\n", "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.7->matplotlib>=3.3.0->ultralytics) (1.17.0)\n", "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from sympy>=1.13.3->torch>=1.8.0->ultralytics) (1.3.0)\n", "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/dist-packages (from jinja2->torch>=1.8.0->ultralytics) (3.0.3)\n" ] } ], "source": [ "# ===== INSTALL DEPENDENCIES =====\n", "!pip install huggingface_hub\n", "!pip install boto3 -q\n", "!pip install opencv-python torch numpy torchvision tqdm pandas ipywidgets\n", "!pip install lap\n", "!pip install ultralytics" ] }, { "cell_type": "markdown", "metadata": { "id": "PMr99Yo7x8N1" }, "source": [ "# Please double, triple, quadruple check that the below code runs without errors before submitting." ] }, { "cell_type": "markdown", "metadata": { "id": "4YhoE1nF2Pee" }, "source": [ "## TODO 1 - Enter your HuggingFace username below:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "ENyfncieqs6i" }, "outputs": [], "source": [ "hf_username = \"maatt4face\"" ] }, { "cell_type": "markdown", "metadata": { "id": "0lC0jUquq_06" }, "source": [ "## TODO 2 - Define your model EXACTLY as you did in your training code (otherwise there will be errors, and, possibly, tears).\n", "\n", "Note below the classname is 'YourModelArchitecture'. That's because it literally needs to be YOUR MODEL ARCHITECTURE. This class definition is later referred to below in the 'load_model_from_hub' method. The architecture must match here, or it will not be able to instantiate the model weights correctly once it downloads them from HuggingFace. Pay very close attention to getting this right, please.\n", "\n", "Replace the below code, and replace the corresponding line in the 'load_model_from_hub' method." ] }, { "cell_type": "markdown", "metadata": { "id": "gX0WITdlisN1" }, "source": [ "### Parameters and Global Variables" ] }, { "cell_type": "code", "source": [ "# Import the required libraries\n", "import torch\n", "import torch.nn as nn\n", "from torch.utils.data import Dataset, DataLoader\n", "from huggingface_hub import hf_hub_download\n", "import boto3\n", "from botocore import UNSIGNED\n", "from botocore.config import Config\n", "import os\n", "import cv2\n", "import numpy as np\n", "from tqdm import tqdm\n", "import time" ], "metadata": { "id": "sjIQLKZAj2fz" }, "execution_count": 3, "outputs": [] }, { "cell_type": "code", "execution_count": 4, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "jtZ65UWcisN1", "outputId": "bf6e9791-300e-4975-f87b-42ad8caaf317" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Device: cuda\n", "Training Start Time (UNIX): 1767655163\n" ] } ], "source": [ "# Admin =================================================================================\n", "import os\n", "DOWNLOAD_DIR = 'test-data'\n", "CACHE_DIR = 'cache-data'\n", "WEIGHTS_DIR = 'model-weights'\n", "os.makedirs(DOWNLOAD_DIR, exist_ok=True)\n", "os.makedirs(CACHE_DIR, exist_ok=True)\n", "os.makedirs(WEIGHTS_DIR, exist_ok=True)\n", "\n", "# To GPU or not to GPU =================================================================\n", "import torch\n", "DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", "print(f\"Device: {DEVICE}\")\n", "\n", "# Landmark Extraction ==================================================================\n", "from ultralytics import YOLO\n", "YOLO_DIR = './yolo-weights'\n", "os.makedirs(YOLO_DIR, exist_ok=True)\n", "YOLO_MODEL = \"yolov8s\"\n", "YOLO_MODEL_FILE = os.path.join(YOLO_DIR, f\"{YOLO_MODEL}-pose.pt\")\n", "#assert os.path.exists(YOLO_MODEL_FILE)\n", "YOLO_TRACKER = \"bytetrack.yaml\"\n", "YOLO_TRACKER_FILE = os.path.join(YOLO_DIR, YOLO_TRACKER)\n", "#assert os.path.exists(YOLO_TRACKER_FILE)\n", "\n", "# Model Stuff ===========================================================================\n", "DEBUG = False\n", "RAND_SEED = 4227\n", "VID_STRIDE = 2\n", "MAX_FRAMES = 1000\n", "MAX_EPOCHS = 150\n", "MAX_FRAME_LENGTH = 640\n", "HUGGING_FACE_REPO_ID = f\"{hf_username}/mv-final-assignment\"\n", "\n", "# Mandatory timestamps for the 5-hour limit\n", "print(f\"Training Start Time (UNIX): {int(time.time())}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "2z-u_-hmisN2" }, "source": [ "### Model" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "id": "cm-y1pPnOGkK" }, "outputs": [], "source": [ "import torch\n", "import torch.nn as nn\n", "from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence\n", "import torch.nn.init as init\n", "\n", "class PushupCounterSingleOutput(nn.Module):\n", " def __init__(self, input_size=1, hidden_size=64):\n", " super().__init__()\n", " self.bn = nn.BatchNorm1d(input_size)\n", " self.lstm = nn.LSTM(\n", " input_size,\n", " hidden_size,\n", " bidirectional=True,\n", " batch_first=True\n", " )\n", "\n", " # Regression head to map the 'essence' of the video to a count\n", " self.regressor = nn.Sequential(\n", " nn.Linear(hidden_size * 2, 64),\n", " nn.ReLU(),\n", " nn.Linear(64, 1) # Output: Single scalar\n", " )\n", "\n", " # Apply custom initialization\n", " self.apply(self._init_weights)\n", "\n", " def _init_weights(self, module):\n", " if isinstance(module, nn.Linear):\n", " # He initialization for Linear layers\n", " init.kaiming_normal_(module.weight, mode='fan_out', nonlinearity='relu')\n", " if module.bias is not None:\n", " init.constant_(module.bias, 0)\n", "\n", " elif isinstance(module, nn.LSTM):\n", " # Orthogonal initialization for recurrent weights\n", " for name, param in module.named_parameters():\n", " if 'weight_ih' in name:\n", " init.xavier_uniform_(param.data)\n", " elif 'weight_hh' in name:\n", " init.orthogonal_(param.data)\n", " elif 'bias' in name:\n", " init.constant_(param.data, 0)\n", " # Forget gate bias initialization (standard trick for LSTMs)\n", " n = param.size(0)\n", " param.data[n//4:n//2].fill_(1.0)\n", "\n", " elif isinstance(module, nn.BatchNorm1d):\n", " # Standard BN initialization\n", " init.constant_(module.weight, 1)\n", " init.constant_(module.bias, 0)\n", "\n", " def forward(self, data, lengths):\n", " # If input is (Batch, SeqLen), turn it into (Batch, SeqLen, 1)\n", " if data.dim() == 2:\n", " data = data.unsqueeze(-1)\n", "\n", " # 1. Normalize (ignoring camera angle effects)\n", " data = data.transpose(1, 2)\n", " data = self.bn(data)\n", " data = data.transpose(1, 2)\n", "\n", " # 2. Pack the padded sequence\n", " packed_x = pack_padded_sequence(data, lengths.cpu(), batch_first=True, enforce_sorted=False)\n", " packed_out, (hn, cn) = self.lstm(packed_x)\n", "\n", " # 3. Use the Final Hidden State\n", " # In a Bi-LSTM, the final context is the concatenation of:\n", " # - The last hidden state of the forward pass\n", " # - The first hidden state of the backward pass\n", " # This represents the 'summary' of the entire erratic video.\n", "\n", " # hn shape: (num_layers * num_directions, batch, hidden_size)\n", " # Extract the last layer's forward and backward hidden states\n", " h_forward = hn[-2, :, :]\n", " h_backward = hn[-1, :, :]\n", " combined = torch.cat((h_forward, h_backward), dim=1) # (Batch, hidden_size * 2)\n", "\n", " # 4. Predict the final count\n", " count = self.regressor(combined)\n", " return count\n" ] }, { "cell_type": "markdown", "metadata": { "id": "R6OEci-2isN2" }, "source": [ "### Secret Sauce" ] }, { "cell_type": "markdown", "metadata": { "id": "zhXVPMi6isN2" }, "source": [ "#### Transformation" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "id": "FSFBSh2BisN2" }, "outputs": [], "source": [ "import torchvision.transforms.functional as TF\n", "import random\n", "import torch # Needed for TF.to_tensor which uses torch internally\n", "\n", "\n", "class VideoResizerSingle:\n", " def __init__(self, largest_dim):\n", " # resize such that aspect ratio is retained\n", " self.largest_dim = largest_dim\n", "\n", " def __call__(self, single_frame):\n", " original_width, original_height = single_frame.size\n", " scale = max(self.largest_dim/original_width, self.largest_dim/original_height)\n", " new_width = int(original_width * scale)\n", " new_height = int(original_height * scale)\n", " single_frame = TF.resize(single_frame, (new_height, new_width))\n", " return single_frame\n", "\n", "\n", "class VideoResizer:\n", " def __init__(self, largest_dim):\n", " self.largest_dim = largest_dim\n", "\n", " def __call__(self, frames_list_pil):\n", " if not frames_list_pil:\n", " return []\n", "\n", " transformed_frames_pil = []\n", " helper = VideoResizerSingle(self.largest_dim)\n", " for img_pil in frames_list_pil:\n", " img_pil = helper(img_pil)\n", " transformed_frames_pil.append(img_pil)\n", " return transformed_frames_pil\n", "\n", "\n", "class VideoTransform:\n", " def __init__(self, rotation_degrees, hflip_p, vflip_p, color_jitter_params, resize_dims=None):\n", " self.rotation_degrees = rotation_degrees\n", " self.hflip_p = hflip_p\n", " self.vflip_p = vflip_p\n", " self.color_jitter_params = color_jitter_params # (brightness, contrast, saturation, hue)\n", " self.resize_dims = resize_dims # (height, width)\n", "\n", " def __call__(self, frames_list_pil):\n", " if not frames_list_pil:\n", " return []\n", "\n", " # Sample parameters once per video\n", " angle = random.uniform(-self.rotation_degrees, self.rotation_degrees)\n", " h_flip = random.random() < self.hflip_p\n", " v_flip = random.random() < self.vflip_p\n", "\n", " # Manually compute ColorJitter factors for consistency across frames\n", " brightness_param = self.color_jitter_params[0]\n", " contrast_param = self.color_jitter_params[1]\n", " saturation_param = self.color_jitter_params[2]\n", " hue_param = self.color_jitter_params[3]\n", "\n", " brightness_factor = random.uniform(max(0, 1 - brightness_param), 1 + brightness_param)\n", " contrast_factor = random.uniform(max(0, 1 - contrast_param), 1 + contrast_param)\n", " saturation_factor = random.uniform(max(0, 1 - saturation_param), 1 + saturation_param)\n", " hue_factor = random.uniform(-hue_param, hue_param)\n", "\n", "\n", " transformed_frames_pil = [] # Changed variable name to reflect return type\n", " for img_pil in frames_list_pil:\n", " # Apply same random parameters to each frame\n", " img_pil = TF.rotate(img_pil, angle)\n", " if h_flip:\n", " img_pil = TF.hflip(img_pil)\n", " if v_flip:\n", " img_pil = TF.vflip(img_pil)\n", " img_pil = TF.adjust_brightness(img_pil, brightness_factor)\n", " img_pil = TF.adjust_contrast(img_pil, contrast_factor)\n", " img_pil = TF.adjust_saturation(img_pil, saturation_factor)\n", " img_pil = TF.adjust_hue(img_pil, hue_factor)\n", "\n", " # Resize while maintaining aspect ratio\n", " if self.resize_dims:\n", " original_width, original_height = img_pil.size\n", " target_height, target_width = self.resize_dims\n", "\n", " # Calculate new dimensions to fit within target while maintaining aspect ratio\n", " scale_w = target_width / original_width\n", " scale_h = target_height / original_height\n", " scale = min(scale_w, scale_h)\n", "\n", " new_width = int(original_width * scale)\n", " new_height = int(original_height * scale)\n", "\n", " img_pil = TF.resize(img_pil, (new_height, new_width))\n", "\n", " # Pad if necessary to reach target_dims\n", " pad_left = (target_width - new_width) // 2\n", " pad_right = target_width - new_width - pad_left\n", " pad_top = (target_height - new_height) // 2\n", " pad_bottom = target_height - new_height - pad_top\n", "\n", " img_pil = TF.pad(img_pil, (pad_left, pad_top, pad_right, pad_bottom))\n", "\n", " transformed_frames_pil.append(img_pil) # Append PIL image directly, remove TF.to_tensor\n", "\n", " return transformed_frames_pil # Return list of PIL images\n", "\n", "TRANSFORMATIONS = VideoTransform(\n", " rotation_degrees=180,\n", " hflip_p=0.5,\n", " vflip_p=0.5,\n", " color_jitter_params=(0.2, 0.2, 0.2, 0.2),\n", " resize_dims=(640, 640) # Example: resize to 224x224\n", ")\n", "\n", "RESIZER = VideoResizerSingle(MAX_FRAME_LENGTH)" ] }, { "cell_type": "markdown", "metadata": { "id": "GeoiEjgGisN3" }, "source": [ "#### Signal Calculators and Utils" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "id": "Ppmdr9qxisN3" }, "outputs": [], "source": [ "import pandas as pd\n", "import torch\n", "import torch.nn as nn\n", "import numpy as np\n", "\n", "class SignalPerturbator:\n", " @staticmethod\n", " def __call__(self, signals):\n", " \"\"\"Applies dynamic 1D augmentation to the signal sequence.\"\"\"\n", " # Add Gaussian noise\n", " noise = torch.randn_like(signals) * 0.01\n", " # Random scaling (simulates different range of motion)\n", " scale = random.uniform(0.9, 1.1)\n", " # Random baseline shift\n", " shift = random.uniform(-0.05, 0.05)\n", " return torch.clamp(signals * scale + noise + shift, 0, 1)\n", "\n", "\n", "class SignalAverager:\n", " @staticmethod\n", " def __call__(signal_tensor, window_size=3):\n", " \"\"\"\n", " Applies a moving average to smooth signals.\n", " \"\"\"\n", " # 1. Ensure the tensor is at least 2D (Batch, Length)\n", " if signal_tensor.dim() == 1:\n", " signal_tensor = signal_tensor.unsqueeze(0)\n", "\n", " # 2. Reshape to (Batch, Channels, Length) for avg_pool1d\n", " # We treat the elbow angle as a single channel\n", " x = signal_tensor.unsqueeze(1)\n", "\n", " # 3. Apply average pooling\n", " # stride=1 keeps the resolution the same\n", " # padding=1 ensures the output length matches the input length\n", " smoothed = nn.functional.avg_pool1d(x, kernel_size=window_size, stride=1, padding=window_size//2)\n", "\n", " # 4. Remove the extra channel dimension and return\n", " return smoothed.squeeze(1)\n", "\n", "class SignalMedianator:\n", " @staticmethod\n", " def __call__(signal_tensor, window_size=3):\n", " \"\"\"\n", " Applies a moving media to smooth the signals.\n", " Good for bring out local signals dwarfed by global normalization\n", " \"\"\"\n", " def moving_median(data, window):\n", " return np.array([np.median(data[max(0, i-window//2):min(len(data), i+window//2+1)])\n", " for i in range(len(data))])\n", " smoothed = moving_median(signal_tensor, window_size)\n", " mean = np.mean(smoothed)\n", " std = np.std(smoothed) + 1e-6\n", " smoothed = (smoothed - mean) / std\n", " return torch.tensor(smoothed, dtype=torch.float32)" ] }, { "cell_type": "markdown", "metadata": { "id": "rppHgwaxisN3" }, "source": [ "#### Elbow Calculation" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "id": "MiihAqg9isN3" }, "outputs": [], "source": [ "import numpy as np\n", "from enum import Enum\n", "\n", "\n", "class COCO(Enum):\n", " R_SHOULDER = 5\n", " L_SHOULDER = 6\n", " R_ELBOW = 7\n", " L_ELBOW = 8\n", " R_WRIST = 9\n", " L_WRIST = 10\n", " L_HIP = 11\n", " R_HIP = 12\n", " L_KNEE = 13\n", " R_KNEE = 14\n", "\n", "\n", "class AngleCalculator:\n", " \"\"\"Calculates the elbow angle of a person from landmarks.\"\"\"\n", " def __init__(self, landmarks) -> None:\n", " # landmarks as returned by Ultralytics: np.ndarray[np._AnyShape, np.dtype[np.Any]] | np.Any\n", " self.landmarks = landmarks\n", "\n", " def _ref_len(self):\n", " # Determine Scale Reference (Hips or Fallback to Shoulder Width)\n", " if self.landmarks[COCO.L_HIP.value].any() and self.landmarks[COCO.R_HIP.value].any():\n", " ref_len = np.linalg.norm(self.landmarks[COCO.L_SHOULDER.value] - self.landmarks[COCO.L_HIP.value])\n", " else:\n", " ref_len = np.linalg.norm(self.landmarks[COCO.L_SHOULDER.value] - self.landmarks[COCO.R_SHOULDER.value]) / 0.75\n", " return ref_len\n", "\n", " def _uplift_arm_to_3d(self, sh_idx, el_idx, wr_idx, ref_len):\n", " \"\"\"Uplifts 2D keypoints to 3D using Da Vinci's ratios\"\"\"\n", " # Ratios (relative to torso length)\n", " R_UPPER_ARM = 0.45\n", " R_FOREARM = 0.42\n", "\n", " # 1. Shoulder is the root (z=0)\n", " sh_3d = np.array([self.landmarks[sh_idx][0], self.landmarks[sh_idx][1], 0.0])\n", "\n", " # 2. Lift Elbow (Relative to Shoulder)\n", " L_upper = R_UPPER_ARM * ref_len\n", " dx1 = self.landmarks[el_idx][0] - self.landmarks[sh_idx][0]\n", " dy1 = self.landmarks[el_idx][1] - self.landmarks[sh_idx][1]\n", " dz1 = np.sqrt(max(0, L_upper**2 - (dx1**2 + dy1**2)))\n", " el_3d = np.array([self.landmarks[el_idx][0], self.landmarks[el_idx][1], dz1])\n", "\n", " # 3. Lift Wrist (Relative to Elbow)\n", " L_fore = R_FOREARM * ref_len\n", " dx2 = self.landmarks[wr_idx][0] - self.landmarks[el_idx][0]\n", " dy2 = self.landmarks[wr_idx][1] - self.landmarks[el_idx][1]\n", " dz2 = np.sqrt(max(0, L_fore**2 - (dx2**2 + dy2**2)))\n", " # Z-coordinate is cumulative\n", " wr_3d = np.array([self.landmarks[wr_idx][0], self.landmarks[wr_idx][1], dz1 + dz2])\n", "\n", " return sh_3d, el_3d, wr_3d\n", "\n", " def __call__(self) -> float:\n", " def get_angle(a, b, c) -> float:\n", " ba, bc = a - b, c - b\n", " cosine = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))\n", " return np.degrees(np.arccos(np.clip(cosine, -1.0, 1.0)))\n", "\n", " ref_len = self._ref_len()\n", "\n", " # Calculate elbow angle for both arms\n", " l_sh, l_el, l_wr = self._uplift_arm_to_3d(COCO.L_SHOULDER.value, COCO.L_ELBOW.value, COCO.L_WRIST.value, ref_len)\n", " r_sh, r_el, r_wr = self._uplift_arm_to_3d(COCO.R_SHOULDER.value, COCO.R_ELBOW.value, COCO.R_WRIST.value, ref_len)\n", "\n", " l_angle = get_angle(l_sh, l_el, l_wr)\n", " r_angle = get_angle(r_sh, r_el, r_wr)\n", "\n", " # Use average angle for robustness (handles side-on views better)\n", " avg_angle = (l_angle + r_angle) / 2\n", " return avg_angle" ] }, { "cell_type": "markdown", "source": [ "#### Adhoc Code" ], "metadata": { "id": "aysexnGW37EM" } }, { "cell_type": "code", "source": [ "import psutil\n", "import os\n", "import gc\n", "import ctypes\n", "import platform\n", "\n", "class Janitor:\n", " @staticmethod\n", " def __clean_system_memory():\n", " \"\"\"\n", " Forces the system to release unreferenced memory back to the OS.\n", " Crucial for staying under the 12GB RAM limit in Colab.\n", " \"\"\"\n", " # 1. Force Python's Garbage Collector\n", " # This deletes Python objects that are no longer referenced\n", " gc.collect()\n", "\n", " # 2. Force Libc to release memory back to the OS (Linux/Colab only)\n", " # Python's allocator usually keeps freed memory for future use.\n", " # malloc_trim(0) forces it to give that memory back to the system immediately.\n", " if platform.system() == \"Linux\":\n", " try:\n", " libc = ctypes.CDLL(\"libc.so.6\")\n", " libc.malloc_trim(0)\n", " except (OSError, AttributeError):\n", " pass # Ignore if we can't find libc (e.g., on Windows/Mac)\n", "\n", " # Optional: Verification\n", " # You can verify the drop in RAM using psutil if you wish\n", " # import psutil\n", " # process = psutil.Process(os.getpid())\n", " # print(f\"System RAM used: {process.memory_info().rss / 1e9:.2f} GB\")\n", "\n", " @staticmethod\n", " def smart_cleanup(threshold_gb=8):\n", " \"\"\"\n", " Only releases memory if usage exceeds threshold_gb.\n", " Prevents unnecessary latency overhead.\n", " \"\"\"\n", " process = psutil.Process(os.getpid())\n", " current_usage = process.memory_info().rss / (1024 ** 3) # GB\n", "\n", " if current_usage > threshold_gb:\n", " # Calls the deep clean function provided earlier\n", " Janitor.__clean_system_memory()\n", " # Optional: Print for your report analysis\n", " # print(f\"Memory cleaned. Was at {current_usage:.2f} GB\")" ], "metadata": { "id": "cH7-Ild239hO" }, "execution_count": 9, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "qahq0xG2rs4h" }, "source": [ "## Download the test data from s3, and create the corresponding dataset + dataloader.\n", "\n", "There's no TODO for you here. This text is just here to explain to you what this code does.\n", "\n", "In this instance, the test data IS the training data you were provided in the Model Training notebook. This is by design. You do not have access to the test data. This is a simple check to make sure the mechanics of this notebook work.\n", "\n", "You should achieve the same accuracy here in this notebook, as you did in your previous notebook (random seed notwithstanding)." ] }, { "cell_type": "markdown", "metadata": { "id": "_Fw6Rn1JisN3" }, "source": [ "### Dataset" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "id": "XBukVn9qrnFZ", "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "outputId": "3005a19c-5923-4064-a56d-5647d0c80d50" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Downloading test data:\n", "\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 77/77 [00:00<00:00, 23414.88it/s]" ] }, { "output_type": "stream", "name": "stdout", "text": [ "File already exists, skipping: 1_dksksjfwijf.mp4\n", "File already exists, skipping: 2_dfsaeklnvvalkej.mp4\n", "File already exists, skipping: 2_difficult_2.mp4\n", "File already exists, skipping: 2_difficult_sdafkljsalkfj.mp4\n", "File already exists, skipping: 2_dkdjwkndkfw.mp4\n", "File already exists, skipping: 2_dkdmkejkeimdh.mp4\n", "File already exists, skipping: 2_dkjd823kjf.mp4\n", "File already exists, skipping: 2_dsalkfjalwkenlke.mp4\n", "File already exists, skipping: 2_kling_20251205_Text_to_Video_On_a_sandy_4976_0.mp4\n", "File already exists, skipping: 2_kling_20251206_Text_to_Video_Generate_a_71_1.mp4\n", "File already exists, skipping: 2_sadfasjldkfjaseifj.mp4\n", "File already exists, skipping: 2_sdafkjaslkclaksdjkas.mp4\n", "File already exists, skipping: 2_sdfkjsaleijflaskdjf.mp4\n", "File already exists, skipping: 2_sdjfhafsldkjhjk.mp4\n", "File already exists, skipping: 2_sdkjdsflkjfwa.mp4\n", "File already exists, skipping: 2_sdlfjlewlkjkj.mp4\n", "File already exists, skipping: 2_sdlkjsaelijfksdjf.mp4\n", "File already exists, skipping: 3_asldkfjalwieaskdfaskdf.mp4\n", "File already exists, skipping: 3_dkk873lkjlksajdf.mp4\n", "File already exists, skipping: 3_dsjlaeijlksjdfie.mp4\n", "File already exists, skipping: 3_dsksdfjbvsdkj.mp4\n", "File already exists, skipping: 3_dslkaldskjflakjs.mp4\n", "File already exists, skipping: 3_ewdfkjwaeoihjlkasdjf.mp4\n", "File already exists, skipping: 3_kling_20251205_Text_to_Video_In_a_grass_4697_0.mp4\n", "File already exists, skipping: 3_kling_20251205_Text_to_Video_On_a_playg_5028_0.mp4\n", "File already exists, skipping: 3_kling_20251205_Text_to_Video_On_a_playg_5064_0.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_17_0.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_315_0.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_315_2.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_712_3.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_71_0.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_71_2.mp4\n", "File already exists, skipping: 3_kling_20251206_Text_to_Video_Generate_a_71_3.mp4\n", "File already exists, skipping: 3_kling_20251209_Image_to_Video_Generate_a_613_1.mp4\n", "File already exists, skipping: 3_kling_20251209_Image_to_Video_Generate_a_635_0.mp4\n", "File already exists, skipping: 3_kling_20251209_Text_to_Video_Generate_a_190_1.mp4\n", "File already exists, skipping: 3_kling_20251209_Text_to_Video_Generate_a_403_1.mp4\n", "File already exists, skipping: 3_kling_20251209_Text_to_Video_Generate_a_491_0.mp4\n", "File already exists, skipping: 3_kling_20251209_Text_to_Video_Generate_a_491_1.mp4\n", "File already exists, skipping: 3_kling_20251209_Text_to_Video_Generate_a_491_2.mp4\n", "File already exists, skipping: 3_kling_dskfseu.mp4\n", "File already exists, skipping: 3_kling_kdjflaskdjf.mp4\n", "File already exists, skipping: 3_sadklfjasbnlkjlfkj.mp4\n", "File already exists, skipping: 3_sadlfkjasldkfjasleijlkjfd.mp4\n", "File already exists, skipping: 3_sadlfkjawelnflksdjf.mp4\n", "File already exists, skipping: 3_sdfjwaiejflkasjdf.mp4\n", "File already exists, skipping: 3_sdflkjliejkjdf.mp4\n", "File already exists, skipping: 3_sdlkfjaleknaksej.mp4\n", "File already exists, skipping: 3_sdlkfjalkjejafe.mp4\n", "File already exists, skipping: 3_sdlkjfaslkjfalskjdf.mp4\n", "File already exists, skipping: 3_sdlkjslndflkseijlkjef.mp4\n", "File already exists, skipping: 4_20251209_Text_to_Video_Generate_a_561_0.mp4\n", "File already exists, skipping: 4_asdlkfjalsflnekj.mp4\n", "File already exists, skipping: 4_aslkcasckmwlejk.mp4\n", "File already exists, skipping: 4_aslkjasmcalkewjlkje.mp4\n", "File already exists, skipping: 4_dssalsdkfjweijf.mp4\n", "File already exists, skipping: 4_kling_20251206_Text_to_Video_Generate_a_28_0.mp4\n", "File already exists, skipping: 4_kling_20251206_Text_to_Video_Generate_a_315_3.mp4\n", "File already exists, skipping: 4_kling_20251206_Text_to_Video_Generate_a_58_0.mp4\n", "File already exists, skipping: 4_kling_20251207_Text_to_Video_Generate_a_521_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Image_to_Video_Generate_a_635_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_190_0.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_218_0.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_263_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_377_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_452_0.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_452_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_561_1.mp4\n", "File already exists, skipping: 4_kling_20251209_Text_to_Video_Generate_a_588_2.mp4\n", "File already exists, skipping: 4_pushup_1f2da596-7619-4d55-9376-069e15a42a1a_h264.mp4\n", "File already exists, skipping: 4_sadflkjasldkjfalseij.mp4\n", "File already exists, skipping: 4_sadlfkjlknewkjejk.mp4\n", "File already exists, skipping: 5_sadfjhaslfkjasdlkfjsa.mp4\n", "File already exists, skipping: 5_sdfkljweoijlkjdsflkjweaij.mp4\n", "File already exists, skipping: 6_dfjewaijsldkjfsaef.mp4\n", "File already exists, skipping: 6_kling_20251209_Text_to_Video_Generate_a_218_1.mp4\n", "File already exists, skipping: 7_sadkjfkljekj.mp4\n", "\n", "Downloaded 77 test videos\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ "'test-data'" ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" } }, "metadata": {}, "execution_count": 10 } ], "source": [ "# =============================================================================\n", "# DOWNLOAD TEST DATA FROM S3\n", "# =============================================================================\n", "def download_test_data(bucket_name='training-and-validation-data', download_dir=DOWNLOAD_DIR):\n", " s3 = boto3.client('s3', config=Config(signature_version=UNSIGNED))\n", "\n", " bucket_name = 'prism-mvta'\n", " prefix = 'training-and-validation-data/'\n", "\n", " os.makedirs(download_dir, exist_ok=True)\n", "\n", " paginator = s3.get_paginator('list_objects_v2')\n", " pages = paginator.paginate(Bucket=bucket_name, Prefix=prefix)\n", "\n", " video_names = []\n", "\n", " for page in pages:\n", " if 'Contents' not in page:\n", " print(\"No files found at the specified path!\")\n", " break\n", "\n", " print(\"Downloading test data:\\n\")\n", " for obj in tqdm(page['Contents']):\n", " key = obj['Key']\n", " filename = os.path.basename(key)\n", "\n", " if not filename:\n", " continue\n", "\n", " video_names.append(filename)\n", " local_path = os.path.join(download_dir, filename)\n", " if os.path.exists(local_path):\n", " print(f\"File already exists, skipping: {filename}\")\n", " continue\n", " # print(f\"Downloading: {filename}\")\n", " s3.download_file(bucket_name, key, local_path)\n", "\n", " print(f\"\\nDownloaded {len(video_names)} test videos\")\n", " return download_dir\n", "\n", "download_test_data()" ] }, { "cell_type": "markdown", "metadata": { "id": "5sVIgQPSisN4" }, "source": [ "### Dataloader" ] }, { "cell_type": "markdown", "metadata": { "id": "KMBBOeBZisN4" }, "source": [ "#### Clear Cache and Hug a Face" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "qJsxm9qTisN4", "outputId": "0b06e2ac-c4f9-471b-cfc6-e1b955f005e0" }, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n", "The secret `HF_TOKEN` does not exist in your Colab secrets.\n", "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n", "You will be able to reuse this secret in all of your notebooks.\n", "Please note that authentication is recommended but still optional to access public models or datasets.\n", " warnings.warn(\n" ] } ], "source": [ "import os\n", "\n", "def ls_r():\n", " for root, dirs, files in os.walk(os.getcwd()):\n", " if DOWNLOAD_DIR not in root and CACHE_DIR not in root:\n", " for file in files:\n", " # Create the full path by joining the directory path and file name\n", " full_path = os.path.join(root, file)\n", " print(full_path)\n", "\n", "def clear_cache():\n", " for root, dirs, files in os.walk(os.getcwd()):\n", " if CACHE_DIR in root:\n", " for file in files:\n", " # Create the full path by joining the directory path and file name\n", " full_path = os.path.join(root, file)\n", " print(f\"Removing: {full_path}\")\n", " os.remove(full_path)\n", "\n", "def hug_a_face(repo_id):\n", " from huggingface_hub import hf_hub_download\n", " yolo_model_file = hf_hub_download(repo_id=repo_id, filename=f\"{YOLO_MODEL_FILE}\", local_dir=os.getcwd())\n", " yolo_tracker_file = hf_hub_download(repo_id=repo_id, filename=f\"{YOLO_TRACKER_FILE}\", local_dir=os.getcwd())\n", "# ls_r()\n", "clear_cache()\n", "hug_a_face(HUGGING_FACE_REPO_ID)" ] }, { "cell_type": "markdown", "metadata": { "id": "Fwkkq_wSisN4" }, "source": [ "#### Prep Data" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ffd1td1ZisN4", "outputId": "90bf3b96-2992-4f8c-9ce5-0bd895f2b23a" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Train: 62 videos, Val: 15 videos\n", "\n" ] } ], "source": [ "import gc\n", "import torch\n", "import torch.nn as nn\n", "from torch.utils.data import Dataset, DataLoader, random_split, Subset, WeightedRandomSampler\n", "import os\n", "import cv2\n", "import numpy as np\n", "import PIL.Image # Import PIL\n", "import torchvision.transforms as T # Keep this for T.ToTensor() in else branch\n", "\n", "\n", "class VideoDataset(Dataset):\n", " \"\"\"Dataset for loading videos from a folder, calculating elbow angles.\"\"\"\n", "\n", " def __init__(self, video_dir, transform=None, feature_dir=None, signal_transform=None):\n", " self.video_dir = video_dir\n", " self.transform = transform # Store transform\n", " self.feature_dir = feature_dir\n", " self.signal_transform = signal_transform\n", "\n", " self.video_files = [\n", " f for f in os.listdir(video_dir)\n", " if f.endswith(('.mp4', '.avi', '.mov'))\n", " ]\n", "\n", " self.labels = [\n", " int(f.split('_')[0]) for f in self.video_files\n", " ]\n", "\n", " self.lengths = [0] * len(self.video_files)\n", "\n", " def __len__(self):\n", " return len(self.video_files)\n", "\n", " def __getitem__(self, idx):\n", " if self.feature_dir:\n", " feature_path = os.path.join(self.feature_dir, self.video_files[idx] + \".pt\")\n", " if os.path.exists(feature_path):\n", " data = torch.load(feature_path, weights_only=False)\n", " angles = data['angles']\n", " self.lengths[idx] = data['length']\n", " if self.signal_transform:\n", " angles = self.signal_transform(angles)\n", " return angles, data['label'], self.lengths[idx]\n", "\n", " video_path = os.path.join(self.video_dir, self.video_files[idx])\n", "\n", " list_of_pil_frames = [] # Collect PIL images first\n", " cap = cv2.VideoCapture(video_path)\n", " while True:\n", " ret, frame = cap.read()\n", " if not ret:\n", " break\n", " # Convert frame from BGR to RGB numpy array, then to PIL Image\n", " rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n", " list_of_pil_frames.append(PIL.Image.fromarray(rgb_frame))\n", " cap.release()\n", "\n", " if not list_of_pil_frames:\n", " # Handle empty video or no frames\n", " return torch.tensor([]), self.labels[idx]\n", "\n", " frames_for_yolo = self.__get_frames_for_yolo(list_of_pil_frames)\n", "\n", "\n", " # Use YOLO to track on all frames at once. \"trackers\" only exists once you have called the model\n", " pose_model = YOLO(YOLO_MODEL_FILE, task='pose', verbose=False).to(DEVICE)\n", " if hasattr(pose_model, \"trackers\") and pose_model.predictor:\n", " pose_model.predictor.trackers[0].reset()\n", " results_generator = pose_model.track(frames_for_yolo, verbose=False, device=DEVICE, persist=True, vid_stride=VID_STRIDE, show=False, tracker=YOLO_TRACKER_FILE)\n", "\n", " # Calculate angles using landmarks\n", " angles = self.__calculate_angles(results_generator)\n", " angles_tensor = torch.tensor(angles, dtype=torch.float32)\n", " signal_cleanser = SignalMedianator()\n", " angles_tensor = signal_cleanser(angles_tensor)\n", "\n", " # clean up\n", " del frames_for_yolo\n", " del results_generator\n", " del pose_model\n", " torch.cuda.empty_cache()\n", " gc.collect()\n", "\n", " return angles_tensor, self.labels[idx], angles_tensor.shape[0]\n", "\n", " def __get_frames_for_yolo(self, list_of_pil_frames):\n", " frames_for_yolo = []\n", " if self.transform:\n", " # Our custom VideoTransform expects a list of PIL images\n", " if isinstance(self.transform, VideoTransform):\n", " frames_for_yolo = self.transform(list_of_pil_frames)\n", " else:\n", " # If a different transform is provided (e.g., a torchvision.transforms.Compose\n", " # that expects single images), apply it to each image individually.\n", " transformed_frames_tensor = []\n", " for img_pil in list_of_pil_frames:\n", " # Apply the transform (which might return another PIL image or a tensor)\n", " transformed_img = self.transform(img_pil)\n", " transformed_frames_tensor.append(transformed_img)\n", " frames_for_yolo = transformed_frames_tensor\n", " else:\n", " frames_for_yolo = list_of_pil_frames\n", " return frames_for_yolo\n", "\n", " @staticmethod\n", " def __calculate_angles(results_generator):\n", " angles = []\n", " # max_num_humans_in_any_frame = 0 # To report max humans in any frame\n", "\n", " for frame_idx, results in enumerate(results_generator): # results_generator yields one result object per frame\n", " current_frame_angle = 0.0 # Default if no person detected or angle cannot be calculated\n", "\n", " # Filter for detections that are persons (class 0)\n", " person_mask = (results.boxes.cls == 0)\n", " person_boxes = results.boxes[person_mask]\n", " num_persons_in_frame = len(person_boxes)\n", "\n", " # max_num_humans_in_any_frame = max(max_num_humans_in_any_frame, num_persons_in_frame)\n", "\n", " if num_persons_in_frame == 0:\n", " angles.append(current_frame_angle)\n", " del results # Explicitly clear results from GPU/memory\n", " continue\n", "\n", " # Find the index of the most confident person within the filtered person_boxes\n", " max_conf_person_idx_in_filtered = person_boxes.conf.argmax().item()\n", "\n", " # Get the actual index of this most confident person in the original results.boxes\n", " # This maps the index from the filtered list back to the original list\n", " original_indices_of_persons = person_mask.nonzero(as_tuple=True)[0]\n", " original_index_of_most_confident_person = original_indices_of_persons[max_conf_person_idx_in_filtered]\n", "\n", " # Get the Keypoints object for this specific person\n", " most_confident_person_keypoints_obj = results.keypoints[original_index_of_most_confident_person]\n", "\n", " if num_persons_in_frame > 1:\n", " # Removed print statement for verbosity during dataloading\n", " pass # Or uncomment: print(f\"Multiple Humans in Frame {frame_idx}: {num_persons_in_frame} detected. Selecting most confident.\")\n", "\n", " # Access keypoint data directly from the returned human (which is a Keypoints object for one person)\n", " if most_confident_person_keypoints_obj.xy.shape[0] > 0: # Check if keypoints exist for the human\n", " landmarks_tensor = most_confident_person_keypoints_obj.xy[0] # Already (num_keypoints, 3)\n", " landmarks_np = landmarks_tensor.cpu().numpy()\n", "\n", " if landmarks_np.shape[0] > max(COCO.R_WRIST.value, COCO.L_WRIST.value):\n", " calculator = AngleCalculator(landmarks_np)\n", " try:\n", " angle = calculator()\n", " if 0 <= angle <= 180:\n", " current_frame_angle = angle\n", " except Exception as e:\n", " pass\n", " angles.append(current_frame_angle)\n", " del results # Explicitly clear results from GPU/memory\n", "\n", " # Now, after processing all frames, perform normalization\n", " if not angles: # If no angles were calculated (e.g., empty video or no detections)\n", " return []\n", "\n", " angles_np = np.array(angles, dtype=np.float32)\n", " min_angle, max_angle = np.min(angles_np), np.max(angles_np)\n", " if max_angle > 0:\n", " angles_np = (angles_np - min_angle) / (max_angle - min_angle) # Normalize to [0, 1]\n", "\n", " return angles_np.tolist() # Return as list or keep as np array based on preference.\n", "\n", "\n", "def collate_fn(batch):\n", " \"\"\"Pad all sequences of angles to a target length.\"\"\"\n", " angles_list, labels, lengths = zip(*batch)\n", "\n", " padded_angles = []\n", " for angle_tensor, current_length in zip(angles_list, lengths):\n", " angle_tensor = angle_tensor.flatten()\n", " if current_length < MAX_FRAMES:\n", " # Pad with zeros at the end\n", " padding = torch.zeros(MAX_FRAMES - current_length, dtype=torch.float32)\n", " angles = torch.cat([angle_tensor.flatten(), padding], dim=0)\n", " elif current_length > MAX_FRAMES:\n", " # Truncate if longer\n", " angles = angle_tensor[:MAX_FRAMES]\n", " else:\n", " angles = angle_tensor\n", " padded_angles.append(angles)\n", "\n", " angles_batch = torch.stack(padded_angles, dim=0)\n", " labels_batch = torch.tensor(labels)\n", " lengths_batch = torch.tensor(lengths)\n", "\n", " return angles_batch, labels_batch, lengths_batch\n", "\n", "\n", "def get_dataloaders(video_dir, batch_size=4, val_split=0.2, transform=None, feature_dir=None, signal_transform=None):\n", " \"\"\"Create train and validation dataloaders.\"\"\"\n", "\n", " full_dataset = VideoDataset(video_dir, transform=transform, feature_dir=feature_dir, signal_transform=signal_transform)\n", "\n", " val_size = int(len(full_dataset) * val_split)\n", " train_size = len(full_dataset) - val_size\n", "\n", " train_dataset, val_dataset = random_split(\n", " full_dataset,\n", " [train_size, val_size],\n", " generator=torch.Generator().manual_seed(42)\n", " )\n", "\n", " train_loader = DataLoader(\n", " train_dataset,\n", " batch_size=batch_size,\n", " shuffle=True,\n", " num_workers=0, # Changed from 2 to 0 to avoid multiprocessing issues with YOLO model\n", " collate_fn=collate_fn\n", " )\n", "\n", " val_loader = DataLoader(\n", " val_dataset,\n", " batch_size=batch_size,\n", " shuffle=False,\n", " num_workers=0, # Changed from 2 to 0\n", " collate_fn=collate_fn\n", " )\n", "\n", " print(f\"Train: {len(train_dataset)} videos, Val: {len(val_dataset)} videos\\n\")\n", "\n", " return train_loader, val_loader\n", "\n", "\n", "def get_balanced_dataloaders(video_dir, batch_size=4, val_split=0.2, transform=None, feature_dir=None, signal_transform=None):\n", " \"\"\"Create balanced train and validation dataloaders using WeightedRandomSampler.\"\"\"\n", "\n", " # 1. Initialize the full dataset\n", " full_dataset = VideoDataset(video_dir, transform=transform, feature_dir=feature_dir, signal_transform=signal_transform)\n", "\n", " # 2. Manual Index Split (to keep track of labels for the sampler)\n", " dataset_size = len(full_dataset)\n", " indices = list(range(dataset_size))\n", " split = int(np.floor(val_split * dataset_size))\n", "\n", " # Shuffle indices to ensure random distribution before splitting\n", " np.random.seed(42)\n", " np.random.shuffle(indices)\n", "\n", " train_indices, val_indices = indices[split:], indices[:split]\n", "\n", " # 3. Calculate Weights for the Training Set ONLY\n", " # We pull the labels corresponding to our training indices\n", " train_labels = [full_dataset.labels[i] for i in train_indices]\n", " train_labels = np.array(train_labels).astype(int)\n", "\n", " # Count occurrences of each push-up count (class)\n", " class_sample_count = np.bincount(train_labels)\n", " # Avoid division by zero for classes that might not exist in the subset\n", " class_sample_count[class_sample_count == 0] = 1\n", "\n", " weight = 1. / class_sample_count\n", "\n", " # Assign a weight to every sample in the training set\n", " samples_weight = torch.from_numpy(weight[train_labels]).double()\n", "\n", " # 4. Create the Sampler\n", " # replacement=True is required to oversample rare classes (like 1s, 6s, 7s)\n", " sampler = WeightedRandomSampler(samples_weight, len(samples_weight), replacement=True)\n", "\n", " # 5. Create Subsets and DataLoaders\n", " train_dataset = Subset(full_dataset, train_indices)\n", " val_dataset = Subset(full_dataset, val_indices)\n", "\n", " train_loader = DataLoader(\n", " train_dataset,\n", " batch_size=batch_size,\n", " sampler=sampler, # SHUFFLE must be False when using a Sampler\n", " num_workers=0,\n", " collate_fn=collate_fn\n", " )\n", "\n", " val_loader = DataLoader(\n", " val_dataset,\n", " batch_size=batch_size,\n", " shuffle=False,\n", " num_workers=0,\n", " collate_fn=collate_fn\n", " )\n", "\n", " print(f\"Dataset Balanced: Oversampling rare labels to match common labels.\")\n", " print(f\"Train: {len(train_dataset)} videos, Val: {len(val_dataset)} videos\\n\")\n", "\n", " return train_loader, val_loader\n", "\n", "\n", "# # Run pre-computation once. This will take time but saves hours during training.\n", "# def precompute_features(video_dir, output_dir, transform=None):\n", "# os.makedirs(output_dir, exist_ok=True)\n", "# # Create a temporary dataset without transforms for extraction\n", "# ds = VideoDataset(video_dir, transform=transform)\n", "# for i in range(len(ds)):\n", "# filename = ds.video_files[i]\n", "# save_path = os.path.join(output_dir, filename + \".pt\")\n", "# if os.path.exists(save_path):\n", "# continue\n", "# print(f\"Extracting features for {filename} ({i+1}/{len(ds)})...\")\n", "# angles, label, length = ds[i]\n", "# torch.save({'angles': angles, 'length': length, 'label': label}, save_path)\n", "\n", "# precompute_features(DOWNLOAD_DIR, CACHE_DIR, RESIZER)\n", "\n", "\n", "train_loader, val_loader = get_dataloaders( # get_balanced_dataloaders\n", " DOWNLOAD_DIR,\n", " batch_size=4,\n", " val_split=0.2,\n", " transform=RESIZER,\n", " feature_dir=CACHE_DIR,\n", " signal_transform=None\n", ")\n" ] }, { "cell_type": "markdown", "metadata": { "id": "B9PVSdWKsP94" }, "source": [ "## TODO 3 - Download your model from HuggingFace and instantiate it\n", "\n", "Replace line 8 of the below code. Line 8 is where you instantiate YOUR MODEL ARCHITECTURE (which you re-defined above) with the weights you download from HuggingFace. Make sure you get the class name, and the arguments to the __init__ method correct.\n", "\n", "\n", "This code just downloads the same model which you uploaded in the last notebook." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "LWuMOqY_sOdg", "outputId": "86e6afe4-77a4-46fa-d6ef-26d056f5e570" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Model loaded from maatt4face/mv-final-assignment\n" ] } ], "source": [ "# =============================================================================\n", "# DOWNLOAD MODEL FROM HUGGING FACE\n", "# =============================================================================\n", "\n", "def load_model_from_hub(repo_id):\n", " model_name = \"PushupCounterSingleOutput\"\n", " model_weights = os.path.join(WEIGHTS_DIR, f\"final_model_weights_{model_name}_{YOLO_MODEL}.pth\")\n", " model_path = hf_hub_download(repo_id=repo_id, filename=model_weights, local_dir=os.getcwd())\n", "\n", " model = PushupCounterSingleOutput()\n", " model_checkpoint = torch.load(model_path, map_location='cpu')\n", " model.load_state_dict(model_checkpoint['model_state_dict'])\n", "\n", " print(f\"Model loaded from {repo_id}\")\n", " return model\n", "\n", "model = load_model_from_hub(HUGGING_FACE_REPO_ID)" ] }, { "cell_type": "markdown", "metadata": { "id": "NycfLBRksum4" }, "source": [ "## TODO 4\n", "\n", "Make sure the below code correctly evaluates your model performance on the given data!\n", "\n", "This is your last chance to verify this before submission." ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "QzgdieGiw4_k", "outputId": "140e4e3b-1831-4c44-8981-141e248e10ee" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Using device: cuda\n", "\n", "Running inference on 77 test videos...\n", "\n", "\n", "✓ pred=4 true=4 | 8.8ms | 4_pushup_1f2da596-7619-4d55-9376-069e15a42a1a_h264.mp4\n", "✓ pred=3 true=3 | 5.3ms | 3_sadlfkjasldkfjasleijlkjfd.mp4\n", "✓ pred=3 true=3 | 4.9ms | 3_sadklfjasbnlkjlfkj.mp4\n", "✓ pred=2 true=2 | 4.5ms | 2_kling_20251206_Text_to_Video_Generate_a_71_1.mp4\n", "✓ pred=3 true=3 | 10.3ms | 3_kling_20251209_Text_to_Video_Generate_a_491_2.mp4\n", "✗ pred=4 true=3 | 5.2ms | 3_ewdfkjwaeoihjlkasdjf.mp4\n", "✓ pred=2 true=2 | 4.7ms | 2_sdjfhafsldkjhjk.mp4\n", "✗ pred=3 true=2 | 3.7ms | 2_kling_20251205_Text_to_Video_On_a_sandy_4976_0.mp4\n", "✓ pred=4 true=4 | 6.2ms | 4_kling_20251209_Text_to_Video_Generate_a_561_1.mp4\n", "✓ pred=3 true=3 | 5.7ms | 3_sdfjwaiejflkasjdf.mp4\n", "✓ pred=4 true=4 | 6.0ms | 4_kling_20251209_Text_to_Video_Generate_a_377_1.mp4\n", "✓ pred=2 true=2 | 5.0ms | 2_sdlfjlewlkjkj.mp4\n", "✓ pred=2 true=2 | 3.6ms | 2_difficult_sdafkljsalkfj.mp4\n", "✗ pred=4 true=3 | 4.9ms | 3_kling_20251205_Text_to_Video_On_a_playg_5064_0.mp4\n", "✗ pred=1 true=2 | 5.2ms | 2_dkdjwkndkfw.mp4\n", "✗ pred=3 true=4 | 5.9ms | 4_kling_20251209_Text_to_Video_Generate_a_452_1.mp4\n", "✓ pred=3 true=3 | 4.3ms | 3_dsjlaeijlksjdfie.mp4\n", "✓ pred=3 true=3 | 5.9ms | 3_kling_20251209_Text_to_Video_Generate_a_190_1.mp4\n", "✓ pred=3 true=3 | 5.0ms | 3_sdflkjliejkjdf.mp4\n", "✓ pred=3 true=3 | 5.5ms | 3_sdlkjfaslkjfalskjdf.mp4\n", "✓ pred=3 true=3 | 5.3ms | 3_sdlkfjalkjejafe.mp4\n", "✓ pred=4 true=4 | 5.1ms | 4_sadlfkjlknewkjejk.mp4\n", "✓ pred=2 true=2 | 5.0ms | 2_sdlkjsaelijfksdjf.mp4\n", "✓ pred=3 true=3 | 5.2ms | 3_dslkaldskjflakjs.mp4\n", "✗ pred=2 true=3 | 5.5ms | 3_kling_dskfseu.mp4\n", "✓ pred=6 true=6 | 5.9ms | 6_kling_20251209_Text_to_Video_Generate_a_218_1.mp4\n", "✗ pred=3 true=2 | 4.5ms | 2_sdfkjsaleijflaskdjf.mp4\n", "✓ pred=3 true=3 | 7.6ms | 3_kling_20251209_Text_to_Video_Generate_a_491_1.mp4\n", "✗ pred=3 true=2 | 4.3ms | 2_sadfasjldkfjaseifj.mp4\n", "✗ pred=6 true=7 | 5.2ms | 7_sadkjfkljekj.mp4\n", "✗ pred=2 true=3 | 5.4ms | 3_kling_20251205_Text_to_Video_On_a_playg_5028_0.mp4\n", "✓ pred=4 true=4 | 5.1ms | 4_aslkjasmcalkewjlkje.mp4\n", "✗ pred=3 true=2 | 4.1ms | 2_difficult_2.mp4\n", "✓ pred=3 true=3 | 5.4ms | 3_kling_20251206_Text_to_Video_Generate_a_315_2.mp4\n", "✓ pred=2 true=2 | 4.6ms | 2_sdkjdsflkjfwa.mp4\n", "✓ pred=2 true=2 | 3.5ms | 2_dfsaeklnvvalkej.mp4\n", "✗ pred=3 true=4 | 3.5ms | 4_dssalsdkfjweijf.mp4\n", "✓ pred=4 true=4 | 6.0ms | 4_kling_20251209_Text_to_Video_Generate_a_588_2.mp4\n", "✓ pred=3 true=3 | 5.4ms | 3_dkk873lkjlksajdf.mp4\n", "✓ pred=2 true=2 | 3.2ms | 2_sdafkjaslkclaksdjkas.mp4\n", "✓ pred=4 true=4 | 5.9ms | 4_kling_20251206_Text_to_Video_Generate_a_58_0.mp4\n", "✓ pred=4 true=4 | 5.2ms | 4_aslkcasckmwlejk.mp4\n", "✓ pred=4 true=4 | 6.4ms | 4_kling_20251206_Text_to_Video_Generate_a_315_3.mp4\n", "✓ pred=4 true=4 | 6.6ms | 4_kling_20251207_Text_to_Video_Generate_a_521_1.mp4\n", "✓ pred=6 true=6 | 4.6ms | 6_dfjewaijsldkjfsaef.mp4\n", "✗ pred=4 true=3 | 4.8ms | 3_sdlkjslndflkseijlkjef.mp4\n", "✓ pred=4 true=4 | 6.8ms | 4_kling_20251209_Text_to_Video_Generate_a_218_0.mp4\n", "✗ pred=5 true=4 | 4.1ms | 4_sadflkjasldkjfalseij.mp4\n", "✓ pred=3 true=3 | 8.7ms | 3_sdlkfjaleknaksej.mp4\n", "✓ pred=2 true=2 | 4.1ms | 2_dsalkfjalwkenlke.mp4\n", "✓ pred=5 true=5 | 4.7ms | 5_sadfjhaslfkjasdlkfjsa.mp4\n", "✗ pred=2 true=1 | 3.9ms | 1_dksksjfwijf.mp4\n", "✗ pred=3 true=2 | 6.9ms | 2_dkjd823kjf.mp4\n", "✓ pred=4 true=4 | 5.4ms | 4_asdlkfjalsflnekj.mp4\n", "✓ pred=5 true=5 | 6.9ms | 5_sdfkljweoijlkjdsflkjweaij.mp4\n", "✗ pred=4 true=3 | 3.5ms | 3_sadlfkjawelnflksdjf.mp4\n", "✓ pred=3 true=3 | 5.2ms | 3_kling_kdjflaskdjf.mp4\n", "✓ pred=2 true=2 | 4.0ms | 2_dkdmkejkeimdh.mp4\n", "✓ pred=3 true=3 | 6.1ms | 3_kling_20251209_Text_to_Video_Generate_a_491_0.mp4\n", "✓ pred=3 true=3 | 6.9ms | 3_asldkfjalwieaskdfaskdf.mp4\n", "✓ pred=3 true=3 | 3.6ms | 3_kling_20251206_Text_to_Video_Generate_a_71_2.mp4\n", "✓ pred=3 true=3 | 3.7ms | 3_kling_20251206_Text_to_Video_Generate_a_71_0.mp4\n", "✓ pred=4 true=4 | 7.5ms | 4_kling_20251206_Text_to_Video_Generate_a_28_0.mp4\n", "✓ pred=3 true=3 | 5.9ms | 3_kling_20251209_Text_to_Video_Generate_a_403_1.mp4\n", "✓ pred=3 true=3 | 3.8ms | 3_kling_20251206_Text_to_Video_Generate_a_71_3.mp4\n", "✓ pred=3 true=3 | 4.6ms | 3_dsksdfjbvsdkj.mp4\n", "✓ pred=3 true=3 | 6.2ms | 3_kling_20251209_Image_to_Video_Generate_a_613_1.mp4\n", "✗ pred=3 true=4 | 5.9ms | 4_kling_20251209_Image_to_Video_Generate_a_635_1.mp4\n", "✓ pred=3 true=3 | 7.9ms | 3_kling_20251206_Text_to_Video_Generate_a_712_3.mp4\n", "✓ pred=4 true=4 | 6.9ms | 4_kling_20251209_Text_to_Video_Generate_a_190_0.mp4\n", "✓ pred=4 true=4 | 5.9ms | 4_20251209_Text_to_Video_Generate_a_561_0.mp4\n", "✓ pred=3 true=3 | 5.1ms | 3_kling_20251206_Text_to_Video_Generate_a_17_0.mp4\n", "✓ pred=3 true=3 | 8.6ms | 3_kling_20251206_Text_to_Video_Generate_a_315_0.mp4\n", "✓ pred=3 true=3 | 6.0ms | 3_kling_20251209_Image_to_Video_Generate_a_635_0.mp4\n", "✓ pred=3 true=3 | 3.6ms | 3_kling_20251205_Text_to_Video_In_a_grass_4697_0.mp4\n", "✓ pred=4 true=4 | 6.8ms | 4_kling_20251209_Text_to_Video_Generate_a_452_0.mp4\n", "✓ pred=4 true=4 | 5.9ms | 4_kling_20251209_Text_to_Video_Generate_a_263_1.mp4\n", "\n", "==================================================\n", "SUMMARY\n", "==================================================\n", "Total videos: 77\n", "Correct: [59]\n", "Incorrect: [18]\n", "\n", "ACCURACY: 76.62%\n", "\n", "Total time: 569.25s\n", "Avg per video: 5.4ms\n", "Min latency: 3.2ms\n", "Max latency: 10.3ms\n", "==================================================\n" ] } ], "source": [ "def evaluate(model, test_loader, dataset, device):\n", " model.eval()\n", " correct = 0\n", " total = 0\n", "\n", " all_preds = []\n", " all_labels = []\n", " all_times = []\n", "\n", " janitor = Janitor()\n", "\n", " print(\"\\n\")\n", "\n", " with torch.no_grad():\n", " for idx, (angles, labels, lengths) in enumerate(test_loader):\n", " angles, labels = angles.to(device), labels.to(device)\n", "\n", " # Time the forward pass\n", " start_time = time.time()\n", " outputs = model(angles, lengths)\n", " if device.type == 'cuda':\n", " torch.cuda.synchronize() # wait for GPU to finish\n", " end_time = time.time()\n", "\n", " inference_time = (end_time - start_time) * 1000 # ms\n", " all_times.append(inference_time)\n", "\n", " preds = outputs\n", "\n", " for i in range(labels.size(0)):\n", " batch_idx = idx * test_loader.batch_size + i\n", " video_name = dataset.video_files[batch_idx]\n", " pred = int(torch.round(preds[i]).item())\n", " true_label = labels[i].item()\n", " is_correct = \"✓\" if pred == true_label else \"✗\"\n", "\n", " print(f\"{is_correct} pred={pred} true={true_label} | {inference_time:>7.1f}ms | {video_name}\")\n", " correct += pred == true_label\n", "\n", " # correct += preds.eq(labels).sum().item()\n", " total += labels.size(0)\n", "\n", " all_preds.extend(preds.cpu().numpy())\n", " all_labels.extend(labels.cpu().numpy())\n", " janitor.smart_cleanup()\n", "\n", " accuracy = correct / total\n", " return accuracy, all_preds, all_labels, all_times\n", "\n", "\n", "# =============================================================================\n", "# RUN INFERENCE\n", "# =============================================================================\n", "\n", "def run_inference(model):\n", " device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", " print(f\"Using device: {device}\")\n", "\n", " # Download test data\n", " test_dir = DOWNLOAD_DIR\n", "\n", " model = model.to(device)\n", "\n", " # Create dataloader\n", " test_dataset = VideoDataset(test_dir)\n", " test_loader = DataLoader(\n", " test_dataset,\n", " batch_size=1,\n", " shuffle=False,\n", " num_workers=0,\n", " collate_fn=collate_fn\n", " )\n", "\n", " print(f\"\\nRunning inference on {len(test_dataset)} test videos...\")\n", "\n", " # Warmup (optional, helps get consistent GPU timings)\n", " if device.type == 'cuda':\n", " dummy_data = torch.randn(1, MAX_FRAMES, device=device) # (Batch, SeqLen)\n", " dummy_lengths = torch.tensor([MAX_FRAMES], dtype=torch.long, device=device) # (Batch,)\n", " with torch.no_grad():\n", " _ = model(dummy_data, dummy_lengths)\n", " torch.cuda.synchronize()\n", "\n", " total_start = time.time()\n", " accuracy, preds, labels, times = evaluate(model, test_loader, test_dataset, device)\n", " total_end = time.time()\n", "\n", " # Summary\n", " preds = np.round(preds).astype(int)\n", " num_correct = sum(p == l for p, l in zip(preds.astype(int), labels))\n", " num_wrong = len(preds) - num_correct\n", "\n", " print(\"\\n\" + \"=\"*50)\n", " print(\"SUMMARY\")\n", " print(\"=\"*50)\n", " print(f\"Total videos: {len(preds)}\")\n", " print(f\"Correct: {num_correct}\")\n", " print(f\"Incorrect: {num_wrong}\")\n", " print(f\"\")\n", " print(f\"ACCURACY: {accuracy*100:.2f}%\")\n", " print(f\"\")\n", " print(f\"Total time: {total_end - total_start:.2f}s\")\n", " print(f\"Avg per video: {sum(times) / len(times):.1f}ms\")\n", " print(f\"Min latency: {min(times):.1f}ms\")\n", " print(f\"Max latency: {max(times):.1f}ms\")\n", " print(\"=\"*50)\n", " return accuracy, preds, labels, times\n", "\n", "accuracy, preds, labels, latencys = run_inference(model)" ] }, { "cell_type": "markdown", "source": [ "#### Performance Analysis" ], "metadata": { "id": "F3YDAhQmGfNJ" } }, { "cell_type": "code", "source": [ "import pandas as pd\n", "import re\n", "import matplotlib.pyplot as plt\n", "\n", "# 1. Put outputs into dataframe\n", "labels_array = np.array(labels).flatten()\n", "preds_array = preds.flatten()\n", "\n", "df = pd.DataFrame({\n", " 'prediction': labels_array,\n", " 'ground_truth': preds_array,\n", " 'latency': latencys\n", "})\n", "df[['prediction', 'ground_truth']] = df[['prediction', 'ground_truth']].astype(int)\n", "df['latency'] = df['latency'].astype(float)\n", "df['error'] = df['prediction'] - df['ground_truth']\n", "\n", "# 2. RUN-TIME (LATENCY) DISTRIBUTION\n", "plt.hist(df['latency'], bins=15, color='skyblue', edgecolor='black')\n", "plt.title('Run-time (Latency) Distribution')\n", "plt.xlabel('Latency (ms)')\n", "plt.ylabel('Frequency')\n", "plt.axvline(df['latency'].mean(), color='red', linestyle='--', label=f\"Mean: {df['latency'].mean():.2f}ms\")\n", "plt.legend()\n", "plt.savefig('latency_distribution.png')\n", "plt.show()\n", "\n", "# 3. ERROR DISTRIBUTION\n", "error_counts = df['error'].value_counts().sort_index()\n", "plt.bar(error_counts.index, error_counts.values, color='salmon', edgecolor='black')\n", "plt.title('Error Distribution (Pred - True)')\n", "plt.xlabel('Error Magnitude')\n", "plt.ylabel('Count')\n", "plt.xticks(error_counts.index)\n", "plt.savefig('error_distribution.png')\n", "plt.show()\n", "\n", "# 4. TABLES: Summary & Confusion Matrix\n", "summary = pd.DataFrame({\n", " \"Metric\": [\"Accuracy\", \"Mean Latency\", \"MAE (Mean Abs Error)\"],\n", " \"Value\": [f\"{(df['error'] == 0).mean():.2%}\", f\"{df['latency'].mean():.2f} ms\", f\"{df['error'].abs().mean():.2f}\"]\n", "})\n", "conf_matrix = pd.crosstab(df['ground_truth'], df['prediction'], rownames=['Actual'], colnames=['Predicted'])\n", "\n", "print(summary)\n", "print(\"\\nConfusion Matrix:\\n\", conf_matrix)" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "id": "QmGaFcQQGe31", "outputId": "ec49d899-3b90-4ffb-99c7-7ff76f33197b" }, "execution_count": 15, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "
" ], "image/png": "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UomEhASMHj0aW7duRYcOHfDw4UOdZdVqNbp37w4XFxd8+umn6NSpE+bPn48VK1ZUOSeZGUEkU6tWrRIAxI8//iju3bsnbt68KbZs2SLc3NyESqUSN2/e1C7bqVMn0alTp1LbiI2NFb6+vtrxa9euCQDCxcVF3L9/Xzv9v//9rwAgvv3222fmioqK0tnm09tetWqVzv4BiI8++kg77cGDB8LW1lYoFAqxceNG7fQ//vhDABAzZ87UTpszZ46wt7cXly5d0tnX1KlThVKpFKmpqRVmbdiwoXj11VdLTS95b48fP17uusXFxaKgoEBn2oMHD0T9+vXFiBEjtNPu3btXKneJrl27irCwMJGfn6+dptFoRLt27URgYGCpPJGRkUKj0Winv/POO0KpVIqHDx8KIYR4+PChcHBwEG3atBF5eXk6+3pyvbZt24o2bdrozN+6dasAIPbv318qp7W1tXjzzTfLfS9KABDx8fHlzh8/frwAIM6cOSOEKP2ZePDggQAgPvnkkwr3ExISUubnueR96tChgyguLi5z3rVr17TTfH19BQDxn//8RzstMzNTeHp6ioiICO20mTNnirL+XJS1zfKy7d+/X+f9LSwsFO7u7iI0NFTnZ7Vz504BQMyYMUM7reT35IMPPtDZZkREhGjVqlWpfVHtwhYZkr3IyEi4ubnB29sb/fr1g729PXbs2KFtUq+OgQMH6rTolJyuunr1ao3zlmXUqFHaYScnJzRt2hT29vY6fXyaNm0KJycnnQybN29Gx44d4ezsjIyMDO0rMjISarUahw4dqnC/f/31V7VbrpRKJaytrQE87sh5//59FBcXo3Xr1jh58uQz179//z727duHAQMG4NGjR9rsf/31F7p3746UlJRSpxhef/11nVMcHTt2hFqtxo0bNwAAe/bswaNHjzB16tRS/TGeXC8mJgbHjh3TOf24bt06eHt7o1OnTqWylry/NVWnTh0AwKNHj8qcb2trC2traxw4cAAPHjyo9n5Gjx5d6f4wXl5e6Nu3r3bc0dERMTExOHXqVKWv1qqOEydOID09HW+99ZbOzyoqKgpBQUH47rvvSq0zZswYnfGOHTsa7HeS5IOFDMne0qVLsWfPHmzZsgUvv/wyMjIyoFKparRNHx8fnfGSP/Ylf1zy8vJw584dnVd12djYaPuRlKhbty4aNmxYql9C3bp1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\n" }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "
" ], "image/png": "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1p133unSvmXLFkuSdf/997u0P/7445Yk67PPPnO2RUREWJKsJUuWVKvupKQkq3nz5pX2f/nll5Yka8yYMc62Xr16Wb169XKuDxw40OrUqVOV+3nxxRctSVZWVla5PkmWm5ub9e2331bYl5aW5lwve49uvvlml3EPP/ywJcn66quvLMv63/s8e/bsM85ZVW0RERFWUlKSc3306NGWJGv16tXOtsOHD1tRUVFWZGSkdfLkScuyLGv58uWWJCsmJsYqKSlxjn3llVcsSdY333xTbl+nevrppy1J1uHDhyusPzk52dq/f7+1b98+a8OGDda1115rSbJefvlll/1HR0dbv/zyi3Pb0tJSq127dlZCQoJVWlrqbP/ll1+sqKgo67rrrnO2DRo0yPL29rZ+/PFHZ9u2bdusJk2aWOfyn/EBAwZYERERLm2Vfa5r8nNOTk62WrVqZeXn57uMu+OOOyw/Pz+X98myLGvevHmWJGvDhg1n9XrQMHFqCQ1Wenq6li1b5rIsXry43LjExMRKf+v/wx/+4LL+n//8R5KUkpLi0v7YY49JUrnTGlFRUUpISKj1azhVixYtJEmHDx+udEzLli31008/6fPPP6/1fnr16qWOHTtWe/yIESNc1keNGiXpf+/VufKf//xH3bt3d57yk357jx588EHt3r1b27Ztcxl/7733ulxT9Lvf/U7Sb6enqnLgwAG5u7s73//TzZo1S8HBwbrgggvUo0cPrVmzRikpKeWOKiQlJblcR7Jlyxbt3LlTd911lw4cOKD8/Hzl5+fr6NGjuvbaa7Vq1SqVlpbq5MmTWrp0qQYNGqTw8HDn9jExMXX22aqps/lcW5alhQsX6qabbpJlWc7XnZ+fr4SEBBUWFmrz5s0u2/j7+0vSebvVH2bh1BIarO7du1frYt+K7myqrO/HH3+Um5tbudMGoaGhatmypX788cdqz11TR44ckST5+PhUOubJJ5/Up59+qu7du+uiiy5Sv379dNddd6lnz57V3k9Na27Xrp3Letu2beXm5nbOb9H98ccf1aNHj3LtMTExzv5LLrnE2X5qCJD+95/joUOHzqqOgQMHauTIkXI4HPLx8VGnTp0qvCj89Pd1586dkn4LOJUpLCxUSUmJjh07Vu59lqT27dufMTAeOXLE+dmRpCZNmpz1XUdn87nev3+/CgoKNHPmTM2cObPCMfv27XNZt/7/ad2G/Mwc1B5BBo1eVXdbVNZX3X9Q6/JOjrKHglV1+3hMTIwyMzP10UcfacmSJVq4cKGmTZumZ599VuPGjavWfs625tPfm8req5MnT57VfmqqSZMmFbaX/SdZmcDAQP366686fPhwhSHywgsvrNaFwKe/r2XPBXrxxRd12WWXVbhNixYtyl1EXlMvvfSSy88+IiLirENmRZ+R6v6cy173sGHDKg1xnTt3dlkvC5tl100BpyLIADUQERGh0tJS7dy50/mbv/TbxYsFBQWKiIg4Z/t+88035XA4dN1111U5rnnz5rr99tt1++236/jx4xo8eLAmTJig1NRUeXt71/lvtTt37nT5DX3Xrl0qLS11XiRcduTj9IfcnX70SqrZb9wRERHKzMws175jxw5nf13o0KGDpN/uXjr9P9izUfZMFF9f3yqDUHBwsJo2beo8gnOqil7/6e655x6X02/n6jbp6v6cg4OD5ePjo5MnT1b7TrCsrCy5ublVepE7GjeukQFq4IYbbpAkTZ061aV98uTJkqQBAwack/1OmjRJn3zyiW6//fYKTzGUOXDggMu6p6enOnbsKMuydOLECUlynvaoq6fnpqenu6z/9a9/lSTnM1R8fX0VFBSkVatWuYybNm1aublqUtsNN9ygjRs3at26dc62o0ePaubMmYqMjKzRdT5ViYuLkyR98cUXdTJfmdjYWLVt21YvvfSSy6mfMvv375f025GkhIQEvffee8rOznb2b9++XUuXLj3jfqKjoxUfH+9canKasSaq+3Nu0qSJEhMTtXDhwgq/eqDsdZ9q06ZN6tSpk/z8/Oq2aDQIHJFBg7V48WLnb+enuuqqqxQdHV2rObt06aKkpCTNnDlTBQUF6tWrlzZu3Ki5c+dq0KBB6tOnz1nV/Ouvv+qf//ynpN8emvbjjz/qgw8+0Ndff60+ffpUek1BmX79+ik0NFQ9e/ZUSEiItm/frtdee00DBgxwnhaJjY2VJP3pT3/SHXfcIQ8PD9100021fthfVlaWbr75Zl1//fVat26d/vnPf+quu+5Sly5dnGPuv/9+TZo0Sffff7+6du2qVatWuTwPp0xNanvqqaf09ttvq3///nrkkUcUEBCguXPnKisrSwsXLqyzh81FR0frkksu0aeffqr77ruvTuaUJDc3N/39739X//791alTJ917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Xtd1AAAA1FitrpEBAACoD2p1RKZPnz5VnkL67LPPal0QAABAddUqyJRdH1PmxIkT2rJli7Zu3VruyyQBAADOlVoFmSlTplTYPnbsWB05cuSsCgIAAKiuOr1GZtiwYXzPEgAAOG/qNMisW7dO3t7e1R4/ffp0de7cWb6+vvL19VVcXJwWL17s7C8uLtaIESMUGBioFi1aKDExUXl5eXVZMgAAMFitTi0NHjzYZd2yLO3du1dffPGFnnnmmWrPc+GFF2rSpElq166dLMvS3LlzNXDgQH355Zfq1KmTxowZo48//lgLFiyQn5+fRo4cqcGDB2vNmjW1KRsAADQwtQoyfn5+Lutubm5q3769xo8fr379+lV7nptuusllfcKECZo+fbrWr1+vCy+8ULNmzdK8efPUt29fSdLs2bMVExOj9evX68orr6xN6QAAoAGpVZCZPXt2XdehkydPasGCBTp69Kji4uK0adMmnThxQvHx8c4xHTp0UHh4uNatW0eQAQAAtQsyZTZt2qTt27dLkjp16qTLL7+8xnN88803iouLU3FxsVq0aKFFixapY8eO2rJlizw9PdWyZUuX8SEhIcrNza10vpKSEpWUlDjXi4qKalwTAAAwQ62CzL59+3THHXdoxYoVzqBRUFCgPn36aP78+QoODq72XO3bt9eWLVtUWFiof/3rX0pKStLKlStrU5YkaeLEiRo3blyttwcAAOao1V1Lo0aN0uHDh/Xtt9/q4MGDOnjwoLZu3aqioiI98sgjNZrL09NTF110kWJjYzVx4kR16dJFr7zyikJDQ3X8+HEVFBS4jM/Ly1NoaGil86WmpqqwsNC55OTk1OYlAgAAA9TqiMySJUv06aefKiYmxtnWsWNHpaen1+hi34qUlpaqpKREsbGx8vDwUEZGhhITEyVJmZmZys7OVlxcXKXbe3l5ycvL66xqAAAAZqhVkCktLZWHh0e5dg8PD5WWllZ7ntTUVPXv31/h4eE6fPiw5s2bpxUrVmjp0qXy8/NTcnKyUlJSFBAQIF9fX40aNUpxcXFc6AsAACTVMsj07dtXjz76qN5++221bt1akrRnzx6NGTNG1157bbXn2bdvn+655x7t3btXfn5+6ty5s5YuXarrrrtO0m9fheDm5qbExESVlJQoISFB06ZNq03JAACgAapVkHnttdd08803KzIyUmFhYZKknJwcXXLJJfrnP/9Z7XlmzZpVZb+3t7fS09OVnp5emzIBAEADV6sgExYWps2bN+vTTz/Vjh07JEkxMTEuz3wBAAA412p019Jnn32mjh07qqioSA6HQ9ddd51GjRqlUaNGqVu3burUqZNWr159rmoFAABwUaMgM3XqVD3wwAPy9fUt1+fn56ff//73mjx5cp0VBwAAUJUaBZmvvvpK119/faX9/fr106ZNm866KAAAgOqoUZDJy8ur8LbrMu7u7tq/f/9ZFwUAAFAdNQoybdq00datWyvt//rrr9WqVauzLgoAAKA6ahRkbrjhBj3zzDMqLi4u13fs2DGlpaXpxhtvrLPiAAAAqlKj26+ffvpp/fvf/9bFF1+skSNHqn379pKkHTt2KD09XSdPntSf/vSnc1IoAADA6WoUZEJCQrR27Vo99NBDSk1NlWVZkiSHw6GEhASlp6crJCTknBQKAABwuho/EC8iIkL/+c9/dOjQIe3atUuWZaldu3by9/c/F/UBAABUqlZP9pUkf39/devWrS5rAQAAqJEaXewLAABQnxBkAACAsQgyAADAWAQZAABgLIIMAAAwFkEGAAAYiyADAACMRZABAADGIsgAAABjEWQAAICxCDIAAMBYBBkAAGAsggwAADAWQQYAABiLIAMAAIxFkAEAAMYiyAAAAGMRZAAAgLEIMgAAwFgEGQAAYCyCDAAAMBZBBgAAGIsgAwAAjEWQAQAAxiLIAAAAYxFkAACAsQgyAADAWAQZAABgLIIMAAAwFkEGAAAYiyADAACMRZABAADGIsgAAABjEWQAAICxCDIAAMBYBBkAAGAsggwAADAWQQYAABjL1iAzceJEdevWTT4+Prrgggs0aNAgZWZmuowpLi7WiBEjFBgYqBYtWigxMVF5eXk2VQwAAOoTW4PMypUrNWLECK1fv17Lli3TiRMn1K9fPx09etQ5ZsyYMfrwww+1YMECrVy5Uj///LMGDx5sY9UAAKC+cLdz50uWLHFZnzNnji644AJt2rRJ11xzjQoLCzVr1izNmzdPffv2lSTNnj1bMTExWr9+va688ko7ygYAAPVEvbpGprCwUJIUEBAgSdq0aZNOnDih+Ph455gOHTooPDxc69atq3COkpISFRUVuSwAAKBhqjdBprS0VKNHj1bPnj11ySWXSJJyc3Pl6empli1buowNCQlRbm5u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79q1a7ntIiIiFBwc7Fzfvn27wsLCFBYW5mzr2LGjWrZsqe3bt6tbt24Vbncm9913nx599FENGzZM69at04IFC7R69WqXMSdPntTzzz+vd999V3v27NHx48dVUlKiZs2aSZJ++OEHnThxQt27d3du4+fnp/bt25fbX+fOnZ1/djgcCg0N1b59+6pdb3WUvVetW7d2tsXFxbmM+eqrr7Rr1y75+Pi4tBcXF+v777+v03qAhoggAzQSzZs310UXXXTGMdVpq+7+aqJ///568MEHlZycrJtuukmBgYHlxrz44ot65ZVXNHXqVF166aVq3ry5Ro8eXatTMB4eHi7rDodDpaWlNZrDzc1NlmW5tFV0kXRVjhw5otjYWL311lvl+moSBIHGimtkANRITEyMcnJylJOT42zbtm2bCgoK1LFjx1rP6+7urnvuuUcrVqxwXp9zujVr1mjgwIEaNmyYunTpoujoaH333XfO/ujoaHl4eOjzzz93thUWFrqMqQ1PT09Jvx0ROlVwcLD27t3rXC8qKlJWVpZzvey9OnXM+vXrXea44oortHPnTl1wwQW66KKLXBY/P7+zqhtoDAgyQCNRUlKi3NxclyU/P7/G88THx+vSSy/V0KFDtXnzZm3cuFH33HOPevXqVeGpqZp47rnntH//fiUkJFTY365dOy1btkxr167V9u3b9fvf/97lbikfHx8lJSXpiSee0PLly/Xtt98qOTlZbm5uZ/U8moiICDkcDn300Ufav3+/jhw5Iknq27ev3nzzTa1evVrffPONkpKS1KRJE+d28fHxuvjii5WUlKSvvvpKq1ev1p/+9CeXuYcOHaqgoCANHDhQq1evVlZWllasWKFHHnlEP/30U61rBhoLggzQSCxZskStWrVyWa6++uoaz+NwOPT+++/L399f11xzjeLj4xUdHa133nnnrGv09PRUUFBQpaHj6aef1hVXXKGEhAT17t1boaGhGjRokMuYyZMnKy4uTjfeeKPi4+PVs2dPxcTEnPEW8Kq0adNG48aN01NPPaWQkBCNHDlSkpSamqpevXrpxhtv1IABAzRo0CC1bdvWuZ2bm5sWLVqkY8eOqXv37rr//vs1YcIEl7mbNWumVatWKTw8XIMHD1ZMTIySk5NVXFwsX1/fWtcMNBYO6/QTvADQgBw9elRt2rTRyy+/rOTkZLvLAVDHuNgXQIPy5ZdfaseOHerevbsKCws1fvx4SdLAgQNtrgzAuUCQAdDgvPTSS8rMzJSnp6diY2O1evVqBQUF2V0WgHOAU0sAAMBYXOwLAACMRZABAADGIsgAAABjEWQAAICxCDIAAMBYBBkAAGAsggwAADAWQQYAABiLIAMAAIz1/wDr1JzdBMNHwQAAAABJRU5ErkJggg==\n" }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ " Metric Value\n", "0 Accuracy 76.62%\n", "1 Mean Latency 5.44 ms\n", "2 MAE (Mean Abs Error) 0.23\n", "\n", "Confusion Matrix:\n", " Predicted 1 2 3 4 5 6 7\n", "Actual \n", "1 0 1 0 0 0 0 0\n", "2 1 10 2 0 0 0 0\n", "3 0 5 28 3 0 0 0\n", "4 0 0 4 17 0 0 0\n", "5 0 0 0 1 2 0 0\n", "6 0 0 0 0 0 2 1\n" ] } ] }, { "cell_type": "code", "source": [], "metadata": { "id": "ES-eNMG1GxP8" }, "execution_count": 15, "outputs": [] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 0 }