fix(viz): arkit bone bounds + length filter

arkit_segments now accepts max_len and bounds params.
Bones whose endpoints fall outside [-bounds, 1+bounds] or whose
length exceeds max_len are skipped (tracking-loss garbage).
Defaults: max_len=0.5, bounds=0.1.  Renderer will pass
max_len=ARKIT_BONE_MAX explicitly (Part 3).

Tests updated to use realistic bone lengths; new tests added
for the out-of-bounds and overlong-bone cases.
This commit is contained in:
L'électron rare
2026-07-02 10:27:49 +02:00
parent 287c4977a9
commit ac6768d8a8
2 changed files with 72 additions and 8 deletions
+19 -3
View File
@@ -6,6 +6,8 @@ the GPU line buffer.
"""
from __future__ import annotations
import math
def bones_from_parents(parents: list[int]) -> list[tuple[int, int]]:
"""(child, parent) pairs for every joint with a valid parent."""
@@ -13,14 +15,20 @@ def bones_from_parents(parents: list[int]) -> list[tuple[int, int]]:
return [(i, p) for i, p in enumerate(parents) if 0 <= p < n]
def arkit_segments(arr2d, valid, parents):
def arkit_segments(arr2d, valid, parents, max_len: float = 0.5, bounds: float = 0.1):
"""Return (ax, ay, bx, by) for each bone with both endpoints valid.
arr2d: indexable of (x, y) normalized [0,1], length == len(parents).
valid: indexable of bool (length == len(parents)) or None to keep all.
arr2d: indexable of (x, y) normalized [0,1], length == len(parents).
valid: indexable of bool (length == len(parents)) or None to keep all.
parents: parent index per joint (-1 = root).
max_len: maximum bone length in normalized units; bones longer than this
are skipped (garbage coords during ARKit tracking loss).
bounds: margin outside [0,1] that is still accepted; endpoints outside
[-bounds, 1+bounds] in x or y are rejected.
"""
n = len(parents)
lo = -bounds
hi = 1.0 + bounds
segs: list[tuple[float, float, float, float]] = []
for child, parent in bones_from_parents(parents):
if child >= n or parent >= n:
@@ -29,5 +37,13 @@ def arkit_segments(arr2d, valid, parents):
continue
ax, ay = float(arr2d[child][0]), float(arr2d[child][1])
bx, by = float(arr2d[parent][0]), float(arr2d[parent][1])
# Reject out-of-frame endpoints
if not (lo <= ax <= hi and lo <= ay <= hi):
continue
if not (lo <= bx <= hi and lo <= by <= hi):
continue
# Reject implausibly long bones (tracking-loss garbage)
if math.hypot(bx - ax, by - ay) > max_len:
continue
segs.append((ax, ay, bx, by))
return segs
+53 -5
View File
@@ -13,15 +13,16 @@ def test_bones_skip_out_of_range():
def test_segments_basic():
arr2d = [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)]
# Realistic bone lengths < 0.5 (horizontal spine-like chain)
arr2d = [(0.0, 0.0), (0.2, 0.0), (0.4, 0.0)]
valid = [True, True, True]
parents = [-1, 0, 1]
segs = arkit_segments(arr2d, valid, parents)
assert segs == [(0.5, 0.5, 0.0, 0.0), (1.0, 1.0, 0.5, 0.5)]
assert segs == [(0.2, 0.0, 0.0, 0.0), (0.4, 0.0, 0.2, 0.0)]
def test_segments_drop_invalid_endpoint():
arr2d = [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)]
arr2d = [(0.0, 0.0), (0.2, 0.0), (0.4, 0.0)]
valid = [True, False, True] # joint 1 invalid
parents = [-1, 0, 1]
# bone (1,0) drops (1 invalid); bone (2,1) drops (1 invalid)
@@ -29,6 +30,53 @@ def test_segments_drop_invalid_endpoint():
def test_segments_valid_none_keeps_all():
arr2d = [(0.0, 0.0), (0.5, 0.5)]
arr2d = [(0.0, 0.0), (0.3, 0.0)] # bone length 0.3 < max_len=0.5
parents = [-1, 0]
assert arkit_segments(arr2d, None, parents) == [(0.5, 0.5, 0.0, 0.0)]
assert arkit_segments(arr2d, None, parents) == [(0.3, 0.0, 0.0, 0.0)]
# ---------------------------------------------------------------------------
# New filter tests (bounds + max_len)
# ---------------------------------------------------------------------------
def test_segments_out_of_bounds_endpoint_skipped():
"""Endpoint outside [-bounds, 1+bounds] → bone skipped."""
# Joint 1 at x=-0.2 is outside [-0.1, 1.1]
arr2d = [(0.5, 0.5), (-0.2, 0.5), (0.7, 0.5)]
valid = [True, True, True]
parents = [-1, 0, 1]
# bone (1,0): joint 1 out of bounds → skip
# bone (2,1): joint 1 out of bounds → skip
segs = arkit_segments(arr2d, valid, parents, bounds=0.1)
assert segs == []
def test_segments_overlong_bone_skipped():
"""Bone length > max_len → bone skipped."""
# Bone from (0.0, 0.5) to (1.0, 0.5): length=1.0 > max_len=0.5
arr2d = [(0.0, 0.5), (1.0, 0.5)]
valid = [True, True]
parents = [-1, 0]
segs = arkit_segments(arr2d, valid, parents, max_len=0.5)
assert segs == []
def test_segments_normal_skeleton_passes_with_defaults():
"""Realistic bones (length < 0.5, coords in [0,1]) pass all filters."""
arr2d = [(0.5, 0.3), (0.5, 0.6), (0.5, 0.8)] # vertical spine-like
valid = [True, True, True]
parents = [-1, 0, 1]
segs = arkit_segments(arr2d, valid, parents)
# Both bones length = 0.3 < max_len=0.5; all coords in [0,1] ⊂ [-0.1,1.1]
assert len(segs) == 2
assert segs[0] == (0.5, 0.6, 0.5, 0.3)
assert segs[1] == (0.5, 0.8, 0.5, 0.6)
def test_segments_in_bounds_endpoint_passes():
"""Endpoint exactly at boundary (0.0, 1.0) is kept."""
arr2d = [(0.5, 0.5), (0.5, 0.2)] # bone length 0.3 < 0.5
valid = [True, True]
parents = [-1, 0]
segs = arkit_segments(arr2d, valid, parents, bounds=0.1)
assert len(segs) == 1