feat(pose): fist near + established gates

This commit is contained in:
L'électron rare
2026-07-02 11:48:58 +02:00
parent f3ca0ac100
commit 5769af3d26
3 changed files with 60 additions and 2 deletions
+4 -1
View File
@@ -165,8 +165,11 @@ 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 = []
stab = getattr(self, "_stab", None)
fist_enabled = stab.active_flags() if stab is not None else (True, True)
feats = self._hand_ext.step(hands, chirality or None,
swap=getattr(self, "_hand_swap_lr", False))
swap=getattr(self, "_hand_swap_lr", False),
fist_enabled=fist_enabled)
with self.state.lock():
self.state.hand_feats = feats
self.bridge.send_hands(feats, t_now)
+6 -1
View File
@@ -79,7 +79,8 @@ class HandFeatureExtractor:
buf.append((cx, cy))
return _clamp(spd, 0.0, 1.0)
def step(self, hands: list, chirality=None, swap: bool = False) -> dict:
def step(self, hands: list, chirality=None, swap: bool = False,
fist_enabled: tuple = (True, True)) -> dict:
from data_only_viz.hand_slots import route_hands
# swap must mirror the gesture path (HAND_SWAP_LR) or per-voice
# L/R mods contradict pinch/strike in the same performance.
@@ -95,6 +96,8 @@ class HandFeatureExtractor:
if left_lm is not None:
lf = self._features(left_lm)
if not fist_enabled[0]:
lf["fist"] = 0.0
lf["speed"] = self._speed(0, lf["cx"], lf["cy"])
out_l = lf
else:
@@ -102,6 +105,8 @@ class HandFeatureExtractor:
if right_lm is not None:
rf = self._features(right_lm)
if not fist_enabled[1]:
rf["fist"] = 0.0
rf["speed"] = self._speed(1, rf["cx"], rf["cy"])
out_r = rf
else:
+50
View File
@@ -153,3 +153,53 @@ def test_no_swap_keeps_source_chirality():
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)
# ---------------------------------------------------------------------------
# fist_enabled gate
# ---------------------------------------------------------------------------
def test_fist_enabled_false_zeroes_fist_preserves_openness():
"""fist_enabled=(False, False) forces fist=0 but leaves openness intact."""
ext = HandFeatureExtractor()
h = _fist_hand(0.20) # all curled -> fist > 0.9 normally
out = ext.step([h], fist_enabled=(False, False))
hand = out["L"] or out["R"]
assert hand["fist"] == 0.0, "fist must be zeroed when disabled"
assert hand["openness"] < 0.2, "openness unaffected"
def test_fist_enabled_true_computes_fist():
"""fist_enabled=(True, True) leaves fist computation unchanged."""
ext = HandFeatureExtractor()
h = _fist_hand(0.20)
out = ext.step([h], fist_enabled=(True, True))
hand = out["L"] or out["R"]
assert hand["fist"] > 0.9
def test_fist_enabled_per_slot_independence():
"""fist_enabled masks only the specified slot."""
ext = HandFeatureExtractor()
# Build two 21-kp hands with distinct wrist positions for L/R routing
# Left hand: cx~0.2, all fingers curled (fist)
h_fist = _fist_hand(0.20)
h_fist[0] = LM(0.2, 0.6)
h_fist[9] = LM(0.2, 0.5)
for mcp in (5, 13, 17):
h_fist[mcp] = LM(0.2, 0.5)
for tip in (8, 12, 16, 20):
h_fist[tip] = LM(0.2 + 0.20 * 0.10, 0.5)
# Right hand: cx~0.8, fingers extended (open)
h_open = _fist_hand(0.80)
h_open[0] = LM(0.8, 0.6)
h_open[9] = LM(0.8, 0.5)
for mcp in (5, 13, 17):
h_open[mcp] = LM(0.8, 0.5)
for tip in (8, 12, 16, 20):
h_open[tip] = LM(0.8 + 0.80 * 0.10, 0.5)
out = ext.step([h_fist, h_open], fist_enabled=(False, True))
# L slot (cx~0.2) should have fist=0 even though it's curled
assert out["L"]["fist"] == 0.0
# R slot (cx~0.8) should compute normally (open -> fist < 0.1)
assert out["R"]["fist"] < 0.1