In [1]:
# Anny
# Copyright (C) 2025 NAVER Corp.
# Apache License, Version 2.0

Parameterizing shapes with Anny¶

Instantiate the model¶

Anny is shipped as a Python package that can be easily installed (see the README for details).

In [2]:
from IPython.display import Markdown, display
import torch
import roma  # A PyTorch library useful to deal with space transformations.
import anny  # The main library for the Anny model.
import trimesh  # For 3D mesh visualization.
import matplotlib.pyplot as plt
import anny.shape_distribution
import anny.anthropometry


# Instantiate the model, with all shape parameters available.
# Remark: the first instantiation may take a while. Latter calls will be faster thanks to caching.
anny_model = anny.Anny(phenotypes="all", local_changes="default")
# Use 32bit floating point precision on the CPU for this demo.
dtype = torch.float32
device = torch.device("cpu")
anny_model = anny_model.to(device=device, dtype=dtype)

# A simple transform to get a better view angle in 3D mesh visualizations.
trimesh_scene_transform = (
    roma.Rigid(
        linear=roma.euler_to_rotmat("x", [-90.0], degrees=True), translation=None
    )
    .to_homogeneous()
    .cpu()
    .numpy()
)
Warp 1.13.0 initialized:
   CUDA Toolkit 12.9, Driver 12.9
   Devices:
     "cpu"      : "x86_64"
     "cuda:0"   : "NVIDIA A100 80GB PCIe" (79 GiB, sm_80, mempool enabled)
   Kernel cache:
some_path

Template mesh¶

This is the template mesh of Anny:

In [3]:
display(
    Markdown(
        f"{anny_model.template_vertices.shape[0]} vertices -- "
        f"{anny_model.faces.shape[0]} faces composed of "
        f"{anny_model.faces.shape[1]} vertices each."
    )
)
trimesh.Trimesh(
    vertices=anny_model.template_vertices.cpu().numpy(),
    faces=anny_model.faces.cpu().numpy(),
).apply_transform(trimesh_scene_transform).show()

13718 vertices -- 27420 faces composed of 3 vertices each.

Out[3]:

Shape parameterization¶

Anny can model a diversity of morphologies. We follow MakeHuman terminology, and parameterize diversity of body shapes using a set of phenotype parameters, typically between 0 and 1.

Note: the values african, caucasian and asian parameters are normalized so that they sum to 1.

Word of caution regarding phenotypes: Phenotypes are based on preconceptions of artists regarding particular human traits. As a result, they encode by design stereotypes of MakeHuman artists, and one should not expect phenotype parameters to faithfully encode identity-related characteristics, such as gender, age or ethnicity.

In [4]:
# List phenotype parameters
Markdown(
    "**List of phenotype parameters**: "
    + ", ".join([f"{label}" for label in anny_model.phenotype_labels])
)
Out[4]:

List of phenotype parameters: gender, age, muscle, weight, height, proportions, cupsize, firmness, african, asian, caucasian

Example¶

Here we show an example of how the age parameter influences the resulting mesh.

In [5]:
batch_size = 5  # We can process multiple bodies at once in a batch.

phenotype_kwargs = {
    key: torch.full((batch_size,), fill_value=0.5, dtype=dtype, device=device)
    for key in anny_model.phenotype_labels
}
phenotype_kwargs["age"] = torch.linspace(
    0.0, 1.0, batch_size, dtype=dtype, device=device
)  # Example: vary the age parameter across the batch.
output = anny_model(phenotype_kwargs=phenotype_kwargs)

scene = trimesh.Scene()
for i in range(batch_size):
    # Create a mesh for each body in the batch.
    mesh = trimesh.Trimesh(
        vertices=output["vertices"][i].squeeze().cpu().numpy(),
        faces=anny_model.faces.cpu().numpy(),
    )
    transform = (
        roma.Rigid(
            linear=None,
            translation=torch.tensor([i * 1.0, 0.0, 0.0], dtype=dtype, device=device),
        )
        .to_homogeneous()
        .cpu()
        .numpy()
    )
    scene.add_geometry(mesh, transform=transform)

scene.apply_transform(
    trimesh_scene_transform
)  # Rotate the scene to have a better view.
scene.show()  # This will open a window to visualize the scene with all the bodies in it.
Module anny.skinning.warp_skinning ec1249d load on device 'cpu' took 3.09 ms  (cached)
Out[5]:

Local changes¶

Additionnally one specify some more local morphological changes. Local change parameters values are typically expected to be chosen between -1 and 1, but one can use values outside this range to extrapolate changes even further.

Note: it is easy to produce unrealistic meshes when using significant local changes.

In [6]:
display(
    Markdown(
        "**List of local changes parameters:** "
        + ", ".join(anny_model.local_change_labels)
    )
)

List of local changes parameters: l-lowerarm-scale-depth-incr, r-lowerarm-scale-depth-incr, l-upperarm-scale-horiz-incr, r-upperarm-scale-horiz-incr, l-upperarm-fat-incr, r-upperarm-fat-incr, measure-upperarm-length-incr, l-lowerarm-fat-incr, r-lowerarm-fat-incr, l-lowerarm-scale-horiz-incr, r-lowerarm-scale-horiz-incr, l-lowerarm-muscle-incr, r-lowerarm-muscle-incr, l-upperarm-shoulder-muscle-incr, r-upperarm-shoulder-muscle-incr, measure-upperarm-circ-incr, l-upperarm-scale-depth-incr, r-upperarm-scale-depth-incr, l-lowerarm-scale-vert-incr, r-lowerarm-scale-vert-incr, measure-lowerarm-length-incr, l-upperarm-muscle-incr, r-upperarm-muscle-incr, l-upperarm-scale-vert-incr, r-upperarm-scale-vert-incr, breast-point-incr, breast-dist-incr, breast-volume-vert-up, breast-trans-up, buttocks-volume-incr, l-cheek-trans-up, r-cheek-trans-up, l-cheek-volume-incr, r-cheek-volume-incr, l-cheek-inner-incr, r-cheek-inner-incr, l-cheek-bones-incr, r-cheek-bones-incr, chin-jaw-drop-incr, chin-height-incr, chin-prominent-incr, chin-bones-incr, chin-width-incr, chin-cleft-incr, chin-prognathism-incr, l-ear-lobe-incr, r-ear-lobe-incr, l-ear-shape-round, r-ear-shape-round, l-ear-trans-forward, r-ear-trans-forward, l-ear-rot-forward, r-ear-rot-forward, l-ear-trans-up, r-ear-trans-up, l-ear-scale-vert-incr, r-ear-scale-vert-incr, l-ear-flap-incr, r-ear-flap-incr, l-ear-shape-triangle, r-ear-shape-triangle, l-ear-scale-incr, r-ear-scale-incr, l-ear-scale-depth-incr, r-ear-scale-depth-incr, l-ear-wing-incr, r-ear-wing-incr, eyebrows-angle-up, eyebrows-trans-up, eyebrows-trans-forward, l-eye-corner1-up, r-eye-corner1-up, l-eye-height1-incr, r-eye-height1-incr, l-eye-scale-incr, r-eye-scale-incr, l-eye-push1-out, r-eye-push1-out, l-eye-eyefold-up, r-eye-eyefold-up, l-eye-height3-incr, r-eye-height3-incr, l-eye-height2-incr, r-eye-height2-incr, l-eye-bag-incr, r-eye-bag-incr, l-eye-push2-out, r-eye-push2-out, l-eye-trans-up, r-eye-trans-up, l-eye-bag-height-incr, r-eye-bag-height-incr, l-eye-epicanthus-out, r-eye-epicanthus-out, l-eye-eyefold-angle-up, r-eye-eyefold-angle-up, l-eye-bag-out, r-eye-bag-out, l-eye-corner2-up, r-eye-corner2-up, l-eye-trans-out, r-eye-trans-out, l-foot-scale-horiz-incr, r-foot-scale-horiz-incr, l-foot-trans-out, r-foot-trans-out, l-foot-scale-vert-incr, r-foot-scale-vert-incr, l-foot-trans-forward, r-foot-trans-forward, l-foot-scale-depth-incr, r-foot-scale-depth-incr, l-foot-scale-incr, r-foot-scale-incr, l-foot-trans-up, r-foot-trans-up, measure-ankle-circ-incr, forehead-temple-incr, forehead-trans-forward, forehead-scale-vert-incr, forehead-nubian-incr, l-hand-trans-out, r-hand-trans-out, l-hand-fingers-length-incr, r-hand-fingers-length-incr, l-hand-scale-incr, r-hand-scale-incr, l-hand-fingers-diameter-incr, r-hand-fingers-diameter-incr, l-hand-fingers-distance-incr, r-hand-fingers-distance-incr, measure-wrist-circ-incr, head-angle-out, head-scale-vert-incr, head-trans-forward, head-trans-up, head-fat-incr, head-trans-out, head-back-scale-depth-incr, head-age-incr, head-scale-depth-incr, head-scale-horiz-incr, hip-trans-out, hip-scale-horiz-incr, hip-scale-depth-incr, hip-waist-up, hip-trans-forward, hip-scale-vert-incr, hip-trans-up, measure-calf-circ-incr, measure-lowerleg-height-incr, l-upperleg-fat-incr, r-upperleg-fat-incr, l-lowerleg-muscle-incr, r-lowerleg-muscle-incr, l-leg-valgus-incr, r-leg-valgus-incr, l-lowerleg-scale-depth-incr, r-lowerleg-scale-depth-incr, l-lowerleg-scale-vert-incr, r-lowerleg-scale-vert-incr, l-upperleg-scale-depth-incr, r-upperleg-scale-depth-incr, l-upperleg-scale-horiz-incr, r-upperleg-scale-horiz-incr, upperlegs-height-incr, l-lowerleg-fat-incr, r-lowerleg-fat-incr, l-lowerleg-scale-horiz-incr, r-lowerleg-scale-horiz-incr, lowerlegs-height-incr, measure-upperleg-height-incr, l-upperleg-muscle-incr, r-upperleg-muscle-incr, measure-knee-circ-incr, measure-thigh-circ-incr, l-upperleg-scale-vert-incr, r-upperleg-scale-vert-incr, mouth-lowerlip-height-incr, mouth-scale-horiz-incr, mouth-cupidsbow-incr, mouth-laugh-lines-out, mouth-scale-depth-incr, mouth-lowerlip-volume-incr, mouth-angles-up, mouth-upperlip-ext-up, mouth-lowerlip-width-incr, mouth-trans-forward, mouth-upperlip-width-incr, mouth-scale-vert-incr, mouth-dimples-out, mouth-upperlip-height-incr, mouth-upperlip-volume-incr, mouth-cupidsbow-width-incr, mouth-lowerlip-ext-up, mouth-upperlip-middle-up, mouth-trans-up, mouth-trans-out, mouth-lowerlip-middle-up, mouth-philtrum-volume-incr, measure-neck-circ-incr, measure-neck-height-incr, neck-scale-depth-incr, neck-trans-forward, neck-double-incr, neck-trans-out, neck-back-scale-depth-incr, neck-scale-horiz-incr, neck-scale-vert-incr, neck-trans-up, nose-nostrils-angle-up, nose-flaring-incr, nose-curve-convex, nose-scale-depth-incr, nose-scale-vert-incr, nose-point-width-incr, nose-scale-horiz-incr, nose-width1-incr, nose-trans-forward, nose-trans-up, nose-hump-incr, nose-nostrils-width-incr, nose-trans-out, nose-greek-incr, nose-point-up, nose-width2-incr, nose-width3-incr, nose-septumangle-incr, nose-base-up, nose-compression-uncompress, nose-volume-incr, bulge-incr, pelvis-tone-incr, stomach-pregnant-incr, stomach-navel-out, stomach-navel-up, stomach-tone-incr, torso-trans-forward, torso-muscle-pectoral-incr, measure-underbust-circ-incr, torso-scale-horiz-incr, torso-scale-depth-incr, measure-napetowaist-dist-incr, measure-waisttohip-dist-incr, measure-shoulder-dist-incr, measure-hips-circ-incr, torso-vshape-incr, torso-trans-up, torso-muscle-dorsi-incr, measure-waist-circ-incr, measure-frontchest-dist-incr, torso-scale-vert-incr, measure-bust-circ-incr, torso-trans-out

Local change example¶

We show here the effect of the stomach-pregnant-incr local change parameter, as an example.

In [7]:
batch_size = 3  # We can process multiple faces at once in a batch.
pose_parameters = roma.Rigid.identity(
    dim=3, batch_shape=(batch_size, anny_model.bone_count)
).to_homogeneous()
phenotype_kwargs = {
    key: torch.full((batch_size,), fill_value=0.5)
    for key in anny_model.phenotype_labels
}
# In this example, we start from a stereotypical young adult woman mesh
phenotype_kwargs["age"].fill_(0.67)
phenotype_kwargs["gender"].fill_(1.0)

local_changes = {
    "stomach-pregnant-incr": torch.linspace(0, 1.0, batch_size)
}  # Example: vary the upperarm fat increment across the batch.
output = anny_model(
    phenotype_kwargs=phenotype_kwargs, local_changes_kwargs=local_changes
)

scene = trimesh.Scene()
for i in range(batch_size):
    # Create a mesh for each face in the batch.
    mesh = trimesh.Trimesh(
        vertices=output["vertices"][i].squeeze().cpu().numpy(),
        faces=anny_model.faces.cpu().numpy(),
    )
    transform = (
        roma.Rigid(linear=None, translation=torch.tensor([i * 1.0, 0.0, 0.0]))
        .to_homogeneous()
        .cpu()
        .numpy()
    )
    scene.add_geometry(mesh, transform=transform)
scene.apply_transform(
    trimesh_scene_transform
)  # Rotate the scene to have a better view.
scene.show()  # This will open a window to visualize the scene with all the faces in
Out[7]:

Facial actions¶

Full-body and head models can optionally expose facial actions. The dictionary form is convenient for sparse edits, while tensor input is convenient for batched optimization.

In [8]:
face_model = anny.Anny(rig="anny", topology="head", facial_actions="all").to(
    device=device, dtype=dtype
)

# Using with dict unit -> tensor input
facial_actions = {
    "jawOpen": torch.tensor([0.0, 0.6], dtype=dtype, device=device),
    "mouthSmileLeft": torch.tensor([0.0, 0.4], dtype=dtype, device=device),
    "mouthSmileRight": torch.tensor([0.0, 0.4], dtype=dtype, device=device),
}
dict_output = face_model(facial_actions=facial_actions)

# Using with batched Bx52 tensor input
batch_size = 5
values = torch.zeros(
    (batch_size, len(face_model.facial_action_labels)), dtype=dtype, device=device
)
values[:, face_model.facial_action_labels.index("jawOpen")] = torch.linspace(
    0.0, 1.0, batch_size, dtype=dtype, device=device
)
values[:, face_model.facial_action_labels.index("mouthSmileLeft")] = torch.linspace(
    0.0, 1.0, batch_size, dtype=dtype, device=device
)
values[:, face_model.facial_action_labels.index("mouthSmileRight")] = torch.linspace(
    0.0, 1.0, batch_size, dtype=dtype, device=device
)
output = face_model(facial_actions=values)

display(Markdown("**Face unit labels:** " + ", ".join(face_model.facial_action_labels)))

scene = trimesh.Scene()
for i in range(batch_size):
    # Create a mesh for each face in the batch.
    mesh = trimesh.Trimesh(
        vertices=output["vertices"][i].squeeze().cpu().numpy(),
        faces=face_model.faces.cpu().numpy(),
    )
    transform = (
        roma.Rigid(
            linear=roma.euler_to_rotmat("x", [90.0], degrees=True),
            translation=torch.tensor([i * 0.2, 0.0, 0.0]),
        )
        .to_homogeneous()
        .cpu()
        .numpy()
    )
    scene.add_geometry(mesh, transform=transform)
scene.apply_transform(
    trimesh_scene_transform
)  # Rotate the scene to have a better view.
scene.show()  # This will open a window to visualize the scene with all the faces in
Module anny.skinning.warp_skinning 7661868 load on device 'cpu' took 3.13 ms  (cached)

Face unit labels: browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight, cheekPuff, cheekSquintLeft, cheekSquintRight, eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight, eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight, eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight, eyeWideLeft, eyeWideRight, jawForward, jawLeft, jawOpen, jawRight, mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight, mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight, mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower, mouthRollUpper, mouthShrugLower, mouthShrugUpper, mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight, mouthUpperUpLeft, mouthUpperUpRight, noseSneerLeft, noseSneerRight, tongueOut

Out[8]:

Phenotype distribution¶

In [9]:
phenotype_distribution = anny.shape_distribution.SimpleShapeDistribution(
    anny_model,
    morphological_age_distribution=torch.distributions.Uniform(low=0.0, high=60.0),
)

real_age, phenotype_kwargs = phenotype_distribution.sample(batch_size=200)
output = anny_model(phenotype_kwargs=phenotype_kwargs)

scene = trimesh.Scene()
i = -1
for u in range(4):
    for v in range(5):
        i += 1
        assert i < output["vertices"].shape[0], "Batch size is too small for the grid."
        # Create a mesh for each face in the batch.
        mesh = trimesh.Trimesh(
            vertices=output["vertices"][i].squeeze().cpu().numpy(),
            faces=anny_model.faces.cpu().numpy(),
        )
        transform = (
            roma.Rigid(
                linear=None,
                translation=torch.tensor(
                    [v * 1.0, u * 1.0, 0.0], dtype=dtype, device=device
                ),
            )
            .to_homogeneous()
            .cpu()
            .numpy()
        )
        scene.add_geometry(mesh, transform=transform)
scene.apply_transform(
    trimesh_scene_transform
)  # Rotate the scene to have a better view.
scene.show()  # This will open a window to visualize the scene with all the faces in
Module anny.skinning.warp_skinning ec1249d load on device 'cpu' took 1.72 ms  (cached)
Out[9]:

Body measures¶

We additionally provide a class to estimate some anthropometric measurements, assuming a body buoyancy of .98 in water.

In [10]:
real_age, phenotype_kwargs = phenotype_distribution.sample(batch_size=1000)
output = anny_model(phenotype_kwargs=phenotype_kwargs)

measurements = anny.anthropometry.Anthropometry(anny_model)
measures = measurements(output["rest_vertices"])

fig, axes = plt.subplots(1, 3, squeeze=True, figsize=(10, 5))
axes[0].scatter(real_age.cpu().numpy(), measures["height"].cpu().numpy())
axes[0].set_xlabel("Morphological age (year)")
axes[0].set_ylabel("Height (m)")
axes[1].scatter(real_age.cpu().numpy(), measures["waist_circumference"].cpu().numpy())
axes[1].set_xlabel("Morphological age (year)")
axes[1].set_ylabel("Waist circumference (m)")
axes[2].scatter(real_age.cpu().numpy(), measures["bmi"].cpu().numpy())
axes[2].set_xlabel("Morphological age (year)")
axes[2].set_ylabel("Body Mass Index estimate")
fig.tight_layout()
plt.show()
No description has been provided for this image