Motion correction of a single recording¶
This example shows how to correct volume-to-volume brain motion in one fUSI recording
with register_volumewise. For this
example, we use a short subset of an open-field 2D+t recording from the Cybis Pereira
2026
dataset. After volumewise registration,
we inspect three things that are useful in practice:
- the motion diagnostics returned by
create_motion_dataframe; - a representative voxel trace before and after registration;
- a compact raster view showing how volumewise registration stabilized the recording.
Fetch and load a short motion-corrupted window¶
The selected acquisition is a recording from a single 2D slice, so the data shape is
(time, z=1, y, x).
from pathlib import Path
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
import confusius as cf
bg_color = mpl.colors.to_hex(mpl.rcParams["figure.facecolor"])
xr.set_options(display_expand_data=False)
subject = "rat75"
session = "20220523"
acq = "slice32"
start_frame = 220
n_frames = 120
bids_root = cf.datasets.fetch_cybis_pereira_2026(
datasets="rawdata",
subjects=subject,
sessions=session,
acqs=acq,
)
If you use this dataset in your work, please cite the following source: Cybis Pereira, F., Castedo, S. H., Meur-Diebolt, S. L., Ialy-Radio, N., Bhattacharya, S., Ferrier, J., Osmanski, B. F., Cocco, S., Monasson, R., Pezet, S., & Tanter, M. (2026). A vascular code for speed in the spatial navigation system. Cell Reports, 45(1). https://doi.org/10.1016/j.celrep.2025.116791
pwd_path = (
Path(bids_root)
/ f"sub-{subject}"
/ f"ses-{session}"
/ "fusi"
/ f"sub-{subject}_ses-{session}_task-openfield_acq-{acq}_pwd.nii.gz"
)
data = cf.load(pwd_path).isel(time=slice(start_frame, start_frame + n_frames)).compute()
data
- time: 120
- z: 1
- y: 112
- x: 128
- 85.26 116.2 244.4 326.7 220.4 89.93 ... 35.0 31.79 27.72 32.78 35.08
array([[[[ 85.26095862, 116.22205806, 244.40663846, ..., 81.49985325, 72.6656052 , 84.75890358], [ 94.30669678, 168.48894542, 370.41283569, ..., 77.94853297, 75.73787961, 80.90911372], [ 77.33881401, 85.13303516, 107.57782349, ..., 74.0115976 , 84.09536198, 80.31733092], ..., [ 370.21667854, 293.17560413, 283.58816699, ..., 56.02650583, 45.62883065, 44.52256835], [ 144.96484446, 147.96791438, 137.12604831, ..., 50.16398943, 65.04411943, 63.9631805 ], [ 87.65224311, 78.54026893, 66.43983123, ..., 65.42629254, 73.38085286, 70.04022849]]], [[[ 389.93191371, 641.48380496, 1115.08191581, ..., 96.22182585, 74.23019108, 82.51393297], [ 359.81698545, 559.63210265, 864.14534089, ..., 127.96252563, 110.07846579, 93.85637685], [ 536.68046542, 982.18576292, 1179.88564376, ..., ... [ 55.50365905, 54.83884733, 50.36205525, ..., 29.36021998, 23.86886065, 23.52851437], [ 43.72547414, 35.05400817, 30.45671777, ..., 29.9262347 , 33.29741235, 33.45943534]]], [[[ 100.0586512 , 195.05050789, 474.78743741, ..., 48.95520004, 38.07772308, 40.22238393], [ 191.54714471, 299.21769241, 349.65676208, ..., 39.14976869, 32.07769081, 32.27574316], [ 145.40563748, 155.22389012, 239.30868664, ..., 36.21480796, 35.76955383, 34.44912312], ..., [ 154.07177111, 134.59463793, 117.71987771, ..., 35.25394061, 30.67498287, 32.57377449], [ 60.53621928, 60.32421715, 51.64683992, ..., 35.02957069, 32.21901694, 26.78651635], [ 43.42175231, 40.90431247, 39.44968175, ..., 27.71583093, 32.78066543, 35.08132628]]]], shape=(120, 1, 112, 128)) - time(time)float6488.4 88.8 89.2 ... 135.6 136.0
- units :
- s
- volume_acquisition_reference :
- start
- volume_acquisition_duration :
- 0.4000000000000057
array([ 88.4014 , 88.8014 , 89.2014 , 89.601425, 90.001425, 90.401425, 90.801425, 91.20145 , 91.60145 , 92.00145 , 92.40145 , 92.801475, 93.201475, 93.601475, 94.001475, 94.4015 , 94.8015 , 95.2015 , 95.6015 , 96.001525, 96.401525, 96.801525, 97.20155 , 97.60155 , 98.00155 , 98.40155 , 98.801575, 99.201575, 99.601575, 100.001575, 100.4016 , 100.8016 , 101.2016 , 101.6016 , 102.001625, 102.401625, 102.801625, 103.201625, 103.60165 , 104.00165 , 104.40165 , 104.80165 , 105.201675, 105.601675, 106.001675, 106.401675, 106.8017 , 107.2017 , 107.6017 , 108.0017 , 108.401725, 108.801725, 109.201725, 109.601725, 110.00175 , 110.40175 , 110.80175 , 111.20175 , 111.601775, 112.001775, 112.401775, 112.8018 , 113.2018 , 113.6018 , 114.0018 , 114.401825, 114.801825, 115.201825, 115.601825, 116.00185 , 116.40185 , 116.80185 , 117.20185 , 117.601875, 118.001875, 118.401875, 118.801875, 119.2019 , 119.6019 , 120.0019 , 120.4019 , 120.801925, 121.201925, 121.601925, 122.001925, 122.40195 , 122.80195 , 123.20195 , 123.60195 , 124.001975, 124.401975, 124.801975, 125.201975, 125.602 , 126.002 , 126.402 , 126.802 , 127.202025, 127.602025, 128.002025, 128.402025, 128.80205 , 129.20205 , 129.60205 , 130.002075, 130.402075, 130.802075, 131.202075, 131.6021 , 132.0021 , 132.4021 , 132.8021 , 133.202125, 133.602125, 134.002125, 134.402125, 134.80215 , 135.20215 , 135.60215 , 136.00215 ]) - z(z)float640.0
- units :
- mm
- voxdim :
- 0.3999999761581421
array([0.])
- y(y)float645.0 5.099 5.197 ... 15.84 15.94
- units :
- mm
- voxdim :
- 0.09855999797582626
array([ 5. , 5.09856, 5.19712, 5.29568, 5.39424, 5.4928 , 5.59136, 5.68992, 5.78848, 5.88704, 5.9856 , 6.08416, 6.18272, 6.28128, 6.37984, 6.4784 , 6.57696, 6.67552, 6.77408, 6.87264, 6.9712 , 7.06976, 7.16832, 7.26688, 7.36544, 7.464 , 7.56256, 7.66112, 7.75968, 7.85824, 7.9568 , 8.05536, 8.15392, 8.25248, 8.35104, 8.4496 , 8.54816, 8.64672, 8.74528, 8.84384, 8.9424 , 9.04096, 9.13952, 9.23808, 9.33664, 9.4352 , 9.53376, 9.63232, 9.73088, 9.82944, 9.928 , 10.02656, 10.12512, 10.22368, 10.32224, 10.4208 , 10.51936, 10.61792, 10.71648, 10.81504, 10.9136 , 11.01216, 11.11072, 11.20928, 11.30784, 11.4064 , 11.50496, 11.60352, 11.70208, 11.80064, 11.8992 , 11.99776, 12.09632, 12.19488, 12.29344, 12.392 , 12.49056, 12.58912, 12.68768, 12.78624, 12.8848 , 12.98336, 13.08192, 13.18048, 13.27904, 13.3776 , 13.47616, 13.57472, 13.67328, 13.77184, 13.8704 , 13.96896, 14.06752, 14.16608, 14.26464, 14.3632 , 14.46176, 14.56032, 14.65888, 14.75744, 14.856 , 14.95456, 15.05312, 15.15168, 15.25024, 15.3488 , 15.44736, 15.54592, 15.64448, 15.74304, 15.8416 , 15.94016]) - x(x)float64-6.985 -6.875 ... 6.875 6.985
- units :
- mm
- voxdim :
- 0.10999999940395355
array([-6.985, -6.875, -6.765, -6.655, -6.545, -6.435, -6.325, -6.215, -6.105, -5.995, -5.885, -5.775, -5.665, -5.555, -5.445, -5.335, -5.225, -5.115, -5.005, -4.895, -4.785, -4.675, -4.565, -4.455, -4.345, -4.235, -4.125, -4.015, -3.905, -3.795, -3.685, -3.575, -3.465, -3.355, -3.245, -3.135, -3.025, -2.915, -2.805, -2.695, -2.585, -2.475, -2.365, -2.255, -2.145, -2.035, -1.925, -1.815, -1.705, -1.595, -1.485, -1.375, -1.265, -1.155, -1.045, -0.935, -0.825, -0.715, -0.605, -0.495, -0.385, -0.275, -0.165, -0.055, 0.055, 0.165, 0.275, 0.385, 0.495, 0.605, 0.715, 0.825, 0.935, 1.045, 1.155, 1.265, 1.375, 1.485, 1.595, 1.705, 1.815, 1.925, 2.035, 2.145, 2.255, 2.365, 2.475, 2.585, 2.695, 2.805, 2.915, 3.025, 3.135, 3.245, 3.355, 3.465, 3.575, 3.685, 3.795, 3.905, 4.015, 4.125, 4.235, 4.345, 4.455, 4.565, 4.675, 4.785, 4.895, 5.005, 5.115, 5.225, 5.335, 5.445, 5.555, 5.665, 5.775, 5.885, 5.995, 6.105, 6.215, 6.325, 6.435, 6.545, 6.655, 6.765, 6.875, 6.985])
- qform_code :
- 1
- manufacturer :
- Iconeus
- manufacturers_model_name :
- Iconeus One
- probe_type :
- linear
- probe_model :
- IcoPrime light
- probe_central_frequency :
- 15.625
- probe_number_of_elements :
- 128
- probe_pitch :
- 0.11
- probe_radius_of_curvature :
- 0
- ProbeElevationAperture :
- 1.5
- ProbeElevationFocus :
- 8
- pulse_repetition_frequency :
- 2000.0
- plane_wave_angles :
- [-3.0, -1.0, 1.0, 3.0]
- IsHadamard :
- True
- UltrafastSamplingFrequency :
- 500.0
- probe_voltage :
- 15
- clutter_filter_window_duration :
- 200.0
- clutter_filters :
- ['svd:remove_first_60_components']
- power_doppler_integration_duration :
- 200.0
- affines :
- {'physical_to_qform': array([[1., 0., 0., 0.], [0., 1., 0., 0.], [0., 0., 1., 0.], [0., 0., 0., 1.]])}
Register every frame to the first frame¶
Volumewise registration can be applied using
register_volumewise. By default, each
volume (or frame in this case) is registered to the first acquired volume. You may
choose any other reference timepoint using the reference_time. Here, we're trying to
correct for rigid motion—that is, translation and rotations. A learning rate of 1.0
leads to good convergence across frames.
registered = cf.registration.register_volumewise(
data,
transform="rigid",
metric="correlation",
learning_rate=1.0,
)
register_volumewise adds a
motion_params attribute to the motion-corrected DataArray. motion_params is a
DataFrame created using
create_motion_dataframe and
containing the motion parameters and registration diagnostics: rotations (rot_x,
rot_y, rot_z), translations (trans_x, trans_y, trans_z), the framewise
displacement (FD)1 summaries (mean_fd, max_fd, rms_fd), and the optimizer
summaries (final_metric_value, n_iterations) added by
register_volumewise. Since we
registered a 2D+t recording, only the rotation along the z-axis and the translations
along the x- and y-axes are non-zero.
| rot_x | rot_y | rot_z | trans_x | trans_y | trans_z | mean_fd | max_fd | rms_fd | final_metric_value | n_iterations | status | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| time | ||||||||||||
| 88.401400 | 0.000000e+00 | -0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000e+00 | 0.008461 | 0.012281 | 0.008704 | -1.000000 | 31 | completed |
| 88.801400 | 1.496784e-21 | -6.225587e-21 | 0.000512 | -0.005748 | -0.008282 | 1.570482e-20 | 0.035006 | 0.043871 | 0.035202 | -0.966427 | 31 | completed |
| 89.201400 | -5.956531e-21 | -2.754602e-21 | 0.001581 | 0.011977 | -0.027575 | -6.231969e-20 | 0.035979 | 0.052521 | 0.036672 | -0.971177 | 33 | completed |
| 89.601425 | -2.058074e-20 | -1.901582e-21 | 0.003542 | 0.016896 | -0.051908 | -2.154101e-19 | 0.005747 | 0.007470 | 0.005799 | -0.961348 | 31 | completed |
| 90.001425 | -3.390010e-21 | -1.791389e-21 | 0.003781 | 0.019862 | -0.053373 | -3.542251e-20 | 0.007024 | 0.007902 | 0.007034 | -0.962170 | 35 | completed |
Plot the motion diagnostics¶
ConfUSIus provides the function
plot_motion_diagnostics to plot
motion_params using four panels:
- The first two panels show the rotations and translations found at each frame.
- The third panel shows the mean, maximum, and RMS of the framewise displacement (FD). The FD is a metric used to quantify brain motion between consecutive volumes by summing the absolute temporal derivatives of the realignment parameters (translations and rotations)1.
- The fourth panel shows the final registration metric and iteration count for each
volume. Volumes showing outlier metric values or reaching the maximum number of
iterations configured in
register_volumewiseare likely to not be registered correctly. In that case, you might want to tweak the registration parameters, or remove the corresponding volumes from further analysis.
Compare a representative time series before and after registration¶
To highlight the effect of the volumewise registration, we pick the voxel with the largest temporal standard deviation in the unregistered recording. In practice that usually lands on a large vessel or near the borders of the brain, where brain motion can induce high intensity changes.
std_map = data.std("time")
highest_std_voxel = std_map.isel(std_map.argmax(dim=["x", "y", "z"]))
voxel_before = data.sel(highest_std_voxel.coords)
voxel_after = registered.sel(highest_std_voxel.coords)
x_value = highest_std_voxel.x.values
y_value = highest_std_voxel.y.values
units = highest_std_voxel.x.units
xy_position = f"(x, y) = ({x_value:.2f}, {y_value:.2f}) {units}"
fig, axs = plt.subplots(1, 2, figsize=(9, 3.5), constrained_layout=True)
fig.patch.set_facecolor(bg_color)
plotter = (
data.mean("time")
.fusi.scale.db()
.fusi.plot.volume(axes=axs[0], bg_color=bg_color, show_colorbar=False)
)
# Explicitly show the high std voxel on the brain plot.
axs[0].scatter(highest_std_voxel.x, highest_std_voxel.y, color="r")
axs[1].plot(voxel_before["time"], voxel_before, label="Before", lw=1.6, alpha=0.8)
axs[1].plot(voxel_after["time"], voxel_after, label="After", lw=1.6)
axs[1].set_xlabel("Time (s)")
axs[1].set_ylabel("Power Doppler intensity")
axs[1].set_title(f"Voxel at {xy_position}")
_ = axs[1].legend(frameon=False, ncols=2)
Check the alignment over time¶
A compact way to inspect the correction inside the notebook is to follow one column
across time. Motion appears as slanted or wobbling vessel traces in this y × time
raster, while a good correction makes those traces more horizontal and stable (note
that this will only clearly capture translation motion in the y axis).
raster_before = data.fusi.scale.db().sel(z=0, x=x_value)
raster_after = registered.fusi.scale.db().sel(z=0, x=x_value)
fig, axes = plt.subplots(1, 2, figsize=(10, 4), constrained_layout=True, sharey=True)
fig.patch.set_facecolor(bg_color)
vmin = float(np.nanpercentile(raster_before, 2))
vmax = float(np.nanpercentile(raster_before, 99.8))
for ax, raster, title in [
(axes[0], raster_before, "Before motion correction"),
(axes[1], raster_after, "After motion correction"),
]:
ax.imshow(
raster.T,
aspect="auto",
origin="upper",
cmap="gray",
vmin=vmin,
vmax=vmax,
extent=[
float(raster["time"].values[0]),
float(raster["time"].values[-1]),
float(raster["y"].values[-1]),
float(raster["y"].values[0]),
],
)
ax.set_title(title)
ax.set_xlabel("Time (s)")
_ = axes[0].set_ylabel("y (mm)")
For a full visual check of the corrected movie, open the result in napari with
plot_napari or inspect it in the ConfUSIus plugin.
We intentionally do not embed a full before/after GIF here because it would make the
rendered notebook unnecessarily heavy. For a full preprocessing workflow, you would
usually run the same correction on the complete recording before downstream QC,
decomposition, or connectivity analysis.
Total running time: 41.6 s





