fix(pose): pinch closest-finger-wins + margin
CI build oscope-of / build-check (push) Has been cancelled

This commit is contained in:
L'électron rare
2026-06-30 18:06:34 +02:00
parent 4c0794a3db
commit 16cd6a3655
3 changed files with 56 additions and 6 deletions
+1
View File
@@ -186,6 +186,7 @@ class ActionHeadPublisher(threading.Thread):
ratio_on=float(os.environ.get("PINCH_RATIO_ON", "0.45")),
ratio_off=float(os.environ.get("PINCH_RATIO_OFF", "0.65")),
refractory_ms=float(os.environ.get("PINCH_REFRACTORY_MS", "250")),
margin=float(os.environ.get("PINCH_MARGIN", "0.20")),
)
self._finger_dbg = os.environ.get("FINGER_DEBUG", "0") not in (
"0", "", "false", "False",
+21 -6
View File
@@ -149,10 +149,15 @@ class PinchDetector:
"""
def __init__(self, ratio_on: float = 0.45, ratio_off: float = 0.65,
refractory_ms: float = 250.0, history_slots: int = 2) -> None:
refractory_ms: float = 250.0, history_slots: int = 2,
margin: float = 0.20) -> None:
self.ratio_on = ratio_on
self.ratio_off = ratio_off
self.refractory_s = refractory_ms / 1000.0
# Only the single nearest fingertip may engage, and only if it is at
# least `margin` (in size-normalized units) nearer than the runner-up.
# Rejects the adjacent-finger ambiguity when fingers curl together.
self.margin = margin
self._state = [[_PinchState() for _ in range(4)]
for _ in range(history_slots)]
@@ -169,17 +174,27 @@ class PinchDetector:
my = _finite(_coord(lm[MIDDLE_MCP], "y", 1), 0.5)
size = math.hypot(mx - wx, my - wy)
size = size if size > 1e-4 else 1e-4
for i, tip_idx in enumerate(PINCH_TIPS):
ratios = []
for tip_idx in PINCH_TIPS:
fx = _finite(_coord(lm[tip_idx], "x", 0), 0.5)
fy = _finite(_coord(lm[tip_idx], "y", 1), 0.5)
ratio = math.hypot(fx - tx, fy - ty) / size
ratios.append(math.hypot(fx - tx, fy - ty) / size)
# closest-wins + margin: pick the single nearest fingertip, and treat
# it as a pinch only if it is clearly nearer than the runner-up.
order = sorted(range(4), key=lambda j: ratios[j])
nearest, runner = order[0], order[1]
winner = nearest if (
ratios[nearest] < self.ratio_on
and (ratios[runner] - ratios[nearest]) >= self.margin
) else -1
for i in range(4):
st = self._state[slot][i]
if st.engaged:
if ratio > self.ratio_off:
# release when this finger opens, or another finger took over.
if i != winner or ratios[i] > self.ratio_off:
st.engaged = False
events.append(PinchEvent(hand=slot, finger=i + 1, state=0))
elif ratio < self.ratio_on and (
t_now - st.last_t) >= self.refractory_s:
elif i == winner and (t_now - st.last_t) >= self.refractory_s:
st.engaged = True
st.last_t = t_now
events.append(PinchEvent(hand=slot, finger=i + 1, state=1))
+34
View File
@@ -144,3 +144,37 @@ def test_pinch_hand_disappear_emits_release():
release = det.step([], 0.2) # hand gone
assert len(engage) == 1 and engage[0].state == 1
assert len(release) == 1 and release[0].state == 0
def _pinch_hand_multi(thumb_xy, tips):
"""21-kp hand (size 0.3); `tips` maps finger 0..3 (index/middle/ring/pinky)
-> (x,y) for that fingertip. Unlisted fingertips are parked far from thumb."""
lm = [[0.5, 0.5, 0.0] for _ in range(21)]
lm[0] = [0.5, 0.8, 0.0] # WRIST
lm[9] = [0.5, 0.5, 0.0] # MIDDLE_MCP -> size 0.3
lm[4] = [thumb_xy[0], thumb_xy[1], 0.0] # THUMB_TIP
far = [(0.9, 0.5), (0.9, 0.6), (0.9, 0.7), (0.9, 0.8)]
for i, idx in enumerate((8, 12, 16, 20)): # PINCH_TIPS
x, y = tips.get(i, far[i])
lm[idx] = [x, y, 0.0]
return lm
def test_pinch_closest_finger_wins():
# index ratio ~0.067, middle ratio ~0.33 -> margin 0.27 >= 0.20 -> index only.
det = PinchDetector(ratio_on=0.45, ratio_off=0.65, refractory_ms=0)
det.step([_pinch_hand_multi((0.5, 0.5), {})], 0.0) # open
ev = det.step([_pinch_hand_multi((0.5, 0.5),
{0: (0.52, 0.5), 1: (0.60, 0.5)})], 0.1)
assert len(ev) == 1
assert ev[0].finger == 1 and ev[0].state == 1 # index only
def test_pinch_ambiguous_adjacent_fires_nothing():
# index ratio ~0.067, middle ratio ~0.10 -> margin 0.033 < 0.20 -> no winner.
# (both are below ratio_on, so the OLD per-finger logic would fire both.)
det = PinchDetector(ratio_on=0.45, ratio_off=0.65, refractory_ms=0)
det.step([_pinch_hand_multi((0.5, 0.5), {})], 0.0) # open
ev = det.step([_pinch_hand_multi((0.5, 0.5),
{0: (0.52, 0.5), 1: (0.53, 0.5)})], 0.1)
assert ev == []