MayaFlux
0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
HampelFilter.hpp
Go to the documentation of this file.
1
#pragma once
2
3
#include "
MayaFlux/Transitive/Memory/RingBuffer.hpp
"
4
5
#include <glm/glm.hpp>
6
7
namespace
MayaFlux::Kinesis
{
8
9
/**
10
* @class HampelFilter
11
* @brief Holds the last accepted value when a new sample looks like an
12
* isolated outlier relative to its recent neighbors.
13
*
14
* Implements the Hampel identifier: a candidate is rejected when its
15
* deviation from the median of a recent window exceeds threshold_mad
16
* scaled median absolute deviations. This is the standard robust
17
* alternative to a mean/standard-deviation outlier test, chosen
18
* because a single genuine spike should not be allowed to inflate the
19
* threshold that screens for itself: one wild sample shifts a window's
20
* mean and stddev substantially, but only shifts a median by at most
21
* one rank. The hold-last-value response on rejection is the filtering
22
* variant of the Hampel identifier, as opposed to flag-only detection.
23
*
24
* Differential's derivatives amplify noise: a single bad raw sample that
25
* survives upstream smoothing (Stochastic::Estimate) still gets
26
* multiplied by 1/dt^2 or 1/dt^3, producing one wildly spiking
27
* acceleration or jerk reading even though the underlying motion was
28
* smooth. HampelFilter screens for that specific shape of problem, an
29
* isolated one-sample spike in an already-differentiated signal, which
30
* is a different concern from Estimate's job of characterizing an
31
* evolving raw stream's noise floor. HampelFilter has no notion of a
32
* learned floor and does not filter a raw signal; it only asks whether
33
* one candidate value is consistent with its immediate recent history.
34
*
35
* On rejection, this returns the last accepted value rather than the
36
* candidate, silently. This adds up to one sample of lag on a genuine
37
* fast transient that happens to also look statistically unusual,
38
* which is the deliberate tradeoff: a caller feeding this into a
39
* visual parameter or gesture classifier is generally better served
40
* by a held value than a single-frame glitch. A caller that needs to
41
* know a rejection happened rather than have it silently smoothed over
42
* should not use this class; it is intentionally not a flag-only tool.
43
*
44
* @tparam T Sample type. Must support operator-, and the resulting
45
* difference type must be usable with scalar_t<T> reductions
46
* (a plain arithmetic value directly, a glm vector via its
47
* magnitude). See magnitude_of() below for the exact rule.
48
*/
49
template
<
typename
T>
50
class
HampelFilter
{
51
public
:
52
/**
53
* @brief Construct a filter
54
* @param window Number of recent accepted samples to test new
55
* candidates against, minimum 3 since MAD needs at least a
56
* few points to be meaningful
57
* @param threshold_mad Number of scaled-MAD units beyond which a
58
* candidate is rejected, typically 3.0 to 4.0
59
* @param max_consecutive_rejections Forces the next candidate to be
60
* accepted unconditionally, and clears history to restart
61
* the window from it, once this many candidates in a row
62
* have been rejected. Without this, a real sustained change
63
* in the signal (which any consecutive Differential reading
64
* during real motion looks like relative to an older,
65
* now-stale window) can never be re-accepted: once every
66
* recent candidate is rejected against a frozen median, the
67
* filter locks onto whatever value it last accepted and
68
* holds it indefinitely, since a rejected candidate is never
69
* added to the window that future candidates are tested
70
* against. Default 3, since three consecutive genuine
71
* outliers from the same underlying cause is already an
72
* unusual coincidence; more than that is far more likely to
73
* be a real change the window has fallen behind.
74
*/
75
explicit
HampelFilter
(
size_t
window = 8,
double
threshold_mad = 3.5,
76
size_t
max_consecutive_rejections = 3)
77
:
m_history
(window < 3 ? 3 : window)
78
,
m_threshold_mad
(threshold_mad)
79
,
m_max_consecutive_rejections
(max_consecutive_rejections)
80
,
m_last_good
(T {})
81
,
m_has_history
(false)
82
{
83
}
84
85
/**
86
* @brief Test a candidate value and return the value to actually use
87
* @param candidate Newly computed value, typically straight out of
88
* a Kinesis::Differential function
89
* @return candidate if it looks consistent with recent history, or
90
* the last accepted value if candidate looks like an
91
* isolated spike
92
*
93
* The first call and the next (window - 1) calls always accept,
94
* since MAD is not meaningful against a window that has not yet
95
* filled with real data; a held value from an uninitialized filter
96
* would be an arbitrary default, not a real prior reading.
97
*/
98
T
accept
(
const
T& candidate)
99
{
100
if
(!
m_has_history
) {
101
m_history
.push(candidate);
102
m_last_good
= candidate;
103
++
m_sample_count
;
104
if
(
m_sample_count
>=
m_history
.capacity())
105
m_has_history
=
true
;
106
return
candidate;
107
}
108
109
const
auto
view =
m_history
.linearized_view();
110
111
std::vector<double> magnitudes;
112
magnitudes.reserve(view.size());
113
for
(
const
auto
& v : view)
114
magnitudes.push_back(
magnitude_of
(v));
115
116
std::vector<double> sorted = magnitudes;
117
std::ranges::sort(sorted);
118
const
double
median_mag = sorted[sorted.size() / 2];
119
120
std::vector<double> deviations;
121
deviations.reserve(sorted.size());
122
for
(
double
m : sorted)
123
deviations.push_back(std::abs(m - median_mag));
124
std::ranges::sort(deviations);
125
const
double
mad = deviations[deviations.size() / 2] * 1.4826;
126
127
const
double
candidate_mag =
magnitude_of
(candidate);
128
const
bool
looks_like_spike = (mad > 1e-12)
129
&& (std::abs(candidate_mag - median_mag) / mad >
m_threshold_mad
);
130
131
if
(looks_like_spike &&
m_consecutive_rejections
<
m_max_consecutive_rejections
) {
132
++
m_consecutive_rejections
;
133
return
m_last_good
;
134
}
135
136
if
(looks_like_spike) {
137
m_history
.reset();
138
m_sample_count
= 0;
139
m_has_history
=
false
;
140
}
141
142
m_consecutive_rejections
= 0;
143
m_history
.push(candidate);
144
m_last_good
= candidate;
145
++
m_sample_count
;
146
if
(
m_sample_count
>=
m_history
.capacity())
147
m_has_history
=
true
;
148
return
candidate;
149
}
150
151
/**
152
* @brief Reset to uninitialized state
153
*
154
* Call on a known discontinuity (device reconnect, deliberate
155
* large jump the caller does not want screened as a spike) so the
156
* next window's worth of candidates re-accepts unconditionally
157
* rather than being tested against now-stale history.
158
*/
159
void
reset
()
160
{
161
m_history
.reset();
162
m_last_good
= T {};
163
m_has_history
=
false
;
164
m_sample_count
= 0;
165
m_consecutive_rejections
= 0;
166
}
167
168
/**
169
* @brief Current last accepted value
170
*/
171
[[nodiscard]]
const
T&
last_good
()
const
{
return
m_last_good
; }
172
173
/**
174
* @brief Whether the filter has enough history to actually screen candidates
175
*
176
* False during the initial window-filling period, when accept()
177
* always returns its argument unconditionally.
178
*/
179
[[nodiscard]]
bool
is_active
()
const
{
return
m_has_history
; }
180
181
private
:
182
/**
183
* @brief Scalar reduction used for the MAD comparison
184
*
185
* Plain arithmetic T reduces to its absolute value directly.
186
* glm vector T reduces to its length. This means HampelFilter
187
* screens on magnitude of change, not direction: a candidate whose
188
* magnitude is consistent with recent history is accepted even if
189
* its direction differs, which is deliberate, since a real sharp
190
* turn is a direction change with unremarkable magnitude and
191
* should not be screened out as if it were a spike.
192
*/
193
template
<
typename
U>
194
static
double
magnitude_of
(
const
U& v)
noexcept
195
{
196
if
constexpr
(
requires
{ glm::length(v); }) {
197
return
static_cast<
double
>
(glm::length(v));
198
}
else
{
199
return
static_cast<
double
>
(std::abs(v));
200
}
201
}
202
203
Memory::HistoryBuffer<T>
m_history
;
204
double
m_threshold_mad
;
205
size_t
m_max_consecutive_rejections
;
206
T
m_last_good
;
207
bool
m_has_history
;
208
size_t
m_sample_count
{ 0 };
209
size_t
m_consecutive_rejections
{ 0 };
210
};
211
212
}
// namespace MayaFlux::Kinesis
RingBuffer.hpp
MayaFlux::Kinesis::HampelFilter::m_threshold_mad
double m_threshold_mad
Definition
HampelFilter.hpp:204
MayaFlux::Kinesis::HampelFilter::last_good
const T & last_good() const
Current last accepted value.
Definition
HampelFilter.hpp:171
MayaFlux::Kinesis::HampelFilter::accept
T accept(const T &candidate)
Test a candidate value and return the value to actually use.
Definition
HampelFilter.hpp:98
MayaFlux::Kinesis::HampelFilter::m_last_good
T m_last_good
Definition
HampelFilter.hpp:206
MayaFlux::Kinesis::HampelFilter::m_history
Memory::HistoryBuffer< T > m_history
Definition
HampelFilter.hpp:203
MayaFlux::Kinesis::HampelFilter::is_active
bool is_active() const
Whether the filter has enough history to actually screen candidates.
Definition
HampelFilter.hpp:179
MayaFlux::Kinesis::HampelFilter::m_max_consecutive_rejections
size_t m_max_consecutive_rejections
Definition
HampelFilter.hpp:205
MayaFlux::Kinesis::HampelFilter::m_sample_count
size_t m_sample_count
Definition
HampelFilter.hpp:208
MayaFlux::Kinesis::HampelFilter::HampelFilter
HampelFilter(size_t window=8, double threshold_mad=3.5, size_t max_consecutive_rejections=3)
Construct a filter.
Definition
HampelFilter.hpp:75
MayaFlux::Kinesis::HampelFilter::m_consecutive_rejections
size_t m_consecutive_rejections
Definition
HampelFilter.hpp:209
MayaFlux::Kinesis::HampelFilter::reset
void reset()
Reset to uninitialized state.
Definition
HampelFilter.hpp:159
MayaFlux::Kinesis::HampelFilter::m_has_history
bool m_has_history
Definition
HampelFilter.hpp:207
MayaFlux::Kinesis::HampelFilter::magnitude_of
static double magnitude_of(const U &v) noexcept
Scalar reduction used for the MAD comparison.
Definition
HampelFilter.hpp:194
MayaFlux::Kinesis::HampelFilter
Holds the last accepted value when a new sample looks like an isolated outlier relative to its recent...
Definition
HampelFilter.hpp:50
MayaFlux::Memory::HistoryBuffer
History buffer for difference equations and recursive relations.
Definition
RingBuffer.hpp:426
MayaFlux::Kinesis
Definition
API/Random.hpp:5
src
MayaFlux
Kinesis
HampelFilter.hpp
Generated by
1.9.8