All spatial outputs use metres, all angle tensors use radians, and quaternions use (w, x, y, z).
Match the hand side, device and dtype of the MANO and anatomical layers.
MANO articulation contains 15 local joint rotations plus the global wrist orientation. The anatomical basis labels three directions at each joint: back/twist, up/spread, and left/bend. These are the columns of its rotation matrix.
AxisAdaptiveLayer derives those axes from the supplied posed joints and transforms. AxisLayerFK instead builds
fixed reference bases from a flat, mean-shape hand during initialization, then expresses the supplied joint rotations
relative to them. The latter is the basis used for the Euler loss and hand composition. It is independent of the pose
and shape being evaluated, and it does not adapt the reference basis to each predicted shape.
import torch
from manotorch.manolayer import ManoLayer
from manotorch.axislayer import AxisLayerFK
side = "left"
mano = ManoLayer(side=side, use_pca=False, flat_hand_mean=True)
axis = AxisLayerFK(side=side)
out = mano(torch.zeros(2, 48))
anatomical_transforms, anatomical_rotations, angles = axis(out.transforms_abs)
# Shapes: (2, 16, 4, 4), (2, 16, 3, 3), (2, 16, 3)
# angles[..., 0:3] = twist, spread, bend, in radiansTranslation and center_idx change the positions in anatomical_transforms, not the extracted articulation angles.
For the left hand, the returned local rotation matrices and Euler angles follow the right-hand anatomical convention.
Mirrored poses give equal articulation angles. The global transforms still describe the actual left hand.
The anatomical utilities use the 16 MANO joints, whose order differs from the 21 output joints:
| MANO slots | Joint chain |
|---|---|
| 0 | wrist |
| 1, 2, 3 | index MCP, PIP, DIP |
| 4, 5, 6 | middle MCP, PIP, DIP |
| 7, 8, 9 | little MCP, PIP, DIP |
| 10, 11, 12 | ring MCP, PIP, DIP |
| 13, 14, 15 | thumb CMC, MCP, final hinge |
The 21-joint ManoLayer.joints output orders wrist, thumb, index, middle, ring, little, with four slots per finger
including the tip. Do not index an angle tensor with those 21-joint slot numbers.
AnatomyConstraintLossEE penalizes Euler angles outside configured intervals. For lower and upper bounds l, u,
the penalty is relu(l - angle) + relu(angle - u), with angles within 1e-6 radians of zero suppressed. The three
axis penalties are added per joint; the wrist is excluded. This is a pose prior, not a collision or contact loss.
from manotorch.anatomy_loss import AnatomyConstraintLossEE
loss = AnatomyConstraintLossEE(reduction="mean")
loss.setup(finger_mcp=("+-:0", "+-:5", "+:90,-:0"))
value = loss(angles)Configuration strings use degrees: "+-:45" allows [-45, 45]; "+:45,-:15" allows [-15, 45].
Each joint configuration contains three entries in twist/spread/bend order. All tolerances must be finite and
nonnegative. The defaults are:
| Joint type | Twist | Spread | Bend |
|---|---|---|---|
| Thumb CMC | ±45° | −15° to +45° | 0° to +45° |
| Thumb MCP | 0° | ±10° | 0° to +90° |
| Thumb final hinge | 0° | 0° | 0° to +90° |
| Finger MCP | 0° | ±5° | 0° to +90° |
| Finger PIP / DIP | 0° | 0° | 0° to +90° |
reduction="none" returns (B, 15) in this order: index/middle/ring/little MCPs, those fingers' PIPs, their DIPs,
then the three thumb joints. mean averages these 15 joint penalties over the batch; sum sums them.
setup() copies the supplied configurations. Public configuration lists remain editable; an edit rebuilds the
limits on the next call. Configure the loss before compiling or capturing CUDA graphs. Limits and indices follow
.to(device); for historical callers they also move to the angle device on the first call. Device/dtype/configuration
changes are initialization work, and steady-state calls have no host-device synchronization. Prefer warming up the
entire pipeline before performance measurements.
Euler extraction uses intrinsic XYZ: R = Rx(twist) @ Ry(spread) @ Rz(bend). At gimbal lock the inverse cannot be
smooth or uniquely recover the original angles. The conversion sets the last angle to zero and chooses an equivalent
rotation. Avoid interpreting a derivative through the singularity as a physical anatomical gradient.
AxisLayerFK.compose(angles) maps (B, 16, 3) anatomical Euler angles to (B, 16, 3) MANO axis-angles. It preserves
the input tensor, uses the same angle convention for both hands, and returns articulation with a zero global rotation.
The wrist angle slot does not carry a recoverable global orientation. Retain and apply that orientation separately.
angles = torch.zeros(2, 16, 3)
angles[:, 1, 2] = torch.pi / 3 # index MCP bend
pose = axis.compose(angles).reshape(2, 48)
pose[:, :3] = global_axis_angle # caller's (2, 3) wrist orientation
out = mano(pose, betas, transl)This example requires use_pca=False, flat_hand_mean=True. A PCA layer expects PCA coefficients; a layer with
flat_hand_mean=False adds the MANO mean articulation to its input. Directly feeding the composed absolute pose
to either setting changes the pose interpretation. Rotation round-trips should compare matrices, since axis-angle
and Euler representatives are not unique.
Older left-hand AxisLayerFK.forward() returned different twist/spread signs. When consuming old stored angles,
convert those two signs or regenerate the angles from the original transforms. Preserve the dataset's shapedirs
choice independently: fitted official-model data usually needs the default fix_left_shapedirs=False, whereas
pipelines fitted with a corrected basis must keep that correction. Apply it exactly once.
AnchorLayer returns 32 anchors from (B, 778, 3) vertices. The package supplies its triangle indices, barycentric
weights and region mapping, so construction does not depend on the working directory.
from manotorch.anchorlayer import AnchorLayer
anchors = AnchorLayer()(out.verts) # (B, 32, 3), same coordinate system and units as out.vertsFor a triangle (v0, v1, v2) and the two stored weights, the anchor is
v0 + w1 * (v1 - v0) + w2 * (v2 - v0). This is differentiable in the mesh vertices. Translate, center or rotate the
mesh before interpolating to obtain anchors in the corresponding frame. Move the anchor layer with the mesh device.
The vertex topology must remain the MANO topology; joints_only=True supplies no mesh and cannot be used here.
Pass anchor_root="..." to use custom definitions. Custom mapping pickles are trusted local resources, unlike the
restricted MANO model loader. Inspect anchor_mapping and merged_vertex_assignment for contact-region bookkeeping.