File size: 6,482 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 | # SPDX-FileCopyrightText: © 2026 Tenstorrent Inc.
# SPDX-License-Identifier: Apache-2.0
"""Render every annotated hand's predicted MANO mesh on the original image.
Bbox annotations are a JSON of the form::
{
"image1.jpg": [
{"bbox": [x1, y1, x2, y2], "is_right": 1},
{"bbox": [x1, y1, x2, y2], "is_right": 0}
],
...
}
Mirrors the upstream HaMeR demo:
* Each hand bbox is processed independently (HaMeR is single-hand).
* For ``is_right=0`` (left hand) the crop is mirrored horizontally
before the model, then the predicted ``cam.x`` and ``verts.x`` are
flipped back — see ``hamer/demo.py`` lines 136-138 / 178-179.
* Mesh is projected onto the original frame via ``cam_crop_to_full``
with ``focal = 5000 / 256 · max(W, H)`` (no spurious x-flip — that
only exists in upstream because ``pyrender`` places the camera at
``cam_t``; here we translate the mesh in front of a fixed camera).
Run from the repo root:
python scripts/visualize.py \
--ckpt ... --mean ... --mano ... \
--images media --bboxes media/bboxes.json --out media
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import torch
from PIL import Image
_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
from hamer.rendering import overlay_mesh, project_mesh # noqa: E402
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, help="MANO_RIGHT.pkl")
p.add_argument("--images", type=Path, required=True, help="dir of images")
p.add_argument("--bboxes", type=Path, required=True,
help="JSON: {image_name: [{bbox, is_right}, ...]}")
p.add_argument("--out", type=Path, default=Path("media"))
p.add_argument("--device-id", type=int, default=int(os.environ.get("HAMER_DEVICE", "0")))
return p.parse_args()
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 normalize_entry(e):
"""Accept either ``[x1,y1,x2,y2]`` (legacy single-hand) or
``{bbox: [...], is_right: 0/1}``. Returns dict with ``bbox`` +
``is_right``."""
if isinstance(e, dict):
return {"bbox": tuple(e["bbox"]), "is_right": int(e.get("is_right", 1))}
return {"bbox": tuple(e), "is_right": 1}
def main() -> int:
args = parse_args()
args.out.mkdir(parents=True, exist_ok=True)
raw_bboxes = json.loads(args.bboxes.read_text())
bboxes = {
name: [normalize_entry(e) for e in (entries if isinstance(entries, list) else [entries])]
for name, entries in raw_bboxes.items()
}
print("Loading reference + checkpoint…")
torch.manual_seed(0)
ref = Hamer(); ref.eval()
load_hamer_checkpoint(ref, args.ckpt, args.mean)
print("Loading MANO model…")
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)
# Distinct colour per detection so multi-hand overlays read clearly.
REF_COLORS = [(0.20, 0.85, 1.00), (1.00, 0.85, 0.20), (0.30, 1.00, 0.50), (1.00, 0.50, 0.85)]
TT_COLORS = [(1.00, 0.55, 0.20), (0.55, 0.85, 1.00), (1.00, 0.20, 0.40), (0.40, 0.95, 0.65)]
for name, hands in bboxes.items():
img_path = args.images / name
if not img_path.exists():
print(f" skip {name} — file not found")
continue
original = np.asarray(Image.open(img_path).convert("RGB"))
h, w = original.shape[:2]
ref_projs = []
tt_projs = []
for hand in hands:
x, bbox_meta = preprocess(img_path, bbox_xyxy=hand["bbox"], is_right=bool(hand["is_right"]))
cx, cy, bbox_size = bbox_meta
with torch.no_grad():
ref_out = ref(x).detach()
tt_out = tt_model(x).detach()
rot_ref, betas_ref, cam_ref = split_output(ref_out)
rot_tt, betas_tt, cam_tt = split_output(tt_out)
with torch.no_grad():
verts_ref = mano(rot_ref, betas_ref)[0]
verts_tt = mano(rot_tt, betas_tt)[0]
ref_projs.append(project_mesh(verts_ref, cam_ref[0], (cx, cy), bbox_size, w, h, is_right=bool(hand["is_right"])))
tt_projs.append(project_mesh(verts_tt, cam_tt[0], (cx, cy), bbox_size, w, h, is_right=bool(hand["is_right"])))
fig, axes = plt.subplots(1, 3, figsize=(3 * w / 90, h / 90))
axes[0].imshow(original)
axes[0].set_title(f"input — {name}", fontsize=11)
axes[0].axis("off")
# Ref panel — every hand in cyan family
axes[1].imshow(original)
axes[1].set_xlim(0, w); axes[1].set_ylim(h, 0)
axes[1].set_aspect("equal"); axes[1].axis("off")
axes[1].set_title("torch CPU reference meshes", fontsize=11)
for i, (hand, (v2d, vz)) in enumerate(zip(hands, ref_projs)):
overlay_mesh(axes[1], original, v2d, vz, mano.faces, color=REF_COLORS[i % len(REF_COLORS)], is_right=bool(hand["is_right"]))
# TT panel — every hand in orange family
axes[2].imshow(original)
axes[2].set_xlim(0, w); axes[2].set_ylim(h, 0)
axes[2].set_aspect("equal"); axes[2].axis("off")
axes[2].set_title("tt-nn port meshes", fontsize=11)
for i, (hand, (v2d, vz)) in enumerate(zip(hands, tt_projs)):
overlay_mesh(axes[2], original, v2d, vz, mano.faces, color=TT_COLORS[i % len(TT_COLORS)], is_right=bool(hand["is_right"]))
plt.tight_layout(pad=0.5)
out_path = args.out / f"{Path(name).stem}_result.png"
plt.savefig(out_path, dpi=110, bbox_inches="tight")
plt.close(fig)
print(f" → {out_path}")
if hasattr(tt_model, "close"):
tt_model.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
|