Skip to content

Commit b1071b4

Browse files
committed
fixed gcv filtering (careful if series too short: noise can be considered as signal -> no filtering)
1 parent 12c16de commit b1071b4

File tree

6 files changed

+60
-78
lines changed

6 files changed

+60
-78
lines changed

Pose2Sim/Demo_Batch/Config.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ make_c3d = true # also save triangulated data in c3d format
208208

209209
# Most intuitive and standard filter in biomechanics
210210
[filtering.butterworth]
211-
order = 4
212211
cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
212+
order = 4
213213

214214
# Used in countless applications, this one is a simplified Kalman filter
215215
[filtering.kalman]
@@ -219,8 +219,8 @@ make_c3d = true # also save triangulated data in c3d format
219219

220220
# Automatically determines optimal parameters for each point, which is good when some move faster than others (eg fingers vs hips).
221221
[filtering.gcv_spline]
222-
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is sometimes unstable
223-
smoothing_factor = 0.1 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
222+
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered)
223+
smoothing_factor = 1.0 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
224224

225225
[filtering.loess]
226226
nb_values_used = 5 # = fraction of data used * nb frames

Pose2Sim/Demo_Batch/Trial_1/Config.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@
207207

208208
# # Most intuitive and standard filter in biomechanics
209209
# [filtering.butterworth]
210-
# order = 4
211210
# cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
211+
# order = 4
212212

213213
# # Used in countless applications, this one is a simplified Kalman filter
214214
# [filtering.kalman]
@@ -218,8 +218,8 @@
218218

219219
# # Automatically determines optimal parameters for each point, which is good when some move faster than others (eg fingers vs hips).
220220
# [filtering.gcv_spline]
221-
# cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is sometimes unstable
222-
# smoothing_factor = 0.1 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
221+
# cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered)
222+
# smoothing_factor = 1.0 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
223223

224224
# [filtering.loess]
225225
# nb_values_used = 5 # = fraction of data used * nb frames

Pose2Sim/Demo_Batch/Trial_2/Config.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ keypoints_to_consider = ['RWrist'] # 'all' if all points should be considered, f
207207

208208
# # Most intuitive and standard filter in biomechanics
209209
# [filtering.butterworth]
210-
# order = 4
211210
# cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
211+
# order = 4
212212

213213
# # Used in countless applications, this one is a simplified Kalman filter
214214
# [filtering.kalman]
@@ -218,8 +218,8 @@ keypoints_to_consider = ['RWrist'] # 'all' if all points should be considered, f
218218

219219
# # Automatically determines optimal parameters for each point, which is good when some move faster than others (eg fingers vs hips).
220220
# [filtering.gcv_spline]
221-
# cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is sometimes unstable
222-
# smoothing_factor = 0.1 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
221+
# cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered)
222+
# smoothing_factor = 1.0 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
223223

224224
# [filtering.loess]
225225
# nb_values_used = 5 # = fraction of data used * nb frames

Pose2Sim/Demo_MultiPerson/Config.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,15 @@ make_c3d = true # save triangulated data in c3d format in addition to trc
201201
[filtering]
202202
reject_outliers = true # Hampel filter for outlier rejection before other filtering methods. Can be slow. Rejects outliers that are outside of a 95% confidence interal from the median in a sliding window of size 7.
203203
filter = true # Proceed to further filtering after outlier rejection.
204-
type = 'butterworth' # butterworth, gvc_spline, kalman, gaussian, LOESS, median, butterworth_on_speed
204+
type = 'butterworth' # butterworth, kalman, gvc_spline, gaussian, LOESS, median, butterworth_on_speed
205205

206206
display_figures = true # true or false (lowercase)
207207
make_c3d = true # also save triangulated data in c3d format
208208

209209
# Most intuitive and standard filter in biomechanics
210210
[filtering.butterworth]
211-
order = 4
212211
cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
212+
order = 4
213213

214214
# Used in countless applications, this one is a simplified Kalman filter
215215
[filtering.kalman]
@@ -219,8 +219,8 @@ make_c3d = true # also save triangulated data in c3d format
219219

220220
# Automatically determines optimal parameters for each point, which is good when some move faster than others (eg fingers vs hips).
221221
[filtering.gcv_spline]
222-
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is sometimes unstable
223-
smoothing_factor = 0.1 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
222+
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered)
223+
smoothing_factor = 1.0 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
224224

225225
[filtering.loess]
226226
nb_values_used = 5 # = fraction of data used * nb frames

Pose2Sim/Demo_SinglePerson/Config.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,15 @@ make_c3d = true # save triangulated data in c3d format in addition to trc
201201
[filtering]
202202
reject_outliers = true # Hampel filter for outlier rejection before other filtering methods. Can be slow. Rejects outliers that are outside of a 95% confidence interal from the median in a sliding window of size 7.
203203
filter = true # Proceed to further filtering after outlier rejection.
204-
type = 'butterworth' # butterworth, gvc_spline, kalman, gaussian, LOESS, median, butterworth_on_speed
204+
type = 'butterworth' # butterworth, kalman, gvc_spline, gaussian, LOESS, median, butterworth_on_speed
205205

206206
display_figures = true # true or false (lowercase)
207207
make_c3d = true # also save triangulated data in c3d format
208208

209209
# Most intuitive and standard filter in biomechanics
210210
[filtering.butterworth]
211-
order = 4
212211
cut_off_frequency = 6 # Hz # Will be divided by slowmo_factor to be equivalent to non slowed-down video
212+
order = 4
213213

214214
# Used in countless applications, this one is a simplified Kalman filter
215215
[filtering.kalman]
@@ -219,8 +219,8 @@ make_c3d = true # also save triangulated data in c3d format
219219

220220
# Automatically determines optimal parameters for each point, which is good when some move faster than others (eg fingers vs hips).
221221
[filtering.gcv_spline]
222-
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is sometimes unstable
223-
smoothing_factor = 0.1 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
222+
cut_off_frequency = 'auto' # 'auto' or int # If int, behaves like a Butterworth filter. 'auto' is usually better, unless the signal is too short (noise can then be considered as signal -> trajectories not filtered)
223+
smoothing_factor = 1.0 # >=0, ignored if cut_off_frequency != 'auto'. Biases results towards more smoothing (>1) or more fidelity to data (<1)
224224

225225
[filtering.loess]
226226
nb_values_used = 5 # = fraction of data used * nb frames

Pose2Sim/filtering.py

Lines changed: 43 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@
3131
import logging
3232

3333
from scipy import signal
34+
from scipy.interpolate import make_smoothing_spline
35+
3436
from scipy.interpolate._bsplines import _coeff_of_divided_diff, _compute_optimal_gcv_parameter
35-
from scipy._lib._array_api import array_namespace
3637
from scipy.interpolate import BSpline
37-
from scipy.linalg import solve_banded
38-
from numpy.core.multiarray import normalize_axis_index
3938
from scipy.ndimage import gaussian_filter1d
4039
from statsmodels.nonparametric.smoothers_lowess import lowess
4140
from filterpy.kalman import KalmanFilter
@@ -81,19 +80,35 @@ def hampel_filter(col, window_size=7, n_sigma=2):
8180
return col_filtered
8281

8382

84-
def make_smoothing_spline_with_bias(x, y, bias_factor=1.0, w=None, lam=None, *, axis=0):
85-
83+
def _compute_optimal_gcv_parameter_numstable(x, y):
8684
'''
87-
Reimplemented the full make_smoothing_spline function but make it accept a bias factor towards smoothing or fidelity to data.
88-
Identical to https://github.com/scipy/scipy/blob/main/scipy/interpolate/_bsplines.py#L2210
89-
but multiplies the automatically estimated optimal lambda value by the bias factor.
90-
91-
INPUTS:
92-
- bias_factor > 1 for smoother output, < 1 for more fidelity to data
85+
Makes x values spaced 1 apart, to make sure the optimal lambda value
86+
is correctly computed.
87+
See https://stackoverflow.com/a/79740481/12196632
9388
'''
89+
90+
x_spacing = np.diff(x)
91+
assert (x_spacing >= 0).all(), "x must be sorted"
92+
x_spacing_avg = x_spacing.mean()
93+
assert x_spacing_avg != 0, "div by zero"
94+
95+
# x values spaced 1 apart
96+
new_x = x / x_spacing_avg
97+
X, wE, y, w = get_smoothing_spline_intermediate_arrays(new_x, y)
98+
99+
# Rescale the value of lambda we found back to the original problem
100+
lam = _compute_optimal_gcv_parameter(X, wE, y, w) * x_spacing_avg ** 3
101+
102+
return lam
94103

95-
xp = array_namespace(x, y)
96104

105+
def get_smoothing_spline_intermediate_arrays(x, y, w=None):
106+
'''
107+
Used by _compute_optimal_gcv_parameter_numstable to compute the optimal lambda value for a smoothing spline.
108+
See https://stackoverflow.com/a/79740481/12196632
109+
'''
110+
111+
axis = 0
97112
x = np.ascontiguousarray(x, dtype=float)
98113
y = np.ascontiguousarray(y, dtype=float)
99114

@@ -116,7 +131,6 @@ def make_smoothing_spline_with_bias(x, y, bias_factor=1.0, w=None, lam=None, *,
116131
if n <= 4:
117132
raise ValueError('``x`` and ``y`` length must be at least 5')
118133

119-
axis = normalize_axis_index(axis, y.ndim)
120134
y = np.moveaxis(y, axis, 0)
121135
y_shape1 = y.shape[1:]
122136
if y_shape1 != ():
@@ -146,40 +160,7 @@ def make_smoothing_spline_with_bias(x, y, bias_factor=1.0, w=None, lam=None, *,
146160
wE[:-1, -2] = -_coeff_of_divided_diff(x[-4:]) / w[-4:]
147161
wE[:-2, -1] = _coeff_of_divided_diff(x[-3:]) / w[-3:]
148162
wE *= 6
149-
150-
# MULTIPLY LAM BY BIAS FACTOR
151-
if lam is None:
152-
lam = _compute_optimal_gcv_parameter(X, wE, y, w)
153-
lam *= bias_factor
154-
# print(lam)
155-
elif lam < 0.:
156-
raise ValueError('Regularization parameter should be non-negative')
157-
158-
# solve the initial problem in the basis of natural splines
159-
if np.ndim(lam) == 0:
160-
c = solve_banded((2, 2), X + lam * wE, y)
161-
elif np.ndim(lam) == 1:
162-
# XXX: solve_banded does not suppport batched `ab` matrices; loop manually
163-
c = np.empty((n, lam.shape[0]))
164-
for i in range(lam.shape[0]):
165-
c[:, i] = solve_banded((2, 2), X + lam[i] * wE, y[:, i])
166-
else:
167-
# this should not happen, ever
168-
raise RuntimeError("Internal error, please report it to SciPy developers.")
169-
c = c.reshape((c.shape[0], *y_shape1))
170-
171-
# hack: these are c[0], c[1] etc, shape-compatible with np.r_ below
172-
c0, c1 = c[0:1, ...], c[1:2, ...] # c[0], c[1]
173-
cm0, cm1 = c[-1:-2:-1, ...], c[-2:-3:-1, ...] # c[-1], c[-2]
174-
175-
c_ = np.r_[c0 * (t[5] + t[4] - 2 * t[3]) + c1,
176-
c0 * (t[5] - t[3]) + c1,
177-
c[1: -1, ...],
178-
cm0 * (t[-4] - t[-6]) + cm1,
179-
cm0 * (2 * t[-4] - t[-5] - t[-6]) + cm1]
180-
181-
t, c_ = xp.asarray(t), xp.asarray(c_)
182-
return BSpline.construct_fast(t, c_, 3, axis=axis)
163+
return X, wE, y, w
183164

184165

185166
def gcv_spline_filter_1d(config_dict, frame_rate, col):
@@ -219,34 +200,35 @@ def gcv_spline_filter_1d(config_dict, frame_rate, col):
219200

220201
# Automatically determine optimal lambda value when cutoff is 'auto'
221202
if cutoff == 'auto':
222-
# Normalize x (0-100), because (0-1) leads to instabilities in lam = _compute_optimal_gcv_parameter(X, wE, y, w)
223-
# See https://stackoverflow.com/questions/79736522/automatic-smoothing-parameters-with-make-smoothing-spline-go-wrong
224-
min_x, max_x = np.min(x), np.max(x)
225-
range_x = max_x - min_x if max_x > min_x else 1.0
226-
x_norm = 100 * (x - min_x) / range_x
227-
228203
# Normalize col around 1, because zero mean leads to unstabilities
229204
median_col = np.median(col_filtered[seq_f]) # median of time series
230205
mad_col = np.median(np.abs(col_filtered[seq_f] - median_col)) # median absolute deviation from median
231206
mad_col = mad_col if mad_col > 0 else 1.0
232207
col_norm = 1 + (col_filtered[seq_f] - median_col) / (1.4826 * mad_col) # 1.4826*mad_col equivalent to dividing by std (for col_norm to have a std of 1)
233-
# mean_col = np.mean(col_filtered[seq_f])
234-
# std_col = np.std(col_filtered[seq_f]) if np.std(col_filtered[seq_f]) > 0 else 1.0
235-
# col_norm = (col_filtered[seq_f] - mean_col) / std_col
236208

237-
# Bias lambda value towards either smoothing or fidelity to data
238-
spline = make_smoothing_spline_with_bias(x_norm, col_norm, bias_factor=smoothing_factor, lam=None)
239-
col_filtered_norm = spline(x_norm)
209+
# Compute optimal lam
210+
# See https://stackoverflow.com/a/79740481/12196632
211+
lam = _compute_optimal_gcv_parameter_numstable(x, col_norm)
240212

213+
# More smoothing if smoothing_factor > 1, more fidelity to data if < 1
214+
lam *= smoothing_factor
215+
216+
# Compute spline
217+
spline = make_smoothing_spline(x, col_norm, w=None, lam=lam)
218+
col_filtered_norm = spline(x)
219+
241220
# Denormalize data
242221
col_filtered[seq_f] = (col_filtered_norm - 1) * (1.4826 * mad_col) + median_col
243-
# col_filtered[seq_f] = col_filtered_norm * std_col + mean_col
244222

245223
else:
246224
# Estimate lam from cutoff frequency
247225
lam = ((frame_rate / (2 * np.pi * float(cutoff))) ** 4)
226+
227+
# More smoothing if smoothing_factor > 1, more fidelity to data if < 1
228+
lam *= smoothing_factor
248229

249-
spline = make_smoothing_spline_with_bias(x, col_filtered[seq_f], bias_factor=1.0, lam=lam)
230+
# Compute spline
231+
spline = make_smoothing_spline(x, col_filtered[seq_f], w=None, lam=lam)
250232
col_filtered[seq_f] = spline(x)
251233

252234
return col_filtered

0 commit comments

Comments
 (0)