fix(pose): pinch closest-finger-wins + margin
CI build oscope-of / build-check (push) Has been cancelled
CI build oscope-of / build-check (push) Has been cancelled
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 == []
|
||||
|
||||
Reference in New Issue
Block a user