Skip to main content

motionbids

PyPI Python License: MIT Tests codecov

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 &amp; shape"| ARR
    RAW --> CHDEF
  end

  subgraph pkg["motionbids core"]
    direction TB
    CH["Channel<br/>validates component &amp; 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-&lt;id&gt;/[ses-&lt;id&gt;/]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. motionbids deliberately 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

Full Documentation

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}
}

License

MIT License - see LICENSE for details.

Metadata

Release files for motionbids 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for motionbids 0.6.0
File Size Uploaded Provenance Actions
motionbids-0.6.0.tar.gz 424.2 kB Without provenance Details for motionbids-0.6.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for motionbids 0.6.0
File Interpreter ABI Platform Actions
motionbids-0.6.0-py3-none-any.whl Python 3 none any Details for motionbids-0.6.0-py3-none-any.whl

Total release size: 460.2 kB

Release files / motionbids-0.6.0.tar.gz

Download URL motionbids-0.6.0.tar.gz
Size 424.2 kB
Tags Source
SHA-256 checksum
How to use checksums
cbbce9b19c3cf11b5acf355b0a258313e7c25f00bdc7cbc6142268eedfd8f81e
BLAKE2b-256 checksum
How to use checksums
3ce3ec975a2e82f587e9d1f35d525d36a0bd41f015a65fa9fd39a03a2a87a495
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / motionbids-0.6.0-py3-none-any.whl

Download URL motionbids-0.6.0-py3-none-any.whl
Size 36.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ee89518ca28053945ae17d4af3f7c2101f0504ddf74a02dfa295c8c216ac286
BLAKE2b-256 checksum
How to use checksums
4c9210aef094fc6f81dd5fe6184f1a870fb4e3a6e1f0c07c58199f5b6d5b7530
Upload date
Uploaded using Trusted Publishing?
What is 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}

Security for motionbids 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page