Skip to content

Commit c487d1a

Browse files
authored
Merge pull request #205 from LegendaryFire/tristan-peak-suppression
Neighboring pixel suppression for better multi-touch gestures
2 parents 59256bf + e26847b commit c487d1a

File tree

7 files changed

+184
-3
lines changed

7 files changed

+184
-3
lines changed

etc/iptsd.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@
6565
##
6666
# NeutralValue = 0
6767

68+
##
69+
## Radius in pixels around each maxima to darken (excluding the maxima pixel itself).
70+
## Setting this value to zero disables local maxima surrounding pixel suppression.
71+
##
72+
# PeakSuppressionRadius = 0
73+
74+
##
75+
## The factor in which to darken surrounding pixels when using peak suppression.
76+
## Multiplies neighbors by this factor (e.g., 0.7 = reduce brightness by 30%).
77+
##
78+
# PeakSuppressionFactor = 0.00
79+
6880
##
6981
## The activation threshold for blob detection (Range 0 - 255).
7082
## If a pixel of the heatmap is larger than this value plus the neutral value, the blob detector
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[Device]
2+
Vendor = 0x045E
3+
Product = 0x0C46
4+
5+
[Contacts]
6+
Neutral = average
7+
NeutralValue = 10
8+
PeakSuppressionRadius = 2
9+
PeakSuppressionFactor = 0.25
10+
ActivationThreshold = 20
11+
DeactivationThreshold = 16
12+
OrientationThresholdMax = 90
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
#ifndef IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP
4+
#define IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP
5+
6+
#include <common/casts.hpp>
7+
#include <common/types.hpp>
8+
9+
#include <algorithm>
10+
#include <utility>
11+
#include <vector>
12+
13+
namespace iptsd::contacts::detection::suppression {
14+
15+
/*!
16+
* Darken pixels around each maxima by a constant factor, while leaving maxima pixels unchanged.
17+
*
18+
* This is intended to "sharpen" peaks / deepen valleys between peaks so that cluster spanning
19+
* is less likely to connect neighboring contacts.
20+
*
21+
* Behavior:
22+
* - out is initialized as a copy of in
23+
* - for each maxima, all pixels within euclidean radius (excluding the center pixel) are reduced:
24+
* out(y,x) = min(out(y,x), in(y,x) * factor)
25+
* - maxima pixels are restored to their original values (exactly unchanged)
26+
*
27+
* Notes:
28+
* - If neighborhoods overlap, the result is still "at most factor" darkening (no stacking),
29+
* because we always compare against (in * factor), not (out * factor).
30+
*/
31+
template <class Derived>
32+
void darken_around_maximas(const DenseBase<Derived> &in,
33+
const std::vector<Point> &maximas,
34+
const Eigen::Index radius,
35+
const typename DenseBase<Derived>::Scalar factor,
36+
DenseBase<Derived> &out)
37+
{
38+
using T = typename DenseBase<Derived>::Scalar;
39+
40+
out = in;
41+
42+
if (maximas.empty())
43+
return;
44+
45+
if (radius <= 0)
46+
return;
47+
48+
// Clamp factor to sane range. (factor = 1 => no-op, factor = 0 => nuke neighbors to 0)
49+
const T f = std::clamp(factor, casts::to<T>(0), casts::to<T>(1));
50+
if (f >= casts::to<T>(1))
51+
return;
52+
53+
const Eigen::Index cols = in.cols();
54+
const Eigen::Index rows = in.rows();
55+
56+
// Precompute offsets in a disk (euclidean radius).
57+
const isize r = casts::to_signed(radius);
58+
const isize r2 = r * r;
59+
60+
std::vector<std::pair<isize, isize>> offsets;
61+
offsets.reserve(static_cast<size_t>((2 * r + 1) * (2 * r + 1)));
62+
63+
for (isize dy = -r; dy <= r; ++dy) {
64+
for (isize dx = -r; dx <= r; ++dx) {
65+
if (dx == 0 && dy == 0)
66+
continue;
67+
68+
if (((dx * dx) + (dy * dy)) <= r2)
69+
offsets.emplace_back(dx, dy);
70+
}
71+
}
72+
73+
// Darken neighbors.
74+
for (const Point &p : maximas) {
75+
const Eigen::Index cx = p.x();
76+
const Eigen::Index cy = p.y();
77+
78+
// Skip invalid points defensively (shouldn't happen, but costs nothing).
79+
if (cx < 0 || cx >= cols || cy < 0 || cy >= rows)
80+
continue;
81+
82+
for (const auto &[dx, dy] : offsets) {
83+
const isize nx_s = casts::to_signed(cx) + dx;
84+
const isize ny_s = casts::to_signed(cy) + dy;
85+
86+
if (nx_s < 0 || ny_s < 0)
87+
continue;
88+
89+
const Eigen::Index nx = casts::to_eigen(nx_s);
90+
const Eigen::Index ny = casts::to_eigen(ny_s);
91+
92+
if (nx >= cols || ny >= rows)
93+
continue;
94+
95+
const T target = in(ny, nx) * f;
96+
97+
// Use min() against (in*factor) to prevent multi-maxima "stacking" darkness.
98+
out(ny, nx) = std::min(out(ny, nx), target);
99+
}
100+
}
101+
102+
// Restore maxima pixels exactly.
103+
for (const Point &p : maximas) {
104+
const Eigen::Index x = p.x();
105+
const Eigen::Index y = p.y();
106+
107+
if (x < 0 || x >= cols || y < 0 || y >= rows)
108+
continue;
109+
110+
out(y, x) = in(y, x);
111+
}
112+
}
113+
114+
} // namespace iptsd::contacts::detection::suppression
115+
116+
#endif // IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP

src/contacts/detection/config.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ struct Config {
4949
* the recursive cluster search will stop once it reaches it.
5050
*/
5151
T deactivation_threshold = casts::to<T>(20);
52+
53+
/*
54+
* Radius in pixels around each maxima to darken (excluding the maxima pixel itself).
55+
* Setting this value to zero disables local maxima surrounding pixel suppression.
56+
*/
57+
usize peak_suppression_radius = 0;
58+
59+
/*
60+
* The factor in which to darken surrounding pixels when using peak suppression.
61+
* Multiplies neighbors by this factor (e.g., 0.5 = reduce brightness by 50%).
62+
*/
63+
T peak_suppression_factor = casts::to<T>(0);
5264
};
5365

5466
} // namespace iptsd::contacts::detection

src/contacts/detection/detector.hpp

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "algorithms/maximas.hpp"
1313
#include "algorithms/neutral.hpp"
1414
#include "algorithms/overlaps.hpp"
15+
#include "algorithms/suppression.hpp"
1516
#include "config.hpp"
1617

1718
#include <common/casts.hpp>
@@ -43,6 +44,9 @@ class Detector {
4344
// The blurred heatmap.
4445
Image<T> m_img_blurred {};
4546

47+
// A copy of the blurred heatmap used for cluster spanning when suppression is enabled.
48+
Image<T> m_img_span {};
49+
4650
// The kernel that is used for blurring.
4751
Matrix3<T> m_kernel_blur = kernels::gaussian<T, 3, 3>(gsl::narrow_cast<T>(0.75));
4852

@@ -97,6 +101,7 @@ class Detector {
97101
if (brows != rows || bcols != cols) {
98102
m_img_neutral.conservativeResize(rows, cols);
99103
m_img_blurred.conservativeResize(rows, cols);
104+
m_img_span.conservativeResize(rows, cols);
100105
m_fitting_temp.conservativeResize(rows, cols);
101106

102107
if (m_config.normalize)
@@ -126,12 +131,29 @@ class Detector {
126131
const T athresh = m_config.activation_threshold;
127132
const T dthresh = m_config.deactivation_threshold;
128133

129-
// Search for local maximas
134+
// Find local maximas on the original blurred heatmap (stable maxima positions)
130135
maximas::find(m_img_blurred, athresh, m_maximas);
131136

132-
// Iterate over the maximas and start building clusters
137+
// Create a suppressed copy used only for cluster spanning
138+
const Image<T> *span_src = &m_img_blurred;
139+
140+
const Eigen::Index suppression_radius = casts::to_eigen(m_config.peak_suppression_radius);
141+
if (suppression_radius >= 1) {
142+
const T suppression_factor = m_config.peak_suppression_factor;
143+
suppression::darken_around_maximas(
144+
m_img_blurred,
145+
m_maximas,
146+
suppression_radius,
147+
suppression_factor,
148+
m_img_span
149+
);
150+
151+
span_src = &m_img_span;
152+
}
153+
154+
// Iterate over the maximas and start building clusters (on suppressed image if enabled)
133155
for (const Point &point : m_maximas) {
134-
Box cluster = cluster::span(m_img_blurred, point, athresh, dthresh);
156+
Box cluster = cluster::span(*span_src, point, athresh, dthresh);
135157

136158
if (cluster.isEmpty())
137159
continue;

src/core/generic/config.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class Config {
5050
f64 contacts_size_max = 2;
5151
f64 contacts_aspect_min = 1;
5252
f64 contacts_aspect_max = 2.5;
53+
usize contacts_peak_suppression_radius = 0;
54+
f64 contacts_peak_suppression_factor = 0;
5355

5456
// [Stylus]
5557
bool stylus_disable = false;
@@ -100,6 +102,9 @@ class Config {
100102
config.detection.neutral_value_offset = nval_offset / 255.0;
101103
config.detection.neutral_value_backoff = 16; // TODO: config option
102104

105+
config.detection.peak_suppression_factor = this->contacts_peak_suppression_factor;
106+
config.detection.peak_suppression_radius = this->contacts_peak_suppression_radius;
107+
103108
const f64 diagonal = std::hypot(this->width, this->height);
104109

105110
config.validation.track_validity = true;

src/core/linux/config-loader.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ class ConfigLoader {
175175
this->get(ini, "Contacts", "SizeMax", m_config.contacts_size_max);
176176
this->get(ini, "Contacts", "AspectMin", m_config.contacts_aspect_min);
177177
this->get(ini, "Contacts", "AspectMax", m_config.contacts_aspect_max);
178+
this->get(ini, "Contacts", "PeakSuppressionRadius", m_config.contacts_peak_suppression_radius);
179+
this->get(ini, "Contacts", "PeakSuppressionFactor", m_config.contacts_peak_suppression_factor);
178180

179181
this->get(ini, "Stylus", "Disable", m_config.stylus_disable);
180182
this->get(ini, "Stylus", "TipDistance", m_config.stylus_tip_distance);

0 commit comments

Comments
 (0)