fix(viz): gate arkit overlay on 2d freshness

persons_arkit_2d_t was written by IphoneUSBSource but never
read. A USB drop left the frozen skeleton on screen indefinitely.

- Add arkit_2d_fresh(ts_by_pid, now, max_age=1.0) pure helper
  in hand_display.py (8 TDD tests, all green)
- renderer._update_skeleton: use_arkit now requires at least one
  pid younger than 1 s (replaces bool check)
- Per-pid freshness gate in draw loop skips stale pids
This commit is contained in:
L'électron rare
2026-07-02 10:13:05 +02:00
parent 06b868e377
commit 48b04084fa
3 changed files with 77 additions and 2 deletions
+19
View File
@@ -30,6 +30,25 @@ class _HasXYC(Protocol):
c: float
def arkit_2d_fresh(ts_by_pid: dict, now: float, max_age: float = 1.0) -> bool:
"""Return True if any pid has a timestamp younger than *max_age* seconds.
Args:
ts_by_pid: mapping pid -> perf_counter timestamp (from
``State.persons_arkit_2d_t``).
now: current ``time.perf_counter()`` value.
max_age: staleness threshold in seconds (exclusive: age < max_age).
Returns:
True if at least one entry satisfies ``now - ts < max_age``.
False if the dict is empty or every entry is stale.
"""
for ts in ts_by_pid.values():
if (now - ts) < max_age:
return True
return False
def hand_size(kp: list[_HasXYC]) -> float:
"""Euclidean distance wrist(0) -> middle-MCP(9) in normalised image units."""
w = kp[0]
+6 -2
View File
@@ -44,7 +44,7 @@ from Metal import (
from MetalKit import MTKView # noqa: F401 (utilise par d'autres modules)
from .arkit_skeleton import arkit_segments
from .hand_display import hand_plausible, hand_size, segment_ok, panel_frame, panel_segments
from .hand_display import arkit_2d_fresh, hand_plausible, hand_size, segment_ok, panel_frame, panel_segments
from .mesh_topology import (
BODY_TRIANGLES,
FACE_TRIANGLES,
@@ -320,8 +320,9 @@ class MetalRenderer(NSObject):
Priorise MediaPipe (body 33 + face 478 + 2 mains 21) si disponible
et present ; sinon fallback COCO 17 keypoints YOLO."""
_arkit_now = time.perf_counter()
use_arkit = (ARKIT_FULL and bool(s.arkit_parents)
and bool(s.persons_arkit_2d))
and arkit_2d_fresh(s.persons_arkit_2d_t, _arkit_now))
if not use_arkit and not s.pose_alive():
return 0
@@ -385,6 +386,9 @@ class MetalRenderer(NSObject):
if use_arkit:
parents = s.arkit_parents
for pid, arr2d in s.persons_arkit_2d.items():
ts = s.persons_arkit_2d_t.get(pid)
if ts is None or (_arkit_now - ts) >= 1.0:
continue # skip stale or unknown pid
valid = s.persons_arkit_2d_valid.get(pid)
for (ax, ay, bx, by) in arkit_segments(arr2d, valid, parents):
if not push_seg(ax, ay, bx, by, 1.0, pid):
+52
View File
@@ -10,6 +10,7 @@ from dataclasses import dataclass
import pytest
from data_only_viz.hand_display import (
arkit_2d_fresh,
hand_plausible,
hand_size,
segment_ok,
@@ -345,3 +346,54 @@ def test_panel_frame_traces_rect():
for cx, cy in corners
)
assert on_corner, f"Endpoint ({ex:.4f}, {ey:.4f}) not a corner of panel rect"
# ---------------------------------------------------------------------------
# arkit_2d_fresh
# ---------------------------------------------------------------------------
def test_arkit_2d_fresh_empty_dict_is_false() -> None:
"""No pids → never fresh."""
assert arkit_2d_fresh({}, now=1000.0) is False
def test_arkit_2d_fresh_single_fresh_pid() -> None:
"""A timestamp 0.5 s old with max_age=1.0 → fresh."""
ts_by_pid = {0: 999.5}
assert arkit_2d_fresh(ts_by_pid, now=1000.0) is True
def test_arkit_2d_fresh_single_stale_pid() -> None:
"""A timestamp 2 s old with max_age=1.0 → stale."""
ts_by_pid = {0: 998.0}
assert arkit_2d_fresh(ts_by_pid, now=1000.0) is False
def test_arkit_2d_fresh_exactly_at_max_age_is_stale() -> None:
"""age == max_age is NOT fresh (strict less-than)."""
ts_by_pid = {0: 999.0}
assert arkit_2d_fresh(ts_by_pid, now=1000.0, max_age=1.0) is False
def test_arkit_2d_fresh_mix_stale_and_fresh() -> None:
"""At least one fresh pid makes the whole dict fresh."""
ts_by_pid = {0: 990.0, 1: 999.8} # pid 0 stale, pid 1 fresh
assert arkit_2d_fresh(ts_by_pid, now=1000.0) is True
def test_arkit_2d_fresh_all_pids_stale() -> None:
"""All pids stale → False."""
ts_by_pid = {0: 990.0, 1: 980.0}
assert arkit_2d_fresh(ts_by_pid, now=1000.0) is False
def test_arkit_2d_fresh_custom_max_age() -> None:
"""max_age=0.5 makes a 0.4 s old timestamp fresh."""
ts_by_pid = {5: 999.6}
assert arkit_2d_fresh(ts_by_pid, now=1000.0, max_age=0.5) is True
def test_arkit_2d_fresh_custom_max_age_stale() -> None:
"""max_age=0.5 makes a 0.6 s old timestamp stale."""
ts_by_pid = {5: 999.4}
assert arkit_2d_fresh(ts_by_pid, now=1000.0, max_age=0.5) is False