Skip to content

Commit 08ee199

Browse files
author
Paul Ramirez
committed
Reimplement Chromium's OneEuroFilter to InputConsumer
Reimplemented Chromium's OneEuroFilter usage to InputConsumerNoResampling. There are a few differences between Chromium's work and this CL. We reimplemented One Euro filter an adaptive cutoff frequency low pass made in this implementation as in the Chromium's implementation. The class FilteredResampler filters the output of LegacyResampler using the One Euro filter approach. Here's the link to Chromium's to One Euro filter: https://source.chromium.org/chromium/chromium/src/+/main:ui/base/prediction/one_euro_filter.h Bug: 297226446 Flag: EXEMPT bugfix Test: TEST=libinput_tests; m $TEST && $ANDROID_HOST_OUT/nativetest64/$TEST/$TEST Change-Id: I0316cb1e81c73b1dc28dc809f55dee3a1cc0ebd2
1 parent 4ef4068 commit 08ee199

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)