feat(edge): One-Euro keypoint smoothing (INT8 jitter)
This commit is contained in:
parent
444c01407a
commit
991178a896
2 changed files with 71 additions and 0 deletions
49
edge/smoothing.py
Normal file
49
edge/smoothing.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import math
|
||||||
|
|
||||||
|
from edge.models import Keypoint
|
||||||
|
|
||||||
|
|
||||||
|
class OneEuroFilter:
|
||||||
|
"""1€ filter (Casiez et al.). 저지연 지터 감쇠."""
|
||||||
|
|
||||||
|
def __init__(self, min_cutoff: float = 1.0, beta: float = 0.0, d_cutoff: float = 1.0):
|
||||||
|
self.min_cutoff = min_cutoff
|
||||||
|
self.beta = beta
|
||||||
|
self.d_cutoff = d_cutoff
|
||||||
|
self._x_prev: float | None = None
|
||||||
|
self._dx_prev: float = 0.0
|
||||||
|
self._t_prev: float | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _alpha(cutoff: float, dt: float) -> float:
|
||||||
|
tau = 1.0 / (2 * math.pi * cutoff)
|
||||||
|
return 1.0 / (1.0 + tau / dt)
|
||||||
|
|
||||||
|
def filter(self, value: float, t: float) -> float:
|
||||||
|
if self._x_prev is None or self._t_prev is None or t <= self._t_prev:
|
||||||
|
self._x_prev, self._t_prev, self._dx_prev = value, t, 0.0
|
||||||
|
return value
|
||||||
|
dt = t - self._t_prev
|
||||||
|
dx = (value - self._x_prev) / dt
|
||||||
|
a_d = self._alpha(self.d_cutoff, dt)
|
||||||
|
dx_hat = a_d * dx + (1 - a_d) * self._dx_prev
|
||||||
|
cutoff = self.min_cutoff + self.beta * abs(dx_hat)
|
||||||
|
a = self._alpha(cutoff, dt)
|
||||||
|
x_hat = a * value + (1 - a) * self._x_prev
|
||||||
|
self._x_prev, self._dx_prev, self._t_prev = x_hat, dx_hat, t
|
||||||
|
return x_hat
|
||||||
|
|
||||||
|
|
||||||
|
class KeypointSmoother:
|
||||||
|
"""관절 17개 각각 x,y에 독립 1€ 필터."""
|
||||||
|
|
||||||
|
def __init__(self, min_cutoff: float = 1.0, beta: float = 0.007):
|
||||||
|
self._fx = [OneEuroFilter(min_cutoff, beta) for _ in range(17)]
|
||||||
|
self._fy = [OneEuroFilter(min_cutoff, beta) for _ in range(17)]
|
||||||
|
|
||||||
|
def smooth(self, keypoints: list[Keypoint], t: float) -> list[Keypoint]:
|
||||||
|
out = []
|
||||||
|
for i, k in enumerate(keypoints):
|
||||||
|
out.append(Keypoint(self._fx[i].filter(k.x, t),
|
||||||
|
self._fy[i].filter(k.y, t), k.conf))
|
||||||
|
return out
|
||||||
22
tests/test_smoothing.py
Normal file
22
tests/test_smoothing.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from edge.models import Keypoint
|
||||||
|
from edge.smoothing import OneEuroFilter, KeypointSmoother
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_euro_first_sample_returns_input():
|
||||||
|
f = OneEuroFilter(min_cutoff=1.0, beta=0.0)
|
||||||
|
assert f.filter(5.0, t=0.0) == 5.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_euro_attenuates_jitter():
|
||||||
|
f = OneEuroFilter(min_cutoff=0.5, beta=0.0)
|
||||||
|
f.filter(0.0, t=0.0)
|
||||||
|
out = f.filter(10.0, t=0.1) # big jump at low cutoff -> damped below input
|
||||||
|
assert 0.0 < out < 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_keypoint_smoother_preserves_conf_and_length():
|
||||||
|
s = KeypointSmoother()
|
||||||
|
kps = [Keypoint(float(i), float(i), 0.8) for i in range(17)]
|
||||||
|
out = s.smooth(kps, t=0.0)
|
||||||
|
assert len(out) == 17
|
||||||
|
assert out[3].conf == 0.8
|
||||||
Loading…
Reference in a new issue