File size: 3,571 Bytes
3972fea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 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
|