mocapgod/engine_patches/smoke_test.py
type-two f8037f7b85 Lane B / session-5 item 1: durable-ize the ARM engine fixes into git
The torchvision detector drop-in, headless pyrender/OpenGL shims, and the
detector+smoke tests existed only on ultra's disk inside gitignored .engine/.
Captured byte-identical copies into engine_patches/ (mirrors the HSMR subtree)
and moved setup_hsmr.sh to the repo root (tracked, path-relative) with a
--patch step that overlays them onto the clone.

Acceptance PASSED: fresh shallow clone of HSMR in a temp dir + './setup_hsmr.sh
--patch' + validated venv -> test_detector.py green (7/7 demo imgs, 15 patches),
no hand edits. A re-clone can no longer silently drop the ARM fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 09:34:39 +10:00

101 lines
4.6 KiB
Python

#!/usr/bin/env python
"""
HSMR smoke test on Apple Silicon (MPS) — Path B engine verification.
Answers the Lane B smoke-test questions on HSMR's bundled demo clips (no John footage,
no SKEL license needed beyond the model files John downloaded):
1. Does the HSMR forward run on MPS? (device + CPU-fallback)
2. Timing (target < 2x realtime).
3. Pose sanity (shape, NaNs) + an overlay render to eyeball quality.
Detection uses the torchvision person detector (lib.modeling.pipelines.vitdet, the
detectron2-free drop-in). The one remaining gate is SKEL: build_inference_pipeline
instantiates the SKEL body model, so this completes only once John's SKEL files are in place.
Run (once SKEL files are in data_inputs/body_models/skel/):
cd ~/Documents/MOCAPGOD/.engine/HSMR && source .venv/bin/activate
PYTHONPATH=. python smoke_test.py -i data_inputs/demo/example_videos/gymnasts.mp4
PYTHONPATH=. python smoke_test.py -i data_inputs/demo/example_imgs # image folder
"""
import os, time, argparse
os.environ.setdefault('PYTORCH_ENABLE_MPS_FALLBACK', '1') # some ops lack MPS kernels
import _headless # noqa: F401 fake pyrender/OpenGL for headless macOS — MUST precede lib.* imports
# One import line: every helper is an attribute of hsmr_demo (via its star-imports).
from lib.kits.hsmr_demo import (
load_inputs, imgs_det2patches, build_inference_pipeline, prepare_mesh,
visualize_full_img, IMG_MEAN_255, IMG_STD_255, asb, assemble_dict,
get_logger, save_video, save_img, np, torch, Path, DEFAULT_HSMR_ROOT,
build_detector,
)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('-i', '--input_path', required=True)
ap.add_argument('-o', '--output_path', default='data_outputs/smoke')
ap.add_argument('-d', '--device', default='mps')
ap.add_argument('-m', '--model_root', default=None)
ap.add_argument('--rec_bs', type=int, default=64)
ap.add_argument('--ignore_skel', action='store_true')
args = ap.parse_args()
log = get_logger(brief=True)
model_root = Path(args.model_root) if args.model_root else DEFAULT_HSMR_ROOT
out = Path(args.output_path); out.mkdir(parents=True, exist_ok=True)
# Inputs (reuse the demo loader; it only reads .input_path / .input_type).
fake = argparse.Namespace(input_path=args.input_path, input_type='auto')
raw_imgs, meta = load_inputs(fake)
log.info(f'device={args.device} type={meta["type"]} frames={len(raw_imgs)}')
# Detection -> patches (real torchvision person detector, on CPU).
dets, ratios = build_detector(device=args.device)(raw_imgs)
patches, det_meta = imgs_det2patches(raw_imgs, dets, ratios, 5)
log.info(f'patches={len(patches)}')
# Build pipeline — instantiates SKEL, so this is where a missing SKEL file fails.
pipe = build_inference_pipeline(model_root=model_root, device=args.device)
# Recovery loop (mirrors run_demo), timed.
t0 = time.time()
pd_params, pd_cam_t = [], []
for bw in asb(total=len(patches), bs_scope=args.rec_bs, enable_tqdm=True):
p = patches[bw.sid:bw.eid]
pn = ((p - IMG_MEAN_255) / IMG_STD_255).transpose(0, 3, 1, 2)
with torch.no_grad():
o = pipe(pn)
pd_params.append({k: v.detach().cpu().clone() for k, v in o['pd_params'].items()})
pd_cam_t.append(o['pd_cam_t'].detach().cpu().clone())
dt = time.time() - t0
pd_params = assemble_dict(pd_params, expand_dim=False)
pd_cam_t = torch.cat(pd_cam_t, dim=0)
poses = pd_params['poses']
fps = len(patches) / dt if dt else 0
log.info(f'RECOVERY ok: {len(patches)} patches in {dt:.2f}s ({fps:.2f} fps) '
f'poses={tuple(poses.shape)} nan={bool(torch.isnan(poses).any())}')
# Dump raw params regardless (this is the Path-B payload: SKEL rotations).
np.savez(out / 'params.npz', poses=poses.numpy(),
betas=pd_params['betas'].numpy(), cam_t=pd_cam_t.numpy())
log.info(f'params -> {out/"params.npz"}')
# Mesh geometry for visual QC in Blender (pyrender/EGL is dead headless on macOS, and
# Blender is the fleet renderer + Lane C's tool). prepare_mesh uses skel_model, no GL.
try:
m_skin, m_skel = prepare_mesh(pipe, pd_params)
np.savez(out / 'mesh_qc.npz',
skin_v=m_skin['v'].detach().cpu().numpy(), skin_f=np.asarray(m_skin['f']),
skel_v=m_skel['v'].detach().cpu().numpy(), skel_f=np.asarray(m_skel['f']))
log.info(f'mesh geometry -> {out/"mesh_qc.npz"} (import in Blender to eyeball)')
except Exception as e:
log.warning(f'prepare_mesh failed (params still saved): {e!r}')
log.info('smoke test complete.')
if __name__ == '__main__':
main()