fix(pose): swap chirality only, default on
CI build oscope-of / build-check (push) Has been cancelled
CI build oscope-of / build-check (push) Has been cancelled
User-confirmed live: ARBodyTracker Vision chirality arrives inverted vs the mirrored view. HAND_SWAP_LR now inverts the chirality mapping inside route_hands (cx fallback untouched, it is already screen relative) and defaults to 1. Also: HandPersistenceGate greedy-match limitation documented + malformed-entry guard (review follow-ups).
This commit is contained in:
@@ -82,7 +82,7 @@ Toujours répondre en français à l'utilisateur. Code, commentaires de code, co
|
||||
| `PINCH_DEBOUNCE_FRAMES` | `3` | consecutive qualifying frames before a pinch engage fires (release stays immediate); `1` disables |
|
||||
| `HAND_CONF_MIN` | `0.45` | min Vision hand confidence to draw (raised from 0.3; applies to both overlay and side panels) |
|
||||
| `HAND_PERSIST_FRAMES` | `3` | consecutive frames a hand track must match before being drawn (1 = off, all hands drawn immediately) |
|
||||
| `HAND_SWAP_LR` | `0` | swap the two routed hand slots (gestures + panels). Safety knob: set `1` live if the iPhone app's camera config inverts Vision chirality — fixes the L/R direction without redeploy |
|
||||
| `HAND_SWAP_LR` | `1` | invert the Vision chirality interpretation when routing hands (gestures + panels); cx fallback untouched. Default `1`: ARBodyTracker chirality arrives inverted vs the mirrored view (observed live 2026-07-02); set `0` if a future app build fixes it at source |
|
||||
| `HAND_NEAR_MIN` | `0.10` | Minimum wrist-to-middle-MCP distance (normalised) for a hand to trigger pinch/strike. Hands smaller than this are gated out. Display panels are unaffected |
|
||||
| `ARKIT_BONE_MAX` | `0.5` | max ARKit body bone length in normalized units; longer bones are skipped (tracking-loss garbage filter) |
|
||||
|
||||
|
||||
@@ -200,9 +200,10 @@ class ActionHeadPublisher(threading.Thread):
|
||||
# "iphone" / "mediapipe".
|
||||
self._finger_source = os.environ.get("FINGER_SOURCE", "auto").lower()
|
||||
self._fdbg_n = 0
|
||||
# Safety knob: swap the two routed slots if the iPhone app's camera
|
||||
# config inverts Vision chirality (fix live without redeploy).
|
||||
self._hand_swap_lr = os.environ.get("HAND_SWAP_LR", "0") not in (
|
||||
# ARBodyTracker's Vision chirality arrives inverted vs the user's
|
||||
# mirrored view (observed live 2026-07-02); default-swap corrects it.
|
||||
# Set HAND_SWAP_LR=0 if a future app build fixes chirality at source.
|
||||
self._hand_swap_lr = os.environ.get("HAND_SWAP_LR", "1") not in (
|
||||
"0", "", "false", "False",
|
||||
)
|
||||
self._hand_near_min = float(os.environ.get("HAND_NEAR_MIN", "0.10"))
|
||||
|
||||
@@ -263,6 +263,11 @@ class HandPersistenceGate:
|
||||
|
||||
min_frames=1 disables gating: every incoming hand is drawable on the
|
||||
first call (useful for the single-person MediaPipe fallback path).
|
||||
|
||||
Known limit: matching is greedy nearest-track — two hands crossing
|
||||
within `radius` can inherit each other's track (and a ghost appearing
|
||||
exactly where a real hand just left inherits its count). Acceptable
|
||||
for a display gate; do not reuse for gesture-state tracking.
|
||||
"""
|
||||
|
||||
def __init__(self, min_frames: int = 3, radius: float = 0.15) -> None:
|
||||
@@ -288,8 +293,11 @@ class HandPersistenceGate:
|
||||
used: set[int] = set()
|
||||
|
||||
for h_idx, hand in enumerate(hands):
|
||||
wx = hand[0].x
|
||||
wy = hand[0].y
|
||||
try:
|
||||
wx = hand[0].x
|
||||
wy = hand[0].y
|
||||
except (TypeError, IndexError, AttributeError):
|
||||
continue # malformed entry: never drawable, no track
|
||||
|
||||
# Find the nearest existing track within radius (greedy).
|
||||
best_dist = float("inf")
|
||||
|
||||
+12
-10
@@ -7,9 +7,11 @@ First match per slot wins; extras dropped.
|
||||
Fallback (chirality absent/misaligned): sort valid hands by screen cx ascending
|
||||
(flipped when mirror=True) and assign leftmost to slot 0, next to slot 1.
|
||||
|
||||
`swap=True`: swaps the two slots at the very end (safety knob: if the iPhone
|
||||
app's camera config inverts Vision chirality, HAND_SWAP_LR=1 fixes the
|
||||
direction live without redeploy).
|
||||
`swap=True`: inverts the chirality INTERPRETATION only (chir 0 -> right slot,
|
||||
1 -> left slot). ARBodyTracker's Vision chirality arrives inverted vs the
|
||||
user's mirrored view (observed live 2026-07-02), so HAND_SWAP_LR defaults to
|
||||
1 in the consumers; set 0 if a future app build fixes chirality at the
|
||||
source. The cx fallback is screen-relative and is never swapped.
|
||||
|
||||
`near_min > 0`: after routing, replace a slot with None when the hand's
|
||||
wrist-to-middle-MCP distance (normalised) is below `near_min`.
|
||||
@@ -72,8 +74,9 @@ def route_hands(
|
||||
chirality: int list aligned 1-to-1 with `hands` (0=left, 1=right).
|
||||
None / wrong length -> cx fallback.
|
||||
mirror: flip cx for the fallback path so screen-left stays slot 0.
|
||||
swap: swap the two slots at the very end (both paths). Safety knob
|
||||
for a source whose Vision chirality arrives inverted.
|
||||
swap: invert the chirality mapping (0 -> right, 1 -> left). Safety
|
||||
knob for a source whose Vision chirality arrives inverted.
|
||||
Never touches the cx fallback (already screen-relative).
|
||||
near_min: replace a routed slot with None when hand_size < near_min.
|
||||
"""
|
||||
chir_aligned = (
|
||||
@@ -94,9 +97,12 @@ def route_hands(
|
||||
slots: list = [None, None]
|
||||
|
||||
if chir_aligned and any(c is not None for c, _ in valid_pairs):
|
||||
# ---- Chirality path: chir 0 -> slot 0 (left), 1 -> slot 1 (right) ----
|
||||
# ---- Chirality path: chir 0 -> slot 0 (left), 1 -> slot 1 (right);
|
||||
# swap inverts the mapping when the source chirality is flipped ----
|
||||
for c, hand in valid_pairs:
|
||||
target = c if c in (0, 1) else None
|
||||
if target is not None and swap:
|
||||
target = 1 - target
|
||||
if target is not None and slots[target] is None:
|
||||
slots[target] = hand
|
||||
else:
|
||||
@@ -118,8 +124,4 @@ def route_hands(
|
||||
if slots[i] is not None and _hand_size(slots[i]) < near_min:
|
||||
slots[i] = None
|
||||
|
||||
# ---- swap: exchange the two slots at the very end (safety knob) ----
|
||||
if swap:
|
||||
slots[0], slots[1] = slots[1], slots[0]
|
||||
|
||||
return slots
|
||||
|
||||
@@ -165,9 +165,10 @@ class MetalRenderer(NSObject):
|
||||
self._hand_gate = HandPersistenceGate(
|
||||
min_frames=int(os.environ.get("HAND_PERSIST_FRAMES", "3"))
|
||||
)
|
||||
# Safety knob: swap the two routed hand slots if the iPhone app's
|
||||
# camera config inverts Vision chirality (fix live without redeploy).
|
||||
self._hand_swap_lr = os.environ.get("HAND_SWAP_LR", "0") not in (
|
||||
# ARBodyTracker's Vision chirality arrives inverted vs the user's
|
||||
# mirrored view (observed live 2026-07-02); default-swap corrects it.
|
||||
# Set HAND_SWAP_LR=0 if a future app build fixes chirality at source.
|
||||
self._hand_swap_lr = os.environ.get("HAND_SWAP_LR", "1") not in (
|
||||
"0", "", "false", "False",
|
||||
)
|
||||
self._init_skel_cpu_buffer()
|
||||
|
||||
@@ -89,24 +89,25 @@ def test_mirror_flips_cx_fallback_order():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# swap: exchanges the two slots at the very end (both paths)
|
||||
# swap: inverts the CHIRALITY interpretation only (source chirality flipped);
|
||||
# the cx fallback is screen-relative and must never be affected.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_swap_swaps_chirality_slots():
|
||||
def test_swap_inverts_chirality_mapping():
|
||||
h_l = _hand(cx=0.8) # Vision says left (chir=0) but source is inverted
|
||||
h_r = _hand(cx=0.2) # Vision says right (chir=1)
|
||||
result = route_hands([h_l, h_r], [0, 1], swap=True)
|
||||
assert result[0] is h_r # slots exchanged at the end
|
||||
assert result[1] is h_l
|
||||
assert result[0] is h_r # chir 1 -> left slot under swap
|
||||
assert result[1] is h_l # chir 0 -> right slot under swap
|
||||
|
||||
|
||||
def test_swap_swaps_cx_fallback_slots():
|
||||
"""swap=True also exchanges the slots on the cx fallback path."""
|
||||
def test_swap_does_not_alter_cx_fallback():
|
||||
"""swap only concerns chirality; the cx fallback stays screen-relative."""
|
||||
h_a = _hand(cx=0.2)
|
||||
h_b = _hand(cx=0.8)
|
||||
result = route_hands([h_a, h_b], None, swap=True)
|
||||
assert result[0] is h_b
|
||||
assert result[1] is h_a
|
||||
assert result[0] is h_a # unchanged: leftmost keeps slot 0
|
||||
assert result[1] is h_b
|
||||
|
||||
|
||||
def test_swap_single_hand_moves_slot():
|
||||
|
||||
Reference in New Issue
Block a user