File size: 6,834 Bytes
25e8396 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | # SPDX-FileCopyrightText: © 2026 Tenstorrent Inc.
# SPDX-License-Identifier: Apache-2.0
"""3-D joint MPJPE evaluation for the tt-nn port.
Two modes:
* **default** — torch CPU reference treated as GT. Reports the MPJPE
between tt-nn-predicted joints and reference-predicted joints in mm,
matching the convention used by InterHand2.6M / FreiHAND papers. The
number is the joint-space analogue of the regression-vector PCC reported
by ``scripts/eval.py``.
* **--gt-json /path/to/interhand_test.json** — when an InterHand2.6M-style
annotation file is available, treats those 3-D joints as GT and reports
the absolute MPJPE / PA-MPJPE per sample. The JSON is expected to look
like::
[
{"image": "...", "bbox": [x1, y1, x2, y2], "joints_3d_mm": [[...]],
"is_right": 1},
...
]
Run from the repo root:
python scripts/mpjpe.py \
--ckpt /path/to/_DATA/hamer_ckpts/checkpoints/hamer.ckpt \
--mean /path/to/_DATA/data/mano_mean_params.npz \
--mano /path/to/_DATA/data/mano/MANO_RIGHT.pkl \
--images /path/to/hamer/example_data
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
import numpy as np
import torch
_REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_REPO_ROOT))
from hamer import Hamer, load_hamer_checkpoint # noqa: E402
from hamer.preprocess import preprocess # noqa: E402
from hamer.mano import MANO # noqa: E402
def split_output(out: torch.Tensor):
rotmats = out[:, :144].reshape(-1, 16, 3, 3)
betas = out[:, 144:154]
cam = out[:, 154:157]
return rotmats, betas, cam
def root_relative(joints_mm: np.ndarray) -> np.ndarray:
"""Subtract the wrist joint (joint 0) — root-relative MPJPE matches the
reporting convention of InterHand2.6M and FreiHAND."""
return joints_mm - joints_mm[..., :1, :]
def mpjpe(pred: np.ndarray, gt: np.ndarray) -> float:
"""Mean per-joint position error in mm — both inputs ``(J, 3)``."""
return float(np.linalg.norm(pred - gt, axis=-1).mean())
def procrustes_align(pred: np.ndarray, gt: np.ndarray) -> np.ndarray:
"""Similarity-align ``pred`` onto ``gt`` (Procrustes / PA-MPJPE pre-step).
Returns the aligned ``pred``."""
mu_p, mu_g = pred.mean(0), gt.mean(0)
p, g = pred - mu_p, gt - mu_g
s_p = np.linalg.norm(p)
s_g = np.linalg.norm(g)
p_n = p / (s_p + 1e-12)
g_n = g / (s_g + 1e-12)
U, _, Vt = np.linalg.svd(p_n.T @ g_n)
R = (U @ Vt).T
if np.linalg.det(R) < 0:
Vt[-1] *= -1
R = (U @ Vt).T
s = (s_g / (s_p + 1e-12)) * np.trace((p_n.T @ g_n) @ R.T)
return s * (pred - mu_p) @ R.T + mu_g
def parse_args():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--ckpt", type=Path, required=True)
p.add_argument("--mean", type=Path, required=True)
p.add_argument("--mano", type=Path, required=True)
p.add_argument("--images", type=Path, required=True, help="dir of input images")
p.add_argument("--bboxes", type=Path, required=True,
help="JSON {image_name: [x1,y1,x2,y2]} for the synthetic-GT mode")
p.add_argument("--gt-json", type=Path, default=None,
help="Optional InterHand2.6M-style GT (image/bbox/joints_3d_mm). "
"Without this, the torch CPU reference is treated as GT.")
p.add_argument("--device-id", type=int, default=int(os.environ.get("HAMER_DEVICE", "0")))
return p.parse_args()
def load_gt_samples(gt_json: Path):
return json.loads(gt_json.read_text())
def collect_demo_samples(images_dir: Path, bboxes_path: Path):
"""Build a sample list from a directory + multi-hand bboxes JSON. GT
joints come from the torch CPU reference at run time (the
synthetic-GT mode)."""
bboxes_map = json.loads(bboxes_path.read_text())
out = []
for name, entries in bboxes_map.items():
p = images_dir / name
if not p.exists():
continue
if isinstance(entries, dict):
entries = [entries]
elif entries and not isinstance(entries[0], (dict, list)):
entries = [entries]
for i, e in enumerate(entries):
if isinstance(e, dict):
bbox, is_right = e["bbox"], int(e.get("is_right", 1))
else:
bbox, is_right = e, 1
out.append({
"image": str(p),
"bbox": list(bbox),
"is_right": is_right,
"joints_3d_mm": None,
"label": f"{name}#{i}",
})
return out
def main() -> int:
args = parse_args()
print("Loading reference + checkpoint…")
torch.manual_seed(0)
ref = Hamer(); ref.eval()
load_hamer_checkpoint(ref, args.ckpt, args.mean)
mano = MANO(args.mano)
os.environ["HAMER_USE_TT"] = "1"
os.environ["HAMER_FORCE_TT"] = "1"
from hamer import build_from_reference
print("Opening tt-nn device…")
tt_model = build_from_reference(ref, device_id=args.device_id)
samples = load_gt_samples(args.gt_json) if args.gt_json else collect_demo_samples(args.images, args.bboxes)
if not samples:
print("ERROR: no samples")
return 2
rows = []
for s in samples:
x, _ = preprocess(s["image"], bbox_xyxy=tuple(s["bbox"]), is_right=bool(s.get("is_right", 1)))
with torch.no_grad():
ref_out = ref(x)
tt_out = tt_model(x)
rot_ref, betas_ref, _ = split_output(ref_out)
rot_tt, betas_tt, _ = split_output(tt_out)
with torch.no_grad():
j_ref = mano.joints_21(rot_ref, betas_ref)[0].cpu().numpy() * 1000.0 # → mm
j_tt = mano.joints_21(rot_tt, betas_tt)[0].cpu().numpy() * 1000.0
gt = j_ref if s["joints_3d_mm"] is None else np.asarray(s["joints_3d_mm"])
gt_rr, ref_rr, tt_rr = root_relative(gt), root_relative(j_ref), root_relative(j_tt)
m_ref = mpjpe(ref_rr, gt_rr) # 0 in synthetic-GT mode
m_tt = mpjpe(tt_rr, gt_rr)
m_tt_pa = mpjpe(procrustes_align(tt_rr, gt_rr), gt_rr)
label = s.get("label") or Path(s["image"]).name
rows.append((label, m_ref, m_tt, m_tt_pa))
print()
print(f"{'image':<24} {'MPJPE(ref)':>11} {'MPJPE(tt)':>10} {'PA-MPJPE(tt)':>13} [mm]")
print("-" * 76)
for name, m_r, m_t, m_t_pa in rows:
print(f"{name:<24} {m_r:>10.3f} {m_t:>9.3f} {m_t_pa:>12.3f}")
print("-" * 76)
arr = np.array([r[1:] for r in rows])
print(f"{'mean':<24} {arr[:, 0].mean():>10.3f} {arr[:, 1].mean():>9.3f} {arr[:, 2].mean():>12.3f}")
if hasattr(tt_model, "close"):
tt_model.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
|