Skeleton Metadata¶
skeleton
¶
Skeleton graph metadata for GCN and graph-based models.
Provides edge lists, left/right pairs in both index spaces, forward- kinematics topology, and unified skeleton descriptors — the topology data that GCN and Transformer models consume. Only uses pybvh's public API.
Two index spaces, and they are not interchangeable. Joint space
(bvh.joint_angles order, J entries) excludes end sites; node
space (bvh.nodes order, N >= J entries) includes them. Node
indices diverge from joint indices as soon as any end site precedes a
paired joint in file order, so an index list from one space silently
addresses the wrong vertices in the other. Every key here says which
space it is in, and fk_topology["joint_idx"] is the map between
them: joint_idx[n] >= 0 marks node n as a joint and gives its
joint column.
get_edge_list(bvh: Bvh, include_end_sites: bool = False) -> list[tuple[int, int]]
¶
Get skeleton edge list as (child_idx, parent_idx) tuples.
Thin re-export of pybvh's edge-list properties.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bvh
|
Bvh
|
|
required |
include_end_sites
|
bool
|
If False (default), use |
False
|
Returns:
| Type | Description |
|---|---|
list of (int, int)
|
|
Notes
Which packing this indexes directly. Joint-space edges index the
vertices of pack_to_*(streams=("joint_pos",)), where V = J,
one-to-one; node-space edges do the same for
streams=("node_pos",), where V = N. Any packing that
includes "root_pos" — the default ("root_pos", "joint_rot")
among them — puts the root at vertex 0 and shifts every joint by
one, so those edges need (child + 1, parent + 1).
get_lr_pairs(bvh: Bvh) -> list[tuple[int, int]]
¶
Detect left/right joint pairs as index tuples.
Returns list(bvh.lr_pairs) — the cached, auto-detected
index-space pair list from pybvh. An empty list means no pairs
were detected on this skeleton.
Returns:
| Type | Description |
|---|---|
list of (int, int)
|
|
get_node_lr_pairs(bvh: Bvh) -> list[tuple[int, int]]
¶
Detect left/right node pairs as index tuples.
Node-space counterpart of :func:get_lr_pairs, covering joints and
their end sites — what :func:~pybvh_ml.mirror needs to swap every
paired vertex of a node_pos stream, fingertips and toe tips
included.
Returns:
| Type | Description |
|---|---|
list of (int, int)
|
|
Notes
A joint pair whose two sides carry different numbers of end sites
is returned with its end sites dropped: pybvh has no well-defined tip
correspondence there, and its property filters rather than raises,
matching lr_pairs. That silently produces a half-swapped
skeleton at mirror time, which is why
:func:get_skeleton_info records the offending pairs under
mismatched_end_site_pairs while the Bvh is still open — see
:func:find_mismatched_end_site_pairs.
find_mismatched_end_site_pairs(bvh: Bvh) -> list[tuple[int, int]]
¶
L/R joint pairs whose two sides carry different numbers of end sites.
Detected here, where the :class:~pybvh.Bvh is still open, because
the consequence lands far away: we persist node_lr_pairs and
mirror at train time, and pybvh's property drops the end sites of
such a pair rather than raising. A dropped tip is exactly the
half-swapped skeleton :func:pybvh.transforms.mirror refuses to
emit — right policy for a property, wrong outcome for stored
metadata nobody re-checks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bvh
|
Bvh
|
|
required |
Returns:
| Type | Description |
|---|---|
list of (int, int)
|
Offending pairs in node index space, empty when every paired
joint's two sides agree. A non-empty list means a train-time
mirror over |
Notes
Both sides of the comparison are node-space, and that is
load-bearing. Mixing in joint-space lr_pairs does not fail
loudly: it indexes the end-site counter with the wrong keys and
returns arbitrary answers — not a uniform false negative, which
would at least be noticeable, but a wrong pair that looks like a
result. The two spaces diverge as soon as any end site precedes a
paired joint in file order.
get_fk_topology_dict(bvh: Bvh) -> dict
¶
The four :class:pybvh.FkTopology fields, JSON-serializable.
What makes the train-time FK refresh in
:func:~pybvh_ml.add_joint_rotation_noise possible: store these
with the dataset, rebuild an FkTopology once per dataset with
:func:build_fk_topology, and forward kinematics runs from arrays
alone with the source .bvh long closed.
Returns:
| Type | Description |
|---|---|
dict
|
|
Notes
Stored as lists rather than arrays because this dict is persisted as
JSON inside skeleton_info; :func:build_fk_topology converts
back, and pybvh's constructor validates what it gets.
build_fk_topology(skeleton_info: dict) -> FkTopology
¶
Rebuild a :class:pybvh.FkTopology from stored metadata.
The train-time counterpart of :func:get_fk_topology_dict: call it
once per dataset (pybvh's constructor validates, which is not free)
and hand the result to
:func:~pybvh_ml.add_joint_rotation_noise as fk_topology=.
:meth:~pybvh_ml.AugmentationPipeline.standard does exactly this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeleton_info
|
dict
|
From :func: |
required |
Returns:
| Type | Description |
|---|---|
FkTopology
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
get_skeleton_info(bvh: Bvh, include_partitions: bool = False) -> dict
¶
Get unified skeleton metadata dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bvh
|
Bvh
|
|
required |
include_partitions
|
bool
|
If True, include heuristic body-part partitions under
|
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Joint-space keys:
|
Notes
Which key pairs with which stream. A joint_pos stream (and
joint_rot) indexes with edges / lr_pairs /
joint_names; a node_pos stream indexes with node_edges /
node_lr_pairs / node_names. Mixing them addresses the wrong
vertices without any shape error. Note also that the rotation
layouts put the root at vertex 0, so edges needs the documented
off-by-one shift there — pack_to_ctv(streams=("joint_pos",))
(V = J) and streams=("node_pos",) (V = N) are the two
packings where the edge lists index packed vertices directly.
mismatched_end_site_pairs is empty on a well-formed rig; a
non-empty list means a train-time node-space mirror would leave
those pairs' end sites unswapped. See
:func:find_mismatched_end_site_pairs.
Everything here is JSON-serializable, because
:func:~pybvh_ml.preprocess_directory persists the whole dict.
get_body_partitions(bvh: Bvh) -> dict[str, list[int]]
¶
Heuristic body-part grouping by joint name patterns.
Groups joints by matching English keywords against joint names
("LeftForeArm" → left_arm), with the side read from a
Left/Right substring or an L/R prefix before an
uppercase letter. This is a guess from naming, not a fact read from
the skeleton: pybvh-ml has no anatomical model, and the alternative — a
partition supplied by whoever knows the rig — is always more reliable
where it exists. Rigs named in another language, or with opaque names
("joint12", "Bip01 L UpperArm" variants outside the keyword
lists), will be grouped wrongly or not at all.
other is the "no keyword matched" bucket and is the signal to check
before trusting the result: on a normal humanoid it should be small
(typically end-effector helpers and unnamed props). A large other,
or an empty left_arm / right_leg on a skeleton that visibly has
those limbs, means the naming convention was not recognized — pass your
own joint indices instead of this dict. There is no signal beyond that:
a joint that matched the wrong keyword lands in a named group and is
indistinguishable from a correct match.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bvh
|
Bvh
|
|
required |
Returns:
| Type | Description |
|---|---|
dict
|
Keys: |
See Also
get_lr_pairs : Left/right pairing, also name-derived, but detected by
pybvh and overridable per-skeleton via Bvh.lr_mapping.