.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "examples/advanced/annotate_boris_events.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code or to run this example in your browser via Binder. .. rst-class:: sphx-glr-example-title .. _sphx_glr_examples_advanced_annotate_boris_events.py: Annotate and load events with BORIS ====================================== Manually annotate events in BORIS and load them into a ``movement`` dataset as per-frame labels. .. GENERATED FROM PYTHON SOURCE LINES 9-17 .. admonition:: Acknowledgements :class: acknowledgements This example was originally contributed by `Holly Morley `_—a PhD student at the `Sainsbury Wellcome Centre `_ studying sensory-guided predictive movements in mice—and uses data she collected for her PhD project. .. GENERATED FROM PYTHON SOURCE LINES 19-47 Overview -------- This example demonstrates how to label events using `BORIS (Behavioural Observation Research Interactive Software) `_ and load them into a ``movement``-compatible format for downstream analysis. Specifically, we will be annotating gait phases in this example. Gait phase describes whether each limb is in stance (in contact with the ground), swing (in the air), or unknown (e.g. when the limb is off-screen or occluded). Like many events of interest, gait phase can be hard to detect reliably from the pose tracking data using simple thresholds alone, so here we label it by eye from the video. These manual annotations can then be used directly for analysis, or used as training data for supervised behavioural classification models, which can scale the analysis to larger datasets. We will use the ``DLC_single-mouse_DBTravelator_3D`` dataset. This contains 3D pose estimates of the limbs and body of a single mouse locomoting on a dual-belt travelator, transitioning from one belt onto a second faster belt. .. note:: While this example focuses on gait phases, the workflow it demonstrates—annotating events in BORIS and integrating them into ``movement``—generalises to any project that combines events of interest (behavioural or otherwise) with tracking data. .. GENERATED FROM PYTHON SOURCE LINES 49-51 Imports ------- .. GENERATED FROM PYTHON SOURCE LINES 51-62 .. code-block:: Python from pathlib import Path import matplotlib.pyplot as plt import numpy as np import pandas as pd import pooch from matplotlib.patches import Patch from movement import sample_data from movement.filtering import filter_by_confidence .. GENERATED FROM PYTHON SOURCE LINES 63-69 Load sample dataset and media ----------------------------- First, let's load the 3D pose dataset and separately download one of the source videos from which it was derived using `pooch `_. We will use this video to manually annotate gait phase events in BORIS. .. GENERATED FROM PYTHON SOURCE LINES 69-86 .. code-block:: Python ds = sample_data.fetch_dataset( "DLC_single-mouse_DBTravelator_3D.predictions.h5" ) # Download the video vid_name = "single-mouse_DBTravelator_video.avi" video_path = pooch.retrieve( url=( f"https://gin.g-node.org/neuroinformatics/movement-test-data/raw" f"/master/videos/{vid_name}" ), known_hash=None, fname=vid_name, path=Path.home() / ".movement/data/videos", ) .. GENERATED FROM PYTHON SOURCE LINES 87-92 We saved the video to our local ``movement`` cache directory here, but feel free to modify the ``path`` in the ``pooch.retrieve`` call. Either way, make a note of this path as you will need it to open the video in BORIS. .. GENERATED FROM PYTHON SOURCE LINES 94-228 Annotate gait phase events in BORIS ----------------------------------- The following steps describe how to annotate gait phase events for four limbs in the downloaded video, producing labels that we will later load into the associated dataset ``ds``. For a more complete guide, please refer to the `BORIS user guide `_. **Step 1: Install BORIS** Download and install BORIS from the `BORIS website `_. Full installation instructions can be found in the `BORIS installation guide `_. .. note:: This example was created using BORIS **v9.8.5**. The steps described here may differ slightly for other versions. **Step 2: Create a new project** Open BORIS and create a new project via **Project > New Project**. In the dialogue that appears: - Set a **Project name**, e.g. ``label_gait``. - Add a brief **Project description**. - Set **Project time format** to ``seconds``. **Step 3: Build the behaviour ethogram** Navigate to the **Ethogram** tab. An ethogram is a catalogue of all events (or behaviours) to be annotated, where each event is assigned a keyboard shortcut for fast labelling. Here, each event represents a gait phase state for a single limb, e.g. the front-left paw in stance. The four limbs are the front-left (``FL``), front-right (``FR``), hind-left (``HL``), and hind-right (``HR``) paws. For each combination of limb and phase ( ``stance``, ``swing``, ``unknown``), add a new behaviour: 1. Click **Behaviour > Add new behaviour**. 2. Under **Behaviour type**, select **State event**—a state event has a duration, defined by a start and end time, as opposed to a point event which is instantaneous. 3. Set the **Code**, e.g. ``FL_stance`` for front-left paw in stance. 4. Assign a unique **Key**, e.g. ``q`` for ``FL_stance``. 5. Repeat until all 12 behaviours (4 limbs × 3 phases) are defined. .. image:: /_static/events_ethogram.png :width: 600 6. Because gait phase states cannot co-occur within a limb, we need to set exclusion criteria to define which behaviours are mutually exclusive. Open the **Exclusion matrix** and tick all mutually exclusive pairs (e.g. ``FL_stance`` with ``FL_swing``). With this configured, starting a new state within a limb will automatically close the previous state for the same limb. Click **OK**. .. image:: /_static/events_exclusions.png :width: 600 7. Now you have configured your project, click **OK** to close the project creation pop-up. You can always edit your project at a later stage via **Project > Edit project**. Before we move on to annotating our video, make sure to save the configured project via **Project > Save project** or **Ctrl + S**. **Step 4: Start an observation** Create a new observation via **Observations > New observation**: - Set an **Observation ID**, e.g. ``0001``. - Tick **Observation from media file(s)**. - Click **Add media > with absolute path** and navigate to the video file downloaded above at ``video_path``. - Click **Start**. **Step 5: Annotate events** .. image:: /_static/events_observation.png :width: 600 - Use the ``←`` and ``→`` arrow keys or the upper panel buttons to step through the video frame-by-frame. - The on-screen ethogram lists each event's keyboard shortcut. As each event occurs across the video, press its assigned keyboard shortcut (e.g. ``q`` for the front-left paw in stance) at the frame where it begins. This **opens** the state event, recording its start. Every open state event must also be **closed**. There are two ways to do this in BORIS: - **Implicit closing**: an open event is automatically closed when you start another event that is mutually exclusive with it, as defined by the exclusion matrix. For example, because we set the front-left paw's phases as mutually exclusive, pressing the shortcut for its swing phase while ``FL_stance`` is open closes ``FL_stance`` and opens ``FL_swing`` in one action. N.B. BORIS places this stop just before the frame where the new event begins (at ``next_event_start - 0.001 s``), so it falls between frames and its frame index is left empty (NA). - **Explicit closing**: an open event is closed manually by pressing its own shortcut again, which stops it at the current frame (with the frame index filled in). In this example, each limb is always in one of its phases (stance, swing, or unknown), with one beginning as the last ends, so we can use implicit closing to annotate almost the entire video. The only exception is the final phase of each limb, which has no following event to close it: - At the **last frame** of the video, press the shortcut of each still-open event again to close it **explicitly**. Because the implicit stops fall between frames, run **Observations > Add frame indexes** before exporting to populate their empty frame indexes. .. note:: If you were instead annotating isolated or non-contiguous state events (e.g. ``groom`` for occasional bouts of grooming behaviours), you would simply close every event explicitly. Note that in this case there is no need to run **Observations > Add frame indexes** as all frames are explicitly defined. Alternatively, if the events are instantaneous ( e.g. ``lick`` to mark the occurrence of a single lick), you could define them as **point events** rather than state events (see **Step 3**): point events have no start and stop time and are instead recorded at a single frame with a single keypress. - Make sure to save the project along with your new annotations via **Project > Save project** or **Ctrl + S**. **Step 6: Export the event data** Export the annotations via **Observations > Export events > Aggregated events** and save as a TSV (the default here, though you can choose CSV if you prefer). .. GENERATED FROM PYTHON SOURCE LINES 230-244 Import BORIS event data into movement ------------------------------------- We will now load the exported TSV file into a pandas DataFrame. If you have created your own annotations following the steps above, replace ``gait_events_path`` with the path to your exported TSV file, e.g.: .. code-block:: python gait_events_path = "/your/path/to/aggregated.tsv" Load the event data from TSV into a pandas Dataframe using :func:`pandas.read_csv`. We pass ``sep="\t"`` because a TSV file separates columns with tabs rather than commas (drop it if you exported a CSV). .. GENERATED FROM PYTHON SOURCE LINES 244-258 .. code-block:: Python df = pd.read_csv(gait_events_path, sep="\t") show_columns = [ "Behaviour", "Start (s)", "Stop (s)", "Image index start", "Image index stop", ] print("Key columns from the BORIS file:") print(df[show_columns].head(10)) .. rst-class:: sphx-glr-script-out .. code-block:: none Key columns from the BORIS file: Behaviour Start (s) Stop (s) Image index start Image index stop 0 FL_unknown 0.000 0.627 0 155 1 HL_unknown 0.000 0.716 0 177 2 FR_unknown 0.000 0.525 0 130 3 HR_unknown 0.000 0.635 0 157 4 FR_stance 0.526 0.627 130 155 5 FL_stance 0.628 0.712 155 176 6 FR_swing 0.628 0.716 155 177 7 HR_stance 0.636 0.712 157 176 8 FL_swing 0.713 0.809 176 200 9 HR_swing 0.713 0.793 176 196 .. GENERATED FROM PYTHON SOURCE LINES 330-338 Each row corresponds to a single annotated event, with columns for the behaviour code, start and stop times (in seconds), and start and stop frame indices. To attach these labels to ``ds`` as per-frame gait phase labels, we first reformat the event data: we split each behaviour code into separate ``limb`` and ``state`` columns, and copy across the corresponding start and stop frame indices. .. GENERATED FROM PYTHON SOURCE LINES 338-351 .. code-block:: Python limbs = ["FL", "FR", "HL", "HR"] events = ( df["Behaviour"] .str.split("_", expand=True) .rename(columns={0: "limb", 1: "state"}) ) events["start_frame"] = df["Image index start"] events["stop_frame"] = df["Image index stop"] print(f"Parsed events:\n{events.head(10)}") .. rst-class:: sphx-glr-script-out .. code-block:: none Parsed events: limb state start_frame stop_frame 0 FL unknown 0 155 1 HL unknown 0 177 2 FR unknown 0 130 3 HR unknown 0 157 4 FR stance 130 155 5 FL stance 155 176 6 FR swing 155 177 7 HR stance 157 176 8 FL swing 176 200 9 HR swing 176 196 .. GENERATED FROM PYTHON SOURCE LINES 352-359 Note that consecutive events for the same limb share a boundary frame in the BORIS export (e.g. ``FR_stance`` ends at frame 155 and ``FR_swing`` starts at frame 155). We will therefore treat each event as a half-open interval ``[start_frame, stop_frame)``, which naturally assigns each shared frame to the next event and avoids double-counting. The final event per limb has no successor, so we extend its ``stop_frame`` by 1 to make sure its actual last frame is still included. .. GENERATED FROM PYTHON SOURCE LINES 359-362 .. code-block:: Python events.loc[events.groupby("limb")["stop_frame"].idxmax(), "stop_frame"] += 1 .. GENERATED FROM PYTHON SOURCE LINES 363-370 Each row in ``events`` defines a phase label over a range of frames rather than for a single frame. We therefore expand this into a per-frame representation, initialising a 2-D array of shape ``(n_frames, n_limbs)`` with ``NaN`` and filling each interval with the corresponding phase label. The ``NaN`` initial values act as a fallback for any frames not covered by an annotated event. Every frame is covered in this example but this keeps the approach robust to partial annotations in other cases. .. GENERATED FROM PYTHON SOURCE LINES 370-385 .. code-block:: Python phase_data = np.full((ds.time.size, len(limbs)), np.nan, dtype=object) for limb_idx, limb in enumerate(limbs): limb_events = events[events["limb"] == limb] for _, ev in limb_events.iterrows(): start, stop = ev["start_frame"], ev["stop_frame"] phase_data[start:stop, limb_idx] = ev["state"] print( f"10 sample frames from phase_data (shape: n_frames, n_limbs):\n" f"{phase_data[210:220]}" ) .. rst-class:: sphx-glr-script-out .. code-block:: none 10 sample frames from phase_data (shape: n_frames, n_limbs): [['stance' 'swing' 'swing' 'stance'] ['stance' 'swing' 'swing' 'stance'] ['stance' 'swing' 'swing' 'stance'] ['stance' 'swing' 'swing' 'stance'] ['stance' 'swing' 'swing' 'stance'] ['stance' 'swing' 'swing' 'swing'] ['stance' 'swing' 'swing' 'swing'] ['swing' 'swing' 'swing' 'swing'] ['swing' 'stance' 'stance' 'swing'] ['swing' 'stance' 'stance' 'swing']] .. GENERATED FROM PYTHON SOURCE LINES 386-399 .. dropdown:: A caveat on BORIS stop times at high frame rates :color: info :icon: info You may recall from Step 5 that BORIS subtracts 1 ms from the stop time of each implicitly closed event. For video frame rates below ~500 fps, this small difference is rounded out when the empty frame indices are filled in (via **Observations > Add frame indexes**), so consecutive events stay contiguous. At higher frame rates it may instead leave an unlabelled one-frame gap (here a ``NaN``) between events that should be back-to-back. This does not affect our approach here, which is robust to ``NaN`` gaps, but is worth being aware of. .. GENERATED FROM PYTHON SOURCE LINES 401-402 Before attaching the labels, let's inspect ``ds``. .. GENERATED FROM PYTHON SOURCE LINES 402-405 .. code-block:: Python print(ds) .. rst-class:: sphx-glr-script-out .. code-block:: none Size: 675kB Dimensions: (time: 418, space: 3, keypoint: 50, individual: 1) Coordinates: * time (time) float64 3kB 0.0 0.004049 0.008097 ... 1.68 1.684 1.688 * space (space) `_. We use :meth:`xarray.Dataset.assign_coords` to attach one non-dimension coordinate per limb on the ``time`` dimension, named ``gait_``, holding the per-frame gait phase label for that limb. This will allow us to select data by gait phase in the next section. .. GENERATED FROM PYTHON SOURCE LINES 425-431 .. code-block:: Python for limb_idx, limb in enumerate(limbs): ds = ds.assign_coords({f"gait_{limb}": ("time", phase_data[:, limb_idx])}) print(ds) .. rst-class:: sphx-glr-script-out .. code-block:: none Size: 689kB Dimensions: (time: 418, space: 3, keypoint: 50, individual: 1) Coordinates: * time (time) float64 3kB 0.0 0.004049 0.008097 ... 1.68 1.684 1.688 gait_FL (time) object 3kB 'unknown' 'unknown' ... 'unknown' 'unknown' gait_FR (time) object 3kB 'unknown' 'unknown' ... 'unknown' 'unknown' gait_HL (time) object 3kB 'unknown' 'unknown' ... 'unknown' 'unknown' gait_HR (time) object 3kB 'unknown' 'unknown' ... 'unknown' 'unknown' * space (space) `` coordinate against a phase label produces a **boolean mask** that is ``True`` for matching frames and ``False`` elsewhere. Passing this mask to :meth:`~xarray.DataArray.sel` keeps only the matching timepoints. For example, to select all timepoints where the front-right paw is in stance: .. GENERATED FROM PYTHON SOURCE LINES 444-449 .. code-block:: Python mask = ds.gait_FR == "stance" ds_fr_stance = ds.position.sel(time=mask) print(ds_fr_stance) .. rst-class:: sphx-glr-script-out .. code-block:: none Size: 164kB -0.7868 0.009323 -0.7108 -0.377 0.0 470.7 ... 11.39 17.04 1.525 2.343 0.0 18.4 Coordinates: * time (time) float64 1kB 0.5263 0.5304 0.5344 ... 1.474 1.534 1.538 gait_FL (time) object 1kB 'unknown' 'unknown' ... 'stance' 'stance' gait_FR (time) object 1kB 'stance' 'stance' ... 'stance' 'stance' gait_HL (time) object 1kB 'unknown' 'unknown' ... 'swing' 'swing' gait_HR (time) object 1kB 'unknown' 'unknown' ... 'stance' 'stance' * space (space) Size: 4kB -0.8506 -0.3681 -0.3983 -0.1385 nan 470.3 ... 14.85 -0.4401 4.266 11.73 14.0 Coordinates: * time (time) float64 24B 1.336 1.445 1.449 gait_FL (time) object 24B 'stance' 'stance' 'stance' gait_FR (time) object 24B 'stance' 'stance' 'stance' gait_HL (time) object 24B 'stance' 'stance' 'stance' gait_HR (time) object 24B 'stance' 'stance' 'stance' * space (space) Size: 301kB -0.7868 0.009323 -0.7108 -0.377 nan 470.7 ... 11.39 17.04 1.525 2.343 nan 18.4 Coordinates: * time (time) float64 2kB 0.5263 0.5304 0.5344 ... 1.53 1.534 1.538 gait_FL (time) object 2kB 'unknown' 'unknown' ... 'stance' 'stance' gait_FR (time) object 2kB 'stance' 'stance' ... 'stance' 'stance' gait_HL (time) object 2kB 'unknown' 'unknown' ... 'swing' 'swing' gait_HR (time) object 2kB 'unknown' 'unknown' ... 'stance' 'stance' * space (space) Size: 333kB Dimensions: (time: 200, space: 3, keypoint: 50, individual: 1) Coordinates: * time (time) float64 2kB 0.6073 0.6113 0.6154 ... 1.405 1.409 1.413 gait_FL (time) object 2kB 'unknown' 'unknown' ... 'swing' 'swing' gait_FR (time) object 2kB 'stance' 'stance' ... 'stance' 'stance' gait_HL (time) object 2kB 'unknown' 'unknown' ... 'stance' 'stance' gait_HR (time) object 2kB 'unknown' 'unknown' ... 'swing' 'swing' stride_FR (time) float64 2kB 0.0 0.0 0.0 0.0 0.0 ... 5.0 5.0 5.0 5.0 5.0 * space (space) ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: annotate_boris_events.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: annotate_boris_events.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_