blob: ac3c84108093a2863fb3d46c7f4e9b03483c1e56 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/events/ozone/evdev/touch_filter/neural_stylus_palm_detection_filter_util.h"
#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if defined(__ANDROID__) || defined(__ANDROID_HOST__)
#include <linux/input-event-codes.h>
#include "chrome_to_android_compatibility_test_support.h"
#else
#include "ui/events/ozone/evdev/event_device_test_util.h"
#endif
#include "ui/events/ozone/evdev/touch_evdev_types.h"
#include "ui/events/ozone/evdev/touch_filter/palm_detection_filter.h"
#include "ui/events/ozone/evdev/touch_filter/shared_palm_detection_filter_state.h"
using ::testing::ElementsAre;
using ::testing::IsEmpty;
namespace ui {
#if defined(__ANDROID__) || defined(__ANDROID_HOST__)
/**
* The tests that require an actual device (something that responds to ioctls)
* have been removed. The rest of the tests were simplified by modifying the
* 'CreatePalmFilterDeviceInfo' method.
*/
enum DeviceType {
kNocturneTouchScreen,
kLinkTouchscreen,
kKohakuTouchscreen,
};
static PalmFilterDeviceInfo CreatePalmFilterDeviceInfo(DeviceType deviceType) {
// Default value for resolution is 1 if it's not specified (or set to 0). See
// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/ozone/evdev/touch_filter/
// neural_stylus_palm_detection_filter_util.cc;l=11;drc=7f74e1d22e2d0b91856ea6b3619098cd05ef6158
switch (deviceType) {
case kNocturneTouchScreen:
return PalmFilterDeviceInfo{
.max_x = 10404,
.max_y = 6936,
.x_res = 40,
.y_res = 40,
.major_radius_res = 1,
.minor_radius_res = 1,
.minor_radius_supported = true,
};
case kLinkTouchscreen:
return PalmFilterDeviceInfo{
.max_x = 2559,
.max_y = 1699,
.x_res = 20,
.y_res = 20,
.major_radius_res = 1,
.minor_radius_res = 1,
.minor_radius_supported = false,
};
case kKohakuTouchscreen:
return PalmFilterDeviceInfo{
.max_x = 2559,
.max_y = 1699,
.x_res = 1,
.y_res = 1,
.major_radius_res = 1,
.minor_radius_res = 1,
.minor_radius_supported = false,
};
}
}
class EventDeviceInfo {
public:
float GetAbsResolution(int /*axis*/) { return 20; }
};
bool CapabilitiesToDeviceInfo(DeviceType, EventDeviceInfo*) {
return true;
}
#endif
MATCHER_P(SampleTime, time, "Does the sample have given time.") {
*result_listener << "Sample time" << arg.time << " is not " << time;
return time == arg.time;
}
class NeuralStylusPalmDetectionFilterUtilTest
: public testing::TestWithParam<bool> {
public:
NeuralStylusPalmDetectionFilterUtilTest() = default;
NeuralStylusPalmDetectionFilterUtilTest(
const NeuralStylusPalmDetectionFilterUtilTest&) = delete;
NeuralStylusPalmDetectionFilterUtilTest& operator=(
const NeuralStylusPalmDetectionFilterUtilTest&) = delete;
void SetUp() override {
EXPECT_TRUE(
CapabilitiesToDeviceInfo(kNocturneTouchScreen, &nocturne_touchscreen_));
touch_.major = 25;
touch_.minor = 24;
touch_.pressure = 23;
touch_.tracking_id = 22;
touch_.x = 21;
touch_.y = 20;
model_config_.max_sample_count = 3;
const bool resample_touch = GetParam();
if (resample_touch) {
model_config_.resample_period = base::Milliseconds(8);
}
}
protected:
InProgressTouchEvdev touch_;
EventDeviceInfo nocturne_touchscreen_;
NeuralStylusPalmDetectionFilterModelConfig model_config_;
};
INSTANTIATE_TEST_SUITE_P(ParametricUtilTest,
NeuralStylusPalmDetectionFilterUtilTest,
::testing::Bool(),
[](const auto& paramInfo) {
return paramInfo.param ? "ResamplingEnabled"
: "ResamplingDisabled";
});
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistilledNocturneTest) {
const PalmFilterDeviceInfo nocturne_distilled =
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
EXPECT_FLOAT_EQ(nocturne_distilled.max_x,
nocturne_touchscreen_.GetAbsMaximum(ABS_MT_POSITION_X));
EXPECT_FLOAT_EQ(nocturne_distilled.max_y,
nocturne_touchscreen_.GetAbsMaximum(ABS_MT_POSITION_Y));
EXPECT_FLOAT_EQ(nocturne_distilled.x_res,
nocturne_touchscreen_.GetAbsResolution(ABS_MT_POSITION_X));
EXPECT_FLOAT_EQ(nocturne_distilled.y_res,
nocturne_touchscreen_.GetAbsResolution(ABS_MT_POSITION_Y));
EXPECT_FLOAT_EQ(nocturne_distilled.major_radius_res,
nocturne_touchscreen_.GetAbsResolution(ABS_MT_TOUCH_MAJOR));
EXPECT_TRUE(nocturne_distilled.minor_radius_supported);
EXPECT_FLOAT_EQ(nocturne_distilled.minor_radius_res,
nocturne_touchscreen_.GetAbsResolution(ABS_MT_TOUCH_MINOR));
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, NoMinorResTest) {
// Nocturne has minor resolution, but let's pretend it doesn't. we should
// recover "1" as the resolution.
auto abs_info = nocturne_touchscreen_.GetAbsInfoByCode(ABS_MT_TOUCH_MINOR);
abs_info.resolution = 0;
nocturne_touchscreen_.SetAbsInfo(ABS_MT_TOUCH_MINOR, abs_info);
const PalmFilterDeviceInfo nocturne_distilled =
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
EXPECT_EQ(1, nocturne_distilled.minor_radius_res);
EXPECT_EQ(1, nocturne_distilled.major_radius_res);
}
#endif
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistillerKohakuTest) {
EventDeviceInfo kohaku_touchscreen;
ASSERT_TRUE(
CapabilitiesToDeviceInfo(kKohakuTouchscreen, &kohaku_touchscreen));
const PalmFilterDeviceInfo kohaku_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(kohaku_touchscreen);
#else
CreatePalmFilterDeviceInfo(kKohakuTouchscreen);
#endif
EXPECT_FALSE(kohaku_distilled.minor_radius_supported);
EXPECT_EQ(1, kohaku_distilled.x_res);
EXPECT_EQ(1, kohaku_distilled.y_res);
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, DistilledLinkTest) {
EventDeviceInfo link_touchscreen;
ASSERT_TRUE(CapabilitiesToDeviceInfo(kLinkTouchscreen, &link_touchscreen));
const PalmFilterDeviceInfo link_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(link_touchscreen);
#else
CreatePalmFilterDeviceInfo(kLinkTouchscreen);
#endif
EXPECT_FALSE(link_distilled.minor_radius_supported);
EXPECT_FLOAT_EQ(1.f, link_distilled.major_radius_res);
EXPECT_FLOAT_EQ(link_distilled.major_radius_res,
link_distilled.minor_radius_res);
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, PalmFilterSampleTest) {
base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
const PalmFilterDeviceInfo nocturne_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
#else
CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
#endif
const PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
EXPECT_EQ(time, sample.time);
EXPECT_EQ(25, sample.major_radius);
EXPECT_EQ(24, sample.minor_radius);
EXPECT_EQ(23, sample.pressure);
EXPECT_EQ(22, sample.tracking_id);
EXPECT_EQ(gfx::PointF(21 / 40.f, 20 / 40.f), sample.point);
EXPECT_EQ(0.5, sample.edge);
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, LinkTouchscreenSampleTest) {
EventDeviceInfo link_touchscreen;
base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
ASSERT_TRUE(CapabilitiesToDeviceInfo(kLinkTouchscreen, &link_touchscreen));
const PalmFilterDeviceInfo link_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(link_touchscreen);
#else
CreatePalmFilterDeviceInfo(kLinkTouchscreen);
#endif
touch_.minor = 0; // no minor from link.
// use 40 as a base since model is trained on that kind of device.
model_config_.radius_polynomial_resize = {
link_touchscreen.GetAbsResolution(ABS_MT_POSITION_X) / 40.0f, 0.0};
const PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, link_distilled);
EXPECT_FLOAT_EQ(12.5, sample.major_radius);
EXPECT_FLOAT_EQ(12.5, sample.minor_radius);
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, PalmFilterStrokeTest) {
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 55);
touch_.tracking_id = 55;
// With no points, center is 0.
EXPECT_EQ(gfx::PointF(0., 0.), stroke.GetCentroid());
base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
const PalmFilterDeviceInfo nocturne_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
#else
CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
#endif
// Deliberately long test to ensure floating point continued accuracy.
for (int i = 0; i < 500000; ++i) {
touch_.x = 15 + i;
PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
time += base::Milliseconds(8);
stroke.ProcessSample(std::move(sample));
EXPECT_EQ(touch_.tracking_id, stroke.tracking_id());
if (i < 3) {
if (i == 0) {
EXPECT_FLOAT_EQ(gfx::PointF(15 / 40.f, 0.5).x(),
stroke.GetCentroid().x());
} else if (i == 1) {
EXPECT_FLOAT_EQ(gfx::PointF((30 + 1) / (2 * 40.f), 0.5).x(),
stroke.GetCentroid().x());
} else if (i == 2) {
EXPECT_FLOAT_EQ(gfx::PointF((45 + 1 + 2) / (3 * 40.f), 0.5).x(),
stroke.GetCentroid().x());
}
continue;
}
float expected_x = (45 + 3 * i - 3) / (3 * 40.f);
gfx::PointF expected_centroid = gfx::PointF(expected_x, 0.5);
ASSERT_FLOAT_EQ(expected_centroid.x(), stroke.GetCentroid().x())
<< "failed at i " << i;
}
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest,
PalmFilterStrokeBiggestSizeTest) {
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
PalmFilterStroke no_minor_stroke(model_config_, /*tracking_id*/ 0);
touch_.tracking_id = stroke.tracking_id();
EXPECT_EQ(0, stroke.BiggestSize());
base::TimeTicks time = base::TimeTicks() + base::Seconds(30);
const PalmFilterDeviceInfo nocturne_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
#else
CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
#endif
for (int i = 0; i < 500; ++i) {
touch_.major = 2 + i;
touch_.minor = 1 + i;
PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
EXPECT_EQ(static_cast<uint64_t>(i), stroke.samples_seen());
stroke.ProcessSample(sample);
EXPECT_FLOAT_EQ((1 + i) * (2 + i), stroke.BiggestSize());
PalmFilterSample second_sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
second_sample.minor_radius = 0;
no_minor_stroke.ProcessSample(std::move(second_sample));
EXPECT_FLOAT_EQ((2 + i) * (2 + i), no_minor_stroke.BiggestSize());
ASSERT_EQ(std::min(3ul, 1ul + i), stroke.samples().size());
time += base::Milliseconds(8);
}
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, UnscaledMajorMinorResolution) {
model_config_.radius_polynomial_resize = {};
PalmFilterDeviceInfo device_info;
device_info.x_res = 2;
device_info.y_res = 5;
device_info.major_radius_res = 2;
device_info.minor_radius_res = 5;
device_info.minor_radius_supported = true;
touch_.major = 20;
touch_.minor = 10;
touch_.orientation = 0;
base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, device_info);
EXPECT_EQ(20 / 2, sample.major_radius);
EXPECT_EQ(10 / 5, sample.minor_radius);
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, StrokeGetMaxMajorTest) {
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
touch_.tracking_id = stroke.tracking_id();
EXPECT_FLOAT_EQ(0, stroke.MaxMajorRadius());
base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
const PalmFilterDeviceInfo nocturne_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
#else
CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
#endif
for (int i = 1; i < 50; ++i) {
touch_.major = i;
touch_.minor = i - 1;
PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
time += base::Milliseconds(8);
EXPECT_EQ(static_cast<uint64_t>(i - 1), stroke.samples_seen());
stroke.ProcessSample(sample);
EXPECT_FLOAT_EQ(i, stroke.MaxMajorRadius());
}
}
TEST_P(NeuralStylusPalmDetectionFilterUtilTest, SampleRadiusConversion) {
// A single number: a _constant_.
model_config_.radius_polynomial_resize = {71.3};
base::TimeTicks time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
const PalmFilterDeviceInfo nocturne_distilled =
#if !defined(__ANDROID__) && !defined(__ANDROID_HOST__)
CreatePalmFilterDeviceInfo(nocturne_touchscreen_);
#else
CreatePalmFilterDeviceInfo(kNocturneTouchScreen);
#endif
PalmFilterSample sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
EXPECT_FLOAT_EQ(71.3, sample.major_radius);
EXPECT_FLOAT_EQ(71.3, sample.minor_radius);
// 0.1*r^2 + 0.4*r - 5.0
model_config_.radius_polynomial_resize = {0.1, 0.4, -5.0};
sample =
CreatePalmFilterSample(touch_, time, model_config_, nocturne_distilled);
EXPECT_FLOAT_EQ(0.1 * 25 * 25 + 0.4 * 25 - 5.0, sample.major_radius);
EXPECT_FLOAT_EQ(0.1 * 24 * 24 + 0.4 * 24 - 5.0, sample.minor_radius);
}
TEST(PalmFilterStrokeTest, NumberOfResampledValues) {
NeuralStylusPalmDetectionFilterModelConfig model_config_;
model_config_.max_sample_count = 3;
model_config_.resample_period = base::Milliseconds(8);
base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
const PalmFilterDeviceInfo device_info;
// Initially, no samples
ASSERT_THAT(stroke.samples(), IsEmpty());
ASSERT_EQ(0u, stroke.samples_seen());
// Add first sample at time = T
InProgressTouchEvdev touch_;
touch_.tracking_id = stroke.tracking_id();
PalmFilterSample sample =
CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
stroke.ProcessSample(sample);
ASSERT_THAT(stroke.samples(), ElementsAre(SampleTime(down_time)));
ASSERT_EQ(1u, stroke.samples_seen());
// Add second sample at time = T + 2ms. It's not yet time for the new frame,
// so no new sample should be generated.
base::TimeTicks time = down_time + base::Milliseconds(4);
sample = CreatePalmFilterSample(touch_, time, model_config_, device_info);
stroke.ProcessSample(sample);
ASSERT_THAT(stroke.samples(), ElementsAre(SampleTime(down_time)));
ASSERT_EQ(1u, stroke.samples_seen());
// Add third sample at time = T + 10ms. An event at time = T + 8ms should be
// generated.
time = down_time + base::Milliseconds(10);
sample = CreatePalmFilterSample(touch_, time, model_config_, device_info);
stroke.ProcessSample(sample);
ASSERT_THAT(stroke.samples(),
ElementsAre(SampleTime(down_time),
SampleTime(down_time + base::Milliseconds(8))));
ASSERT_EQ(2u, stroke.samples_seen());
}
TEST(PalmFilterStrokeTest, ResamplingTest) {
NeuralStylusPalmDetectionFilterModelConfig model_config_;
model_config_.max_sample_count = 3;
model_config_.resample_period = base::Milliseconds(8);
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
PalmFilterDeviceInfo device_info;
device_info.minor_radius_supported = true;
// Add first sample at time = T
InProgressTouchEvdev touch_;
touch_.tracking_id = stroke.tracking_id();
touch_.x = 1;
touch_.y = 2;
touch_.major = 4;
touch_.minor = 3;
base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
PalmFilterSample sample1 =
CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
stroke.ProcessSample(sample1);
// First sample should not be modified
ASSERT_THAT(stroke.samples(), ElementsAre(sample1));
// Add second sample at time = T + 2ms. It's not yet time for the new frame,
// so no new sample should be generated.
base::TimeTicks time = down_time + base::Milliseconds(4);
touch_.x = 100;
touch_.y = 20;
touch_.major = 12;
touch_.minor = 11;
PalmFilterSample sample2 =
CreatePalmFilterSample(touch_, time, model_config_, device_info);
stroke.ProcessSample(sample2);
// The samples should remain unchanged
ASSERT_THAT(stroke.samples(), ElementsAre(sample1));
// Add third sample at time = T + 12ms. A resampled event at time = T + 8ms
// should be generated.
time = down_time + base::Milliseconds(12);
touch_.x = 200;
touch_.y = 24;
touch_.major = 14;
touch_.minor = 13;
PalmFilterSample sample3 =
CreatePalmFilterSample(touch_, time, model_config_, device_info);
stroke.ProcessSample(sample3);
ASSERT_THAT(
stroke.samples(),
ElementsAre(sample1, SampleTime(down_time + base::Milliseconds(8))));
EXPECT_EQ(150, stroke.samples().back().point.x());
EXPECT_EQ(22, stroke.samples().back().point.y());
EXPECT_EQ(14, stroke.samples().back().major_radius);
EXPECT_EQ(13, stroke.samples().back().minor_radius);
}
TEST(PalmFilterStrokeTest, MultipleResampledValues) {
NeuralStylusPalmDetectionFilterModelConfig model_config_;
model_config_.max_sample_count = 3;
model_config_.resample_period = base::Milliseconds(8);
PalmFilterStroke stroke(model_config_, /*tracking_id*/ 0);
PalmFilterDeviceInfo device_info;
device_info.minor_radius_supported = true;
// Add first sample at time = T
InProgressTouchEvdev touch_;
touch_.tracking_id = stroke.tracking_id();
touch_.x = 0;
touch_.y = 10;
touch_.major = 200;
touch_.minor = 100;
base::TimeTicks down_time = base::TimeTicks::UnixEpoch() + base::Seconds(30);
PalmFilterSample sample1 =
CreatePalmFilterSample(touch_, down_time, model_config_, device_info);
stroke.ProcessSample(sample1);
// First sample should go in as is
ASSERT_THAT(stroke.samples(), ElementsAre(sample1));
// Add second sample at time = T + 20ms. Two resampled values should be
// generated: 1) at time = T+8ms 2) at time = T+16ms
base::TimeTicks time = down_time + base::Milliseconds(20);
touch_.x = 20;
touch_.y = 30;
touch_.major = 220;
touch_.minor = 120;
PalmFilterSample sample2 =
CreatePalmFilterSample(touch_, time, model_config_, device_info);
stroke.ProcessSample(sample2);
ASSERT_THAT(stroke.samples(),
ElementsAre(SampleTime(down_time),
SampleTime(down_time + base::Milliseconds(8)),
SampleTime(down_time + base::Milliseconds(16))));
// First sample : time = T + 8ms
EXPECT_EQ(8, stroke.samples()[1].point.x());
EXPECT_EQ(18, stroke.samples()[1].point.y());
EXPECT_EQ(220, stroke.samples()[1].major_radius);
EXPECT_EQ(120, stroke.samples()[1].minor_radius);
// Second sample : time = T + 16ms
EXPECT_EQ(16, stroke.samples().back().point.x());
EXPECT_EQ(26, stroke.samples().back().point.y());
EXPECT_EQ(220, stroke.samples().back().major_radius);
EXPECT_EQ(120, stroke.samples().back().minor_radius);
}
} // namespace ui