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
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user