Skip to main content

PyMotion: A Python Library for Motion Data

PyMotion is a Python library that provides various functions for manipulating and processing motion data in NumPy or PyTorch. It is designed to facilitate the development of neural networks for character animation.

Some features of PyMotion are:

  • A comprehensive set of quaternion operations and conversions to other rotation representations, such as rotation matrix, axis-angle, euler, and 6D representation
  • A dual quaternion representation for rigid displacements, which can help neural networks better understand poses, as proposed by Andreou et al. [2022] and later adopted by Ponton et al. [2023]
  • A continuous 6D rotation representation, as introduced by Zhou et al. [2019]
  • A BVH file reader and preprocessor for loading and transforming motion data
  • Skeletal operations such as Forward Kinematics for computing global joint positions from local joint rotations
  • [Deprecated] A plotly-based visualizer for debugging and visualizing character animation directly in Python
  • PyMotion to Blender automatic communication for debugging and visualizing character animation
  • NumPy and PyTorch implementations and tests for all functions

Contents

  1. Installation
  2. Examples
  3. Roadmap
  4. License

Installation

  1. [Optional] Install PyTorch using Pip as instructed in their webpage.

  2. Install PyMotion:

pip install upc-pymotion
  1. [Optional] Install Plotly and Dash for the visualizer (no needed for Blender visualization):
pip install upc-pymotion[viewer]

Examples

The examples below are small API snippets. Larger, runnable examples live in examples/; each example has its own README and optional dependencies so research-specific packages are not installed with PyMotion itself.

Read and save a BVH file
import numpy as np
from pymotion.io.bvh import BVH

bvh = BVH()
bvh.load("test.bvh")

print(bvh.data["names"])
# Example Output: ['Hips', 'LeftHip', 'LeftKnee', 'LeftAnkle', 'LeftToe', 'RightHip', 'RightKnee', 'RightAnkle', 'RightToe', 'Chest', 'Chest3', 'Chest4', 'Neck', 'Head', 'LeftCollar', 'LeftShoulder', 'LeftElbow', 'LeftWrist', 'RightCollar', 'RightShoulder', 'RightElbow', 'RightWrist']


# Move root joint to (0, 0, 0)
local_rotations, local_positions, parents, offsets, end_sites, end_sites_parents = bvh.get_data()
local_positions[:, 0, :] = np.zeros((local_positions.shape[0], 3))
bvh.set_data(local_rotations, local_positions)

# Scale the skeleton
bvh.set_scale(0.75)

bvh.save("test_out.bvh")
Compute world positions and rotations from a BVH file

NumPy

from pymotion.io.bvh import BVH
from pymotion.ops.skeleton import fk

bvh = BVH()
bvh.load("test.bvh")

local_rotations, local_positions, parents, offsets, end_sites, end_sites_parents = bvh.get_data()
global_positions = local_positions[:, 0, :]  # root joint
pos, rotmats = fk(local_rotations, global_positions, offsets, parents)

PyTorch

from pymotion.io.bvh import BVH
from pymotion.ops.skeleton_torch import fk
import torch

bvh = BVH()
bvh.load("test.bvh")

local_rotations, local_positions, parents, offsets, end_sites, end_sites_parents = bvh.get_data()
global_positions = local_positions[:, 0, :]  # root joint
pos, rotmats = fk(
    torch.from_numpy(local_rotations),
    torch.from_numpy(global_positions),
    torch.from_numpy(offsets),
    torch.from_numpy(parents),
)
Quaternion conversion to other representations

NumPy

import pymotion.rotations.quat as quat
import numpy as np

angles = np.array([np.pi / 2, np.pi, np.pi / 4])[..., np.newaxis]
# angles.shape = [3, 1]
axes = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
# axes.shape = [3, 3]

q = quat.from_angle_axis(angles, axes)

rotmats = quat.to_matrix(q)

euler = quat.to_euler(q, np.array([["x", "y", "z"], ["z", "y", "x"], ["y", "z", "x"]]))
euler_degrees = np.degrees(euler)

scaled_axis = quat.to_scaled_angle_axis(q)

PyTorch

import pymotion.rotations.quat_torch as quat
import numpy as np
import torch

angles = torch.Tensor([torch.pi / 2, torch.pi, torch.pi / 4]).unsqueeze(-1)
# angles.shape = [3, 1]
axes = torch.Tensor([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
# axes.shape = [3, 3]

q = quat.from_angle_axis(angles, axes)

rotmats = quat.to_matrix(q)

euler = quat.to_euler(q, np.array([["x", "y", "z"], ["z", "y", "x"], ["y", "z", "x"]]))
euler_degrees = torch.rad2deg(euler)

scaled_axis = quat.to_scaled_angle_axis(q)
Root-centered dual quaternions from a BVH file

NumPy

from pymotion.io.bvh import BVH
import pymotion.ops.skeleton as sk
import numpy as np

bvh = BVH()
bvh.load("test.bvh")

local_rotations, local_positions, parents, offsets, end_sites, end_sites_parents = bvh.get_data()

root_dual_quats = sk.to_root_dual_quat(
    local_rotations, local_positions[:, 0, :], parents, offsets
)

local_translations, local_rotations = sk.from_root_dual_quat(root_dual_quats, parents)
global_positions = local_translations[:, 0, :]
offsets = local_translations.copy()
offsets[:, 0, :] = np.zeros((offsets.shape[0], 3))

PyTorch

from pymotion.io.bvh import BVH
import pymotion.ops.skeleton_torch as sk
import torch

bvh = BVH()
bvh.load("test.bvh")

local_rotations, local_positions, parents, offsets, end_sites, end_sites_parents = bvh.get_data()

root_dual_quats = sk.to_root_dual_quat(
    torch.from_numpy(local_rotations),
    torch.from_numpy(local_positions[:, 0, :]),
    torch.from_numpy(parents),
    torch.from_numpy(offsets),
)

local_translations, local_rotations = sk.from_root_dual_quat(root_dual_quats, parents)
global_positions = local_translations[:, 0, :]
offsets = local_translations.clone()
offsets[:, 0, :] = torch.zeros((offsets.shape[0], 3))
6D representation from a BVH file

NumPy

from pymotion.io.bvh import BVH
import pymotion.rotations.ortho6d as sixd

bvh = BVH()
bvh.load("test.bvh")

local_rotations, _, _, _, _, _ = bvh.get_data()

continuous = sixd.from_quat(local_rotations)

local_rotations = sixd.to_quat(continuous)

PyTorch

from pymotion.io.bvh import BVH
import pymotion.rotations.ortho6d_torch as sixd
import torch

bvh = BVH()
bvh.load("test.bvh")

local_rotations, _, _, _, _, _ = bvh.get_data()

continuous = sixd.from_quat(torch.from_numpy(local_rotations))

local_rotations = sixd.to_quat(continuous)
Skeleton local rotations from root-centered positions

NumPy

import numpy as np
from pymotion.io.bvh import BVH
from pymotion.ops.skeleton import fk
from pymotion.ops.skeleton import from_root_positions

bvh = BVH()
bvh.load("test.bvh")
local_rotations, local_positions, parents, offsets, _, _ = bvh.get_data()
pos, _ = fk(local_rotations, np.zeros((local_positions.shape[0], 3)), offsets, parents)

pred_rots = from_root_positions(pos, parents, offsets)

bvh.set_data(pred_rots, local_positions)
bvh.save("test_out.bvh")  # joint positions should be similar as test.bvh

PyTorch

import torch
from pymotion.io.bvh import BVH
from pymotion.ops.skeleton_torch import fk
from pymotion.ops.skeleton_torch import from_root_positions

bvh = BVH()
bvh.load("test.bvh")
local_rotations, local_positions, parents, offsets, _, _ = bvh.get_data()
offsets = torch.from_numpy(offsets)
parents = torch.from_numpy(parents)
pos, _ = fk(
    torch.from_numpy(local_rotations),
    torch.zeros((local_positions.shape[0], 3)),
    offsets,
    parents,
)

pred_rots = from_root_positions(pos, parents, offsets)

bvh.set_data(pred_rots.numpy(), local_positions)
bvh.save("test_out.bvh")  # joint positions should be similar as test.bvh
Mirroring an animation

NumPy

import numpy as np
from pymotion.io.bvh import BVH
from pymotion.ops.skeleton import mirror

bvh = BVH()
bvh.load("test.bvh")

names = bvh.data["names"].tolist()
print(names)
# ['Hips', 'Chest', 'Chest2', 'Chest3', 'Chest4', 'Neck', 'Head', 'RightCollar', 'RightShoulder',
#  'RightElbow', 'RightWrist', 'LeftCollar', 'LeftShoulder', 'LeftElbow', 'LeftWrist', 'RightHip',
#  'RightKnee', 'RightAnkle', 'RightToe', 'LeftHip', 'LeftKnee', 'LeftAnkle', 'LeftToe']

joints_mapping = np.array(
    [
        (
            names.index("Left" + n[5:])
            if n.startswith("Right")
            else (names.index("Right" + n[4:]) if n.startswith("Left") else names.index(n))
        )
        for n in names
    ]
)

local_rotations, local_positions, parents, offsets, end_sites, _ = bvh.get_data()

mirrored_local_rots, mirrored_global_pos, mirrored_offsets, _ = mirror(
    local_rotations,
    local_positions[:, 0, :],  # global position of the root joint
    parents,
    offsets,
    end_sites,
    joints_mapping=joints_mapping,  # joints_mapping is only required for mode="symmetry"
    mode="symmetry",  # other modes: "all" or "positions"
    axis="X",
)

local_positions[:, 0, :] = mirrored_global_pos
bvh.set_data(mirrored_local_rots, local_positions)
# Uncomment when mode == "all"
# bvh.data["offsets"] = mirrored_offsets
bvh.save("test_mirrored.bvh")

PyTorch

import torch
from pymotion.io.bvh import BVH
from pymotion.ops.skeleton_torch import mirror

bvh = BVH()
bvh.load("test.bvh")

names = bvh.data["names"].tolist()
print(names)
# ['Hips', 'Chest', 'Chest2', 'Chest3', 'Chest4', 'Neck', 'Head', 'RightCollar', 'RightShoulder',
#  'RightElbow', 'RightWrist', 'LeftCollar', 'LeftShoulder', 'LeftElbow', 'LeftWrist', 'RightHip',
#  'RightKnee', 'RightAnkle', 'RightToe', 'LeftHip', 'LeftKnee', 'LeftAnkle', 'LeftToe']

joints_mapping = torch.Tensor(
    [
        (
            names.index("Left" + n[5:])
            if n.startswith("Right")
            else (names.index("Right" + n[4:]) if n.startswith("Left") else names.index(n))
        )
        for n in names
    ]
).to(torch.int32)

local_rotations, local_positions, parents, offsets, end_sites, _ = bvh.get_data()

mirrored_local_rots, mirrored_global_pos, mirrored_offsets, _ = mirror(
    torch.from_numpy(local_rotations),
    torch.from_numpy(local_positions[:, 0, :]),  # global position of the root joint
    torch.from_numpy(parents),
    torch.from_numpy(offsets),
    torch.from_numpy(end_sites),
    joints_mapping=joints_mapping,  # joints_mapping is only required for mode="symmetry"
    mode="symmetry",  # other modes: "all" or "positions"
    axis="X",
)

local_positions[:, 0, :] = mirrored_global_pos.numpy()
bvh.set_data(mirrored_local_rots.numpy(), local_positions)
# Uncomment when mode == "all"
# bvh.data["offsets"] = mirrored_offsets.numpy()
bvh.save("test_mirrored.bvh")
Visualize motion in Python
from pymotion.render.viewer import Viewer
from pymotion.io.bvh import BVH
from pymotion.ops.skeleton import fk

bvh = BVH()
bvh.load("test.bvh")

local_rotations, local_positions, parents, offsets, _, _ = bvh.get_data()
global_positions = local_positions[:, 0, :]  # root joint
pos, rotmats = fk(local_rotations, global_positions, offsets, parents)

viewer = Viewer(use_reloader=True, xy_size=5)
viewer.add_skeleton(pos, parents)
# add additional info using add_sphere(...) and/or add_line(...), examples:
# viewer.add_sphere(sphere_pos, color="green")
# viewer.add_line(start_pos, end_pos, color="green")
viewer.add_floor()
viewer.run()
Visualize points, orientations and BVH files in Blender
import numpy as np
from pymotion.io.bvh import BVH
from pymotion.render.blender import BlenderConnection

with BlenderConnection() as conn:
    conn.clear_scene()
    conn.setup_rendering()
    conn.render_checkerboard_floor()
    conn.render_points(
        np.array([[0, -3, 0], [1, 2, 3]]), np.array([[0, 0, 1], [0, 1, 0]]), radius=np.array([[0.25], [0.05]])
    )
    conn.render_orientations(
        np.array([[1, 0, 0, 0], [np.cos(np.pi / 4.0), np.sin(np.pi / 4.0), 0, 0]]),
        np.array([[0, -3, 0], [1, 2, 3]]),
        scale=np.array([[0.5], [0.25]]),
    )
    # BVH files can be rendered directly from file path
    path = "test.bvh"
    conn.render_bvh_from_path(
        path,
        np.array([0, 0, 1]),
        no_render_joints=["RightWrist", "LeftWrist", "RightToe", "LeftToe", "Head"],
    )
    # or by using a BVH object
    bvh = BVH()
    path = "test2.bvh"
    bvh.load(path)
    conn.render_bvh(
        bvh,
        np.array([0, 1, 0]),
        exclude_end_sites=True,
        no_render_joints_pattern=["twist", "helper"],
    )

Roadmap

This repository is authored and maintained by Jose Luis Ponton.

Features will be added when new operations or rotation representations are needed in the development of research projects. Here it is a list of possible features and improvements for the future:

  • Extend documentation and add examples in the description of each function.
  • Include new animation importers such as FBX, USD, ...
  • Include useful operations for data augmentation such as animation mirroring (done), noise addition, temporal warping, etc.
  • Create an Inverse Kinematics module.

License

This work is licensed under the MIT license. Please, see the LICENSE for further details.

Release files for upc-pymotion 0.3.4

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

Source distribution (sdist)

Source distribution for upc-pymotion 0.3.4
File Size Uploaded
upc_pymotion-0.3.4.tar.gz 55.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for upc-pymotion 0.3.4
File Interpreter ABI Platform
upc_pymotion-0.3.4-py3-none-any.whl Python 3 none any Details

Total release size: 135.9 kB

Release files / upc_pymotion-0.3.4.tar.gz

Download URL upc_pymotion-0.3.4.tar.gz
Size 55.4 kB
Tags Source
SHA-256 checksum
How to use checksums
90666ea2fb271d059bedc165f45dc7a6df4de20819d45744179a408b2b991548
BLAKE2b-256 checksum
How to use checksums
49aae8c4d99f65bc7341b05b7bf3245c2578bd0b62ad9aa9b74ffbe939f37994
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 19, 2026.

Transparency log

Release files / upc_pymotion-0.3.4-py3-none-any.whl

Download URL upc_pymotion-0.3.4-py3-none-any.whl
Size 80.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
432f26653dbd2252284ac203851bfdd867033bdb6de7fd0e19051b5e687eb64d
BLAKE2b-256 checksum
How to use checksums
c06c79367b319e4f93a0e0eb35e09d9884924649e4a5ae64abccd568bea7a639
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.4 This release

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.3

2 release files

0.0.1

2 release files

0.0.0

2 release 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