feat(pose): all-fingers-curled fist metric
This commit is contained in:
@@ -10,10 +10,18 @@ from collections import deque
|
||||
|
||||
WRIST, THUMB_TIP, MIDDLE_MCP, PINKY_TIP = 0, 4, 9, 20
|
||||
|
||||
NEUTRAL_HAND = {"cx": 0.5, "cy": 0.5, "openness": 0.0, "speed": 0.0}
|
||||
# index/middle/ring/pinky tips + their MCP knuckles, for the "all fingers
|
||||
# curled" fist metric (thumb excluded — a fist curls the four fingers).
|
||||
_FINGER_TIPS = (8, 12, 16, 20)
|
||||
_FINGER_MCPS = (5, 9, 13, 17)
|
||||
|
||||
NEUTRAL_HAND = {"cx": 0.5, "cy": 0.5, "openness": 0.0, "speed": 0.0, "fist": 0.0}
|
||||
|
||||
# openness calibration: (span/size) maps fist~0.3 -> 0, open~2.0 -> 1
|
||||
_OPEN_LO, _OPEN_HI = 0.5, 2.0
|
||||
# fist calibration: per-finger tip->MCP distance / hand size. curled ~0.25,
|
||||
# extended ~0.75. closedness = 1 at/below LO, 0 at/above HI.
|
||||
_EXT_LO, _EXT_HI = 0.25, 0.75
|
||||
|
||||
|
||||
def _finite(v: float, fallback: float) -> float:
|
||||
@@ -51,7 +59,15 @@ class HandFeatureExtractor:
|
||||
ys[THUMB_TIP] - ys[PINKY_TIP])
|
||||
openness = _clamp((span / size - _OPEN_LO) / (_OPEN_HI - _OPEN_LO),
|
||||
0.0, 1.0)
|
||||
return {"cx": cx, "cy": cy, "openness": openness}
|
||||
# fist = 1.0 only when ALL four fingers are curled to the palm (min over
|
||||
# fingers): one extended finger drops it toward 0, so a true fist is
|
||||
# required rather than just a small thumb-pinky span.
|
||||
fist = 1.0
|
||||
for tip, mcp in zip(_FINGER_TIPS, _FINGER_MCPS):
|
||||
ext = math.hypot(xs[tip] - xs[mcp], ys[tip] - ys[mcp]) / size
|
||||
curled = _clamp(1.0 - (ext - _EXT_LO) / (_EXT_HI - _EXT_LO), 0.0, 1.0)
|
||||
fist = min(fist, curled)
|
||||
return {"cx": cx, "cy": cy, "openness": openness, "fist": fist}
|
||||
|
||||
def _speed(self, slot: int, cx: float, cy: float) -> float:
|
||||
buf = self._buf[slot]
|
||||
|
||||
@@ -100,3 +100,34 @@ def test_numpy_row_landmarks_finite_features():
|
||||
assert math.isfinite(hand["cy"])
|
||||
assert math.isfinite(hand["openness"])
|
||||
assert hand["openness"] > 0.8 # span/size = 2.0, same as test_open_hand
|
||||
|
||||
|
||||
def _fist_hand(tip_ext):
|
||||
"""All 4 finger MCPs at center; each fingertip placed tip_ext*size away
|
||||
(so the per-finger tip->MCP ratio == tip_ext). size (wrist->mcp9) = 0.10."""
|
||||
cx, cy = 0.5, 0.5
|
||||
pts = [LM(cx, cy) for _ in range(21)]
|
||||
pts[0] = LM(cx, cy + 0.10) # wrist -> size 0.10
|
||||
for mcp in (5, 9, 13, 17):
|
||||
pts[mcp] = LM(cx, cy)
|
||||
for tip in (8, 12, 16, 20):
|
||||
pts[tip] = LM(cx + tip_ext * 0.10, cy)
|
||||
pts[4] = LM(cx - 0.05, cy) # thumb
|
||||
return pts
|
||||
|
||||
|
||||
def test_fist_all_fingers_curled():
|
||||
out = HandFeatureExtractor().step([_fist_hand(0.20)]) # tips at knuckles
|
||||
assert (out["L"] or out["R"])["fist"] > 0.9
|
||||
|
||||
|
||||
def test_open_hand_is_not_a_fist():
|
||||
out = HandFeatureExtractor().step([_fist_hand(0.80)]) # fingers extended
|
||||
assert (out["L"] or out["R"])["fist"] < 0.1
|
||||
|
||||
|
||||
def test_one_extended_finger_breaks_the_fist():
|
||||
h = _fist_hand(0.20) # all curled...
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user