bvhio
Lightweight libary for reading, editing and creating Biovision .bvh files. Deserializes files into a hierarchical spatial structure like transforms in Unity or Unreal.
Data for each joint is provided in local and world space and does support modifing the hierarchy itself without losing the keyframe data. The spatial structure does also allow for editing the motion or rest pose data. This libary supports also deserializing and serialising .bvh files into a simplified structure that represents the key data from the file.
Install
pip install bvhio
Why and intention
This libary is a side product of my master thesis, in order to extract conveniently local and world data features from a humanoid skeleton hierarchy. I could not find any libary that could do that, without bloat or the features I required for extraction or modification.
Notes
- The package spatial-transform is used as base object for joints and provides the most properties and methods.
- The package PyGLM is used for matrix, quaternion and vector calculations.
- Same coordination space as openGL and GLM is used, which is right-handed, where Y+ is up and Z- is forward.
- Positive rotations are counter clockwise when viewed from the origin looking in the positive direction.
Features
- Read/Write/Edit
- Read and write .bvh files as simplified structure (
BvhJoint
). - Read and write .bvh files as transform hierarchy (
Joint
). - Animation data can be modified with both methods.
- The transform hierarchy allows for easy modifications of rest and motion data.
- Animation
- Supports modifing keyframe, rest positon and final pose data.
- Supports joint special modifications, like changing the joint-roll
- Keyframes are stored in local space and as difference to the rest pose.
- Keyframes support Position, Rotation and Scale.
- Python
- Every method is documented in code with docstrings.
- Every method has type hinting.
- (Fluent Interface) design.
Examples
Read bvh as simple structure and modify channels
import bvhio
bvh = bvhio.readAsBvh('bvhio/tests/example.bvh')
for joint, index, depth in bvh.Root.layout():
joint.Channels = ['Xposition', 'Yposition', 'Zposition'] if joint.Name == "Hips" else []
joint.Channels.extend(['Zrotation', 'Yrotation', 'Xrotation'])
bvhio.writeBvh('test.bvh', bvh, percision=6)
Read bvh as transform hierarchy
import bvhio
root = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
root.printTree()
root.loadRestPose(recursive=True)
print('\nRest pose position and Y-direction of each joint in world space ')
for joint, index, depth in root.layout():
print(f'{joint.PositionWorld} {joint.UpWorld} {joint.Name}')
Bvh deserialized properties and methods
import bvhio
bvh = bvhio.readAsBvh('bvhio/tests/example.bvh')
print(f'Root: {bvh.Root}')
print(f'Frames: {bvh.FrameCount}')
print(f'Frame time: {bvh.FrameTime}')
bvh.Root.Name
bvh.Root.Offset
bvh.Root.Channels
bvh.Root.EndSite
bvh.Root.Keyframes
bvh.Root.Children
bvh.Root.getRotation()
bvh.Root.getLength()
bvh.Root.getTip()
bvhio joint properties and methods
import bvhio
hierarchy = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
joint = hierarchy.filter('Head')[0]
joint.Keyframes
joint.loadPose(0)
joint.writePose(0)
joint.roll(0)
joint.clearParent()
joint.clearChildren()
joint.attach()
joint.detach()
joint.applyPosition()
joint.applyRotation()
joint.applyScale()
Interacting with joints and animation
import bvhio
root = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
root = bvhio.Joint('Root').attach(root, keep=['position', 'rotation', 'scale'])
root.RestPose.Scale = 0.0254
root.applyRestposeScale(recursive=True, bakeKeyframes=True)
root.RestPose.addEuler((0, 180, 0))
root.loadPose(0)
print('\nPosition and Y-direction of each joint in world space ')
for joint, index, depth in root.layout():
print(f'{joint.PositionWorld} {joint.UpWorld} {joint.Name}')
Compare pose data
import bvhio
root = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
root = bvhio.Joint('Root', restPose=bvhio.Transform(scale=2.54)).attach(root)
pose0positions = [joint.PositionWorld for (joint, index, depth) in root.loadPose(0).layout()]
pose1positions = [joint.PositionWorld for (joint, index, depth) in root.loadPose(1).layout()]
print('Change in positions in centimeters between frame 0 and 1:')
for (joint, index, depth) in root.layout():
print(f'{pose1positions[index] - pose0positions[index]} {joint.Name}')
Convert between bvh and hierarchy structure
import bvhio
bvhRoot = bvhio.readAsBvh('bvhio/tests/example.bvh').Root
hierarchyRoot = bvhio.convertBvhToHierarchy(bvhRoot)
bvhRoot = bvhio.convertHierarchyToBvh(hierarchyRoot, hierarchyRoot.getKeyframeRange()[1] + 1)
bvhio.writeBvh('test.bvh', bvhio.BvhContainer(bvhRoot, len(bvhRoot.Keyframes), 1/30))
Isolate joints from the hierachy (remove root joint)
import bvhio
root = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
chest = root.filter('Chest')[0]
root = chest.clearParent(keep=['position', 'rotation', 'scale', 'rest', 'anim'])
bvhio.writeHierarchy('test.bvh', root, 1/30)
Interpolate between keyframes
import bvhio
root = bvhio.readAsHierarchy('bvhio/tests/example.bvh')
for joint, index, depth in root.layout():
joint.Keyframes = [(frame * 100, key) for frame, key in joint.Keyframes]
bvhio.writeHierarchy('test.bvh', root, 1/30)
Merge BVH files
import bvhio
file1 = bvhio.readAsBvh('bvhio/tests/example.bvh')
file2 = bvhio.readAsBvh('bvhio/tests/example.bvh')
data1 = file1.Root.layout()
data2 = file2.Root.layout()
for joint, index, _ in data1:
joint.Keyframes.extend(data2[index][0].Keyframes)
file1.FrameCount += file2.FrameCount
bvhio.writeBvh('test.bvh', file1, 4)
Create/build animations from code
import bvhio
root = bvhio.Joint('Root', (0,2,0)).setEuler((0,0,0)).attach(
bvhio.Joint('UpperLegL', (+.3,2.1,0)).setEuler((0,0,180)).attach(
bvhio.Joint('LowerLegL', (+.3,1,0)).setEuler((0,0,180))
),
bvhio.Joint('UpperLegR', (-.3,2.1,0)).setEuler((0,0,180)).attach(
bvhio.Joint('LowerLegR', (-.3,1,0)).setEuler((0,0,180))
),
)
root.writeRestPose(recursive=True)
for joint in root.filter('LegL'):
joint.Rotation *= bvhio.Euler.toQuatFrom((+0.523599,0,0))
for joint in root.filter('LegR'):
joint.Rotation *= bvhio.Euler.toQuatFrom((-0.523599,0,0))
root.writePose(0, recursive=True)
root.loadRestPose(recursive=True)
for joint in root.filter('LegL'):
joint.Rotation *= bvhio.Euler.toQuatFrom((-0.523599,0,0))
for joint in root.filter('LegR'):
joint.Rotation *= bvhio.Euler.toQuatFrom((+0.523599,0,0))
root.writePose(20, recursive=True)
bvhio.writeHierarchy('test.bvh', root, 1/30, percision=4)