fix(data-only-viz): action-head review fixes

Adresse final review du feature action-head :
- action_head_pub.py + extract_j3d_offline.py : CoreMLArray
  wraps numpy mais n'a pas __array__ ; unwrap via .numpy()
  avant np.asarray pour eviter object-array silencieux
  quand persons_smplx vient du backend CoreML. extract_j3d
  ramene depuis main (manquait sur feat suite au merge c52271e).
- train_on_studio.sh : TRAIN_ARGS quote defensivement via
  printf %q + reject single quotes pour eviter injection
  via le payload single-quoted sur bastion.
This commit is contained in:
L'électron rare
2026-05-13 23:02:29 +02:00
parent b5e9139317
commit 2a732faffc
3 changed files with 142 additions and 1 deletions
+4
View File
@@ -129,6 +129,10 @@ class ActionHeadPublisher(threading.Thread):
v3d = p.get("v3d")
if v3d is None:
continue
# CoreMLArray wraps a numpy array but has no __array__
# protocol; unwrap via .numpy() before np.asarray.
if hasattr(v3d, "numpy") and not isinstance(v3d, np.ndarray):
v3d = v3d.numpy()
v3d_np = np.asarray(v3d, dtype=np.float32)
if v3d_np.shape[0] < max(SMPLX_JOINT_ANCHOR_VERTS) + 1:
continue
@@ -0,0 +1,127 @@
"""Extract j3d (22 SMPL-X joint anchors) from a recorded MP4 using the
Multi-HMR CoreML backend, write per-frame per-person jsonl rows.
Usage:
uv run python -m data_only_viz.scripts.extract_j3d_offline \
--session sess03 \
--video ~/.cache/av-live-action/raw/sess03.mp4 \
--out ~/.cache/av-live-action/raw/sess03.jsonl
"""
from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
import cv2
import numpy as np
from data_only_viz.action_head_pub import SMPLX_JOINT_ANCHOR_VERTS
from data_only_viz.multihmr_coreml import MultiHMRCoreMLBackend
LOG = logging.getLogger("extract_j3d_offline")
IMG_SIZE = 672
DEFAULT_OUT_DIR = Path("~/.cache/av-live-action/raw").expanduser()
def _default_K(size: int = IMG_SIZE) -> np.ndarray:
"""Synthetic camera intrinsics, focal ~ image size, principal point centred."""
f = float(size)
cx = cy = f * 0.5
return np.array(
[[f, 0.0, cx], [0.0, f, cy], [0.0, 0.0, 1.0]],
dtype=np.float32,
)
def _frame_to_chw(frame_bgr: np.ndarray, size: int = IMG_SIZE) -> np.ndarray:
"""BGR uint8 (H, W, 3) -> float32 CHW (3, size, size) in [0, 1]."""
h, w = frame_bgr.shape[:2]
side = min(h, w)
y0 = (h - side) // 2
x0 = (w - side) // 2
crop = frame_bgr[y0:y0 + side, x0:x0 + side]
resized = cv2.resize(crop, (size, size))
rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
return rgb.transpose(2, 0, 1) # CHW
def _person_to_j3d22(person: dict, anchors: tuple[int, ...]) -> np.ndarray | None:
v3d = person.get("v3d")
if v3d is None:
return None
# CoreMLArray wraps numpy but lacks __array__; unwrap before asarray.
if hasattr(v3d, "numpy") and not isinstance(v3d, np.ndarray):
v3d = v3d.numpy()
v3d_np = np.asarray(v3d, dtype=np.float32)
if v3d_np.shape[0] < max(anchors) + 1:
return None
return v3d_np[list(anchors)].astype(np.float32)
def extract(session: str, video: Path, out: Path,
det_thresh: float = 0.3,
mlpackage_path: Path | None = None,
anchors: tuple[int, ...] = SMPLX_JOINT_ANCHOR_VERTS) -> int:
"""Returns the number of (frame, person) rows written."""
out.parent.mkdir(parents=True, exist_ok=True)
cap = cv2.VideoCapture(str(video))
if not cap.isOpened():
raise RuntimeError(f"cannot open {video}")
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
backend = MultiHMRCoreMLBackend(mlpackage_path) if mlpackage_path \
else MultiHMRCoreMLBackend()
K = _default_K(IMG_SIZE)
n_frames = 0
n_rows = 0
with out.open("w") as f:
while True:
ok, frame = cap.read()
if not ok:
break
chw = _frame_to_chw(frame)
try:
persons = backend.infer(chw, K, det_thresh=det_thresh)
except Exception:
LOG.exception("infer failed at frame=%d", n_frames)
n_frames += 1
continue
ts = n_frames / fps
for i, person in enumerate(persons):
j3d = _person_to_j3d22(person, anchors)
if j3d is None:
continue
f.write(json.dumps({
"ts": ts,
"session": session,
"pid": int(person.get("pid", i)),
"j3d": j3d.tolist(),
}) + "\n")
n_rows += 1
n_frames += 1
if n_frames % 100 == 0:
LOG.info("frame=%d rows=%d", n_frames, n_rows)
cap.release()
LOG.info("done: %d frames, %d rows -> %s", n_frames, n_rows, out)
return n_rows
def _cli() -> None:
p = argparse.ArgumentParser()
p.add_argument("--session", required=True)
p.add_argument("--video", required=True, type=Path)
p.add_argument("--out", type=Path)
p.add_argument("--det-thresh", type=float, default=0.3)
p.add_argument("--mlpackage", type=Path, default=None)
args = p.parse_args()
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(name)s] %(message)s")
DEFAULT_OUT_DIR.mkdir(parents=True, exist_ok=True)
out = args.out or (DEFAULT_OUT_DIR / f"{args.session}.jsonl")
extract(args.session, args.video, out,
det_thresh=args.det_thresh, mlpackage_path=args.mlpackage)
if __name__ == "__main__":
_cli()
+11 -1
View File
@@ -36,7 +36,17 @@ REMOTE_CKPT="$REMOTE_ROOT/checkpoints"
DATASET_FILE="${DATASET_FILE:-$LOCAL_DATASET/dataset.jsonl}"
CKPT_NAME="${CKPT_NAME:-action_head.pt}"
TRAIN_ARGS="$*"
# Quote train args defensively before forwarding through bastion ssh +
# studio ssh (each layer reparses). Reject single quotes — they break
# the single-quoted payload in bastion_ssh and could allow injection.
for a in "$@"; do
if [[ "$a" == *"'"* ]]; then
printf '[train_on_studio] forbidden single quote in arg: %s\n' "$a" >&2
exit 3
fi
done
TRAIN_ARGS="$(printf '%q ' "$@")"
log() { printf '[train_on_studio] %s\n' "$*" >&2; }