ayzeksalimli's picture
Push project (code, README, Docker/compose, models) — no .github/workflows
3972fea verified
Raw
History Blame Contribute Delete
3.57 kB
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