Skip to content

Commit

Permalink
Improve touch filtering.
Browse files Browse the repository at this point in the history
Rather than using separate 1D filters for each of x,y,vx,vy, use a 2x2
matrix to represent position and velocity in a more proper kalman filter.
  • Loading branch information
sletta committed Oct 26, 2016
1 parent 7f4462d commit 0b43ccd
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 55 deletions.
158 changes: 158 additions & 0 deletions src/platformsupport/input/evdevtouch/qevdevtouchfilter_p.h
@@ -0,0 +1,158 @@
/****************************************************************************
**
** Copyright (C) 2016 Jolla Ltd, author: <gunnar.sletta@jollamobile.com>
** Contact: http://www.qt.io/licensing/
**
** This file is part of the plugins module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <qglobal.h>

QT_BEGIN_NAMESPACE

struct QEvdevTouchFilter
{
QEvdevTouchFilter();

void initialize(float pos, float velocity);
void update(float pos, float velocity, float timeDelta);

float position() const { return x.x; }
float velocity() const { return x.y; }

private:
struct vec2 {
vec2(float x = 0.0f, float y = 0.0f) : x(x), y(y) { }
float x, y;

vec2 operator-(vec2 v) {
return vec2(x - v.x, y - v.y);
}

vec2 operator+(vec2 v) {
return vec2(x + v.x, y + v.y);
}
};

struct mat2 {
float a, b, c, d;
mat2(float a = 1.0f, float b = 0.0f, float c = 0.0f, float d = 1.0f)
: a(a)
, b(b)
, c(c)
, d(d)
{
}

mat2 transposed() const {
return mat2(a, c,
b, d);
}

mat2 inverted() const {
float det = 1.0f / (a * d - b * c);
return mat2( d * det, -b * det,
-c * det, a * det);
}

mat2 operator+(mat2 m) const {
return mat2(a + m.a, b + m.b,
c + m.c, d + m.d);
}

mat2 operator-(mat2 m) const {
return mat2(a - m.a, b - m.b,
c - m.c, d - m.d);
}

vec2 operator*(vec2 v) const {
return vec2(a * v.x + b * v.y,
c * v.x + d * v.y);
}

mat2 operator*(mat2 M) const {
return mat2(a * M.a + b * M.c,
a * M.b + b * M.d,
c * M.a + d * M.c,
c * M.b + d * M.d);
}
};

vec2 x;
mat2 A;
mat2 P;
mat2 Q;
mat2 R;
mat2 H;
};

inline QEvdevTouchFilter::QEvdevTouchFilter()
{
}

inline void QEvdevTouchFilter::initialize(float pos, float velocity)
{
x = vec2(pos, velocity);

P = mat2(0.0f, 0.0f,
0.0f, 0.0f);

Q = mat2(0.0f, 0.0f,
0.0f, 0.1f);
R = mat2(0.1f, 0.0f,
0.0f, 0.1f);
}

inline void QEvdevTouchFilter::update(float pos, float velocity, float dT)
{
A.b = dT;

// Prediction setp
x = A * x;
P = A * P * A.transposed() + Q;

// Correction step (complete with H)
// mat2 S = H * P * H.transposed() + R;
// mat2 K = P * H.transposed() * S.inverted();
// vec2 m(pos, velocity);
// vec2 y = m - H * x;
// x = x + K * y;
// P = (mat2() - K * H) * P;

// Correction step (without H as H is currently set to I, so we can ignore
// it in the calculations...)
mat2 S = P + R;
mat2 K = P * S.inverted();
vec2 m(pos, velocity);
vec2 y = m - x;
x = x + K * y;
P = (mat2() - K) * P;

}

QT_END_NAMESPACE
73 changes: 35 additions & 38 deletions src/platformsupport/input/evdevtouch/qevdevtouchhandler.cpp
Expand Up @@ -131,8 +131,6 @@ class QEvdevTouchScreenData
bool m_filtered;

int m_prediction;
int m_smoothness;
int m_velocitySmoothness;

QMutex m_mutex;
};
Expand All @@ -146,7 +144,7 @@ QEvdevTouchScreenData::QEvdevTouchScreenData(QEvdevTouchScreenHandler *q_ptr, co
hw_range_y_min(0), hw_range_y_max(0),
hw_pressure_min(0), hw_pressure_max(0),
m_forceToActiveWindow(false), m_typeB(false), m_singleTouch(false),
m_filtered(false), m_prediction(0), m_smoothness(5), m_velocitySmoothness(50)
m_filtered(false), m_prediction(0)
{
for (int i=0; i<args.size(); ++i) {
const QString &arg = args.at(i);
Expand All @@ -156,10 +154,6 @@ QEvdevTouchScreenData::QEvdevTouchScreenData(QEvdevTouchScreenHandler *q_ptr, co
m_filtered = true;
else if (arg.startsWith(QStringLiteral("prediction=")))
m_prediction = arg.mid(11).toInt();
else if (arg.startsWith(QStringLiteral("smooth=")))
m_smoothness = arg.mid(7).toInt();
else if (arg.startsWith(QStringLiteral("vsmooth=")))
m_velocitySmoothness = arg.mid(8).toInt();
}
}

Expand Down Expand Up @@ -250,7 +244,7 @@ QEvdevTouchScreenHandler::QEvdevTouchScreenHandler(const QString &device, const
d->m_singleTouch ? "single" : "multi",
d->m_filtered ? "yes" : "no");
if (d->m_filtered)
qCDebug(qLcEvdevTouch, " - prediction=%d, smooth=%d, vsmooth=%d", d->m_prediction, d->m_smoothness, d->m_velocitySmoothness);
qCDebug(qLcEvdevTouch, " - prediction=%d", d->m_prediction);

input_absinfo absInfo;
memset(&absInfo, 0, sizeof(input_absinfo));
Expand Down Expand Up @@ -734,6 +728,7 @@ QEvdevTouchScreenHandlerThread::QEvdevTouchScreenHandlerThread(const QString &de
: QDaemonThread(parent), m_device(device), m_spec(spec), m_handler(Q_NULLPTR), m_touchDeviceRegistered(false)
, m_touchUpdatePending(false)
, m_filterWindow(0)
, m_touchRate(-1)
{
start();
}
Expand Down Expand Up @@ -798,13 +793,36 @@ bool QEvdevTouchScreenHandlerThread::eventFilter(QObject *object, QEvent *event)

void QEvdevTouchScreenHandlerThread::filterAndSendTouchPoints()
{
float vsyncDelta = 1.0f / QGuiApplication::primaryScreen()->refreshRate();

QHash<int, FilteredTouchPoint> filteredPoints;

m_handler->d->m_mutex.lock();

double time = m_handler->d->m_timeStamp;
double lastTime = m_handler->d->m_lastTimeStamp;
double touchDelta = time - lastTime;
if (m_touchRate < 0 || touchDelta > vsyncDelta) {
// We're at the very start, with nothing to go on, so make a guess
// that the touch rate will be somewhere in the range of half a vsync.
// This doesn't have to be accurate as we will calibrate it over time,
// but it gives us a better starting point so calibration will be
// slightly quicker. If, on the other hand, we already have an
// estimate, we'll leave it as is and keep it.
if (m_touchRate < 0)
m_touchRate = (1.0 / QGuiApplication::primaryScreen()->refreshRate()) / 2.0;

} else {
// Update our estimate for the touch rate. We're making the assumption
// that this value will be mostly accurate with the occational bump,
// so we're weighting the existing value high compared to the update.
const double ratio = 0.9;
m_touchRate = sqrt(m_touchRate * m_touchRate * ratio + touchDelta * touchDelta * (1.0 - ratio));
}

QList<QWindowSystemInterface::TouchPoint> points = m_handler->d->m_touchPoints;
QList<QWindowSystemInterface::TouchPoint> lastPoints = m_handler->d->m_lastTouchPoints;

m_handler->d->m_mutex.unlock();

for (int i=0; i<points.size(); ++i) {
Expand All @@ -823,30 +841,25 @@ void QEvdevTouchScreenHandlerThread::filterAndSendTouchPoints()

QPointF velocity;
if (lastTime != 0 && ltp.id >= 0)
velocity = (rawPos - ltp.rawPositions[0]) / (time - lastTime);
velocity = (rawPos - ltp.rawPositions[0]) / m_touchRate;
if (m_filteredPoints.contains(tp.id)) {
f = m_filteredPoints.take(tp.id);
f.x.update(rawPos.x());
f.y.update(rawPos.y());
f.vx.update(velocity.x());
f.vy.update(velocity.y());
rawPos = QPointF(f.x.value(), f.y.value());
f.x.update(rawPos.x(), velocity.x(), vsyncDelta);
f.y.update(rawPos.y(), velocity.y(), vsyncDelta);
rawPos = QPointF(f.x.position(), f.y.position());
} else {
f.x.initialize(rawPos.x(), 1, m_handler->d->m_smoothness, 1);
f.y.initialize(rawPos.y(), 1, m_handler->d->m_smoothness, 1);
f.vx.initialize(velocity.x(), 1, m_handler->d->m_velocitySmoothness, 1);
f.vy.initialize(velocity.y(), 1, m_handler->d->m_velocitySmoothness, 1);
f.x.initialize(rawPos.x(), velocity.x());
f.y.initialize(rawPos.y(), velocity.y());
// Make sure the first instance of a touch point we send has the
// 'pressed' state.
if (tp.state != Qt::TouchPointPressed)
tp.state = Qt::TouchPointPressed;
}

tp.velocity = QVector2D(f.vx.value(), f.vy.value());
tp.velocity = QVector2D(f.x.velocity(), f.y.velocity());

// Then use current
rawPos.setX(f.x.value() + f.vx.value() * m_handler->d->m_prediction / 1000.0);
rawPos.setY(f.y.value() + f.vy.value() * m_handler->d->m_prediction / 1000.0);
rawPos.setX(f.x.position() + f.x.velocity() * m_handler->d->m_prediction / 1000.0);
rawPos.setY(f.y.position() + f.y.velocity() * m_handler->d->m_prediction / 1000.0);

tp.area.moveCenter(rawPos);

Expand All @@ -871,21 +884,5 @@ void QEvdevTouchScreenHandlerThread::filterAndSendTouchPoints()
points);
}

void QEvdevTouchScreenHandlerThread::Filter::initialize(float x, float q, float r, float p)
{
m_x = x;
m_q = q;
m_r = r;
m_p = p;
}

void QEvdevTouchScreenHandlerThread::Filter::update(float x)
{
m_p = m_p + m_q;
float k = m_p / (m_p + m_r);
m_x = m_x + k * (x - m_x);
m_p = (1.0f - k) * m_p;
}


QT_END_NAMESPACE
23 changes: 6 additions & 17 deletions src/platformsupport/input/evdevtouch/qevdevtouchhandler_p.h
Expand Up @@ -53,6 +53,8 @@
#include <QtCore/private/qthread_p.h>
#include <qpa/qwindowsysteminterface.h>

#include "qevdevtouchfilter_p.h"

#if !defined(QT_NO_MTDEV)
struct mtdev;
#endif
Expand Down Expand Up @@ -127,27 +129,14 @@ public slots:
bool m_touchUpdatePending;
QWindow *m_filterWindow;

// A very simple 1D Kalman Filter
class Filter
{
public:
void initialize(float x, float q = 1, float r = 10, float p = 1);
void update(float x);
float value() const { return m_x; }
private:
float m_q;
float m_r;
float m_p;
float m_x;
};
struct FilteredTouchPoint {
Filter x;
Filter y;
Filter vx;
Filter vy;
QEvdevTouchFilter x;
QEvdevTouchFilter y;
QWindowSystemInterface::TouchPoint touchPoint;
};
QHash<int, FilteredTouchPoint> m_filteredPoints;

float m_touchRate;
};

QT_END_NAMESPACE
Expand Down

0 comments on commit 0b43ccd

Please sign in to comment.