Download src/smoothing.py from ayzeksalimli/pose-driven-3d-characters: direct link, hf CLI and curl.
- Browser
- Download file 3.57 kB
-
https://huggingface.co/ayzeksalimli/pose-driven-3d-characters/resolve/main/src/smoothing.py
- Command line
-
hf download hf://ayzeksalimli/pose-driven-3d-characters/src/smoothing.py
-
curl -L -o smoothing.py https://huggingface.co/ayzeksalimli/pose-driven-3d-characters/resolve/main/src/smoothing.py
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 | |
| 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 | |