{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":99249,"databundleVersionId":12808057,"sourceType":"competition"},{"sourceId":12481697,"sourceType":"datasetVersion","datasetId":7875632}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":1206.783853,"end_time":"2025-07-16T00:12:28.391679","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-07-15T23:52:21.607826","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"3d9813c4","cell_type":"markdown","source":"# 🐾 Cup-ybara My Submission\nThis notebook provides a fully self-contained submission for the Cup Ybara challenge.\nIt defines the required `BaseModel` interface, implements my `CustomModel`,\nand outputs a properly formatted `submission.csv` file.","metadata":{"papermill":{"duration":0.004233,"end_time":"2025-07-15T23:52:25.745191","exception":false,"start_time":"2025-07-15T23:52:25.740958","status":"completed"},"tags":[]},"attachments":{}},{"id":"a235e2a3","cell_type":"code","source":"import os\nimport random\nimport json\nimport joblib\nimport shutil\nimport pandas as pd\nimport numpy as np\nfrom typing import List\n\nimport torch\nimport cv2\nfrom torchvision.models.video import r2plus1d_18\nfrom torchvision import transforms\n\nfrom sklearn.metrics import f1_score","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:25.753214Z","iopub.status.busy":"2025-07-15T23:52:25.752916Z","iopub.status.idle":"2025-07-15T23:52:37.594183Z","shell.execute_reply":"2025-07-15T23:52:37.593585Z"},"papermill":{"duration":11.847023,"end_time":"2025-07-15T23:52:37.595668","exception":false,"start_time":"2025-07-15T23:52:25.748645","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b84f3135","cell_type":"code","source":"# Hard coded class mappings\nidx_to_class = {\n    0: 'armadillo',\n    1: 'bird',\n    2: 'capybara',\n    3: 'cow',\n    4: 'dusky_legged_guan',\n    5: 'gray_brocket',\n    6: 'hare',\n    7: 'human',\n    8: 'insect',\n    9: 'margay',\n    10: 'no_animal',\n    11: 'skunk',\n    12: 'unknown_animal',\n    13: 'wild_boar'\n}\n\nclass_to_idx = {v: k for k, v in idx_to_class.items()}\nLABELS = [f\"'{idx_to_class[i]}'\" for i in sorted(idx_to_class)]\n\nprint(\"Labels:\", LABELS)","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:37.605194Z","iopub.status.busy":"2025-07-15T23:52:37.604684Z","iopub.status.idle":"2025-07-15T23:52:37.610314Z","shell.execute_reply":"2025-07-15T23:52:37.609705Z"},"papermill":{"duration":0.010239,"end_time":"2025-07-15T23:52:37.611409","exception":false,"start_time":"2025-07-15T23:52:37.601170","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"3efa0177","cell_type":"code","source":"!ls /kaggle/input/cupybara/dataset/","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:37.619032Z","iopub.status.busy":"2025-07-15T23:52:37.618813Z","iopub.status.idle":"2025-07-15T23:52:37.757886Z","shell.execute_reply":"2025-07-15T23:52:37.756932Z"},"papermill":{"duration":0.144918,"end_time":"2025-07-15T23:52:37.759177","exception":false,"start_time":"2025-07-15T23:52:37.614259","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"5cd3ea8e","cell_type":"markdown","source":"## 🏋️ About Train Mode\n\nThe boolean variable TRAIN is used to determine if the portion of the trainng code should be run or not. It will always be set to `False` when running the submission manually. Ensure that if a lengthy part of your notebook is used for training, that is toggled away by this variable","metadata":{"papermill":{"duration":0.002652,"end_time":"2025-07-15T23:52:37.765068","exception":false,"start_time":"2025-07-15T23:52:37.762416","status":"completed"},"tags":[]}},{"id":"1c766ac5","cell_type":"code","source":"DATASET_ROOT = \"/kaggle/input/cupybara/dataset/dataset/\"\nEVAL_DIR =  os.path.join(DATASET_ROOT,\"test\")  # Directory containing evaluation `.mp4` files\nTRAIN = True\n\n# Video model configuration\nMODEL_PATH = \"/kaggle/input/cupybara-80/trained_video_model_r2plus_aug_combined_equal_spacing_80.pt\"\nVIDEO_FRAMES = 16\nFRAME_SIZE = 112  # Model input requirement\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:37.772036Z","iopub.status.busy":"2025-07-15T23:52:37.771362Z","iopub.status.idle":"2025-07-15T23:52:37.863504Z","shell.execute_reply":"2025-07-15T23:52:37.862898Z"},"papermill":{"duration":0.096702,"end_time":"2025-07-15T23:52:37.864527","exception":false,"start_time":"2025-07-15T23:52:37.767825","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"d4bfc776","cell_type":"code","source":"!rm -rf model_weights/ model_weights.zip submission.csv","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:37.871351Z","iopub.status.busy":"2025-07-15T23:52:37.871104Z","iopub.status.idle":"2025-07-15T23:52:38.002780Z","shell.execute_reply":"2025-07-15T23:52:38.001889Z"},"papermill":{"duration":0.136346,"end_time":"2025-07-15T23:52:38.004049","exception":false,"start_time":"2025-07-15T23:52:37.867703","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"79c784dc","cell_type":"code","source":"# Video model transforms and functions\ntransform = transforms.Compose([\n    transforms.Resize((FRAME_SIZE, FRAME_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.43216, 0.394666, 0.37645],\n                         [0.22803, 0.22145, 0.216989]),\n])\n\ndef load_video_tensor(path, num_frames=VIDEO_FRAMES):\n    \"\"\"Load video as tensor with proper frame sampling\"\"\"\n    cap = cv2.VideoCapture(path)\n    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n\n    frame_idxs = np.linspace(0, total_frames - 1, num_frames).astype(int)\n    frames = []\n\n    for idx in range(total_frames):\n        ret, frame = cap.read()\n        if not ret:\n            break\n        if idx in frame_idxs:\n            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n            pil_image = transforms.ToPILImage()(frame)\n            frames.append(transform(pil_image))\n\n    cap.release()\n\n    if len(frames) < num_frames:\n        frames += [frames[-1]] * (num_frames - len(frames))  # pad with last frame if short\n\n    video_tensor = torch.stack(frames)  # [T, C, H, W]\n    return video_tensor.permute(1, 0, 2, 3)  # [C, T, H, W]\n\ndef load_model():\n    \"\"\"Load the trained video model\"\"\"\n    model = r2plus1d_18(pretrained=False)\n    model.fc = torch.nn.Linear(model.fc.in_features, len(class_to_idx))\n    model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))\n    model.eval()\n    return model.to(DEVICE)\n\nif TRAIN: \n    print(\"Video model setup complete. Training mode is enabled but not needed for submission.\")","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:38.011091Z","iopub.status.busy":"2025-07-15T23:52:38.010876Z","iopub.status.idle":"2025-07-15T23:52:38.019635Z","shell.execute_reply":"2025-07-15T23:52:38.018948Z"},"papermill":{"duration":0.01342,"end_time":"2025-07-15T23:52:38.020704","exception":false,"start_time":"2025-07-15T23:52:38.007284","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f3b1a72a","cell_type":"markdown","source":"## 🤖 Uploading Trained Model as Public Dataset\n\n1. Manually download the generated `model_weights.zip` file from the `Output` section on the side panel.\n2. Upload the `model_weights.zip` file as a new **PUBLIC** dataset using the `Input` sectino on the side panel (Upload -> New Dataset).\n3. You can refer to the uploaded dataset by its name (all lowercase and replacing spaces with \"-\"). In our example: `Native Fauna Dummy Model` becomes `native-fauna-dummy-model`\n\n> Ensure that the dataset is **Public** and the license is Attributtion 4.0 International (CC BY 4.0)\n\n> If you had already created a dataset, you can upload a new version by clicking `Open in New Tab`, and then checking for updates. ","metadata":{"papermill":{"duration":0.002693,"end_time":"2025-07-15T23:52:38.026316","exception":false,"start_time":"2025-07-15T23:52:38.023623","status":"completed"},"tags":[]}},{"id":"805868e2","cell_type":"code","source":"!ls /kaggle/input/native-fauna-dummy-model","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:38.033109Z","iopub.status.busy":"2025-07-15T23:52:38.032719Z","iopub.status.idle":"2025-07-15T23:52:38.163094Z","shell.execute_reply":"2025-07-15T23:52:38.162188Z"},"papermill":{"duration":0.135235,"end_time":"2025-07-15T23:52:38.164471","exception":false,"start_time":"2025-07-15T23:52:38.029236","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"50886677","cell_type":"markdown","source":"## 🧱 BaseModel Interface\nThis class defines the interface expected by the competition evaluators.\nYou must subclass `BaseModel` and implement `_load_model()` and `_predict()`.","metadata":{"papermill":{"duration":0.00271,"end_time":"2025-07-15T23:52:38.170968","exception":false,"start_time":"2025-07-15T23:52:38.168258","status":"completed"},"tags":[]}},{"id":"8e15eef5","cell_type":"code","source":"class BaseModel:\n    def __init__(self):\n        self._load_model()\n\n    def _load_model(self) -> None:\n        raise NotImplementedError(\"You must implement `_load_model`.\")\n\n    def _predict(self, video_path: str) -> str:\n        raise NotImplementedError(\"You must implement `_predict`.\")\n\n    def predict(self, video_path: str) -> str:\n        return LABELS[self._predict(video_path)]\n\n    def generate_submission(self, eval_dir: str) -> None:\n\n        output_path: str = \"submission.csv\"\n        filenames: List[str] = sorted(os.listdir(eval_dir))\n        submission = []\n        for filename in filenames:\n            if filename.endswith(\".mp4\"):\n                video_path = os.path.join(eval_dir, filename)\n                prediction = self.predict(video_path)\n                submission.append((filename.split(\".\")[0], f\"{prediction}\"))  # Label in single quotes\n        \n        submission_df = pd.DataFrame(submission, columns=[\"Filename\", \"Species\"])\n        submission_df.to_csv(output_path, index=False)\n        print(f\"✅ Submission file saved to {output_path}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:38.178076Z","iopub.status.busy":"2025-07-15T23:52:38.177523Z","iopub.status.idle":"2025-07-15T23:52:38.183904Z","shell.execute_reply":"2025-07-15T23:52:38.183347Z"},"papermill":{"duration":0.011012,"end_time":"2025-07-15T23:52:38.184846","exception":false,"start_time":"2025-07-15T23:52:38.173834","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e4447fe1","cell_type":"markdown","source":"## 🧠 Not Dummy Model\nThis dummy model returns random predictions from the valid label list.\nReplace this with your actual model and logic.","metadata":{"papermill":{"duration":0.002792,"end_time":"2025-07-15T23:52:38.190652","exception":false,"start_time":"2025-07-15T23:52:38.187860","status":"completed"},"tags":[]}},{"id":"63c24fae","cell_type":"code","source":"class CustomModel(BaseModel):\n\n    def _load_model(self) -> None:\n        \"\"\"\n        Loads the video model from the model weights directory.\n        This guarantees that the model runs OFFLINE\n        \"\"\"\n        # Check if we're in the Kaggle environment with the uploaded dataset\n        model_path = MODEL_PATH\n            \n        self.model = r2plus1d_18(pretrained=False)\n        self.model.fc = torch.nn.Linear(self.model.fc.in_features, len(class_to_idx))\n        self.model.load_state_dict(torch.load(model_path, map_location=DEVICE))\n        self.model.eval()\n        self.model = self.model.to(DEVICE)\n        print(f\"✅ Model loaded from {model_path}\")\n\n    def _predict(self, video_path: str) -> int:\n        \"\"\"\n        Uses the loaded video model to generate a prediction.\n        \"\"\"\n        video = load_video_tensor(video_path).unsqueeze(0).to(DEVICE)  # [1, C, T, H, W]\n        with torch.no_grad():\n            outputs = self.model(video)\n            pred_idx = outputs.argmax(dim=1).item()\n        return pred_idx","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:38.197478Z","iopub.status.busy":"2025-07-15T23:52:38.196974Z","iopub.status.idle":"2025-07-15T23:52:38.202383Z","shell.execute_reply":"2025-07-15T23:52:38.201589Z"},"papermill":{"duration":0.010025,"end_time":"2025-07-15T23:52:38.203570","exception":false,"start_time":"2025-07-15T23:52:38.193545","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8568ae63","cell_type":"markdown","source":"## 🚀 Run Inference and Generate Submission File","metadata":{"papermill":{"duration":0.002826,"end_time":"2025-07-15T23:52:38.209366","exception":false,"start_time":"2025-07-15T23:52:38.206540","status":"completed"},"tags":[]}},{"id":"d89970af","cell_type":"code","source":"model = CustomModel()\nmodel.generate_submission(eval_dir=EVAL_DIR)","metadata":{"execution":{"iopub.execute_input":"2025-07-15T23:52:38.215754Z","iopub.status.busy":"2025-07-15T23:52:38.215579Z","iopub.status.idle":"2025-07-16T00:12:26.490347Z","shell.execute_reply":"2025-07-16T00:12:26.489559Z"},"papermill":{"duration":1188.28463,"end_time":"2025-07-16T00:12:26.496921","exception":false,"start_time":"2025-07-15T23:52:38.212291","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"02823172","cell_type":"markdown","source":"## 🎯 Scoring the submission\n\nAt this point, we are ready to calculate the submission's score. Because the `test` dataset is split between the public and private leaderboard, the score below might not match exactly the one shown in the public leaderboard.\n\nTo generate your final submission, click `Submit` on the side panel and ensure that your internet connection is off.","metadata":{"papermill":{"duration":0.006005,"end_time":"2025-07-16T00:12:26.508430","exception":false,"start_time":"2025-07-16T00:12:26.502425","status":"completed"},"tags":[]}},{"id":"9ba18b0e","cell_type":"code","source":"print(\"scoring\")\ndf_test = pd.read_csv(os.path.join(DATASET_ROOT, \"test.csv\")).sort_values(\"Filename\")\ndf_submission = pd.read_csv(\"submission.csv\").sort_values(\"Filename\")","metadata":{"execution":{"iopub.execute_input":"2025-07-16T00:12:26.516243Z","iopub.status.busy":"2025-07-16T00:12:26.515944Z","iopub.status.idle":"2025-07-16T00:12:26.541464Z","shell.execute_reply":"2025-07-16T00:12:26.540684Z"},"papermill":{"duration":0.030647,"end_time":"2025-07-16T00:12:26.542659","exception":false,"start_time":"2025-07-16T00:12:26.512012","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f389c3f9","cell_type":"code","source":"f1_score(df_test[\"Species\"].values, df_submission[\"Species\"].values, average=\"weighted\")","metadata":{"execution":{"iopub.execute_input":"2025-07-16T00:12:26.550218Z","iopub.status.busy":"2025-07-16T00:12:26.550004Z","iopub.status.idle":"2025-07-16T00:12:26.562049Z","shell.execute_reply":"2025-07-16T00:12:26.561288Z"},"papermill":{"duration":0.017251,"end_time":"2025-07-16T00:12:26.563198","exception":false,"start_time":"2025-07-16T00:12:26.545947","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}