Skip to content

Visualization

Public API

bvhplot

Visualization module for pybvh.

Provides five main functions:

  • :func:rest_pose — T-pose / bind pose visualization (matplotlib).
  • :func:frame — static 3D skeleton snapshot (matplotlib).
  • :func:play — interactive playback with camera controls.
  • :func:render — fast export to video/GIF/HTML.
  • :func:trajectory — 2D top-down root trajectory plot.
Backends

render supports "opencv" (fast, optional dep) and "matplotlib" (default fallback). When backend is "auto" (the default), OpenCV is used if available.

play supports "k3d" (Jupyter notebooks, optional dep), "vedo" (desktop window, optional dep), and "matplotlib" (fallback). When backend is "auto", the best available backend for the current environment is selected automatically.

Install optional backends::

pip install pybvh[opencv]       # fast video rendering
pip install pybvh[interactive]  # k3d for Jupyter
pip install pybvh[viewer]       # vedo for desktop
pip install pybvh[all-viz]      # all of the above

rest_pose(bvh: Bvh | list[Bvh], *, labels: list[str] | None = None, figsize: tuple[float, float] | None = None, show: bool = False, camera: str | tuple[float, float] = 'front', ax: matplotlib.axes.Axes | None = None) -> tuple[matplotlib.figure.Figure, matplotlib.axes.Axes | list[matplotlib.axes.Axes]]

Plot the rest pose (T-pose / bind pose) of one or more skeletons.

All joint angles are zero and root is at the origin.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects. Pass a list for side-by-side comparison.

required
labels list[str]

Subplot titles for side-by-side comparison.

None
figsize (float, float)

Figure size in inches.

None
show bool

If True, call plt.show(). Default False.

False
camera str or (float, float)

Camera preset ("front", "side", "top") or (azimuth_deg, elevation_deg) tuple. Default "front".

'front'
ax Axes

Existing 3D axes to draw on. If provided, no new figure is created. Only supported for a single skeleton (raises ValueError when bvh is a list).

None

Returns:

Name Type Description
fig Figure
ax Axes or list[Axes]

Single axes when one skeleton, list when multiple.

frame(bvh: Bvh | list[Bvh], frame: int | npt.NDArray[np.floating] = 0, *, centered: str = 'world', labels: list[str] | None = None, figsize: tuple[float, float] | None = None, show: bool = False, camera: str | tuple[float, float] = 'front', ax: matplotlib.axes.Axes | None = None) -> tuple[matplotlib.figure.Figure, matplotlib.axes.Axes | list[matplotlib.axes.Axes]]

Plot a static 3D skeleton snapshot.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects. Pass a list for side-by-side comparison.

required
frame int or ndarray

Frame index (default 0) or pre-computed spatial coordinates.

0
centered str

Centering mode: "world" (default), "skeleton", or "first".

'world'
labels list[str]

Subplot titles for side-by-side comparison.

None
figsize (float, float)

Figure size in inches.

None
show bool

If True, call plt.show(). Default False.

False
camera str or (float, float)

Camera preset ("front", "side", "top") or (azimuth_deg, elevation_deg) tuple. Default "front".

'front'
ax Axes

Existing 3D axes to draw on. If provided, no new figure is created. Only supported for a single skeleton (raises ValueError when bvh is a list).

None

Returns:

Name Type Description
fig Figure
ax Axes or list[Axes]

Single axes when one skeleton, list when multiple.

render(bvh: Bvh | list[Bvh], filepath: str | Path = Path('./anim.mp4'), *, centered: str = 'world', labels: list[str] | None = None, fps: int = -1, backend: str = 'auto', camera: str | tuple[float, float] = 'front', resolution: tuple[int, int] = (1920, 1080), show_axis: bool = False, sync: str = 'truncate', follow: bool = False, match_fps: str | None = None) -> Path

Render animation to a video, GIF, or HTML file.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects. Pass a list for side-by-side comparison.

required
filepath str or Path

Output file path (default "./anim.mp4"). Format is inferred from the extension: .mp4, .mov, .avi, .gif, .webp, .apng, .html.

Path('./anim.mp4')
centered str

Centering mode: "world" (default), "skeleton", or "first".

'world'
labels list[str]

Labels for each skeleton when comparing.

None
fps int

Frames per second. -1 (default) uses the BVH frame rate.

-1
backend str

"auto" (default), "opencv", or "matplotlib".

'auto'
camera str or (float, float)

Camera preset ("front", "side", "top") or (azimuth_deg, elevation_deg) tuple. Default "front".

'front'
resolution (int, int)

Output resolution (width, height) in pixels. Default (1920, 1080). Only used by the OpenCV backend.

(1920, 1080)
show_axis bool

Show 3D axes (default False). Only used by matplotlib backend.

False
sync str

How to handle different frame counts in side-by-side comparison: "truncate" (default) stops at the shortest clip; "pad" continues to the longest clip (shorter clips freeze on their last frame).

'truncate'
follow bool

If True, the camera orientation is recomputed every frame using each skeleton's current facing direction (via :meth:~pybvh.bvh.Bvh.forward_at), so the view orbits with the character. Only affects preset cameras ("front", "side", "top"); custom (azimuth, elevation) tuples are fixed and ignore follow. Default False (stable camera).

False
match_fps str or None

How to handle clips with different frame rates in side-by-side rendering. None (default) emits a warning but does not resample. "lowest" resamples all clips to the lowest frame rate. "highest" resamples all clips to the highest frame rate (using SLERP interpolation for added frames).

None

Returns:

Type Description
Path

The path to the written file.

play(bvh: Bvh | list[Bvh], *, centered: str = 'world', labels: list[str] | None = None, fps: int = -1, backend: str = 'auto', camera: str | tuple[float, float] = 'front', sync: str = 'truncate', resolution: tuple[int, int] = (960, 540), quality: str = 'high', match_fps: str | None = None, spacing: float | str = 'auto') -> object

Play back motion data.

Auto-detects the best backend for the current environment:

  • Tier 1 (interactive): k3d in Jupyter notebooks, vedo on desktop.
  • Tier 2 (fast fallback): OpenCV renders to an inline video (notebook) or matplotlib animated window (script).
  • Tier 3 (slow fallback): matplotlib jshtml inline (notebook) or animated window (script).

When falling back, warnings indicate which packages to install for a better experience.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects. Pass a list for side-by-side comparison.

required
centered str

Centering mode: "world" (default), "skeleton", or "first".

'world'
labels list[str]

Labels for each skeleton when comparing.

None
fps int

Frames per second. -1 (default) uses the BVH frame rate, capped at 30 for matplotlib backends (via frame subsampling).

-1
backend str

"auto" (default), "k3d", "vedo", or "matplotlib".

'auto'
camera str or (float, float)

Camera preset ("front", "side", "top") or (azimuth_deg, elevation_deg) tuple. Default "front".

'front'
sync str

How to handle different frame counts in side-by-side comparison: "truncate" (default) stops at the shortest clip; "pad" continues to the longest clip (shorter clips freeze on their last frame).

'truncate'
resolution (int, int)

Output resolution (width, height) in pixels for the OpenCV notebook fallback. Default (960, 540). Ignored by interactive backends (k3d, vedo) and matplotlib.

(960, 540)
quality str

Visual quality for the vedo desktop backend: "high" (default) uses 3D tubes and spheres with lighting; "fast" uses flat lines and points for maximum performance. Ignored by other backends.

'high'
match_fps str or None

How to handle clips with different frame rates. None (default) emits a warning. "lowest" or "highest" resamples all clips to match.

None
spacing float or 'auto'

Lateral separation between skeletons in single-scene backends (k3d, vedo). "auto" (default) spaces skeletons by 1.2 × the lateral bounding-box width of the first skeleton when centered is "first" or "skeleton"; no spacing is applied when centered="world" (raw world coordinates are honoured). Pass a float (in scene units) to override. Ignored by multi-panel backends (matplotlib, OpenCV).

'auto'

Returns:

Type Description
object

Backend-specific return value (widget, plotter, or None).

trajectory(bvh: Bvh | list[Bvh], *, centered: str = 'world', labels: list[str] | None = None, figsize: tuple[float, float] | None = None, show: bool = False, ax: matplotlib.axes.Axes | None = None, facing_arrows: bool = False, tight: bool = False) -> tuple[matplotlib.figure.Figure, matplotlib.axes.Axes]

Plot 2D top-down trajectory of the root joint.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects. Pass a list for overlaid comparison.

required
centered str

Centering mode: "world" (default), "skeleton", or "first".

'world'
labels list[str]

Legend labels.

None
figsize (float, float)

Figure size in inches.

None
show bool

If True, call plt.show(). Default False.

False
ax Axes

Existing 2D axes to draw on. If provided, no new figure is created. Works with single or multiple skeletons (overlaid).

None
facing_arrows bool

If True, overlay small arrowheads along each skeleton's path showing the character's facing direction at ~10 evenly-spaced frames. Arrows use the same color as the trajectory line and are sized at ~8 % of the path's span. Default False.

False
tight bool

If False (default), the axis range matches the full horizontal extent of the skeleton across all joints and frames — the same bounding box bvh.play() uses. Keeps the motion scale honest relative to the character's body so a near-stationary clip doesn't get auto-zoomed into looking like a large walk. If True, axes auto-scale to just the root path — gives maximum detail on the path shape but can exaggerate small motions.

False

Returns:

Name Type Description
fig Figure
ax Axes

Shared Helpers

_common

Shared helpers for all visualization backends.

Pure-data operations: skeleton topology, bounding boxes, camera math, and orthographic projection. No plotting library imports.

get_skeleton_lines(bvh: Bvh) -> list[tuple[int, int]]

Precompute (parent_node_idx, child_node_idx) pairs for bone drawing.

Computed once per skeleton, reused every frame by all backends.

Parameters:

Name Type Description Default
bvh Bvh

The BVH object containing the skeleton hierarchy.

required

Returns:

Name Type Description
lines list of (int, int)

Each tuple is (parent_index, child_index) into the flat nodes list (i.e. the same indexing used by spatial coordinates).

normalize_input(bvh: Bvh | list[Bvh], frames: int | npt.NDArray[np.floating] | None, centered: str) -> tuple[list[Bvh], list[npt.NDArray[np.float64]]]

Normalize single/list Bvh + frame spec into parallel lists.

Parameters:

Name Type Description Default
bvh Bvh or list[Bvh]

One or more BVH objects to visualize.

required
frames int, ndarray, or None
  • None or -1: all frames (spatial coords for entire motion).
  • Non-negative int: single frame index.
  • 2-D array (N, 3): single frame of spatial coordinates (only valid when bvh is a single Bvh).
  • 3-D array (F, N, 3): pre-computed spatial coordinates (only valid when bvh is a single Bvh).
required
centered str

Centering mode passed to bvh.node_positions().

required

Returns:

Name Type Description
bvh_list list[Bvh]

Always a list (length >= 1).

coords_list list[ndarray]

Parallel list of spatial coordinates, each (F, N, 3).

compute_unified_limits(coords_list: list[npt.NDArray[np.float64]]) -> tuple[npt.NDArray[np.float64], float]

Compute a cubic bounding box encompassing all skeletons and frames.

The half-span is the larger of the per-frame body size and the trajectory extent from center. This ensures stationary skeletons fill the frame while walking skeletons never clip.

Parameters:

Name Type Description Default
coords_list list of ndarray

Each element has shape (F, N, 3) or (N, 3).

required

Returns:

Name Type Description
center ndarray of shape (3,)

Center of the bounding box in world coordinates.

half_span float

Half the side length of the cubic bounding box.

get_camera_angles(bvh: Bvh, ref_frame: npt.NDArray[np.float64], camera: str | tuple[float, float] = 'front') -> tuple[float, float, str]

Resolve a camera specification to (azimuth, elevation, up_axis).

Parameters:

Name Type Description Default
bvh Bvh

The BVH object (used for axis detection).

required
ref_frame ndarray of shape (N, 3)

A reference frame of spatial coordinates for axis heuristics.

required
camera str or (float, float)
  • "front" — auto-detected front view (default).
  • "side" — 90 degrees from front.
  • "top" — bird's-eye view looking down the up axis.
  • (azimuth_deg, elevation_deg) — custom angles.
'front'

Returns:

Name Type Description
azimuth float

Azimuth angle in degrees.

elevation float

Elevation angle in degrees.

up_axis str

Single character: 'x', 'y', or 'z'.

build_view_matrix(azimuth_deg: float, elevation_deg: float, up_axis: str) -> npt.NDArray[np.float64]

Build a 3x3 rotation that maps world coordinates to view coordinates.

Uses the same look-at camera math as matplotlib's view_init so that both backends produce identical views for the same (azimuth, elevation, up_axis) parameters.

View coordinate convention: x = right on screen, y = up on screen, z = out of screen (toward viewer).

Parameters:

Name Type Description Default
azimuth_deg float

Azimuth rotation in degrees.

required
elevation_deg float

Elevation rotation in degrees.

required
up_axis str

'x', 'y', or 'z'.

required

Returns:

Name Type Description
view_matrix ndarray of shape (3, 3)

ortho_project(coords_3d: npt.NDArray[np.float64], view_matrix: npt.NDArray[np.float64], center: npt.NDArray[np.float64], half_span: float, resolution: tuple[int, int], fixed_view_half: tuple[float, float] | None = None) -> npt.NDArray[np.int32]

Orthographic projection from 3D world to 2D pixel coordinates.

Parameters:

Name Type Description Default
coords_3d ndarray of shape (N, 3)

World-space joint positions for one frame.

required
view_matrix ndarray of shape (3, 3)

From :func:build_view_matrix.

required
center ndarray of shape (3,)

World-space center of the bounding box.

required
half_span float

Half the side length of the cubic bounding box.

required
resolution (width, height)

Output image dimensions in pixels.

required
fixed_view_half (float, float)

Pre-computed (view_half_u, view_half_v) to use for the scale calculation instead of computing it from the current view matrix. Useful for follow-mode rendering where the view rotates every frame: pass the max over all frames once to get a stable (angle-invariant) scale so the character doesn't appear to zoom in and out as the camera orbits. When None (default), the view-space extents are computed from the bounding box corners under the current view matrix (the default behavior, which gives a tighter fit per frame but oscillates under rotation).

None

Returns:

Name Type Description
pixels ndarray of shape (N, 2)

Integer pixel coordinates (x, y) for each joint.

align_frame_counts(coords_list: list[npt.NDArray[np.float64]], pad: bool = False) -> list[npt.NDArray[np.float64]]

Align all coordinate arrays to the same frame count.

When comparing multiple skeletons with different frame counts, arrays are either truncated to the minimum or padded to the maximum (by repeating the last frame).

Parameters:

Name Type Description Default
coords_list list of ndarray

Each element has shape (F, N, 3).

required
pad bool

If False (default), truncate to the shortest clip. If True, pad shorter clips by repeating their last frame so all clips match the longest.

False

Returns:

Name Type Description
coords_list list of ndarray

Arrays all with the same frame count.