from __future__ import annotations import math import numpy as np class _LowPass: """Exponential low-pass filter.""" def __init__(self) -> None: self.y: np.ndarray | None = None def __call__(self, x: np.ndarray, alpha: float) -> np.ndarray: """Filter one sample.""" if self.y is None: self.y = x.astype(np.float64) else: self.y = alpha * x + (1.0 - alpha) * self.y return self.y def reset(self) -> None: """Clear filter state.""" self.y = None class OneEuroFilter: """One-euro cursor filter.""" def __init__( self, freq: float = 30.0, min_cutoff: float = 1.2, beta: float = 0.05, d_cutoff: float = 1.0, ) -> None: self.freq = freq self.min_cutoff = min_cutoff self.beta = beta self.d_cutoff = d_cutoff self._x = _LowPass() self._dx = _LowPass() self._prev: np.ndarray | None = None @staticmethod def _alpha(cutoff: float, freq: float) -> float: """Smoothing factor from cutoff.""" tau = 1.0 / (2.0 * math.pi * cutoff) te = 1.0 / freq return 1.0 / (1.0 + tau / te) def reset(self) -> None: """Clear filter state.""" self._x.reset() self._dx.reset() self._prev = None def __call__(self, point, dt: float | None = None) -> np.ndarray: """Filter one point.""" x = np.asarray(point, dtype=np.float64) if dt is not None and dt > 1e-6: self.freq = 1.0 / dt prev = self._prev if self._prev is not None else x dx = (x - prev) * self.freq self._prev = x edx = self._dx(dx, self._alpha(self.d_cutoff, self.freq)) cutoff = self.min_cutoff + self.beta * float(np.linalg.norm(edx)) return self._x(x, self._alpha(cutoff, self.freq)) class LandmarkFilter: """One-euro filter for landmarks.""" def __init__(self, min_cutoff: float = 0.8, beta: float = 0.03) -> None: self.min_cutoff = min_cutoff self.beta = beta self._x = _LowPass() self._dx = _LowPass() self._prev: np.ndarray | None = None self.freq = 30.0 def reset(self) -> None: """Clear filter state.""" self._x.reset() self._dx.reset() self._prev = None def __call__(self, points: np.ndarray, dt: float | None = None) -> np.ndarray: """Filter all keypoints.""" x = np.asarray(points, dtype=np.float64) if dt is not None and dt > 1e-6: self.freq = 1.0 / dt if self._prev is None or self._prev.shape != x.shape: self.reset() self._prev = x return self._x(x, 1.0) dx = (x - self._prev) * self.freq self._prev = x edx = self._dx(dx, OneEuroFilter._alpha(self.d_cutoff, self.freq)) speed = float(np.linalg.norm(edx, axis=-1).mean()) cutoff = self.min_cutoff + self.beta * speed return self._x(x, OneEuroFilter._alpha(cutoff, self.freq)) d_cutoff = 1.0 class ScalarEMA: """Scalar exponential moving average.""" def __init__(self, alpha: float = 0.35, value: float = 0.0) -> None: self.alpha = alpha self.value = value def __call__(self, target: float) -> float: """Advance toward target.""" self.value += self.alpha * (target - self.value) return self.value def set(self, value: float) -> None: """Force current value.""" self.value = value