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:
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user