feat(pose): wrist-anchored xy mods + wrist markers
CI build oscope-of / build-check (push) Has been cancelled

The X/Y voice mods were anchored to the 21-landmark centroid, so
opening or pinching the fingers shifted the mod values (gesture/mod
cross-talk). cx/cy now track the WRIST (kp 0). The renderer draws a
cross marker at each wrist on the video whenever features exist, so
the control point stays visible even for far hands the overlay
filters out (user request live 2026-07-02).
This commit is contained in:
L'électron rare
2026-07-02 18:47:57 +02:00
parent fbc6d4efa3
commit 8d6d42ed3c
2 changed files with 16 additions and 2 deletions
+6 -2
View File
@@ -62,8 +62,12 @@ class HandFeatureExtractor:
def _features(self, lm: list) -> dict: def _features(self, lm: list) -> dict:
xs = [_finite(_coord(p, "x", 0), 0.5) for p in lm[:21]] xs = [_finite(_coord(p, "x", 0), 0.5) for p in lm[:21]]
ys = [_finite(_coord(p, "y", 1), 0.5) for p in lm[:21]] ys = [_finite(_coord(p, "y", 1), 0.5) for p in lm[:21]]
cx = _clamp(sum(xs) / len(xs), 0.0, 1.0) # cx/cy anchor = the WRIST (kp 0), not the landmark centroid: the
cy = _clamp(sum(ys) / len(ys), 0.0, 1.0) # centroid shifts when fingers open/pinch, cross-talking gestures
# into the X/Y voice mods. The wrist decouples them (user request
# live 2026-07-02).
cx = _clamp(xs[WRIST], 0.0, 1.0)
cy = _clamp(ys[WRIST], 0.0, 1.0)
size = math.hypot(xs[MIDDLE_MCP] - xs[WRIST], ys[MIDDLE_MCP] - ys[WRIST]) size = math.hypot(xs[MIDDLE_MCP] - xs[WRIST], ys[MIDDLE_MCP] - ys[WRIST])
size = size if size > 1e-4 else 1e-4 size = size if size > 1e-4 else 1e-4
span = math.hypot(xs[THUMB_TIP] - xs[PINKY_TIP], span = math.hypot(xs[THUMB_TIP] - xs[PINKY_TIP],
+10
View File
@@ -575,6 +575,16 @@ class MetalRenderer(NSObject):
content_pid=pid_s, content_pid=pid_s,
): ):
push_panel(ax, ay, bx, by, gconf, gpid) push_panel(ax, ay, bx, by, gconf, gpid)
# Wrist marker ON the video: the X/Y voice mods are anchored
# to the wrist, so its control point stays visible whenever
# features exist (even for far hands the overlay filters
# out). push_seg applies the video mirror like the overlay.
if _g_cx is not None and _g_cy is not None:
_mx = 0.015 / asp
push_seg(_g_cx - _mx, _g_cy, _g_cx + _mx, _g_cy,
1.0, pid_s)
push_seg(_g_cx, _g_cy - 0.015, _g_cx, _g_cy + 0.015,
1.0, pid_s)
if h_kp is not None: if h_kp is not None:
for seg in panel_segments( for seg in panel_segments(
h_kp, side_name, lhand_bones_p, asp, h_kp, side_name, lhand_bones_p, asp,