Metadata-Version: 2.4
Name: motionbids
Version: 0.6.0
Summary: Lightweight converter for motion data to BIDS format
Project-URL: Homepage, https://github.com/JuliusWelzel/motionbids
Project-URL: Documentation, https://juliuswelzel.github.io/motionbids/
Project-URL: Repository, https://github.com/JuliusWelzel/motionbids
Project-URL: Issues, https://github.com/JuliusWelzel/motionbids/issues
Project-URL: Changelog, https://github.com/JuliusWelzel/motionbids/blob/main/CHANGELOG.md
Author-email: Julius Welzel <julius.welzel@gmail.com>
License: MIT
License-File: LICENSE
Keywords: BIDS,IMU,MoBI,data standardization,kinematics,motion capture,neuroscience,reproducibility
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Requires-Python: >=3.10
Requires-Dist: bidsschematools>=0.8.0
Requires-Dist: dataclasses-json>=0.6.0
Requires-Dist: ipykernel>=7.1.0
Requires-Dist: numpy>=1.20.0
Requires-Dist: pip>=26.1.2
Provides-Extra: dev
Requires-Dist: ipykernel>=6.29.5; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.4.0; extra == 'docs'
Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == 'docs'
Provides-Extra: examples
Requires-Dist: ezc3d>=1.7.0; extra == 'examples'
Requires-Dist: matplotlib>=3.10.9; extra == 'examples'
Requires-Dist: pandas>=2.3.3; extra == 'examples'
Requires-Dist: pyxdf>=1.17.5; extra == 'examples'
Description-Content-Type: text/markdown

# motionbids

[![PyPI](https://img.shields.io/pypi/v/motionbids.svg)](https://pypi.org/project/motionbids/)
[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Tests](https://github.com/JuliusWelzel/motionbids/actions/workflows/tests.yml/badge.svg)](https://github.com/JuliusWelzel/motionbids/actions/workflows/tests.yml)
[![codecov](https://codecov.io/gh/JuliusWelzel/motionbids/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliusWelzel/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.

```mermaid
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](https://bids-standard.github.io/bids-validator/).

## Quick Start

```python
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

```bash
pip install motionbids
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv pip install motionbids
```

For development:

```bash
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](https://juliuswelzel.github.io/motionbids/)**

- [Motivation](https://juliuswelzel.github.io/motionbids/motivation/) - Why BIDS for motion?
- [Workflow](https://juliuswelzel.github.io/motionbids/workflow/) - Complete workflow guide
- [Class Reference](https://juliuswelzel.github.io/motionbids/class-reference/) - MotionData API
- [Schema Fields](https://juliuswelzel.github.io/motionbids/schema-fields/) - All BIDS fields

## Required Fields

```python
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:

```python
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](https://bids-specification.readthedocs.io/en/stable/modality-specific-files/motion.html#reference-frame-description-_channelsjson)):

```python
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`:

```json
{
  "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`](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](https://bids-specification.readthedocs.io/en/stable/modality-specific-files/motion.html)):

```python
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:

```python
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](https://bids-standard.github.io/bids-validator/) before sharing your dataset.

```python
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

```python
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

```python
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)

```python
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](https://juliuswelzel.github.io/motionbids/workflow/) for more patterns.

## Development

```bash
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](CONTRIBUTING.md) for how to report issues, get support, and
submit changes.

## Citation

```bibtex
@software{motionbids,
  author = {Welzel, Julius},
  title = {motionbids: BIDS converter for motion capture data},
  year = {2026},
  url = {https://github.com/JuliusWelzel/motionbids}
}
```

## Links

- 📖 [Documentation](https://juliuswelzel.github.io/motionbids/)
- 🐛 [Issues](https://github.com/JuliusWelzel/motionbids/issues)
- 📜 [BIDS Spec](https://bids-specification.readthedocs.io/)

## License

MIT License - see [LICENSE](LICENSE) for details.
