{ "cells": [ { "cell_type": "markdown", "id": "8d507f91", "metadata": {}, "source": [ "# Tool-using mice - Lockbox\n", "\n", "Dataset from Reiske et al., 2025¹, containing video and pose files of individual mice solving mechanical puzzle \"lockboxes\" recorded from three camera perspectives (top, front, side).\n", "\n", "- Full dataset: https://doi.org/10.14279/depositonce-23850\n", "- Subset of dataset (used here): https://www.dropbox.com/scl/fo/h7nkai8574h23qfq9m1b2/AP4gNZOpDJJ7z0yGtbWQiOc?rlkey=w36jzxqjkghg0j0xva5zsxy2v&st=5r9msqjw&dl=0\n", "\n", "---\n", "\n", "¹ Reiske, P., Boon, M. N., Andresen, N., Traverso, S., Hohlbaum, K., Lewejohann, L., Thöne-Reineke, C., Hellwich, O., & Sprekeler, H. (2025). Mouse Lockbox Dataset: Behavior Recognition for Mice Solving Lockboxes (arXiv:2505.15408). arXiv. https://doi.org/10.48550/arXiv.2505.15408\n", "\n", "\n", "\n", "From Fig. 1 in Reiske et al., 2025¹." ] }, { "cell_type": "code", "execution_count": 2, "id": "6ffbf1f4", "metadata": {}, "outputs": [], "source": [ "import requests\n", "import zipfile\n", "import pandas as pd\n", "import xarray as xr\n", "from pathlib import Path\n", "from movement.kinematics import compute_velocity, compute_speed\n", "from movement.utils.vector import compute_norm\n", "from movement.io import load_poses, save_poses\n", "\n", "import ethograph as eto\n", "from ethograph.io.nwb_alignment import align_media_per_trial" ] }, { "cell_type": "markdown", "id": "60c2c674", "metadata": {}, "source": [ "### Download data" ] }, { "cell_type": "code", "execution_count": null, "id": "download", "metadata": {}, "outputs": [], "source": [ "def download_and_extract(url: str, data_folder: Path) -> None:\n", " zip_path = data_folder / \"dataset.zip\"\n", " data_folder.mkdir(parents=True, exist_ok=True)\n", " response = requests.get(url, stream=True)\n", " response.raise_for_status()\n", " with open(zip_path, \"wb\") as f:\n", " for chunk in response.iter_content(chunk_size=8192):\n", " f.write(chunk)\n", " with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n", " zip_ref.extractall(data_folder)\n", " zip_path.unlink()\n", "\n", "\n", "try:\n", " _here = Path(__vsc_ipynb_file__).parent\n", "except NameError:\n", " _here = Path().resolve()\n", "\n", "data_folder = _here.parent / \"data\" / \"lockbox\"\n", "url = \"https://www.dropbox.com/scl/fo/h7nkai8574h23qfq9m1b2/AP4gNZOpDJJ7z0yGtbWQiOc?rlkey=w36jzxqjkghg0j0xva5zsxy2v&e=1&st=5r9msqjw&dl=1\"\n", "download_and_extract(url, data_folder)\n", "\n", "data_folder = data_folder / \"labeled\"" ] }, { "cell_type": "markdown", "id": "cd33e5d8", "metadata": {}, "source": [ "### Build NWB alignment" ] }, { "cell_type": "code", "execution_count": null, "id": "build_alignment", "metadata": {}, "outputs": [], "source": [ "fps = 30\n", "cameras = [\"front-view\", \"side-view\", \"top-down-view\"]\n", "\n", "trials = [\n", " \"2021-02-15_07-32-44_segment1\",\n", " \"2021-03-05_08-36-42_segment1\",\n", " \"2021-05-24_07-36-05_segment1\",\n", " \"2021-05-25_08-19-50_segment2\",\n", " \"2021-05-31_07-34-21_segment2\",\n", " \"2021-05-31_07-34-21_segment3\",\n", "]\n", "\n", "# Discover files for each trial and camera\n", "video_by_trial: dict[str, dict[str, str]] = {}\n", "pose_by_trial: dict[str, dict[str, str]] = {}\n", "\n", "for trial in trials:\n", " for cam in cameras:\n", " # Video: e.g. 2021-02-15_07-32-44_segment1_front-view.avi\n", " video_file = data_folder / f\"{trial}_{cam}.avi\"\n", " if video_file.exists():\n", " video_by_trial.setdefault(trial, {})[cam] = str(video_file)\n", "\n", " # Pose: convert .h5 → .csv first (done during feature extraction below),\n", " # stored as *_individual_0.csv\n", " pose_csv = data_folder / f\"{trial}_{cam}_individual_0.csv\"\n", " if pose_csv.exists():\n", " pose_by_trial.setdefault(trial, {})[cam] = str(pose_csv)\n", "\n", "# Build session table\n", "session_table = pd.DataFrame({\"trial\": trials})\n", "for cam in cameras:\n", " session_table[f\"video_{cam}\"] = [\n", " video_by_trial.get(t, {}).get(cam, \"\") for t in trials\n", " ]\n", " session_table[f\"pose_{cam}\"] = [\n", " pose_by_trial.get(t, {}).get(cam, \"\") for t in trials\n", " ]\n", "session_table = session_table.loc[:, (session_table != \"\").any()]\n", "\n", "nwb_path = data_folder / \".ethograph\" / \"alignment.nwb\"\n", "align_media_per_trial(\n", " trial_table=session_table,\n", " stream_rates={\"video\": float(fps), \"pose\": float(fps)},\n", " output_path=nwb_path,\n", " pose_fps=float(fps),\n", ")" ] }, { "cell_type": "markdown", "id": "convert_header", "metadata": {}, "source": [ "### Build feature datasets" ] }, { "cell_type": "code", "execution_count": null, "id": "ccadd32f", "metadata": {}, "outputs": [], "source": [ "ds_list = []\n", "\n", "for trial in trials:\n", " files = list(data_folder.glob(f\"{trial}*\"))\n", " trial_datasets: dict[str, xr.Dataset] = {}\n", "\n", " for file in files:\n", " name = file.name\n", "\n", " if file.suffix != \".h5\":\n", " continue\n", "\n", " df = pd.read_hdf(file)\n", " ds = load_poses.from_dlc_style_df(df, fps=fps)\n", "\n", " # Save CSV for future use\n", " csv_path = str(file).replace(\".h5\", \".csv\")\n", " save_poses.to_dlc_file(ds, csv_path)\n", "\n", " if \"front-view\" in name:\n", " ds[\"front_velocity\"] = compute_velocity(ds.position)\n", " ds[\"front_speed\"] = compute_speed(ds.position)\n", " trial_datasets[\"front-view\"] = ds\n", "\n", " elif \"top-down-view\" in name:\n", " head_centre_pos = ds.position.sel(\n", " keypoints=[\"ear_left\", \"ear_right\"]\n", " ).mean(\"keypoints\")\n", " ds[\"topview_distance_head_lever_tip\"] = compute_norm(\n", " ds.position.sel(keypoints=\"lever_tip\") - head_centre_pos\n", " )\n", " ds[\"topview_distance_head_stick_head\"] = compute_norm(\n", " ds.position.sel(keypoints=\"stick_head\") - head_centre_pos\n", " )\n", " ds[\"topview_distance_head_ball\"] = compute_norm(\n", " ds.position.sel(keypoints=\"ball\") - head_centre_pos\n", " )\n", " trial_datasets[\"top-down-view\"] = ds\n", "\n", " elif \"side-view\" in name:\n", " trial_datasets[\"side-view\"] = ds\n", "\n", " if not trial_datasets:\n", " continue\n", "\n", " ds_merged = xr.merge(trial_datasets.values(), compat=\"override\")\n", " if \"top-down-view\" in trial_datasets:\n", " ds_merged[\"position\"] = trial_datasets[\"top-down-view\"][\"position\"]\n", " ds_merged.attrs[\"trial\"] = trial\n", " ds_list.append(ds_merged)\n", "\n", "dt = eto.from_datasets(ds_list)\n", "dt.save(data_folder / \"lockbox.nc\")\n", "print(f\"Saved to {data_folder / 'lockbox.nc'}\")" ] } ], "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.12.12" } }, "nbformat": 4, "nbformat_minor": 5 }