Skip to content

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 ReferenceFrame object
  • Write the axis directions as a SpatialAxes string — 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.json alongside the other BIDS files

Requirements

pip install motionbids numpy
examples/reference_frame_optical.py
"""
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:

frame = ReferenceFrame(name="global", spatial_axes="PLS")
frame.handedness  # 'left-hand'

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