fix(pose): plumb swap into hand features slots
CI build oscope-of / build-check (push) Has been cancelled
CI build oscope-of / build-check (push) Has been cancelled
Review C1 (Critical): /pose/hands per-voice L/R mods routed without the HAND_SWAP_LR swap while pinch/strike used it — the two paths contradicted each other on an inverted-chirality source. step() now takes swap, the publisher passes its env-read value, two tests pin the contract. Also hoists the route_hands imports (M5) and fixes the stale slot comment (M1).
This commit is contained in:
@@ -24,6 +24,7 @@ from data_only_viz.action_head import (
|
||||
J3D_FINGERS_PER_HAND,
|
||||
LABELS,
|
||||
)
|
||||
from data_only_viz.hand_slots import route_hands
|
||||
|
||||
LOG = logging.getLogger("action_head_pub")
|
||||
|
||||
@@ -160,7 +161,8 @@ class ActionHeadPublisher(threading.Thread):
|
||||
rkp = getattr(self.state, "right_hand_kp", None)
|
||||
hands = [h for h in (lkp, rkp) if h is not None and len(h) > 0]
|
||||
chirality = []
|
||||
feats = self._hand_ext.step(hands, chirality or None)
|
||||
feats = self._hand_ext.step(hands, chirality or None,
|
||||
swap=getattr(self, "_hand_swap_lr", True))
|
||||
with self.state.lock():
|
||||
self.state.hand_feats = feats
|
||||
self.bridge.send_hands(feats, t_now)
|
||||
@@ -229,11 +231,10 @@ class ActionHeadPublisher(threading.Thread):
|
||||
"""Detect finger strikes (air-piano) and emit /pose/finger."""
|
||||
if not getattr(self, "_finger_enabled", False):
|
||||
return
|
||||
from data_only_viz.hand_slots import route_hands
|
||||
hands, chirality, used, ip_fresh = self._pick_hands()
|
||||
slotted = route_hands(
|
||||
hands, chirality or None,
|
||||
swap=getattr(self, "_hand_swap_lr", False),
|
||||
swap=getattr(self, "_hand_swap_lr", True),
|
||||
near_min=getattr(self, "_hand_near_min", 0.10),
|
||||
)
|
||||
events = self._finger_det.step(slotted, t_now)
|
||||
@@ -250,11 +251,10 @@ class ActionHeadPublisher(threading.Thread):
|
||||
/pose/pinch. Gated by PINCH_ENABLE, independently of the air-piano."""
|
||||
if not getattr(self, "_pinch_enabled", False):
|
||||
return
|
||||
from data_only_viz.hand_slots import route_hands
|
||||
hands, chirality, used, _ip = self._pick_hands()
|
||||
slotted = route_hands(
|
||||
hands, chirality or None,
|
||||
swap=getattr(self, "_hand_swap_lr", False),
|
||||
swap=getattr(self, "_hand_swap_lr", True),
|
||||
near_min=getattr(self, "_hand_near_min", 0.10),
|
||||
)
|
||||
for ev in self._pinch_det.step(slotted, t_now):
|
||||
|
||||
@@ -45,7 +45,8 @@ def _clamp(v: float, lo: float, hi: float) -> float:
|
||||
class HandFeatureExtractor:
|
||||
def __init__(self, history: int = 5):
|
||||
self._history = max(2, history)
|
||||
# two screen slots: 0 = leftmost (L), 1 = rightmost (R)
|
||||
# two routed slots (hand_slots.route_hands): 0 = user's left (L),
|
||||
# 1 = user's right (R); cx fallback when chirality is absent.
|
||||
self._buf = [deque(maxlen=self._history), deque(maxlen=self._history)]
|
||||
|
||||
def _features(self, lm: list) -> dict:
|
||||
@@ -78,9 +79,11 @@ class HandFeatureExtractor:
|
||||
buf.append((cx, cy))
|
||||
return _clamp(spd, 0.0, 1.0)
|
||||
|
||||
def step(self, hands: list, chirality=None) -> dict:
|
||||
def step(self, hands: list, chirality=None, swap: bool = False) -> dict:
|
||||
from data_only_viz.hand_slots import route_hands
|
||||
left_lm, right_lm = route_hands(hands, chirality)
|
||||
# swap must mirror the gesture path (HAND_SWAP_LR) or per-voice
|
||||
# L/R mods contradict pinch/strike in the same performance.
|
||||
left_lm, right_lm = route_hands(hands, chirality, swap=swap)
|
||||
|
||||
if left_lm is None and right_lm is None:
|
||||
self._buf[0].clear()
|
||||
|
||||
@@ -131,3 +131,25 @@ def test_one_extended_finger_breaks_the_fist():
|
||||
h[8] = LM(0.5 + 0.80 * 0.10, 0.5) # ...except the index
|
||||
out = HandFeatureExtractor().step([h])
|
||||
assert (out["L"] or out["R"])["fist"] < 0.1 # min over fingers
|
||||
|
||||
|
||||
def test_swap_routes_inverted_chirality_to_user_slots():
|
||||
"""C1 pin: with the inverted-source swap ON (HAND_SWAP_LR default),
|
||||
a physical LEFT hand arriving labeled chir=1 must land in feature
|
||||
slot L — consistent with the pinch/strike gesture path."""
|
||||
ext = HandFeatureExtractor()
|
||||
left_hand = _hand(0.30, 0.5, span=0.20) # user's left (source says 1)
|
||||
right_hand = _hand(0.70, 0.5, span=0.06) # user's right (source says 0)
|
||||
out = ext.step([left_hand, right_hand], [1, 0], swap=True)
|
||||
assert out["L"] is not None and out["R"] is not None
|
||||
assert out["L"]["cx"] == pytest.approx(0.30, abs=0.01) # left hand in L
|
||||
assert out["R"]["cx"] == pytest.approx(0.70, abs=0.01)
|
||||
|
||||
|
||||
def test_no_swap_keeps_source_chirality():
|
||||
ext = HandFeatureExtractor()
|
||||
a = _hand(0.30, 0.5, span=0.20)
|
||||
b = _hand(0.70, 0.5, span=0.06)
|
||||
out = ext.step([a, b], [1, 0], swap=False)
|
||||
assert out["L"]["cx"] == pytest.approx(0.70, abs=0.01) # chir 0 -> L
|
||||
assert out["R"]["cx"] == pytest.approx(0.30, abs=0.01)
|
||||
|
||||
Reference in New Issue
Block a user