Skip to content

Quick Start

The headline workflow: a directory of BVH files in, a training-ready PyTorch data loader out. Every piece also works standalone — use just the packer, just the augmentor, or just the preprocessor.

Preprocess once

import pybvh_ml

summary = pybvh_ml.preprocess_directory(
    "walks/", "train.npz",
    representation="6d",
    label_fn=lambda stem: 0,   # map filename stem → integer class
)
print(f"{summary['num_clips']} clips, "
      f"{summary['skeleton_info']['num_joints']} joints")

One call loads every .bvh file, converts rotations to your representation of choice ("euler", "quat", "6d", "axisangle"), computes dataset-wide normalization statistics, and writes everything to one .npz (or .hdf5) file. Mixed skeletons, frame rates, or up-axes? See harmonization.

Load back

data = pybvh_ml.load_preprocessed("train.npz")

clip = data["clips"][0]
clip["root_pos"]        # (F, 3) root translation
clip["joint_rot"]       # (F, J, 6) for 6D
mean, std = data["mean"], data["std"]   # per-channel z-score stats
skel = data["skeleton_info"]            # edges, lr_pairs, world_up, ...

The file is self-sufficient: it carries the skeleton metadata (including the axis strings augmentation needs), so you never reopen the source BVHs at training time.

Augment at runtime

import numpy as np
from pybvh_ml import AugmentationPipeline, MotionArrays

pipeline = AugmentationPipeline.standard(
    skel, representation="6d", up_axis=skel["world_up"],
)
rng = np.random.default_rng(42)
arrays = pipeline(
    MotionArrays(root_pos=clip["root_pos"], joint_rot=clip["joint_rot"]),
    rng=rng,
)
root_pos, joint_rot = arrays.root_pos, arrays.joint_rot

The standard factory wires vertical rotation + mirroring + joint noise + speed perturbation from the skeleton metadata. Each step is also a standalone function — the Augmentation guide covers all six.

Train (optional PyTorch layer)

from pybvh_ml.torch import MotionDataset, collate_motion_batch
from torch.utils.data import DataLoader

dataset = MotionDataset(
    data["clips"], labels=data["labels"],
    target_length=128, augmentation=pipeline, seed=42,
)
loader = DataLoader(dataset, batch_size=32, collate_fn=collate_motion_batch)

for epoch in range(10):
    dataset.set_epoch(epoch)   # fresh augmentation per epoch, reproducibly
    for batch in loader:
        batch["data"]     # (B, T_max, D) padded
        batch["mask"]     # (B, T_max) bool validity mask
        batch["lengths"]  # (B,) valid frames per item
        batch["labels"]   # (B,)

With a seed, augmentation is bit-identical across runs regardless of num_workers or shuffle order — the PyTorch guide explains the set_epoch contract.

Or pack arrays yourself

If you'd rather own the loop, the packers convert pybvh's structured arrays into model layouts directly:

from pybvh_ml import pack_to_ctv, pack_to_flat
import pybvh

bvh = pybvh.read_bvh_file("walk.bvh")
root_pos, rot6d = bvh.to_6d()

ctv = pack_to_ctv(root_pos, rot6d)     # (C, T, V) for GCNs
flat = pack_to_flat(root_pos, rot6d)   # (T, D) for MLPs / Transformers

Where to next

  • Find a function — the "I want to… → call" capability map.
  • Tensor Layouts & Packing — the (C, T, V) conventions and center_root semantics; ten minutes that prevent most bugs.
  • Preprocessing Pipelines — harmonization, normalization, and everything the dataset file stores.
  • Tutorials — three notebooks with detailed walkthroughs, ending in a trained classifier.