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

Links

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

motionbids-0.6.0.tar.gz (424.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

motionbids-0.6.0-py3-none-any.whl (36.0 kB view details)

Uploaded Python 3

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

Hashes for motionbids-0.6.0.tar.gz
Algorithm Hash digest
SHA256 cbbce9b19c3cf11b5acf355b0a258313e7c25f00bdc7cbc6142268eedfd8f81e
MD5 184dba19091ae2fa98c00167f5d16a6e
BLAKE2b-256 3ce3ec975a2e82f587e9d1f35d525d36a0bd41f015a65fa9fd39a03a2a87a495

See more details on using hashes here.

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

Hashes for motionbids-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1ee89518ca28053945ae17d4af3f7c2101f0504ddf74a02dfa295c8c216ac286
MD5 2defa5fdb3fbde3c23e0b58f2dea3f0e
BLAKE2b-256 4c9210aef094fc6f81dd5fe6184f1a870fb4e3a6e1f0c07c58199f5b6d5b7530

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

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