Files
L'électron rare 7ed2e2764a feat(av-live): openpos 3D + DINO reid + filter
Three improvements wired end-to-end:

1. Openpos 3D skeleton visible: Skeleton3DRenderer attached to a
   RealityKit AnchorEntity in BodyView, toggled by showSkeleton
   or vizMode==9. PoseOSCListener now parses /pose3d/count and
   /pose3d/kp (plus restored /face/* and /hand/* paths).

2. DINO re-id (dinov2_vits14, ~9 ms ANE forward):
   MeshRigger combines Hungarian IoU with cosine similarity over
   a per-pid embedding history (deque maxlen=10), weighted by
   MULTIHMR_REID_ALPHA (default 0.5). Falls back to pure IoU if
   DINO mlpackage absent or scipy missing. state.last_frame_rgb
   buffer added so the rigger can crop bbox regions for embedding.

3. PoseFilterChain on pose_world_landmarks:
   median (anti-spike) -> Kalman constant-velocity ->
   50 ms lookahead -> IK elbow/knee/ankle clamp. Configurable
   via POSE_FILTER env (default median+kalman+lookahead+ik).
   <2 ms per frame for typical 1-2 persons.

Tests: 5 new in test_dino_reid.py + 6 new in test_pose_filter.py,
all green. Live validated by user: skeleton spawns, mesh stays
stable.
2026-05-14 00:30:42 +02:00

205 lines
7.9 KiB
Python

"""DINOv2 ViT-S/14 person re-id backend (CoreML via pyobjc).
Loads the .mlpackage produced by ``scripts/convert_dinov2.py`` and runs
inference one crop at a time (pyobjc + MLDictionaryFeatureProvider).
Same pattern as ``multihmr_coreml.py`` so Python 3.14 works (no
coremltools dependency at runtime).
Embeddings are L2-normalised inside the CoreML graph, so cosine sim
between two outputs is a plain dot product.
Public API::
reid = DinoReid(mlpackage_path) # optional path
emb = reid.embed_crops(list_of_uint8_HWC) # -> np.ndarray (N, 384)
DinoReid.is_available() # bool
"""
from __future__ import annotations
import logging
import time
from pathlib import Path
from typing import Sequence
import numpy as np
LOG = logging.getLogger("dino_reid")
DEFAULT_MLPACKAGE = (
Path.home() / ".cache" / "av-live-multihmr" / "dinov2_vits14.mlpackage"
)
EMBED_DIM = 384
INPUT_SIZE = 224
# MLMultiArrayDataType raw values (from CoreML headers).
ML_DTYPE_FLOAT32 = 65568
ML_DTYPE_FLOAT16 = 65552
ML_DTYPE_DOUBLE = 65600
def _resize_crop(crop_uint8: np.ndarray) -> np.ndarray:
"""Resize an HxWx3 uint8 crop to (3, 224, 224) float32 in [0, 1].
Uses ``cv2.resize`` when available, falls back to a simple stride
sampler otherwise (avoids hard cv2 dep in test envs)."""
if crop_uint8.ndim != 3 or crop_uint8.shape[2] != 3:
raise ValueError(f"crop must be HxWx3 uint8, got {crop_uint8.shape}")
if crop_uint8.shape[0] == INPUT_SIZE and crop_uint8.shape[1] == INPUT_SIZE:
rgb = crop_uint8
else:
try:
import cv2
rgb = cv2.resize(crop_uint8, (INPUT_SIZE, INPUT_SIZE),
interpolation=cv2.INTER_AREA)
except ImportError:
h, w = crop_uint8.shape[:2]
ys = (np.linspace(0, h - 1, INPUT_SIZE)).astype(np.int32)
xs = (np.linspace(0, w - 1, INPUT_SIZE)).astype(np.int32)
rgb = crop_uint8[ys][:, xs]
return (rgb.astype(np.float32) / 255.0).transpose(2, 0, 1)
class DinoReid:
"""Forward DINOv2 ViT-S/14 over RGB crops, return L2-normalised
embeddings (N, 384)."""
def __init__(self, mlpackage_path: Path | str | None = None) -> None:
self.path = Path(mlpackage_path) if mlpackage_path else DEFAULT_MLPACKAGE
if not self.path.exists():
raise FileNotFoundError(f"mlpackage missing: {self.path}")
import objc
from Foundation import NSURL
self._objc = objc
self._NSURL = NSURL
ns: dict = {}
objc.loadBundle("CoreML", ns,
"/System/Library/Frameworks/CoreML.framework")
self._ns = ns
MLModel = ns["MLModel"]
MLModelConfiguration = ns["MLModelConfiguration"]
cfg = MLModelConfiguration.alloc().init()
try:
# 2 = MLComputeUnitsAll (CPU+GPU+ANE). DINOv2 ViT-S/14
# converts cleanly and ANE serves it well.
cfg.setComputeUnits_(2)
except Exception: # noqa: BLE001
pass
url = NSURL.fileURLWithPath_(str(self.path))
compiled = MLModel.compileModelAtURL_error_(url, None)
if compiled is None:
raise RuntimeError(f"compile failed for {self.path}")
model = MLModel.modelWithContentsOfURL_configuration_error_(
compiled, cfg, None)
if model is None:
raise RuntimeError(f"load failed for {compiled}")
self._model = model
# Discover the output feature name (single tensor).
desc = model.modelDescription()
out_names = [str(n) for n in desc.outputDescriptionsByName().keys()]
self._out_name = out_names[0] if out_names else "embedding"
LOG.info("dino_reid loaded (%s, out=%s)", self.path.name,
self._out_name)
@classmethod
def is_available(cls, mlpackage_path: Path | str | None = None) -> bool:
p = Path(mlpackage_path) if mlpackage_path else DEFAULT_MLPACKAGE
if not p.exists():
return False
try:
import objc # noqa: F401
from Foundation import NSURL # noqa: F401
return True
except Exception: # noqa: BLE001
return False
# ------------------------------------------------------------------
# MLMultiArray plumbing — mirrors multihmr_coreml._np_to_mlarray /
# _mlarray_to_np. Float32 in, float32-or-float16 out.
# ------------------------------------------------------------------
def _np_to_mlarray(self, arr: np.ndarray):
import ctypes
MLMultiArray = self._ns["MLMultiArray"]
arr = np.ascontiguousarray(arr, dtype=np.float32)
shape = [int(s) for s in arr.shape]
ml = MLMultiArray.alloc().initWithShape_dataType_error_(
shape, ML_DTYPE_FLOAT32, None)
if ml is None:
raise RuntimeError("MLMultiArray alloc failed")
ptr = ml.dataPointer()
addr = int(ptr) if isinstance(ptr, int) else ctypes.cast(
ptr, ctypes.c_void_p).value
if addr is None:
raise RuntimeError("dataPointer null")
ctypes.memmove(addr, arr.ctypes.data, arr.nbytes)
return ml
def _mlarray_to_np(self, ml) -> np.ndarray:
import ctypes
shape = tuple(int(s) for s in ml.shape())
dtype_id = int(ml.dataType())
count = 1
for s in shape:
count *= s
ptr = ml.dataPointer()
addr = int(ptr) if isinstance(ptr, int) else ctypes.cast(
ptr, ctypes.c_void_p).value
if addr is None:
raise RuntimeError("dataPointer null")
if dtype_id == ML_DTYPE_FLOAT16:
raw = (ctypes.c_uint16 * count).from_address(addr)
arr = np.ctypeslib.as_array(raw).view(np.float16).astype(np.float32)
elif dtype_id == ML_DTYPE_FLOAT32:
raw = (ctypes.c_float * count).from_address(addr)
arr = np.ctypeslib.as_array(raw).copy()
elif dtype_id == ML_DTYPE_DOUBLE:
raw = (ctypes.c_double * count).from_address(addr)
arr = np.ctypeslib.as_array(raw).astype(np.float32)
else:
raise RuntimeError(f"unsupported dtype {dtype_id}")
return arr.reshape(shape)
def _predict_one(self, image_chw: np.ndarray) -> np.ndarray:
MLDictionaryFeatureProvider = self._ns["MLDictionaryFeatureProvider"]
MLFeatureValue = self._ns["MLFeatureValue"]
x4 = image_chw[np.newaxis, ...] if image_chw.ndim == 3 else image_chw
img_ml = self._np_to_mlarray(x4)
feats = {"image": MLFeatureValue.featureValueWithMultiArray_(img_ml)}
provider = MLDictionaryFeatureProvider.alloc(
).initWithDictionary_error_(feats, None)
if provider is None:
raise RuntimeError("provider alloc failed")
out = self._model.predictionFromFeatures_error_(provider, None)
if out is None:
raise RuntimeError("predict failed")
fv = out.featureValueForName_(self._out_name)
ml = fv.multiArrayValue()
return self._mlarray_to_np(ml).reshape(-1)
def embed_crops(
self, crops_uint8: Sequence[np.ndarray],
) -> np.ndarray:
"""Embed a list of HxWx3 uint8 RGB crops -> (N, 384) float32.
Loops one crop at a time (the CoreML model is traced for B=1).
For typical N <= 4 this is still 10-15 ms total on M5."""
if not crops_uint8:
return np.zeros((0, EMBED_DIM), dtype=np.float32)
t0 = time.perf_counter()
out = np.zeros((len(crops_uint8), EMBED_DIM), dtype=np.float32)
for i, c in enumerate(crops_uint8):
chw = _resize_crop(c)
out[i] = self._predict_one(chw)
dt_ms = (time.perf_counter() - t0) * 1e3
if LOG.isEnabledFor(logging.DEBUG) or dt_ms > 50.0:
LOG.log(
logging.DEBUG if dt_ms <= 50.0 else logging.INFO,
"embedded %d crops in %.1f ms", len(crops_uint8), dt_ms)
return out