Metadata-Version: 2.4
Name: clad-body
Version: 0.4.4
Summary: Anny/MHR body loaders and ISO 8559-1 body measurements
Project-URL: Homepage, https://github.com/datar-psa/clad-body
Project-URL: Repository, https://github.com/datar-psa/clad-body
Author-email: Arkadiusz Kraus <arkadius@datar.app>, Arkadiusz Noster <arek@datar.app>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: anny,anthropometry,body,iso-8559,measurement,mhr
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.12
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.15.3
Requires-Dist: trimesh>=4.11.2
Provides-Extra: anny
Requires-Dist: anny>=0.3.1; extra == 'anny'
Requires-Dist: pillow>=9.0; extra == 'anny'
Requires-Dist: roma>=0.0.2; extra == 'anny'
Requires-Dist: torch>=2.0; extra == 'anny'
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: mhr
Requires-Dist: mhr>=1.0.0; extra == 'mhr'
Requires-Dist: pymomentum-cpu>=0.1.108; extra == 'mhr'
Provides-Extra: render
Requires-Dist: matplotlib>=3.7; extra == 'render'
Requires-Dist: pyopengl>=3.1.0; extra == 'render'
Requires-Dist: pyrender>=0.1.45; extra == 'render'
Description-Content-Type: text/markdown

# clad-body

ISO 8559-1 body measurements for [Anny](https://github.com/naver/anny) and [MHR](https://github.com/facebookresearch/MHR) parametric body models. Eleven keys are differentiable through PyTorch autograd for gradient-based body fitting.

Anny and MHR give you a 14–18K vertex mesh and nothing to measure it with. SMPL tooling doesn't port over, and the plane-sweep algorithms look simple until you hit convex-hull tape simulation, contour-fragment merging, and ISO-compliant landmark detection for bust/hip/crotch. `clad-body` is that work, done once — 25 anthropometric measurements over circumferences, lengths, and body composition (volume, mass, BMI, body fat), calibrated against real scan data. It's used in production at [Clad](https://clad.you) for size-aware virtual try-on.

  Anny body rotating with ISO 8559-1 circumference measurement contours
  
  Anny body — male average, 4-view with ISO 8559-1 circumference measurements

All bodies are normalised to the same coordinate convention: Z-up, metres, XY-centred, feet at Z=0, +Y=front.

## Install

```bash
pip install clad-body

# With Anny body loader (requires torch)
pip install 'clad-body[anny]'

# With MHR body loader (requires pymomentum)
pip install 'clad-body[mhr]'

# With 4-view rendering
pip install 'clad-body[render]'
```

## Quick start

```python
from clad_body.load import load_anny_from_params
from clad_body.measure import measure

body = load_anny_from_params(params)

m = measure(body)                                  # all measurements
m = measure(body, preset="core")                   # 4: height, bust, waist, hip
m = measure(body, preset="standard")               # 9: + thigh, upperarm, shoulder, sleeve, inseam
m = measure(body, preset="tops")                   # garment-relevant subset
m = measure(body, only=["bust_cm", "hip_cm"])       # specific keys
m = measure(body, tags={"type": "circumference", "region": "leg"})  # tag filter
m = measure(body, render_path="body.png")          # with 4-view render
```

MHR works the same way:

```python
from clad_body.load import load_mhr_from_params
body = load_mhr_from_params("path/to/sam3d_params.json")
m = measure(body)
```

### Differentiable path — `measure_grad` (Anny only, experimental)

> **Under active development.** API surface and supported keys may change between minor versions. Eleven keys are differentiable today; more will follow.

For autograd-based optimization of the body mesh, use `measure_grad(body)` instead of `measure(body)`. Same input, same key names — but the returned values are PyTorch tensors with autograd history, so you can put them directly into a loss and backprop into the Anny phenotype parameters.

Pass `requires_grad=True` to `load_anny_from_params` to create the body with gradient-enabled phenotype tensors (stored on `body.phenotype_kwargs`):

```python
import torch
from clad_body.load import load_anny_from_params
from clad_body.measure import measure_grad

body = load_anny_from_params(initial_params, requires_grad=True)
optimizer = torch.optim.Adam(list(body.phenotype_kwargs.values()), lr=0.01)

for step in range(500):
    optimizer.zero_grad()
    m = measure_grad(body, only=["bust_cm", "waist_cm", "inseam_cm"])
    loss = (m["bust_cm"] - 92.0) ** 2 + (m["waist_cm"] - 78.0) ** 2 + (m["inseam_cm"] - 82.0) ** 2
    loss.backward()
    optimizer.step()
```

Each `measure_grad(body)` call re-runs the forward pass using `body.phenotype_kwargs`, so after `optimizer.step()` updates the tensors the next iteration measures the new mesh. If you only want to optimize a subset of parameters, load without `requires_grad=True` and enable it per-tensor: `body.phenotype_kwargs["height"].requires_grad_(True)`.

Supported keys and their calibration error vs the ISO reference that `measure()` uses:

| Key | Error vs ISO |
|---|---|
| `height_cm`, `waist_cm` | exact (same loop / extent) |
| `bust_cm` | MAE 0.06 cm, max 0.18 cm |
| `underbust_cm` | MAE 0.39 cm, max 1.61 cm |
| `hip_cm` | MAE 0.46 cm, max 1.39 cm |
| `stomach_cm` | MAE 0.93 cm, P95 2.83 cm, max 3.61 cm (soft-argmin picks a different Z than the reference's 2 mm band scan; residual is inherent Z-choice noise) |
| `inseam_cm` | RMS 0.06 cm, max 0.10 cm |
| `sleeve_length_cm` | RMS 0.33 cm, max 0.55 cm |
| `upperarm_cm` | ≤ 1 cm |
| `mass_kg` | ≤ 3 kg |
| `thigh_cm` | **broken — gradient direction only** (vertex loop under-reports by 3–6 cm; use `measure()` for reporting) |

#### Circumference = convex hull perimeter, not contour perimeter

Both `measure()` and `measure_grad` report **convex hull circumference**, not the raw cross-section perimeter. This matches ISO 8559-1: a real measuring tape bridges across concavities (e.g., cleavage between breasts, armpit crease) rather than dipping into them. The convex hull perimeter is always ≤ the raw contour perimeter — the difference is most visible at the bust on larger cup sizes where the cleavage concavity can shorten the measurement by 1-3 cm compared to following the actual surface.

`measure_grad` builds a 72-point polygon via differentiable soft edge-plane intersection, then takes its `scipy.spatial.ConvexHull` perimeter. This is used for bust, underbust, hip, and stomach — each at its respective anatomical height. Every `measure_grad()` call recomputes the hull from scratch (new forward pass → new polygon → new hull). The hull decides *which* polygon vertices to keep (discrete, like `argmax` or `tensor[mask]`) — but the perimeter of those vertices is a plain sum of `torch.linalg.norm` over their positions, so `loss.backward()` flows gradients through the kept vertices back to the Anny phenotype params. Dropped vertices (inside the hull, e.g., cleavage or gluteal cleft bins) get zero gradient — correct, since they don't affect the tape measure. The hull indices can change between optimization steps if the body shape changes enough, but the perimeter value is continuous at those transitions so the loss doesn't jump. See `clad_body/measure/_soft_circ.py`.

For `stomach_cm` the cutting-plane height is itself differentiable: a soft-argmin over torso vertex Y (most anterior = most negative) in the belly Z-range picks the height of maximum anterior protrusion, and one `soft_circumference` is taken there. See `measure_stomach_soft` in the same file. The Z selection can drift by 1–2 cm from the reference's 2 mm band-scan argmax on bodies where multiple vertex clusters have near-identical anterior Y — which translates to ~3 cm of circumference error because the body tapers rapidly in the belly region. This is why `stomach_cm` has looser tolerance than the other soft-circ keys.

Strictly speaking, `stomach_cm` is differentiable almost everywhere, with numerically-tiny (~10⁻⁵ cm) step discontinuities where a vertex crosses the hard Z-mask boundary that gates out feet and bust from the belly soft-argmin.

Requesting any other key raises `ValueError`. There is no silent numpy fallback — it would break gradient flow without warning. For non-differentiable keys use `measure()`.

## Public API

| Import | What |
|---|---|
| `clad_body.load.load_anny_from_params` | Load Anny body from phenotype params |
| `clad_body.load.load_mhr_from_params` | Load MHR body from SAM 3D Body params |
| `clad_body.load.AnnyBody`, `MhrBody` | Body dataclasses |
| `clad_body.measure.measure` | Measure a body (numpy reporting path, ISO 8559-1) |
| `clad_body.measure.measure_grad` | Differentiable measurements for autograd loops (Anny only) |
| `clad_body.measure.REGISTRY` | All measurement definitions (`dict[str, MeasurementDef]`) |
| `clad_body.measure.list_measurements` | Query measurements by tags |
| `clad_body.measure.MeasurementDef` | Measurement definition type |

### Selection

`measure()` accepts `preset`, `only`, `tags`, `exclude`. Precedence: `only` > `preset` > `tags` > default (`"all"`). `exclude` is applied last. Only runs computation groups needed for the requested keys.

### Introspection

```python
from clad_body.measure import REGISTRY, list_measurements

REGISTRY["bust_cm"].description   # self-measurement instructions
REGISTRY["bust_cm"].iso_ref       # "5.3.4"
REGISTRY["bust_cm"].type          # "circumference"

list_measurements(type="circumference", region="leg")   # [thigh, knee, calf]
```

## Measurement registry

Every measurement is tagged across 5 dimensions. Each carries a human-readable `description` for self-measurement instructions and i18n key mapping.

### Tags

| Dimension | Values |
|---|---|
| **type** | `circumference`, `length`, `scalar` |
| **standard** | `iso` (ISO 8559-1), `tailor` (industry standard), `derived` (computed) |
| **region** | `neck`, `torso`, `abdomen`, `arm`, `leg`, `full_body` |
| **tier** | `core` > `standard` > `enhanced` > `fitted` (cumulative) |
| **garments** | `tops`, `bottoms`, `dresses`, `outerwear`, `underwear` |

### Tier presets

| Preset | Count | Adds |
|---|---|---|
| `core` | 4 | height, bust, waist, hip |
| `standard` | 9 | thigh, upperarm, shoulder_width, sleeve_length, inseam |
| `enhanced` | 18 | neck, underbust, stomach, mass, volume, bmi, body_fat, belly_depth, back_neck_to_waist |
| `fitted`/`all` | 25 | knee, calf, wrist, crotch_length, front_rise, back_rise, shirt_length |

### Full measurement table

Garment codes: **T**ops, **B**ottoms, **D**resses, **O**uterwear, **U**nderwear.

| Contour | Key | Description | ISO | Type | Std | Region | Tier | Grp | Gar |
|---|---|---|---|---|---|---|---|---|---|
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/bust.png) | `height_cm` | Vertical distance from floor to top of head. Stand erect, feet together. | 5.1.1 | scalar | iso | full_body | core | A | all |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/bust.png) | `bust_cm` | Horizontal circumference at the fullest part of the chest/bust. Tape under armpits, across bust prominence, level and snug. | 5.3.4 | circ | iso | torso | core | A | T,D,O,U |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/waist.png) | `waist_cm` | Horizontal circumference at natural waist, midway between lowest rib and hip bone. Tape at navel height, parallel to floor. | 5.3.10 | circ | iso | torso | core | A | all |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/hip.png) | `hip_cm` | Horizontal circumference at greatest buttock prominence. Feet together, tape around widest part of hips. | 5.3.13 | circ | iso | abdomen | core | A | B,D,O,U |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/thigh.png) | `thigh_cm` | Horizontal circumference at fullest part of upper thigh, just below gluteal fold. Stand with legs slightly apart. | 5.3.20 | circ | iso | leg | std | B | B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/upperarm.png) | `upperarm_cm` | Circumference at fullest part of upper arm, midway between shoulder and elbow. Arm relaxed, not flexed. | 5.3.16 | circ | iso | arm | std | B | T,O |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/shoulder_width.png) | `shoulder_width_cm` | Distance between left and right shoulder points (acromion), measured across back over C7 vertebra. | 5.4.2 | length | iso | torso | std | C | T,D,O |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/sleeve_length.png) | `sleeve_length_cm` | Distance from shoulder point along outside of slightly bent arm, over elbow, to wrist bone. (ISO §5.4.14 + §5.4.15 outer arm length, computed via plane-slice surface walk on rest pose; differentiable runtime is bone chain + linear correction.) | 5.7.8 | length | iso | arm | std | C | T,O |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/inseam.png) | `inseam_cm` | Distance from crotch point straight down to floor. Stand erect, feet slightly apart. | 5.1.15 | length | iso | leg | std | E | B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/neck.png) | `neck_cm` | Circumference just below Adam's apple, perpendicular to neck axis. Comfortably snug. | 5.3.2 | circ | iso | neck | enh | D | T |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/underbust.png) | `underbust_cm` | Horizontal circumference directly below breast tissue, at inframammary crease. Bra band size. | 5.3.6 | circ | iso | torso | enh | A | T,D,U |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/stomach.png) | `stomach_cm` | Horizontal circumference at maximum anterior protrusion of abdomen, usually at/below navel. | -- | circ | tailor | abdomen | enh | A | T,B |
| | `mass_kg` | Total body mass in kilograms. | 5.6.1 | scalar | iso | full_body | enh | G | -- |
| | `volume_m3` | Total body volume in cubic metres, from mesh geometry. | -- | scalar | derived | full_body | enh | G | -- |
| | `bmi` | Body mass index: mass (kg) / height (m)^2. | -- | scalar | derived | full_body | enh | G | -- |
| | `body_fat_pct` | Estimated body fat % via Navy/Weltman equations from circumferences. | -- | scalar | derived | full_body | enh | G | -- |
| | `belly_depth_cm` | How much belly protrudes forward vs underbust/ribcage. Negative = belly prominence. | -- | scalar | derived | abdomen | enh | A | T,B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/knee.png) | `knee_cm` | Horizontal circumference at centre of kneecap. Bend knee slightly (~45 degrees). | 5.3.22 | circ | iso | leg | fit | B | B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/calf.png) | `calf_cm` | Maximum horizontal circumference of the calf. Stand with legs slightly apart. | 5.3.24 | circ | iso | leg | fit | B | B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/wrist.png) | `wrist_cm` | Circumference at wrist, at prominent bone on little finger side (ulnar styloid). | 5.3.19 | circ | iso | arm | fit | D | T |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/crotch.png) | `crotch_length_cm` | Distance from front waist centre, through crotch, to back waist centre. Follow body surface. | 5.4.18 | length | iso | leg | fit | E | B |
| | `front_rise_cm` | Front waist to crotch point, along front body surface. Trouser front panel length. | -- | length | tailor | leg | fit | E | B |
| | `back_rise_cm` | Back waist to crotch point, along back body surface. Trouser back panel length. | -- | length | tailor | leg | fit | E | B |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/shirt_length.png) | `shirt_length_cm` | Side neck point down along front body contour to crotch level. Follow chest/stomach curve. | -- | length | tailor | torso | fit | F | T |
| ![](https://raw.githubusercontent.com/datar-psa/clad-body/main/assets/contours/back_neck_to_waist.png) | `back_neck_to_waist_cm` | Cervicale (C7) down centre back along body contour to waist level. Tape follows spine curvature. | 5.4.5 | length | iso | torso | enh | H | T,D,O |

Tier codes: **core**, **std** (standard), **enh** (enhanced), **fit** (fitted). Anny-only: underbust, mass, volume, bmi, body_fat, belly_depth.

### Computation groups

| Group | Measurements | Cost | Deps |
|---|---|---|---|
| **A** Core torso | height, bust, waist, hip, stomach, underbust, belly_depth | Cheap | -- |
| **B** Limb sweeps | thigh, knee, calf, upperarm | Expensive | -- |
| **C** Joint linear | shoulder_width, sleeve_length (ISO surface walk on re-posed body) | Very expensive | -- |
| **D** Perpendicular | neck, wrist | Medium | -- |
| **E** Mesh geometry | inseam (mesh sweep), crotch_length, front_rise, back_rise | Medium | -- |
| **F** Surface trace | shirt_length | Medium | E |
| **G** Body composition | volume, mass, bmi, body_fat | Cheap | D |
| **H** Back length | back_neck_to_waist | Cheap | A |

Group C and E have differentiable alternatives in [`measure_grad`](#differentiable-path--measure_grad-anny-only-experimental) — use it for hot-loop optimization instead of calling `measure()` repeatedly.

## Performance

`measure()` only runs the computation groups needed for the requested keys — use `only=` or `preset=` to skip expensive groups:

```python
measure(body)                         # all groups — ~800 ms
measure(body, preset="core")          # group A only — ~100 ms
measure(body, only=["bust_cm"])       # group A only — ~100 ms
measure(body, only=["shoulder_width_cm"])  # groups A + C — ~200 ms
```

### GPU acceleration

`measure()` accepts a `device` parameter (`None` = auto-detect CUDA):

```python
measure(body, only=["bust_cm"], device="cuda")  # GPU forward pass
measure(body, device=None)                       # auto: CUDA if available
```

## Optional extras

| Extra | What it enables |
|---|---|
| `[anny]` | Anny body loader (requires torch) |
| `[mhr]` | MHR body loader (requires pymomentum) |
| `[render]` | 4-view body renders (requires matplotlib, pyrender) |

Without extras, only numpy, scipy, and trimesh are required.

## Demo

Try the full pipeline at [clad.you/size-aware/size-me](https://clad.you/size-aware/size-me).

## Background

This library was built for [Clad](https://clad.you)'s size-aware virtual try-on pipeline. Read the full story: [A 3D Body Scan for Nine Cents — Without SMPL](https://clad.you/blog/posts/body-pipeline/).

## License

Apache 2.0 — see [LICENSE](LICENSE).
