quyu666's picture
sync processor from GitLab 32d515b: glove display fixes, GPU acceleration, AI annotation propose-clip fix
95f5290
Raw History Blame Contribute Delete
4.55 kB
"""Self-contained D435 3D tracking helpers for the black-glove workflow.
This file deliberately contains no import from the standalone v1.0 project.
It keeps only the numerical parts needed after the existing YOLO/RTMPose
detector has produced 2D hands: depth outlier gating and two-slot alpha-beta
tracking. Predicted points are always marked as ``propagated`` by the caller.
"""
from __future__ import annotations
import numpy as np
BAND_HALF_M = 0.12
BAND_MIN_VALID = 4
GATE_M = 0.15
def gate_observations(points: np.ndarray, prediction: np.ndarray | None,
gate: float = GATE_M,
wholesale_frac: float = 0.6) -> tuple[np.ndarray, bool]:
"""Reject implausible 3D joints against the slot prediction.
Individual jumps become NaN and are left for the alpha-beta tracker to
predict. If most joints jump together, return ``wholesale=True`` so the
caller can wait for a consistent observation before re-seeding the slot.
"""
points = np.asarray(points, np.float64).reshape(21, 3).copy()
if prediction is None:
return points, False
prediction = np.asarray(prediction, np.float64).reshape(21, 3)
finite = np.isfinite(points).all(axis=1)
pred_finite = np.isfinite(prediction).all(axis=1)
comparable = finite & pred_finite
if int(comparable.sum()) < BAND_MIN_VALID:
return points, False
distance = np.linalg.norm(points - prediction, axis=1)
suspect = comparable & (distance > float(gate))
if int(suspect.sum()) >= wholesale_frac * int(comparable.sum()):
return points, True
points[suspect] = np.nan
return points, False
class AlphaBetaHandSlots:
"""Two fixed hand slots with constant-velocity alpha-beta prediction."""
def __init__(self, max_lost: int = 15, alpha: float = 0.5,
beta: float = 0.1):
self.max_lost = max(1, int(max_lost))
self.alpha = float(alpha)
self.beta = float(beta)
self.slots = [self._new_slot(), self._new_slot()]
@staticmethod
def _new_slot() -> dict:
return {"label": "", "x": None, "v": None,
"last_t": None, "lost": 0}
def reset(self, slot: int, label: str = "") -> None:
self.slots[int(slot)] = self._new_slot()
self.slots[int(slot)]["label"] = str(label or "")
def slot_label(self, slot: int) -> str:
return str(self.slots[int(slot)].get("label") or "")
def observe(self, slot: int, label: str, points: np.ndarray,
frame_index: int) -> None:
slot = int(slot)
state = self.slots[slot]
label = str(label or "")
measured = np.asarray(points, np.float64).reshape(21, 3)
if state["label"] and label and state["label"] != label:
self.reset(slot, label)
state = self.slots[slot]
state["label"] = label or state["label"]
if state["x"] is None:
state["x"] = measured.copy()
state["v"] = np.zeros_like(measured)
state["last_t"] = int(frame_index)
state["lost"] = 0
return
dt = max(float(frame_index - state["last_t"]), 1.0e-3)
if dt > self.max_lost:
state["x"] = measured.copy()
state["v"] = np.zeros_like(measured)
state["last_t"] = int(frame_index)
state["lost"] = 0
return
previous = state["x"]
velocity = state["v"]
prediction = previous + velocity * dt
finite = np.isfinite(measured).all(axis=1)
updated = np.where(finite[:, None],
self.alpha * measured + (1.0 - self.alpha) * prediction,
prediction)
updated_velocity = np.where(
finite[:, None],
self.beta * (updated - previous) / dt + (1.0 - self.beta) * velocity,
velocity,
)
state["x"] = updated
state["v"] = updated_velocity
state["last_t"] = int(frame_index)
state["lost"] = 0
def mark_lost(self, slot: int) -> None:
self.slots[int(slot)]["lost"] += 1
def predict(self, slot: int, frame_index: int) -> np.ndarray | None:
state = self.slots[int(slot)]
if state["x"] is None or int(state["lost"]) > self.max_lost:
return None
dt = max(float(frame_index - state["last_t"]), 0.0)
return np.asarray(state["x"] + state["v"] * dt, np.float64)
def close(self) -> None:
self.slots = [self._new_slot(), self._new_slot()]