Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions etc/iptsd.conf
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@
##
# NeutralValue = 0

##
## Radius in pixels around each maxima to darken (excluding the maxima pixel itself).
## Setting this value to zero disables local maxima surrounding pixel suppression.
##
# PeakSuppressionRadius = 0

##
## The factor in which to darken surrounding pixels when using peak suppression.
## Multiplies neighbors by this factor (e.g., 0.7 = reduce brightness by 30%).
##
# PeakSuppressionFactor = 0.00

##
## The activation threshold for blob detection (Range 0 - 255).
## If a pixel of the heatmap is larger than this value plus the neutral value, the blob detector
Expand Down
12 changes: 12 additions & 0 deletions etc/presets/surface-laptop-studio-2.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Device]
Vendor = 0x045E
Product = 0x0C46

[Contacts]
Neutral = average
NeutralValue = 10
PeakSuppressionRadius = 2
PeakSuppressionFactor = 0.25
ActivationThreshold = 20
DeactivationThreshold = 16
OrientationThresholdMax = 90
116 changes: 116 additions & 0 deletions src/contacts/detection/algorithms/suppression.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: GPL-2.0-or-later

#ifndef IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP
#define IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP

#include <common/casts.hpp>
#include <common/types.hpp>

#include <algorithm>
#include <utility>
#include <vector>

namespace iptsd::contacts::detection::suppression {

/*
* Darken pixels around each maxima by a constant factor, while leaving maxima pixels unchanged.
*
* This is intended to "sharpen" peaks / deepen valleys between peaks so that cluster spanning
* is less likely to connect neighboring contacts.
*
* Behavior:
* - out is initialized as a copy of in
* - for each maxima, all pixels within euclidean radius (excluding the center pixel) are reduced:
* out(y,x) = min(out(y,x), in(y,x) * factor)
* - maxima pixels are restored to their original values (exactly unchanged)
*
* Notes:
* - If neighborhoods overlap, the result is still "at most factor" darkening (no stacking),
* because we always compare against (in * factor), not (out * factor).
*/
template <class Derived>
void darken_around_maximas(const DenseBase<Derived> &in,
const std::vector<Point> &maximas,
const Eigen::Index radius,
const typename DenseBase<Derived>::Scalar factor,
DenseBase<Derived> &out)
{
using T = typename DenseBase<Derived>::Scalar;

out = in;

if (maximas.empty())
return;

if (radius <= 0)
return;

// Clamp factor to sane range. (factor = 1 => no-op, factor = 0 => nuke neighbors to 0)
const T f = std::clamp(factor, casts::to<T>(0), casts::to<T>(1));
if (f >= casts::to<T>(1))
return;

const Eigen::Index cols = in.cols();
const Eigen::Index rows = in.rows();

// Precompute offsets in a disk (euclidean radius).
const isize r = casts::to_signed(radius);
const isize r2 = r * r;

std::vector<std::pair<isize, isize>> offsets;
offsets.reserve(static_cast<size_t>((2 * r + 1) * (2 * r + 1)));

for (isize dy = -r; dy <= r; ++dy) {
for (isize dx = -r; dx <= r; ++dx) {
if (dx == 0 && dy == 0)
continue;

if (((dx * dx) + (dy * dy)) <= r2)
offsets.emplace_back(dx, dy);
}
}

// Darken neighbors.
for (const Point &p : maximas) {
const Eigen::Index cx = p.x();
const Eigen::Index cy = p.y();

// Skip invalid points defensively (shouldn't happen, but costs nothing).
if (cx < 0 || cx >= cols || cy < 0 || cy >= rows)
continue;

for (const auto &[dx, dy] : offsets) {
const isize nx_s = casts::to_signed(cx) + dx;
const isize ny_s = casts::to_signed(cy) + dy;

if (nx_s < 0 || ny_s < 0)
continue;

const Eigen::Index nx = casts::to_eigen(nx_s);
const Eigen::Index ny = casts::to_eigen(ny_s);

if (nx >= cols || ny >= rows)
continue;

const T target = in(ny, nx) * f;

// Use min() against (in*factor) to prevent multi-maxima "stacking" darkness.
out(ny, nx) = std::min(out(ny, nx), target);
}
}

// Restore maxima pixels exactly.
for (const Point &p : maximas) {
const Eigen::Index x = p.x();
const Eigen::Index y = p.y();

if (x < 0 || x >= cols || y < 0 || y >= rows)
continue;

out(y, x) = in(y, x);
}
}

} // namespace iptsd::contacts::detection::suppression

#endif // IPTSD_CONTACTS_DETECTION_ALGORITHMS_SUPPRESSION_HPP
12 changes: 12 additions & 0 deletions src/contacts/detection/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ struct Config {
* the recursive cluster search will stop once it reaches it.
*/
T deactivation_threshold = casts::to<T>(20);

/*
* Radius in pixels around each maxima to darken (excluding the maxima pixel itself).
* Setting this value to zero disables local maxima surrounding pixel suppression.
*/
usize peak_suppression_radius = 0;

/*
* The factor in which to darken surrounding pixels when using peak suppression.
* Multiplies neighbors by this factor (e.g., 0.5 = reduce brightness by 50%).
*/
T peak_suppression_factor = casts::to<T>(0);
};

} // namespace iptsd::contacts::detection
Expand Down
28 changes: 25 additions & 3 deletions src/contacts/detection/detector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "algorithms/maximas.hpp"
#include "algorithms/neutral.hpp"
#include "algorithms/overlaps.hpp"
#include "algorithms/suppression.hpp"
#include "config.hpp"

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

// A copy of the blurred heatmap used for cluster spanning when suppression is enabled.
Image<T> m_img_span {};

// The kernel that is used for blurring.
Matrix3<T> m_kernel_blur = kernels::gaussian<T, 3, 3>(gsl::narrow_cast<T>(0.75));

Expand Down Expand Up @@ -97,6 +101,7 @@ class Detector {
if (brows != rows || bcols != cols) {
m_img_neutral.conservativeResize(rows, cols);
m_img_blurred.conservativeResize(rows, cols);
m_img_span.conservativeResize(rows, cols);
m_fitting_temp.conservativeResize(rows, cols);

if (m_config.normalize)
Expand Down Expand Up @@ -126,12 +131,29 @@ class Detector {
const T athresh = m_config.activation_threshold;
const T dthresh = m_config.deactivation_threshold;

// Search for local maximas
// Find local maximas on the original blurred heatmap (stable maxima positions)
maximas::find(m_img_blurred, athresh, m_maximas);

// Iterate over the maximas and start building clusters
// Create a suppressed copy used only for cluster spanning
const Image<T> *span_src = &m_img_blurred;

const Eigen::Index suppression_radius = m_config.peak_suppression_radius;
if (suppression_radius >= 1) {
const T suppression_factor = m_config.peak_suppression_factor;
suppression::darken_around_maximas(
m_img_blurred,
m_maximas,
suppression_radius,
suppression_factor,
m_img_span
);

span_src = &m_img_span;
}

// Iterate over the maximas and start building clusters (on suppressed image if enabled)
for (const Point &point : m_maximas) {
Box cluster = cluster::span(m_img_blurred, point, athresh, dthresh);
Box cluster = cluster::span(*span_src, point, athresh, dthresh);

if (cluster.isEmpty())
continue;
Expand Down
5 changes: 5 additions & 0 deletions src/core/generic/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class Config {
f64 contacts_size_max = 2;
f64 contacts_aspect_min = 1;
f64 contacts_aspect_max = 2.5;
usize contacts_peak_suppression_radius = 0;
f64 contacts_peak_suppression_factor = 0;

// [Stylus]
bool stylus_disable = false;
Expand Down Expand Up @@ -100,6 +102,9 @@ class Config {
config.detection.neutral_value_offset = nval_offset / 255.0;
config.detection.neutral_value_backoff = 16; // TODO: config option

config.detection.peak_suppression_factor = this->contacts_peak_suppression_factor;
config.detection.peak_suppression_radius = this->contacts_peak_suppression_radius;

const f64 diagonal = std::hypot(this->width, this->height);

config.validation.track_validity = true;
Expand Down
2 changes: 2 additions & 0 deletions src/core/linux/config-loader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ class ConfigLoader {
this->get(ini, "Contacts", "SizeMax", m_config.contacts_size_max);
this->get(ini, "Contacts", "AspectMin", m_config.contacts_aspect_max);
this->get(ini, "Contacts", "AspectMax", m_config.contacts_aspect_max);
this->get(ini, "Contacts", "PeakSuppressionRadius", m_config.contacts_peak_suppression_radius);
this->get(ini, "Contacts", "PeakSuppressionFactor", m_config.contacts_peak_suppression_factor);

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