Skip to content

Packing

packing

Tensor layout conversion for ML pipelines.

Converts between :class:~pybvh_ml.MotionArrays and the tensor layouts that ML models consume: (C, T, V), (T, V, C), and flat (T, D).

Conventions
  • C = channels (the packed streams' channels, concatenated)
  • T = time / frames
  • V = vertices (root is vertex 0 when packed, then the per-vertex stream's J joints or N nodes)
  • D = flat feature dimension (the same channels, per frame)

streams= names what is packed and in what order — channel order in the graph layouts, column order in flat. It defaults to ("root_pos", "joint_rot"), which is the layout every pybvh-ml version has produced.

The root vertex carries 3 position channels; when C > 3 (e.g. quat, 6D, or rotmat joint data), the root vertex's remaining C - 3 channels are zero padding — the position values themselves are unchanged.

DEFAULT_STREAMS: tuple[str, ...] = ('root_pos', 'joint_rot') module-attribute

What the packers pack when nothing else is asked for.

The 0.5.0 layout, byte for byte: root as vertex 0, joints as vertices 1..J, rotation channels first.

DERIVED_STREAMS: dict[str, tuple[str, int]] = {'joint_vel': ('joint_pos', 1), 'joint_acc': ('joint_pos', 2), 'node_vel': ('node_pos', 1), 'node_acc': ('node_pos', 2)} module-attribute

Packable streams computed from a base stream, as (base, order).

Temporal finite differences of the position streams: joint_vel is the first difference of joint_pos, joint_acc the second. They are not :class:~pybvh_ml.MotionArrays fields and cannot be augmented — see :func:pack_to_ctv for why they are derived at packing time instead of carried.

STREAM_VOCABULARY: tuple[str, ...] = (*_BASE_STREAMS, *DERIVED_STREAMS) module-attribute

Every name streams= accepts, base streams first.

channel_count(pack_root: bool, channel_widths: list[int]) -> int

C for a graph-layout pack: the widths, floored by the root's 3.

The floor is the rule that is easy to miss when predicting shapes by hand — a pack whose per-vertex streams total fewer than 3 channels still needs 3, because vertex 0 carries a position. With no per-vertex stream at all (streams=("root_pos",)) it is the only thing setting C.

pack_to_ctv(arrays: MotionArrays, center_root: bool = True, *, streams: tuple[str, ...] | list[str] = DEFAULT_STREAMS) -> npt.NDArray[np.float64]

Pack the named streams into (C, T, V) layout.

The ST-GCN layout. streams=("joint_pos",) is the canonical skeleton-action-recognition input — (3, T, J), with the joint axis indexed directly by skeleton_info["edges"].

Parameters:

Name Type Description Default
arrays MotionArrays

Must carry every stream named in streams.

required
center_root bool

If True, subtract first frame's root position. This flag is for standalone packing of raw extractions. Clips from a dataset preprocessed with center_root=True (see :func:~pybvh_ml.preprocessing.preprocess_directory and the center_root key of :func:~pybvh_ml.preprocessing.load_preprocessed) are already centered — pass False for those. Re-centering a whole already-centered clip is a harmless no-op, but re-centering a windowed sub-clip zeroes the window's first frame and destroys the clip-relative trajectory.

It reaches the position vertices too, which is what keeps them in the same frame as vertex 0 — see the Notes.

True
streams tuple of str

What to pack, and in what order. "root_pos" adds the root as vertex 0 (V = 1 + J); omit it and V = J. The per-vertex streams are concatenated along C in the order given. Default ("root_pos", "joint_rot").

Besides the four :class:~pybvh_ml.MotionArrays streams, the derived names "joint_vel" / "joint_acc" and "node_vel" / "node_acc" are accepted — temporal differences of the matching position stream, computed here rather than carried. Requesting one needs its base stream present but does not require packing it: streams=("joint_vel",) alone is the 2s-AGCN motion stream. See the Notes for the unit, boundary and ordering conventions they follow.

DEFAULT_STREAMS

Returns:

Type Description
(ndarray, shape(C, T, V))

T = F. V is the per-vertex stream's vertex count plus one when "root_pos" is packed; C is the sum of the per-vertex streams' channel counts, floored at 3 when the root is packed. Root is vertex 0: its position fills channels 0:3, and any channels beyond that are zero padding.

Raises:

Type Description
ValueError

If a named stream is absent from arrays; if "node_pos" is combined with a joint-space stream (different V); or if center_root=True meets positions whose position_centering is None.

Notes

Common combinations:

===================================== ================= ================= streams shape note ===================================== ================= ================= ("root_pos", "joint_rot") (max(3, C), T, 1+J) the default ("joint_pos",) (3, T, J) ST-GCN input ("node_pos",) (3, T, N) full skeleton ("joint_pos", "joint_rot") (3+C, T, J) multi-stream ("root_pos", "joint_pos") (3, T, 1+J) vertex 0 duplicates joint 0 under "world" ("joint_vel",) (3, T, J) motion stream ("joint_pos", "joint_vel", "joint_acc") (9, T, J) pos/vel/acc ===================================== ================= =================

Vertex alignment. With V = J (("joint_pos",)) the edges / lr_pairs from skeleton_info index packed vertices one-to-one, and likewise node_edges / node_lr_pairs with ("node_pos",). Any packing that includes "root_pos" shifts every joint by one, so those lists need +1 on both indices.

Centering. Under position_centering="world" or "first" the center_root shift is applied identically to the root and to every position vertex; under "skeleton" the positions are already root-relative and are left alone. Centering only the root would move vertex 0 away from a body that stayed put. It cannot change a derived stream at all: center_root subtracts the first frame's root, a shift constant in time, which cancels in every difference.

Derived streams — units. joint_vel is a raw per-frame difference, p[t] - p[t-1], with no dt: the convention of 2s-AGCN, CTR-GCN and PYSKL, and the one that needs no frame rate (which :class:~pybvh_ml.MotionArrays does not carry). The alternative is physical units — divide by frame_time, or equivalently multiply these values by the clip's fps — which no parameter here does for you.

Derived streams — boundary. An order-k stream has its first k frames zeroed, so joint_vel[0] and joint_acc[0:2] are 0. Prepending the first frame instead would leave joint_acc[1] holding joint_vel[1] — a velocity in an acceleration channel. The alternative is central differences, unbiased in time but non-causal, and therefore a label leak for an autoregressive model.

Derived streams — meaning depends on the centering. A difference of "world" positions is world-frame velocity including the root trajectory; of "skeleton" positions, velocity relative to the root — HumanML3D's local_velocity. Same shape, different quantity, so an undeclared position_centering is refused rather than guessed.

Derived streams — derive before any temporal standardization. These are differences of consecutive frames. Padding, cropping or index-sampling a clip and only then packing a derived stream differences whatever frames survived: a pad boundary produces a phantom spike, and uniform_temporal_sample produces differences scaled by a random gap. Both Dataset classes order this correctly for you — they materialize the derived streams before standardizing the length. Doing your own temporal standardization and then calling a packer does not, which is why the recommendation is to let the Dataset do it.

A consequence worth knowing before you check it: under temporal="resample" a packed derived stream is not the difference of the packed positions, and is not meant to be. It is the true per-frame difference observed at the sampled instants, subsampled alongside the positions rather than recomputed from them; the packed positions' own difference is inflated by the mean sampling gap (and by its square, one order up). Under "pad" and "crop" the two agree on frames k..length-1.

See Also

pack_to_tvc, pack_to_flat : The same streams in the other layouts. unpack_from_ctv : The inverse for the default streams (see its Notes for why it does not take streams=).

pack_to_tvc(arrays: MotionArrays, center_root: bool = True, *, streams: tuple[str, ...] | list[str] = DEFAULT_STREAMS) -> npt.NDArray[np.float64]

Pack the named streams into (T, V, C) layout.

Parameters:

Name Type Description Default
arrays MotionArrays

Must carry every stream named in streams.

required
center_root bool

If True, subtract first frame's root position (from the position vertices too). Arrays from a preprocessed dataset saved with center_root=True are already centered — see :func:pack_to_ctv.

True
streams tuple of str

What to pack, and in what order — the same vocabulary as :func:pack_to_ctv, including the derived "joint_vel" / "joint_acc" / "node_vel" / "node_acc"; its Notes carry the unit, boundary and ordering conventions derived streams follow.

DEFAULT_STREAMS

Returns:

Type Description
(ndarray, shape(T, V, C))

The transpose of what :func:pack_to_ctv returns; the same shape rules apply.

pack_to_flat(arrays: MotionArrays, center_root: bool = True, *, streams: tuple[str, ...] | list[str] = DEFAULT_STREAMS) -> npt.NDArray[np.float64]

Pack the named streams into flat (T, D) layout.

Parameters:

Name Type Description Default
arrays MotionArrays

Must carry every stream named in streams.

required
center_root bool

If True, subtract first frame's root position (from the position vertices too). Arrays from a preprocessed dataset saved with center_root=True are already centered — see :func:pack_to_ctv.

True
streams tuple of str

What to pack, and in what order — here that is column order. Default ("root_pos", "joint_rot"), which is the [root_pos (3), joint_rot flattened over (J, C)] layout :func:~pybvh_ml.describe_features and the mean / std vectors of a preprocessed dataset are written against. The same vocabulary as :func:pack_to_ctv applies, including the derived "joint_vel" / "joint_acc" / "node_vel" / "node_acc" — see its Notes for the unit, boundary and ordering conventions, including why to derive before any temporal standardization you run yourself.

DEFAULT_STREAMS

Returns:

Type Description
(ndarray, shape(T, D))

D is 3 for a packed root plus V * C per per-vertex stream. For the default streams that is 3 + J * C_rot, with root position in columns 0:3.

Notes

The default layout is a public contract — pack_to_flat, :func:~pybvh_ml.describe_features, the stored normalization vectors and HumanML3D's Mean.npy / Std.npy all agree on it — which is why positions get their own stats block in a preprocessed dataset rather than widening mean / std. A D that changed with a preprocessing flag would make one file format mean two things.

unpack_from_ctv(data: npt.NDArray[np.float64], root_channels: int = 3) -> MotionArrays

Unpack (C, T, V) back to root position and joint data.

Parameters:

Name Type Description Default
data (ndarray, shape(C, T, V))
required
root_channels int

Number of channels used by the root vertex (default 3).

3

Returns:

Type Description
MotionArrays

Raises:

Type Description
ValueError

If root_channels exceeds the array's channel count.

Notes

The unpackers take no streams=, so they invert only the default ("root_pos", "joint_rot") packing: everything past the root's channels comes back as joint_rot. Round-tripping a multi-stream pack needs a streams-aware unpacker, which is purely additive whenever it lands; the asymmetry is a deliberate deferral rather than an oversight. Until then, unpack a multi-stream tensor by slicing the channel axis yourself — the widths are the ones :func:pack_to_ctv documents.

unpack_from_tvc(data: npt.NDArray[np.float64], root_channels: int = 3) -> MotionArrays

Unpack (T, V, C) back to root position and joint data.

Parameters:

Name Type Description Default
data (ndarray, shape(T, V, C))
required
root_channels int
3

Returns:

Type Description
MotionArrays

Raises:

Type Description
ValueError

If root_channels exceeds the array's channel count.

unpack_from_flat(data: npt.NDArray[np.float64], root_channels: int = 3, joint_channels: int = 3) -> MotionArrays

Unpack flat (T, D) back to root position and joint data.

Parameters:

Name Type Description Default
data (ndarray, shape(T, D))
required
root_channels int

Number of columns for root position (default 3).

3
joint_channels int

Number of channels per joint (default 3). Used to reshape the remaining columns into (T, J, joint_channels).

3

Returns:

Type Description
MotionArrays