motionbids
A lightweight Python package for exporting motion capture data to BIDS format.
How it works
You shape your recording into a NumPy array plus a list of Channel objects, then build a
MotionData instance — whose class is generated at runtime from the live BIDS schema, so it stays
in sync with the specification — and validate and export it. The result is a ready-to-check BIDS
motion dataset.
flowchart LR
subgraph inputs["Your data (import is your job)"]
direction TB
RAW["Raw mocap: optical / IMU / video<br/>(C3D / XDF / CSV)"]
ARR["NumPy array<br/>(rows = time, cols = channels)"]
CHDEF["List of Channel objects"]
RAW -->|"you import & shape"| ARR
RAW --> CHDEF
end
subgraph pkg["motionbids core"]
direction TB
CH["Channel<br/>validates component & type<br/>against BIDS values"]
MD["MotionData (dataclass)<br/>construct-time validation (__post_init__)"]
VAL["validate_motion_data()<br/>convenience checks"]
EXP["export_bids_motion()<br/>writes JSON, TSV files"]
SCHEMA["BIDS schema (bidsschematools)"]
SCHEMA -.->|"generates the class"| MD
CH --> MD
MD --> VAL --> EXP
end
subgraph out["BIDS motion dataset (output)"]
direction TB
JSON["*_motion.json (sidecar)"]
TSV["*_motion.tsv (time series)"]
CHT["*_channels.tsv (descriptions)"]
CHJ["*_channels.json (reference frames)"]
EVT["*_events.tsv (task markers)"]
SCN["*_scans.tsv (synchronization)"]
TREE["sub-<id>/[ses-<id>/]motion/<br/>dataset_description.json, participants.tsv"]
end
ARR --> MD
CHDEF --> CH
EXP --> JSON
EXP --> TSV
EXP --> CHT
EXP --> CHJ
EXP --> EVT
EXP --> SCN
EXP --> TREE
classDef inp fill:#e8eef7,stroke:#3b5b8c,color:#1a2a44
classDef core fill:#eaf3ea,stroke:#3c6e47,color:#1e3a24
classDef eng fill:#fbf1e0,stroke:#b5761f,color:#5a3a0a
classDef outp fill:#f3eaf3,stroke:#7a3c74,color:#3a1e37
class RAW,ARR,CHDEF inp
class CH,MD,VAL,EXP core
class SCHEMA eng
class JSON,TSV,CHT,CHJ,EVT,SCN,TREE outp
Import is your job, export is ours.
motionbidsdeliberately leaves reading vendor formats (C3D, XDF, CSV, …) to you and focuses on producing a spec-compliant BIDS dataset. Always confirm the result with the official BIDS Validator.
Quick Start
from motionbids import MotionData, Channel, export_bids_motion
import numpy as np
# Your motion data (1200 timepoints, 32 channels)
data = np.random.randn(1200, 32)
# Define channels following BIDS schema
channels = [
Channel(
channel_name=f"marker{i}_{axis}",
channel_component=axis,
channel_type="POS",
channel_tracked_point=f"marker{i}",
channel_units="mm"
)
for i in range(10)
for axis in ['x', 'y', 'z']
]
# Create BIDS motion object
motion = MotionData(
subject="01",
task_name="walk",
tracksys="optical",
sampling_frequency=120.0,
tracked_points_count=10,
manufacturer="Vicon",
data=data,
channels=channels
)
# Export to BIDS format
export_bids_motion(motion, out_dir="bids_dataset/")
Output:
bids_dataset/
├── sub-01_task-walk_tracksys-optical_motion.json # Metadata
├── sub-01_task-walk_tracksys-optical_motion.tsv # Time series
├── sub-01_task-walk_tracksys-optical_channels.tsv # Channel info
└── sub-01_task-walk_tracksys-optical_channels.json # Reference frames (if described)
Installation
pip install motionbids
Or with uv:
uv pip install motionbids
For development:
git clone https://github.com/JuliusWelzel/motionbids.git
cd motionbids
pip install -e ".[dev]"
Features
✅ Schema-driven - Auto-syncs with BIDS specification
✅ Automatic validation - Catches errors before export
✅ Simple API - Minimal code needed
✅ Complete export - JSON, TSV, channels files
✅ Cross-platform - Tested on Linux, macOS, Windows
Documentation
- Motivation - Why BIDS for motion?
- Workflow - Complete workflow guide
- Class Reference - MotionData API
- Schema Fields - All BIDS fields
Required Fields
motion = MotionData(
subject="01", # Subject identifier
task_name="walk", # Task name
tracksys="optical", # Tracking system (optical/imu/video)
sampling_frequency=120.0, # Sampling rate in Hz
tracked_points_count=10, # Number of markers
data=data, # NumPy array (rows=time, cols=channels)
channels=channels # List of Channel objects (BIDS-compliant)
)
Channel Metadata
Each channel requires BIDS-compliant metadata:
from motionbids import Channel
channel = Channel(
channel_name="marker0_x", # Channel name
channel_component="x", # x, y, z, quat_x, quat_y, quat_z, quat_w, n/a
channel_type="POS", # POS, ORNT, VEL, ACCEL, GYRO, MAGN, etc.
channel_tracked_point="marker0", # Label of tracked point
channel_units="mm" # Units (mm, m, rad, deg, etc.)
)
Reference Frames
The reference_frame column of *_channels.tsv only holds a label per channel. What that label
means — where the axes point, how rotations are applied — belongs in the *_channels.json
sidecar (BIDS spec):
from motionbids import Channel, MotionData, ReferenceFrame
# Global frame of an optical system: X to the back, Y to the left, Z up
global_frame = ReferenceFrame(
name="global", # the label used in the reference_frame column
spatial_axes="PLS", # A/P, L/R, S/I per axis ('_' for unused)
rotation_order="ZXY", # XYZ, XZY, YXZ, YZX, ZXY, ZYX, n/a
rotation_rule="left-hand", # left-hand, right-hand, n/a
description="Laboratory frame of the optical system.",
)
channels = [
Channel(
channel_name="LASI_x",
channel_component="x",
channel_type="POS",
channel_tracked_point="LASI",
channel_units="m",
channel_reference_frame="global", # links this channel to the frame
),
# ...
]
motion = MotionData(..., channels=channels, reference_frames=[global_frame])
export_bids_motion() then writes *_channels.json next to *_channels.tsv:
{
"reference_frame": {
"Levels": {
"global": {
"SpatialAxes": "PLS",
"RotationOrder": "ZXY",
"RotationRule": "left-hand",
"Description": "Laboratory frame of the optical system."
}
}
}
}
ReferenceFrame.handedness derives the handedness from the axis directions alone ("PLS" is
left-handed), which is worth checking against the rotation_rule you declared.
validate_motion_data() warns when channels use a label that no frame describes, or vice versa.
See examples/reference_frame_optical.py for a runnable
version.
Events
Set MotionData.events to a list of dicts (each needs at least onset and duration, in seconds
relative to the first sample of the recording; trial_type, value, sample are optional) and
export_bids_motion() writes a *_events.tsv next to *_motion.tsv
(BIDS spec):
motion = MotionData(
...,
events=[
{"onset": 0.5, "duration": 0.0, "trial_type": "gait_start"},
{"onset": 12.3, "duration": 0.0, "trial_type": "gait_stop"},
],
)
If your markers come from an XDF marker stream (e.g. loaded with pyxdf), convert them with
events_from_xdf_markers() instead of building the dicts by hand — it aligns each marker's LSL
timestamp to the motion recording's first sample and rounds it to a sample index:
from motionbids import events_from_xdf_markers
motion.events = events_from_xdf_markers(
marker_stream, # a pyxdf 'Markers' stream
motion_first_timestamp=motion_stream['time_stamps'][0],
sampling_frequency=motion.sampling_frequency,
)
Validation
Important: Package validation is for convenience only and is not officially supported by BIDS. Always use the official BIDS Validator before sharing your dataset.
from motionbids import validate_motion_data
# Convenience validation (checks basic requirements)
validate_motion_data(motion)
# Then validate with official BIDS Validator:
# https://bids-standard.github.io/bids-validator/
Complete Workflow
from motionbids import (
MotionData,
export_bids_motion,
create_bids_directory_structure,
export_dataset_description
)
# 1. Create directory structure
motion_dir = create_bids_directory_structure(
base_dir="my_study",
subject="01",
session="01"
)
# 2. Create dataset description
export_dataset_description(
bids_root="my_study",
name="Motion Study",
authors=["Your Name"]
)
# 3. Export motion data
motion = MotionData(...)
export_bids_motion(motion, out_dir=motion_dir)
Supported Systems
Works with any motion tracking technology:
- Optical: Vicon, Optitrack, Qualisys
- IMU: Xsens, APDM, Movella
- Video: OpenPose, MediaPipe, DeepLabCut
- Other: Custom systems
Importing Data
motionbids focuses on exporting to BIDS format. Importing raw data is left
to the user since motion capture systems vary widely. Here are common patterns:
From CSV / TSV
import pandas as pd
import numpy as np
from motionbids import MotionData, Channel
# Load your data
df = pd.read_csv("recording.csv")
data = df.values
# Map columns to BIDS channels
channels = [
Channel(
channel_name=col,
channel_component=col.split("_")[-1], # e.g. "x", "y", "z"
channel_type="POS",
channel_tracked_point=col.rsplit("_", 1)[0],
channel_units="mm"
)
for col in df.columns
]
motion = MotionData(
subject="01", task_name="walk", tracksys="optical",
sampling_frequency=120.0, tracked_points_count=10,
data=data, channels=channels
)
From C3D (requires ezc3d)
import ezc3d
import numpy as np
from motionbids import MotionData, Channel
c3d = ezc3d.c3d("recording.c3d")
points = c3d["data"]["points"] # (4, n_markers, n_frames)
labels = c3d["parameters"]["POINT"]["LABELS"]["value"]
freq = c3d["parameters"]["POINT"]["RATE"]["value"][0]
# Reshape to (n_frames, n_channels)
data = points[:3, :, :].transpose(2, 1, 0).reshape(-1, len(labels) * 3)
channels = [
Channel(
channel_name=f"{label}_{axis}",
channel_component=axis,
channel_type="POS",
channel_tracked_point=label,
channel_units="mm"
)
for label in labels
for axis in ["x", "y", "z"]
]
motion = MotionData(
subject="01", task_name="walk", tracksys="optical",
sampling_frequency=freq, tracked_points_count=len(labels),
data=data, channels=channels
)
See the Workflow Guide for more patterns.
Development
git clone https://github.com/juliuswelzel/motionbids.git
cd motionbids
pip install -e ".[dev]"
# Run tests
pytest --cov=motionbids
Contributing
Contributions, bug reports, and questions are welcome! See CONTRIBUTING.md for how to report issues, get support, and submit changes.
Citation
@software{motionbids,
author = {Welzel, Julius},
title = {motionbids: BIDS converter for motion capture data},
year = {2026},
url = {https://github.com/JuliusWelzel/motionbids}
}
Links
- 📖 Documentation
- 🐛 Issues
- 📜 BIDS Spec
License
MIT License - see LICENSE for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file motionbids-0.6.0.tar.gz.
File metadata
- Download URL: motionbids-0.6.0.tar.gz
- Upload date:
- Size: 424.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbbce9b19c3cf11b5acf355b0a258313e7c25f00bdc7cbc6142268eedfd8f81e
|
|
| MD5 |
184dba19091ae2fa98c00167f5d16a6e
|
|
| BLAKE2b-256 |
3ce3ec975a2e82f587e9d1f35d525d36a0bd41f015a65fa9fd39a03a2a87a495
|
File details
Details for the file motionbids-0.6.0-py3-none-any.whl.
File metadata
- Download URL: motionbids-0.6.0-py3-none-any.whl
- Upload date:
- Size: 36.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ee89518ca28053945ae17d4af3f7c2101f0504ddf74a02dfa295c8c216ac286
|
|
| MD5 |
2defa5fdb3fbde3c23e0b58f2dea3f0e
|
|
| BLAKE2b-256 |
4c9210aef094fc6f81dd5fe6184f1a870fb4e3a6e1f0c07c58199f5b6d5b7530
|