Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute Forward Vector #276

Merged
merged 18 commits into from
Oct 4, 2024
Merged
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a7e79bf
Basic implementation of `compute_head_direction_vector()`
b-peri Aug 13, 2024
1f86141
Minor fixes docstring
b-peri Aug 19, 2024
e1758a7
Added unit test for `compute_head_direction_vector()`
b-peri Aug 22, 2024
31ca959
Bug fixes for `test_compute_head_direction_vector()`
b-peri Aug 22, 2024
1bc3e2a
Added validator (and test) to ensure input is 2D
b-peri Aug 22, 2024
e38d552
Refactored `navigation.py` and implemented PR review feedback
b-peri Aug 30, 2024
cd9c3b2
Extended testing and added `front_keypoint` argument to `compute_head…
b-peri Sep 10, 2024
39208d4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 10, 2024
dd20af6
Implemented PR feedback and bugfixes for `compute_2d_head_direction_v…
b-peri Sep 16, 2024
6420ce0
Removed uppercase letters from function names
b-peri Sep 16, 2024
75badfc
Tweaked `compute_polar_coordinates.py` to use new function
b-peri Sep 16, 2024
8709337
Implemented feedback from Zulip discussion and created `compute_head_…
b-peri Sep 17, 2024
054d50c
Fixed typo in docstring
b-peri Sep 17, 2024
df6bed9
Tweaked `compute_forward_vector()` to use new validator
b-peri Sep 24, 2024
b00edcd
More fixes for `test_kinematics.py`
b-peri Sep 24, 2024
be75092
Bugfix for `compute_forward_vector()` and expanded `test_compute_forw…
b-peri Sep 24, 2024
b2de46a
Added test coverage for `compute_head_direction_vector()` alias
b-peri Sep 24, 2024
268d395
Reversed changes to `compute_polar_coordinates.py` and implemented fi…
b-peri Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions examples/compute_polar_coordinates.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
from matplotlib import pyplot as plt

from movement import sample_data
from movement.analysis.kinematics import compute_head_direction_vector
from movement.io import load_poses
from movement.utils.vector import cart2pol, pol2cart

@@ -48,20 +49,14 @@
# To demonstrate how polar coordinates can be useful in behavioural analyses,
# we will compute the head vector of the mouse.
#
# We define it as the vector from the midpoint between the ears to the snout.
# In ``movement``, head vector is defined as the vector perpendicular to the
# line connecting two symmetrical keypoints on either side of the head (usually
# the ears), pointing forwards. (See :func:`here\
# <movement.analysis.kinematics.compute_forward_vector>` for a more
# detailed explanation).

# compute the midpoint between the ears
midpoint_ears = position.sel(keypoints=["left_ear", "right_ear"]).mean(
dim="keypoints"
)
head_vector = compute_head_direction_vector(position, "left_ear", "right_ear")

# compute the head vector
head_vector = position.sel(keypoints="snout") - midpoint_ears

# drop the keypoints dimension
# (otherwise the `head_vector` data array retains a `snout` keypoint from the
# operation above)
head_vector = head_vector.drop_vars("keypoints")

# %%
# Visualise the head trajectory
@@ -72,9 +67,12 @@
# We can start by plotting the trajectory of the midpoint between the ears. We
# will refer to this as the head trajectory.

fig, ax = plt.subplots(1, 1)
midpoint_ears = position.sel(keypoints=["left_ear", "right_ear"]).mean(
dim="keypoints"
)
mouse_name = ds.individuals.values[0]

fig, ax = plt.subplots(1, 1)
sc = ax.scatter(
midpoint_ears.sel(individuals=mouse_name, space="x"),
midpoint_ears.sel(individuals=mouse_name, space="y"),
176 changes: 176 additions & 0 deletions movement/analysis/kinematics.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Compute kinematic variables like velocity and acceleration."""

from typing import Literal

import numpy as np
import xarray as xr

from movement.utils.logging import log_error
from movement.utils.vector import compute_norm
from movement.validators.arrays import validate_dims_coords


@@ -165,3 +169,175 @@ def compute_time_derivative(data: xr.DataArray, order: int) -> xr.DataArray:
for _ in range(order):
result = result.differentiate("time")
return result


def compute_forward_vector(
data: xr.DataArray,
left_keypoint: str,
right_keypoint: str,
camera_view: Literal["top_down", "bottom_up"] = "top_down",
):
"""Compute a 2D forward vector given two left-right symmetrical keypoints.

The forward vector is computed as a vector perpendicular to the
line connecting two symmetrical keypoints on either side of the body
(i.e., symmetrical relative to the mid-sagittal plane), and pointing
forwards (in the rostral direction). A top-down or bottom-up view of the
animal is assumed.

To determine the forward direction of the animal, we need to specify
(1) the right-to-left direction of the animal and (2) its upward direction.
We determine the right-to-left direction via the input left and right
keypoints. The upwards direction, in turn, can be determined by passing the
``camera_view`` argument with either ``"top_down"`` or ``"bottom_up"``. If
the camera view is specified as being ``top_down``, or if no additional
information is provided, we assume that the upwards direction matches that
of the vector [0, 0, -1]. If the camera view is ``bottom_up``, the upwards
direction is assumed to be given by [0, 0, 1]. For both cases, we assume
that position values are expressed in the image coordinate system (where
the positive X-axis is oriented to the right, the positive Y-axis faces
downwards, and positive Z-axis faces away from the person viewing the
screen).

If one of the required pieces of information is missing for a frame (e.g.,
the left keypoint is not visible), then the computed head direction vector
is set to NaN.

Parameters
----------
data : xarray.DataArray
The input data representing position. This must contain
the two symmetrical keypoints located on the left and
right sides of the body, respectively.
left_keypoint : str
Name of the left keypoint, e.g., "left_ear"
right_keypoint : str
Name of the right keypoint, e.g., "right_ear"
camera_view : Literal["top_down", "bottom_up"], optional
The camera viewing angle, used to determine the upwards
direction of the animal. Can be either ``"top_down"`` (where the
upwards direction is [0, 0, -1]), or ``"bottom_up"`` (where the
upwards direction is [0, 0, 1]). If left unspecified, the camera
view is assumed to be ``"top_down"``.

Returns
-------
xarray.DataArray
An xarray DataArray representing the forward vector, with
dimensions matching the input data array, but without the
``keypoints`` dimension.

"""
# Validate input data
_validate_type_data_array(data)
validate_dims_coords(
data,
{
"time": [],
"keypoints": [left_keypoint, right_keypoint],
"space": [],
},
)
if len(data.space) != 2:
raise log_error(
ValueError,
"Input data must have 2 (and only 2) spatial dimensions, but "
f"currently has {len(data.space)}.",
)

# Validate input keypoints
if left_keypoint == right_keypoint:
raise log_error(
ValueError, "The left and right keypoints may not be identical."
)

# Define right-to-left vector
right_to_left_vector = data.sel(
keypoints=left_keypoint, drop=True
) - data.sel(keypoints=right_keypoint, drop=True)

# Define upward vector
# default: negative z direction in the image coordinate system
if camera_view == "top_down":
upward_vector = np.array([0, 0, -1])
else:
upward_vector = np.array([0, 0, 1])

upward_vector = xr.DataArray(
np.tile(upward_vector.reshape(1, -1), [len(data.time), 1]),
dims=["time", "space"],
)

# Compute forward direction as the cross product
# (right-to-left) cross (forward) = up
forward_vector = xr.cross(
right_to_left_vector, upward_vector, dim="space"
)[:, :, :-1] # keep only the first 2 dimensions of the result

# Return unit vector

return forward_vector / compute_norm(forward_vector)


def compute_head_direction_vector(
data: xr.DataArray,
left_keypoint: str,
right_keypoint: str,
camera_view: Literal["top_down", "bottom_up"] = "top_down",
):
"""Compute the 2D head direction vector given two keypoints on the head.

This function is an alias for :func:`compute_forward_vector()\
<movement.analysis.kinematics.compute_forward_vector>`. For more
detailed information on how the head direction vector is computed,
please refer to the documentation for this function.

Parameters
----------
data : xarray.DataArray
The input data representing position. This must contain
the two chosen keypoints corresponding to the left and
right of the head.
left_keypoint : str
Name of the left keypoint, e.g., "left_ear"
right_keypoint : str
Name of the right keypoint, e.g., "right_ear"
camera_view : Literal["top_down", "bottom_up"], optional
The camera viewing angle, used to determine the upwards
direction of the animal. Can be either ``"top_down"`` (where the
upwards direction is [0, 0, -1]), or ``"bottom_up"`` (where the
upwards direction is [0, 0, 1]). If left unspecified, the camera
view is assumed to be ``"top_down"``.

Returns
-------
xarray.DataArray
An xarray DataArray representing the head direction vector, with
dimensions matching the input data array, but without the
``keypoints`` dimension.

"""
return compute_forward_vector(
data, left_keypoint, right_keypoint, camera_view=camera_view
)


def _validate_type_data_array(data: xr.DataArray) -> None:
"""Validate the input data is an xarray DataArray.

Parameters
----------
data : xarray.DataArray
The input data to validate.

Raises
------
ValueError
If the input data is not an xarray DataArray.

"""
if not isinstance(data, xr.DataArray):
raise log_error(
TypeError,
f"Input data must be an xarray.DataArray, but got {type(data)}.",
)
Loading