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
Jjoints orNnodes) - 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 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. Besides the four :class: |
DEFAULT_STREAMS
|
Returns:
| Type | Description |
|---|---|
(ndarray, shape(C, T, V))
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a named stream is absent from arrays; if |
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 |
True
|
streams
|
tuple of str
|
What to pack, and in what order — the same vocabulary as
:func: |
DEFAULT_STREAMS
|
Returns:
| Type | Description |
|---|---|
(ndarray, shape(T, V, C))
|
The transpose of what :func: |
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 |
True
|
streams
|
tuple of str
|
What to pack, and in what order — here that is column order.
Default |
DEFAULT_STREAMS
|
Returns:
| Type | Description |
|---|---|
(ndarray, shape(T, D))
|
|
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 |
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 |
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 |
3
|
Returns:
| Type | Description |
|---|---|
MotionArrays
|
|