Skip to content

Commit b8591fb

Browse files
Treehugger RobotAndroid (Google) Code Review
authored andcommitted
Merge "Reimplement Chromium's OneEuroFilter to InputConsumer" into main
2 parents eda34d4 + 08ee199 commit b8591fb

9 files changed

Lines changed: 473 additions & 0 deletions

File tree

include/input/CoordinateFilter.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <chrono>
20+
21+
#include <input/Input.h>
22+
#include <input/OneEuroFilter.h>
23+
24+
namespace android {
25+
26+
/**
27+
* Pair of OneEuroFilters that independently filter X and Y coordinates. Both filters share the same
28+
* constructor's parameters. The minimum cutoff frequency is the base cutoff frequency, that is, the
29+
* resulting cutoff frequency in the absence of signal's speed. Likewise, beta is a scaling factor
30+
* of the signal's speed that sets how much the signal's speed contributes to the resulting cutoff
31+
* frequency. The adaptive cutoff frequency criterion is f_c = f_c_min + β|̇x_filtered|
32+
*/
33+
class CoordinateFilter {
34+
public:
35+
explicit CoordinateFilter(float minCutoffFreq, float beta);
36+
37+
/**
38+
* Filters in place only the AXIS_X and AXIS_Y fields from coords. Each call to filter must
39+
* provide a timestamp strictly greater than the timestamp of the previous call. The first time
40+
* this method is invoked no filtering takes place. Subsequent calls do overwrite `coords` with
41+
* filtered data.
42+
*
43+
* @param timestamp The timestamps at which to filter. It must be greater than the one passed in
44+
* the previous call.
45+
* @param coords Coordinates to be overwritten by the corresponding filtered coordinates.
46+
*/
47+
void filter(std::chrono::duration<float> timestamp, PointerCoords& coords);
48+
49+
private:
50+
OneEuroFilter mXFilter;
51+
OneEuroFilter mYFilter;
52+
};
53+
54+
} // namespace android

include/input/OneEuroFilter.h

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <chrono>
20+
#include <optional>
21+
22+
#include <input/Input.h>
23+
24+
namespace android {
25+
26+
/**
27+
* Low pass filter with adaptive low pass frequency based on the signal's speed. The signal's cutoff
28+
* frequency is determined by f_c = f_c_min + β|̇x_filtered|. Refer to
29+
* https://dl.acm.org/doi/10.1145/2207676.2208639 for details on how the filter works and how to
30+
* tune it.
31+
*/
32+
class OneEuroFilter {
33+
public:
34+
/**
35+
* Default cutoff frequency of the filtered signal's speed. 1.0 Hz is the value in the filter's
36+
* paper.
37+
*/
38+
static constexpr float kDefaultSpeedCutoffFreq = 1.0;
39+
40+
OneEuroFilter() = delete;
41+
42+
explicit OneEuroFilter(float minCutoffFreq, float beta,
43+
float speedCutoffFreq = kDefaultSpeedCutoffFreq);
44+
45+
OneEuroFilter(const OneEuroFilter&) = delete;
46+
OneEuroFilter& operator=(const OneEuroFilter&) = delete;
47+
OneEuroFilter(OneEuroFilter&&) = delete;
48+
OneEuroFilter& operator=(OneEuroFilter&&) = delete;
49+
50+
/**
51+
* Returns the filtered value of rawPosition. Each call to filter must provide a timestamp
52+
* strictly greater than the timestamp of the previous call. The first time the method is
53+
* called, it returns the value of rawPosition. Any subsequent calls provide a filtered value.
54+
*
55+
* @param timestamp The timestamp at which to filter. It must be strictly greater than the one
56+
* provided in the previous call.
57+
* @param rawPosition Position to be filtered.
58+
*/
59+
float filter(std::chrono::duration<float> timestamp, float rawPosition);
60+
61+
private:
62+
/**
63+
* Minimum cutoff frequency. This is the constant term in the adaptive cutoff frequency
64+
* criterion. Units are Hertz.
65+
*/
66+
const float mMinCutoffFreq;
67+
68+
/**
69+
* Slope of the cutoff frequency criterion. This is the term scaling the absolute value of the
70+
* filtered signal's speed. The data member is dimensionless, that is, it does not have units.
71+
*/
72+
const float mBeta;
73+
74+
/**
75+
* Cutoff frequency of the signal's speed. This is the cutoff frequency applied to the filtering
76+
* of the signal's speed. Units are Hertz.
77+
*/
78+
const float mSpeedCutoffFreq;
79+
80+
/**
81+
* The timestamp from the previous call. Units are seconds.
82+
*/
83+
std::optional<std::chrono::duration<float>> mPrevTimestamp;
84+
85+
/**
86+
* The raw position from the previous call.
87+
*/
88+
std::optional<float> mPrevRawPosition;
89+
90+
/**
91+
* The filtered velocity from the previous call. Units are position per second.
92+
*/
93+
std::optional<float> mPrevFilteredVelocity;
94+
95+
/**
96+
* The filtered position from the previous call.
97+
*/
98+
std::optional<float> mPrevFilteredPosition;
99+
};
100+
101+
} // namespace android

include/input/Resampler.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
#include <array>
2020
#include <chrono>
2121
#include <iterator>
22+
#include <map>
2223
#include <optional>
2324
#include <vector>
2425

2526
#include <android-base/logging.h>
2627
#include <ftl/mixins.h>
28+
#include <input/CoordinateFilter.h>
2729
#include <input/Input.h>
2830
#include <input/InputTransport.h>
2931
#include <input/RingBuffer.h>
@@ -293,4 +295,43 @@ class LegacyResampler final : public Resampler {
293295
inline static void addSampleToMotionEvent(const Sample& sample, MotionEvent& motionEvent);
294296
};
295297

298+
/**
299+
* Resampler that first applies the LegacyResampler resampling algorithm, then independently filters
300+
* the X and Y coordinates with a pair of One Euro filters.
301+
*/
302+
class FilteredLegacyResampler final : public Resampler {
303+
public:
304+
/**
305+
* Creates a resampler, using the given minCutoffFreq and beta to instantiate its One Euro
306+
* filters.
307+
*/
308+
explicit FilteredLegacyResampler(float minCutoffFreq, float beta);
309+
310+
void resampleMotionEvent(std::chrono::nanoseconds requestedFrameTime, MotionEvent& motionEvent,
311+
const InputMessage* futureMessage) override;
312+
313+
std::chrono::nanoseconds getResampleLatency() const override;
314+
315+
private:
316+
LegacyResampler mResampler;
317+
318+
/**
319+
* Minimum cutoff frequency of the value's low pass filter. Refer to OneEuroFilter class for a
320+
* more detailed explanation.
321+
*/
322+
const float mMinCutoffFreq;
323+
324+
/**
325+
* Scaling factor of the adaptive cutoff frequency criterion. Refer to OneEuroFilter class for a
326+
* more detailed explanation.
327+
*/
328+
const float mBeta;
329+
330+
/*
331+
* Note: an associative array with constant insertion and lookup times would be more efficient.
332+
* When this was implemented, there was no container with these properties.
333+
*/
334+
std::map<int32_t /*pointerId*/, CoordinateFilter> mFilteredPointers;
335+
};
336+
296337
} // namespace android

libs/input/Android.bp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ cc_library {
217217
],
218218
srcs: [
219219
"AccelerationCurve.cpp",
220+
"CoordinateFilter.cpp",
220221
"Input.cpp",
221222
"InputConsumer.cpp",
222223
"InputConsumerNoResampling.cpp",
@@ -230,6 +231,7 @@ cc_library {
230231
"KeyLayoutMap.cpp",
231232
"MotionPredictor.cpp",
232233
"MotionPredictorMetricsManager.cpp",
234+
"OneEuroFilter.cpp",
233235
"PrintTools.cpp",
234236
"PropertyMap.cpp",
235237
"Resampler.cpp",

libs/input/CoordinateFilter.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#define LOG_TAG "CoordinateFilter"
18+
19+
#include <input/CoordinateFilter.h>
20+
21+
namespace android {
22+
23+
CoordinateFilter::CoordinateFilter(float minCutoffFreq, float beta)
24+
: mXFilter{minCutoffFreq, beta}, mYFilter{minCutoffFreq, beta} {}
25+
26+
void CoordinateFilter::filter(std::chrono::duration<float> timestamp, PointerCoords& coords) {
27+
coords.setAxisValue(AMOTION_EVENT_AXIS_X, mXFilter.filter(timestamp, coords.getX()));
28+
coords.setAxisValue(AMOTION_EVENT_AXIS_Y, mYFilter.filter(timestamp, coords.getY()));
29+
}
30+
31+
} // namespace android

libs/input/OneEuroFilter.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#define LOG_TAG "OneEuroFilter"
18+
19+
#include <chrono>
20+
#include <cmath>
21+
22+
#include <android-base/logging.h>
23+
#include <input/CoordinateFilter.h>
24+
25+
namespace android {
26+
namespace {
27+
28+
inline float cutoffFreq(float minCutoffFreq, float beta, float filteredSpeed) {
29+
return minCutoffFreq + beta * std::abs(filteredSpeed);
30+
}
31+
32+
inline float smoothingFactor(std::chrono::duration<float> samplingPeriod, float cutoffFreq) {
33+
return samplingPeriod.count() / (samplingPeriod.count() + (1.0 / (2.0 * M_PI * cutoffFreq)));
34+
}
35+
36+
inline float lowPassFilter(float rawPosition, float prevFilteredPosition, float smoothingFactor) {
37+
return smoothingFactor * rawPosition + (1 - smoothingFactor) * prevFilteredPosition;
38+
}
39+
40+
} // namespace
41+
42+
OneEuroFilter::OneEuroFilter(float minCutoffFreq, float beta, float speedCutoffFreq)
43+
: mMinCutoffFreq{minCutoffFreq}, mBeta{beta}, mSpeedCutoffFreq{speedCutoffFreq} {}
44+
45+
float OneEuroFilter::filter(std::chrono::duration<float> timestamp, float rawPosition) {
46+
LOG_IF(FATAL, mPrevFilteredPosition.has_value() && (timestamp <= *mPrevTimestamp))
47+
<< "Timestamp must be greater than mPrevTimestamp";
48+
49+
const std::chrono::duration<float> samplingPeriod = (mPrevTimestamp.has_value())
50+
? (timestamp - *mPrevTimestamp)
51+
: std::chrono::duration<float>{1.0};
52+
53+
const float rawVelocity = (mPrevFilteredPosition.has_value())
54+
? ((rawPosition - *mPrevFilteredPosition) / samplingPeriod.count())
55+
: 0.0;
56+
57+
const float speedSmoothingFactor = smoothingFactor(samplingPeriod, mSpeedCutoffFreq);
58+
59+
const float filteredVelocity = (mPrevFilteredVelocity.has_value())
60+
? lowPassFilter(rawVelocity, *mPrevFilteredVelocity, speedSmoothingFactor)
61+
: rawVelocity;
62+
63+
const float positionCutoffFreq = cutoffFreq(mMinCutoffFreq, mBeta, filteredVelocity);
64+
65+
const float positionSmoothingFactor = smoothingFactor(samplingPeriod, positionCutoffFreq);
66+
67+
const float filteredPosition = (mPrevFilteredPosition.has_value())
68+
? lowPassFilter(rawPosition, *mPrevFilteredPosition, positionSmoothingFactor)
69+
: rawPosition;
70+
71+
mPrevTimestamp = timestamp;
72+
mPrevRawPosition = rawPosition;
73+
mPrevFilteredVelocity = filteredVelocity;
74+
mPrevFilteredPosition = filteredPosition;
75+
76+
return filteredPosition;
77+
}
78+
79+
} // namespace android

libs/input/Resampler.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,34 @@ void LegacyResampler::resampleMotionEvent(nanoseconds frameTime, MotionEvent& mo
389389
mLastRealSample = *(mLatestSamples.end() - 1);
390390
}
391391

392+
// --- FilteredLegacyResampler ---
393+
394+
FilteredLegacyResampler::FilteredLegacyResampler(float minCutoffFreq, float beta)
395+
: mResampler{}, mMinCutoffFreq{minCutoffFreq}, mBeta{beta} {}
396+
397+
void FilteredLegacyResampler::resampleMotionEvent(std::chrono::nanoseconds requestedFrameTime,
398+
MotionEvent& motionEvent,
399+
const InputMessage* futureSample) {
400+
mResampler.resampleMotionEvent(requestedFrameTime, motionEvent, futureSample);
401+
const size_t numSamples = motionEvent.getHistorySize() + 1;
402+
for (size_t sampleIndex = 0; sampleIndex < numSamples; ++sampleIndex) {
403+
for (size_t pointerIndex = 0; pointerIndex < motionEvent.getPointerCount();
404+
++pointerIndex) {
405+
const int32_t pointerId = motionEvent.getPointerProperties(pointerIndex)->id;
406+
const nanoseconds eventTime =
407+
nanoseconds{motionEvent.getHistoricalEventTime(sampleIndex)};
408+
// Refer to the static function `setMotionEventPointerCoords` for a justification of
409+
// casting away const.
410+
PointerCoords& pointerCoords = const_cast<PointerCoords&>(
411+
*(motionEvent.getHistoricalRawPointerCoords(pointerIndex, sampleIndex)));
412+
const auto& [iter, _] = mFilteredPointers.try_emplace(pointerId, mMinCutoffFreq, mBeta);
413+
iter->second.filter(eventTime, pointerCoords);
414+
}
415+
}
416+
}
417+
418+
std::chrono::nanoseconds FilteredLegacyResampler::getResampleLatency() const {
419+
return mResampler.getResampleLatency();
420+
}
421+
392422
} // namespace android

libs/input/tests/Android.bp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cc_test {
2525
"InputVerifier_test.cpp",
2626
"MotionPredictor_test.cpp",
2727
"MotionPredictorMetricsManager_test.cpp",
28+
"OneEuroFilter_test.cpp",
2829
"Resampler_test.cpp",
2930
"RingBuffer_test.cpp",
3031
"TestInputChannel.cpp",

0 commit comments

Comments
 (0)