From de2748accbd3d8727637b6b15cb7cb1766edaa7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:52:20 +0200 Subject: [PATCH] fix(viz): numpy-safe hand filter + tests - _emit_hands: replace bare `if h` with numpy-safe predicate `h is not None and len(h) > 0` in single-person fallback - test_hand_features: add numpy-row landmark test for HandFeatureExtractor.step(), asserting finite cx/cy/openness - test_action_head_pub: add fallback-path test and two-pid once-per-tick invariant test for send_hands --- data_only_viz/action_head_pub.py | 2 +- data_only_viz/tests/test_action_head_pub.py | 33 +++++++++++++++++++++ data_only_viz/tests/test_hand_features.py | 20 +++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/data_only_viz/action_head_pub.py b/data_only_viz/action_head_pub.py index 78236ac..9e7a639 100644 --- a/data_only_viz/action_head_pub.py +++ b/data_only_viz/action_head_pub.py @@ -153,7 +153,7 @@ class ActionHeadPublisher(threading.Thread): if not hands and getattr(self.state, "hands_present", False): lkp = getattr(self.state, "left_hand_kp", None) rkp = getattr(self.state, "right_hand_kp", None) - hands = [h for h in (lkp, rkp) if h] + hands = [h for h in (lkp, rkp) if h is not None and len(h) > 0] feats = self._hand_ext.step(hands) self.bridge.send_hands(feats, t_now) diff --git a/data_only_viz/tests/test_action_head_pub.py b/data_only_viz/tests/test_action_head_pub.py index 0d3c623..1b33b12 100644 --- a/data_only_viz/tests/test_action_head_pub.py +++ b/data_only_viz/tests/test_action_head_pub.py @@ -207,3 +207,36 @@ def test_emit_hands_called_once_per_tick_empty() -> None: assert feats["L"] is None assert feats["R"] is None assert feats["dist"] == 0.0 + + +def test_emit_hands_single_person_fallback_sends_once() -> None: + """Single-person fallback (hands_present=True + left/right_hand_kp) → send_hands once.""" + from data_only_viz.action_head_pub import ActionHeadPublisher + state = _FakeState() + bridge = MagicMock() + pub = ActionHeadPublisher(state, bridge, ckpt_path=None) + + # No per-person hands list; exercise the single-person fallback branch. + state.persons_hands = [] + state.hands_present = True + state.left_hand_kp = _fake_hand_21(0) + state.right_hand_kp = _fake_hand_21(1) + state.persons_smplx = [_make_smplx_person(0)] + state.smplx_last_t = 1.0 + pub._tick(t_now=0.0) + + bridge.send_hands.assert_called_once() + + +def test_emit_hands_once_for_two_pids() -> None: + """Two pids in persons_smplx → send_hands called exactly once, not once per pid.""" + from data_only_viz.action_head_pub import ActionHeadPublisher + state = _FakeState() + bridge = MagicMock() + pub = ActionHeadPublisher(state, bridge, ckpt_path=None) + + state.persons_smplx = [_make_smplx_person(0), _make_smplx_person(1)] + state.smplx_last_t = 1.0 + pub._tick(t_now=0.0) + + assert bridge.send_hands.call_count == 1 diff --git a/data_only_viz/tests/test_hand_features.py b/data_only_viz/tests/test_hand_features.py index 03b0548..19b894b 100644 --- a/data_only_viz/tests/test_hand_features.py +++ b/data_only_viz/tests/test_hand_features.py @@ -80,3 +80,23 @@ def test_nan_landmark_falls_back_no_exception(): out = ext.step([h]) # must not raise hand = out["L"] or out["R"] assert math.isfinite(hand["cx"]) and math.isfinite(hand["openness"]) + + +def test_numpy_row_landmarks_finite_features(): + """Landmarks as numpy rows (indexable [x, y, ...]) must yield finite cx/cy/openness.""" + import numpy as np + arr = np.zeros((21, 3), dtype=np.float32) + # replicate _hand(0.5, 0.5, span=0.20) geometry via index access + arr[:] = [0.5, 0.5, 0.0] + arr[0] = [0.5, 0.55, 0.0] # wrist + arr[9] = [0.5, 0.45, 0.0] # middle_mcp → size 0.10 + arr[4] = [0.4, 0.5, 0.0] # thumb_tip + arr[20] = [0.6, 0.5, 0.0] # pinky_tip → span 0.20 + ext = HandFeatureExtractor() + out = ext.step([arr]) # must not raise + hand = out["L"] or out["R"] + assert hand is not None + assert math.isfinite(hand["cx"]) + assert math.isfinite(hand["cy"]) + assert math.isfinite(hand["openness"]) + assert hand["openness"] > 0.8 # span/size = 2.0, same as test_open_hand