Reference frame description (*_channels.json)¶
Describes the global coordinate system of an optical recording in the
*_channels.json sidecar.
This example shows how to:
- Label channels with a reference frame via
channel_reference_frame - Describe what that label means with a
ReferenceFrameobject - Write the axis directions as a
SpatialAxesstring — here"PLS"for a system whose X points to the back, Y to the left and Z up - Check the handedness that follows from those axis directions
- Export
*_channels.jsonalongside the other BIDS files
"""
Example: Describe the reference frame of an optical recording in channels.json
The `reference_frame` column of `*_channels.tsv` only holds a label per channel
(here: "global"). What that label *means* — where the axes point, how rotations
are applied — belongs in the `*_channels.json` sidecar:
https://bids-specification.readthedocs.io/en/stable/modality-specific-files/motion.html#reference-frame-description-_channelsjson
This example describes the global coordinate system of an optical system in
which X points to the back, Y to the left, and Z up. In the BIDS notation each
character stands for the direction the corresponding axis points to
(A/P anterior/posterior, L/R left/right, S/I superior/inferior), so those three
axes are written as "PLS".
Requirements:
pip install motionbids numpy
"""
import json
from pathlib import Path
import numpy as np
from motionbids import (
Channel,
MotionData,
ReferenceFrame,
create_bids_directory_structure,
export_bids_motion,
export_dataset_description,
)
# Configuration
bids_root = Path(__file__).parent / "bids_dataset"
subject = "01"
task_name = "walk"
tracksys = "optical"
sampling_frequency = 120.0
# The label linking the channels to their description in channels.json
REFERENCE_FRAME = "global"
# Stand-in for a real recording: 3 markers × 3 axes, 10 seconds at 120 Hz
markers = ("LASI", "RASI", "SACR")
axes = ("x", "y", "z")
data = np.random.randn(int(10 * sampling_frequency), len(markers) * len(axes))
# Every position channel is expressed in the global frame, so each one carries
# the "global" label in its reference_frame column.
channels = [
Channel(
channel_name=f"{marker}_{axis}",
channel_component=axis,
channel_type="POS",
channel_tracked_point=marker,
channel_units="m",
channel_reference_frame=REFERENCE_FRAME,
)
for marker in markers
for axis in axes
]
# The reference frame itself: X to the back (posterior), Y to the left, Z up
# (superior). RotationOrder and RotationRule describe how rotations around
# those axes are to be read; drop them if your system does not define them.
#
# Note that back/left/up is a *left-handed* triad — X × Y points down, not up.
# The rotation rule declared here follows that. If the system is meant to be
# right-handed, one of the three directions is not what it seems (X forward, or
# Y to the right, would both make it "ALS" / "PRS" and right-handed).
global_frame = ReferenceFrame(
name=REFERENCE_FRAME,
spatial_axes="PLS",
description=(
"Laboratory frame of the optical system. The origin is the calibration "
"L-frame corner; X points to the back of the treadmill, Y to the left, and "
"Z upwards."
),
)
motion = MotionData(
subject=subject,
task_name=task_name,
tracksys=tracksys,
sampling_frequency=sampling_frequency,
tracked_points_count=len(markers),
manufacturer="Vicon",
recording_duration=data.shape[0] / sampling_frequency,
data=data,
channels=channels,
reference_frames=[global_frame],
)
# Export: channels.json is written alongside the other files whenever
# reference_frames is set
motion_dir = create_bids_directory_structure(bids_root, subject)
export_dataset_description(bids_root, name="Optical reference frame example")
files = export_bids_motion(motion, motion_dir, overwrite=True)
print(f"\nWrote {files['channels_json'].name}:")
print(json.loads(files['channels_json'].read_text()))
Reading the SpatialAxes string¶
SpatialAxes has one character per axis, in the order X, Y, Z. Each character
names the direction that axis points to:
| Character | Direction |
|---|---|
A |
anterior (forward) |
P |
posterior (backward) |
L |
left |
R |
right |
S |
superior (up) |
I |
inferior (down) |
_ |
axis not used |
So the optical system above — X to the back, Y to the left, Z up — is
"PLS": Posterior, Left, Superior.
Two character sets, one meaning
The reference frame section of the motion specification uses the anatomical
characters A, P, L, R, S, I, _. The SpatialAxes object in
the BIDS schema documents the equivalent egocentric characters F, B,
L, R, U, D, _ (forward, backward, left, right, up, down), so
"PLS" and "BLU" describe the same frame. motionbids accepts both and
emits a UserWarning when F/B/U/D are used, suggesting the
anatomical spelling.
Back/left/up is a left-handed triad — X × Y points down, not up. The
handedness property derives this from the axis directions alone, which makes
it a useful cross-check against the rotation_rule you declared:
Output¶
sub-01_task-walk_tracksys-optical_channels.json:
{
"reference_frame": {
"Levels": {
"global": {
"SpatialAxes": "PLS",
"RotationOrder": "ZXY",
"RotationRule": "left-hand",
"Description": "Laboratory frame of the optical system. The origin is the calibration L-frame corner; X points to the back of the room, Y to the left, and Z upwards."
}
}
}
}
Next Steps¶
- Vicon example — optical marker conversion from C3D
- Workflow Guide — step-by-step explanation of each stage
- Class Reference —
MotionData,ChannelandReferenceFrameAPI