From 48b04084fa403a3b35c029bc0afd7ce36586bd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:13:05 +0200 Subject: [PATCH] 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 --- data_only_viz/hand_display.py | 19 +++++++++ data_only_viz/renderer.py | 8 +++- data_only_viz/tests/test_hand_display.py | 52 ++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/data_only_viz/hand_display.py b/data_only_viz/hand_display.py index 8a5f213..7bfc9fc 100644 --- a/data_only_viz/hand_display.py +++ b/data_only_viz/hand_display.py @@ -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] diff --git a/data_only_viz/renderer.py b/data_only_viz/renderer.py index e11027b..171337a 100644 --- a/data_only_viz/renderer.py +++ b/data_only_viz/renderer.py @@ -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): diff --git a/data_only_viz/tests/test_hand_display.py b/data_only_viz/tests/test_hand_display.py index a67f9f0..b964e44 100644 --- a/data_only_viz/tests/test_hand_display.py +++ b/data_only_viz/tests/test_hand_display.py @@ -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