|
1 | | -"""Compute and visualise kinematics. |
| 1 | +"""Compute and visualise kinematics |
2 | 2 | ==================================== |
3 | 3 |
|
4 | 4 | Compute displacement, velocity and acceleration, and |
|
12 | 12 | # For interactive plots: install ipympl with `pip install ipympl` and uncomment |
13 | 13 | # the following line in your notebook |
14 | 14 | # %matplotlib widget |
| 15 | +import numpy as np |
15 | 16 | from matplotlib import pyplot as plt |
16 | 17 |
|
17 | 18 | import movement.kinematics as kin |
|
50 | 51 | # --------------------------- |
51 | 52 | # First, let's visualise the trajectories of the mice in the XY plane, |
52 | 53 | # colouring them by individual. |
53 | | -# We use :func:`movement.plots.plot_centroid_trajectory` which is a wrapper |
54 | | -# around :func:`matplotlib.pyplot.scatter` that simplifies plotting the |
55 | | -# trajectories of individuals in the dataset. |
| 54 | +# For this we can use :func:`movement.plots.plot_centroid_trajectory` |
| 55 | +# which is a wrapper around :func:`matplotlib.pyplot.scatter`. |
56 | 56 | # The fig and ax objects returned can be used to further customise the plot. |
57 | 57 |
|
58 | 58 | # Create a single figure and axes |
|
77 | 77 | label=mouse_name, |
78 | 78 | ) |
79 | 79 | ax.legend().set_alpha(1) |
| 80 | +ax.set_xlabel("x (pixels)") |
| 81 | +ax.set_ylabel("y (pixels)") |
80 | 82 | fig.show() |
81 | 83 |
|
82 | 84 | # %% |
|
86 | 88 | # follows the convention for SLEAP and most image processing tools. |
87 | 89 |
|
88 | 90 | # %% |
89 | | -# By default :func:`plot_centroid_trajectory()<movement.plots.\ |
90 | | -# plot_centroid_trajectory>` colours data points based on their timestamps: |
91 | | -fig, axes = plt.subplots(3, 1, sharey=True) |
92 | | -for mouse_name, ax in zip(position.individuals.values, axes, strict=False): |
| 91 | +# We can also plot the trajectories of the mice in the XY plane independently, |
| 92 | +# colouring the data points based on their timestamps. This is the default |
| 93 | +# behaviour of |
| 94 | +# :func:`plot_centroid_trajectory()<movement.plots.plot_centroid_trajectory>` |
| 95 | +# when the ``c`` argument is not provided: |
| 96 | +fig, axes = plt.subplots(2, 2, sharey=True) |
| 97 | +for mouse_name, ax in zip( |
| 98 | + position.individuals.values, axes.flat, strict=False |
| 99 | +): |
93 | 100 | ax.invert_yaxis() |
94 | 101 | fig, ax = plot_centroid_trajectory( |
95 | 102 | position, |
96 | 103 | individual=mouse_name, |
97 | 104 | ax=ax, |
98 | 105 | s=2, |
99 | 106 | ) |
| 107 | + ax.set_aspect("equal") |
| 108 | + ax.set_xlim(150, 1250) |
| 109 | + ax.set_ylim(500, 1100) |
100 | 110 | ax.set_title(f"Trajectory {mouse_name}") |
101 | 111 | ax.set_xlabel("x (pixels)") |
102 | 112 | ax.set_ylabel("y (pixels)") |
103 | 113 | ax.collections[0].colorbar.set_label("Time (frames)") |
| 114 | +# Hide the unused subplot (4th one) |
| 115 | +axes[1, 1].set_visible(False) |
104 | 116 | fig.tight_layout() |
105 | 117 | fig.show() |
106 | 118 |
|
|
111 | 123 | # the third mouse (``AEON3B_TP2``) followed an anti-clockwise direction. |
112 | 124 |
|
113 | 125 | # %% |
114 | | -# We can also easily plot the components of the position vector against time |
| 126 | +# We can also inspect the components of the position vector against time |
115 | 127 | # using ``xarray``'s built-in plotting methods. We use |
116 | 128 | # :meth:`xarray.DataArray.squeeze` to |
117 | 129 | # remove the dimension of length 1 from the data (the ``keypoints`` dimension). |
|
124 | 136 | # and the ``x`` and ``y`` coordinates of the ``position`` are in pixels. |
125 | 137 |
|
126 | 138 | # %% |
127 | | -# Compute displacement |
128 | | -# --------------------- |
| 139 | +# Compute displacement vectors |
| 140 | +# ---------------------------- |
129 | 141 | # The :mod:`movement.kinematics` module |
130 | | -# provides functions to compute various kinematic quantities, |
131 | | -# such as displacement, velocity, and acceleration. |
132 | | -# We can start off by computing the distance travelled by the mice along |
133 | | -# their trajectories: |
134 | | - |
135 | | -displacement = kin.compute_displacement(position) |
| 142 | +# provides functions to compute various kinematic variables, |
| 143 | +# such as displacement, velocity, and acceleration. Below we showcase |
| 144 | +# how these functions can be used. |
| 145 | +# |
| 146 | +# We can compute the forward displacement vectors as follows: |
| 147 | +forward_displacement = kin.compute_forward_displacement(position) |
136 | 148 |
|
137 | 149 | # %% |
138 | | -# The :func:`movement.kinematics.compute_displacement` |
| 150 | +# The :func:`movement.kinematics.compute_forward_displacement` |
139 | 151 | # function will return a data array equivalent to the ``position`` one, |
140 | | -# but holding displacement data along the ``space`` axis, rather than |
141 | | -# position data. |
| 152 | +# but holding displacement data along the ``space`` axis. |
142 | 153 | # |
143 | | -# The ``displacement`` data array holds, for a given individual and keypoint |
144 | | -# at timestep ``t``, the vector that goes from its previous position at time |
145 | | -# ``t-1`` to its current position at time ``t``. |
| 154 | +# The ``forward_displacement`` data array holds, for a given individual and |
| 155 | +# keypoint at timestep ``t``, the vector that goes from its current position |
| 156 | +# at time ``t`` to its next position at time ``t+1``. |
146 | 157 |
|
147 | 158 | # %% |
148 | | -# And what happens at ``t=0``, since there is no previous timestep? |
149 | | -# We define the displacement vector at time ``t=0`` to be the zero vector. |
150 | | -# This way the shape of the ``displacement`` data array is the |
151 | | -# same as the ``position`` array: |
| 159 | +# And what happens in the last timestep, when there is no next timepoint? |
| 160 | +# We define the forward displacement vector then to be the |
| 161 | +# zero vector. This way the shape of the ``forward_displacement`` data array |
| 162 | +# is the same as the ``position`` array: |
152 | 163 | print(f"Shape of position: {position.shape}") |
153 | | -print(f"Shape of displacement: {displacement.shape}") |
| 164 | +print(f"Shape of displacement: {forward_displacement.shape}") |
154 | 165 |
|
155 | 166 | # %% |
156 | | -# We can visualise these displacement vectors with a quiver plot. In this case |
157 | | -# we focus on the mouse ``AEON3B_TP2``: |
| 167 | +# We can visualise the forward displacement vectors with a quiver plot. In |
| 168 | +# this case we focus on the mouse ``AEON3B_TP2``: |
158 | 169 | mouse_name = "AEON3B_TP2" |
159 | 170 |
|
160 | 171 | fig = plt.figure() |
|
169 | 180 | cmap="viridis", |
170 | 181 | ) |
171 | 182 |
|
172 | | -# plot displacement vectors: at t, vector from t-1 to t |
| 183 | +# plot forward displacement vectors: at t, vector from t to t+1 |
173 | 184 | ax.quiver( |
174 | 185 | position.sel(individuals=mouse_name, space="x"), |
175 | 186 | position.sel(individuals=mouse_name, space="y"), |
176 | | - displacement.sel(individuals=mouse_name, space="x"), |
177 | | - displacement.sel(individuals=mouse_name, space="y"), |
| 187 | + forward_displacement.sel(individuals=mouse_name, space="x"), |
| 188 | + forward_displacement.sel(individuals=mouse_name, space="y"), |
178 | 189 | angles="xy", |
179 | 190 | scale=1, |
180 | 191 | scale_units="xy", |
|
183 | 194 | headaxislength=9, |
184 | 195 | ) |
185 | 196 |
|
186 | | -ax.axis("equal") |
187 | | -ax.set_xlim(450, 575) |
188 | | -ax.set_ylim(950, 1075) |
| 197 | +ax.set_xlim(480, 600) |
| 198 | +ax.set_ylim(980, 1080) |
189 | 199 | ax.set_xlabel("x (pixels)") |
190 | 200 | ax.set_ylabel("y (pixels)") |
191 | 201 | ax.set_title(f"Zoomed in trajectory of {mouse_name}") |
192 | 202 | ax.invert_yaxis() |
193 | 203 | fig.colorbar(sc, ax=ax, label="time (s)") |
194 | 204 |
|
| 205 | + |
195 | 206 | # %% |
196 | | -# Notice that this figure is not very useful as a visual check: |
197 | | -# we can see that there are vectors defined for each point in |
198 | | -# the trajectory, but we have no easy way to verify they are indeed |
199 | | -# the displacement vectors from ``t-1`` to ``t``. |
| 207 | +# We can visually verify that indeed the forward displacement vector |
| 208 | +# connects the previous and current positions as expected. |
200 | 209 |
|
201 | 210 | # %% |
202 | | -# If instead we plot |
203 | | -# the opposite of the displacement vector, we will see that at every time |
204 | | -# ``t``, the vectors point to the position at ``t-1``. |
205 | | -# Remember that the displacement vector is defined as the vector at |
206 | | -# time ``t``, that goes from the previous position ``t-1`` to the |
207 | | -# current position at ``t``. Therefore, the opposite vector will point |
208 | | -# from the position point at ``t``, to the position point at ``t-1``. |
| 211 | +# Similarly, with :func:`movement.kinematics.compute_backward_displacement` |
| 212 | +# we can compute the backward displacement vectors, which connect the current |
| 213 | +# position to the previous one: |
| 214 | +backward_displacement = kin.compute_backward_displacement(position) |
209 | 215 |
|
210 | 216 | # %% |
211 | | -# We can easily do this by flipping the sign of the displacement vector in |
212 | | -# the plot above: |
213 | | -mouse_name = "AEON3B_TP2" |
| 217 | +# In this case, the backward displacement vector at the first timestep |
| 218 | +# is defined as the zero vector, since there is no previous position. |
| 219 | + |
| 220 | +# %% |
| 221 | +# Adapting the code snippet from above, we can visually check that the |
| 222 | +# backward displacement vector is indeed the reverse of the forward |
| 223 | +# displacement vector. |
214 | 224 |
|
215 | 225 | fig = plt.figure() |
216 | 226 | ax = fig.add_subplot() |
217 | 227 |
|
218 | | -# plot position data |
219 | 228 | sc = ax.scatter( |
220 | 229 | position.sel(individuals=mouse_name, space="x"), |
221 | 230 | position.sel(individuals=mouse_name, space="y"), |
|
224 | 233 | cmap="viridis", |
225 | 234 | ) |
226 | 235 |
|
227 | | -# plot displacement vectors: at t, vector from t-1 to t |
228 | 236 | ax.quiver( |
229 | 237 | position.sel(individuals=mouse_name, space="x"), |
230 | 238 | position.sel(individuals=mouse_name, space="y"), |
231 | | - -displacement.sel(individuals=mouse_name, space="x"), # flipped sign |
232 | | - -displacement.sel(individuals=mouse_name, space="y"), # flipped sign |
| 239 | + backward_displacement.sel(individuals=mouse_name, space="x"), |
| 240 | + backward_displacement.sel(individuals=mouse_name, space="y"), |
233 | 241 | angles="xy", |
234 | 242 | scale=1, |
235 | 243 | scale_units="xy", |
236 | 244 | headwidth=7, |
237 | 245 | headlength=9, |
238 | 246 | headaxislength=9, |
239 | 247 | ) |
240 | | -ax.axis("equal") |
241 | | -ax.set_xlim(450, 575) |
242 | | -ax.set_ylim(950, 1075) |
| 248 | + |
| 249 | +ax.set_xlim(480, 600) |
| 250 | +ax.set_ylim(980, 1080) |
243 | 251 | ax.set_xlabel("x (pixels)") |
244 | 252 | ax.set_ylabel("y (pixels)") |
245 | 253 | ax.set_title(f"Zoomed in trajectory of {mouse_name}") |
|
248 | 256 |
|
249 | 257 |
|
250 | 258 | # %% |
251 | | -# Now we can visually verify that indeed the displacement vector |
252 | | -# connects the previous and current positions as expected. |
| 259 | +# Compute path length |
| 260 | +# -------------------- |
| 261 | +# We can compute the distance travelled by the |
| 262 | +# mouse as the sum of the lengths of all |
| 263 | +# displacement vectors along its trajectory. |
| 264 | +# Both backward and forward displacement vectors |
| 265 | +# should give the same result: |
| 266 | + |
| 267 | +# length of each forward displacement vector |
| 268 | +forward_displacement_lengths = compute_norm( |
| 269 | + forward_displacement.sel(individuals=mouse_name) |
| 270 | +) |
253 | 271 |
|
254 | | -# %% |
255 | | -# With the displacement data we can compute the distance travelled by the |
256 | | -# mouse along its trajectory. |
| 272 | +# length of each backward displacement vector |
| 273 | +backward_displacement_lengths = compute_norm( |
| 274 | + backward_displacement.sel(individuals=mouse_name) |
| 275 | +) |
257 | 276 |
|
258 | | -# length of each displacement vector |
259 | | -displacement_vectors_lengths = compute_norm( |
260 | | - displacement.sel(individuals=mouse_name) |
| 277 | +# check their lengths are the same |
| 278 | +np.testing.assert_almost_equal( |
| 279 | + forward_displacement_lengths.values[:-1], # exclude last timestep |
| 280 | + backward_displacement_lengths.values[1:], # exclude first timestep |
261 | 281 | ) |
262 | 282 |
|
263 | 283 | # sum the lengths of all displacement vectors (in pixels) |
264 | | -total_displacement = displacement_vectors_lengths.sum(dim="time").values[0] |
| 284 | +total_displacement_fwd = forward_displacement_lengths.sum(dim="time").values[0] |
| 285 | +total_displacement_bwd = backward_displacement_lengths.sum(dim="time").values[ |
| 286 | + 0 |
| 287 | +] |
265 | 288 |
|
266 | 289 | print( |
267 | | - f"The mouse {mouse_name}'s trajectory is {total_displacement:.2f} " |
268 | | - "pixels long" |
| 290 | + f"The mouse {mouse_name}'s path length is {total_displacement_fwd:.2f} " |
| 291 | + "pixels long (using forward displacement)" |
269 | 292 | ) |
| 293 | +print( |
| 294 | + f"The mouse {mouse_name}'s path length is {total_displacement_bwd:.2f} " |
| 295 | + "pixels long (using backward displacement)" |
| 296 | +) |
| 297 | + |
| 298 | + |
| 299 | +# %% |
| 300 | +# We provide a convenience function |
| 301 | +# :func:`movement.kinematics.compute_path_length` |
| 302 | +# to compute the path length for all individuals and keypoints in a position |
| 303 | +# data array. We can verify that using this function gives the same result |
| 304 | +# as before for the ``AEON3B_TP2`` mouse: |
| 305 | + |
| 306 | +path_lengths = kin.compute_path_length(ds.position) |
| 307 | + |
| 308 | +for mouse_name in path_lengths.individuals.values: |
| 309 | + print( |
| 310 | + f"Path length for {mouse_name}: " |
| 311 | + f"{path_lengths.sel(individuals=mouse_name).values[0]:.2f} pixels" |
| 312 | + ) |
270 | 313 |
|
271 | 314 | # %% |
272 | 315 | # Compute velocity |
273 | 316 | # ---------------- |
274 | | -# We can easily compute the velocity vectors for all individuals in our data |
| 317 | +# We can also compute the velocity vectors for all individuals in our data |
275 | 318 | # array: |
276 | 319 | velocity = kin.compute_velocity(position) |
277 | 320 |
|
|
294 | 337 | # %% |
295 | 338 | # The components of the velocity vector seem noisier than the components of |
296 | 339 | # the position vector. |
297 | | -# This is expected, since we are deriving the velocity using differences in |
| 340 | +# This is expected, since we are estimating the velocity using differences in |
298 | 341 | # position (which is somewhat noisy), over small stepsizes. |
299 | | -# More specifically, we use numpy's gradient implementation, which |
| 342 | +# More specifically, we use :func:`numpy.gradient` internally, which |
300 | 343 | # uses second order central differences. |
301 | 344 |
|
302 | 345 | # %% |
|
315 | 358 |
|
316 | 359 | # %% |
317 | 360 | # To visualise the direction of the velocity vector at each timestep, we can |
318 | | -# use a quiver plot: |
| 361 | +# again use a quiver plot: |
319 | 362 | mouse_name = "AEON3B_TP2" |
320 | 363 | fig = plt.figure() |
321 | 364 | ax = fig.add_subplot() |
|
0 commit comments