Skip to content

Commit d01f8fb

Browse files
committed
Add touch-calibrator example
A touchscreen calibration tool for calculating calibration matrices. Supports 3, 4, and 5 point calibration modes with animated crosshair targets and progress tracking. Outputs 6-parameter affine transformation compatible with weston-calibrator and libinput. Features: - 3/4/5 point calibration modes (configurable via command line) - Aspect-ratio aware target positioning - Smooth animations with ease-out-back easing - Least-squares calibration matrix calculation using nalgebra - Progress bar and visual feedback - Internationalization support with @tr() - Restart capability
1 parent f24ad34 commit d01f8fb

File tree

6 files changed

+537
-0
lines changed

6 files changed

+537
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ members = [
2222
'examples/ffmpeg',
2323
'examples/gstreamer-player',
2424
'examples/plotter',
25+
'examples/touch-calibrator',
2526
'demos/printerdemo/rust',
2627
'demos/printerdemo_mcu',
2728
'examples/slide_puzzle',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright © SixtyFPS GmbH <[email protected]>
2+
# SPDX-License-Identifier: MIT
3+
4+
[package]
5+
name = "touch-calibrator"
6+
version = "1.15.0"
7+
authors = ["Slint Developers <[email protected]>"]
8+
edition = "2021"
9+
publish = false
10+
license = "MIT"
11+
12+
[[bin]]
13+
path = "main.rs"
14+
name = "slint-touch-calibrator"
15+
16+
[dependencies]
17+
slint = { path = "../../api/rs/slint" }
18+
nalgebra = "0.34"
19+
20+
[build-dependencies]
21+
slint-build = { path = "../../api/rs/build" }
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Touch Calibrator
2+
3+
A touchscreen calibration tool for calculating calibration matrices, similar to weston-calibrator.
4+
5+
## Features
6+
7+
- **Multi-Point Calibration**: Supports 3, 4, or 5 point calibration modes
8+
- **Aspect-Ratio Aware**: Target positions automatically adjust for screen dimensions
9+
- **Smooth Animations**: Crosshair targets move with ease-out-back animation
10+
- **Progress Tracking**: Visual progress bar and point counter
11+
- **Least-Squares Fitting**: Accurate calibration matrix calculation using nalgebra
12+
- **Restart Capability**: Easy restart of calibration process
13+
- **Internationalization**: UI text ready for translation with @tr()
14+
15+
## How It Works
16+
17+
1. The application displays an animated crosshair target at predefined positions
18+
2. The user touches each target as it appears on the screen
19+
3. The application records both the raw touch position and expected target position
20+
4. After all points are collected, a 6-parameter affine transformation matrix is calculated using least-squares fitting
21+
5. The calibration matrix is displayed on screen and printed to the console
22+
23+
## Usage
24+
25+
Run with default 5-point calibration:
26+
```bash
27+
cargo run
28+
```
29+
30+
Specify the number of calibration points (3, 4, or 5):
31+
```bash
32+
cargo run -- 3 # 3-point calibration (equilateral triangle)
33+
cargo run -- 4 # 4-point calibration (four corners)
34+
cargo run -- 5 # 5-point calibration (four corners + center)
35+
```
36+
37+
Touch each target as it appears. After completing all points, the calibration matrix will be displayed.
38+
39+
## Calibration Modes
40+
41+
### 3-Point Mode
42+
Uses an equilateral triangle centered on the screen, rotated 15 degrees. This is the minimum number of points needed for affine transformation calibration.
43+
44+
### 4-Point Mode
45+
Uses the four corners of the screen with aspect-ratio aware margins (40px from edges).
46+
47+
### 5-Point Mode (Default)
48+
Uses the four corners plus the center point for improved accuracy.
49+
50+
## Calibration Algorithm
51+
52+
The tool calculates a 6-parameter affine transformation matrix using least-squares fitting:
53+
54+
```
55+
calibrated_x = a * raw_x + b * raw_y + c
56+
calibrated_y = d * raw_x + e * raw_y + f
57+
```
58+
59+
The six parameters (a, b, c, d, e, f) are calculated by solving the overdetermined system of equations formed by the calibration points.
60+
61+
## Output Format
62+
63+
The calibration values are displayed in weston-calibrator compatible format:
64+
65+
```
66+
a b c
67+
d e f
68+
```
69+
70+
Example output:
71+
```
72+
0.996256 0.012633 -0.001569
73+
-0.010563 0.984978 0.003896
74+
```
75+
76+
These values can be used with libinput's [LIBINPUT_CALIBRATION_MATRIX](https://wayland.freedesktop.org/libinput/doc/latest/device-configuration-via-udev.html#libinput_calibration_matrix) property or other touch input systems that support affine transformation.

examples/touch-calibrator/build.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright © SixtyFPS GmbH <[email protected]>
2+
// SPDX-License-Identifier: MIT
3+
4+
fn main() {
5+
slint_build::compile("main.slint").unwrap();
6+
}

examples/touch-calibrator/main.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Copyright © SixtyFPS GmbH <[email protected]>
2+
// SPDX-License-Identifier: MIT
3+
4+
use std::cell::RefCell;
5+
use std::rc::Rc;
6+
7+
use nalgebra::{Matrix3, Vector3};
8+
9+
slint::slint! {
10+
export { MainWindow } from "main.slint";
11+
}
12+
13+
#[derive(Debug, Clone, Copy)]
14+
struct CalibrationPoint {
15+
x: f32,
16+
y: f32,
17+
}
18+
19+
#[derive(Debug, Clone)]
20+
struct CalibrationData {
21+
target: CalibrationPoint,
22+
touch: Option<CalibrationPoint>,
23+
}
24+
25+
struct Calibrator {
26+
points: Vec<CalibrationData>,
27+
}
28+
29+
impl Calibrator {
30+
fn new(num_points: usize, width: f32, height: f32) -> Self {
31+
// Calculate margin based on aspect ratio to maintain consistent physical distance
32+
let margin_pixels = 40.0; // Target margin in pixels
33+
let margin_x = margin_pixels / width;
34+
let margin_y = margin_pixels / height;
35+
36+
let target_positions = match num_points {
37+
3 => {
38+
// Equilateral triangle centered at (0.5, 0.5), rotated 15 degrees
39+
let (cx, cy, radius, rotation) = (0.5, 0.5, 0.35, 15.0_f32.to_radians());
40+
(0..3)
41+
.map(|i| {
42+
let angle = rotation + i as f32 * 2.0 * std::f32::consts::PI / 3.0;
43+
(cx + radius * angle.cos(), cy + radius * angle.sin())
44+
})
45+
.collect()
46+
}
47+
4 => vec![
48+
(margin_x, margin_y),
49+
(1.0 - margin_x, margin_y),
50+
(1.0 - margin_x, 1.0 - margin_y),
51+
(margin_x, 1.0 - margin_y),
52+
],
53+
_ => vec![
54+
(margin_x, margin_y),
55+
(1.0 - margin_x, margin_y),
56+
(1.0 - margin_x, 1.0 - margin_y),
57+
(margin_x, 1.0 - margin_y),
58+
(0.5, 0.5),
59+
],
60+
};
61+
62+
Self {
63+
points: target_positions
64+
.iter()
65+
.map(|&(x, y)| CalibrationData {
66+
target: CalibrationPoint { x, y },
67+
touch: None,
68+
})
69+
.collect(),
70+
}
71+
}
72+
73+
fn get_target(&self, index: usize) -> Option<(f32, f32)> {
74+
self.points.get(index).map(|data| (data.target.x, data.target.y))
75+
}
76+
77+
fn add_point(&mut self, x: f32, y: f32, index: usize) {
78+
if let Some(point) = self.points.get_mut(index) {
79+
point.touch = Some(CalibrationPoint { x, y });
80+
}
81+
}
82+
83+
fn calculate_calibration(&self) -> Option<CalibrationMatrix> {
84+
let valid_points: Vec<_> = self
85+
.points
86+
.iter()
87+
.filter_map(|data| data.touch.map(|touch| (data.target, touch)))
88+
.collect();
89+
90+
if valid_points.len() < 3 {
91+
return None;
92+
}
93+
94+
// Least-squares: target = A * [touch_x, touch_y, 1]^T
95+
let (mut ata, mut atb_x, mut atb_y) = (Matrix3::zeros(), Vector3::zeros(), Vector3::zeros());
96+
97+
for (target, touch) in &valid_points {
98+
let row = Vector3::new(touch.x, touch.y, 1.0);
99+
ata += row * row.transpose();
100+
atb_x += row * target.x;
101+
atb_y += row * target.y;
102+
}
103+
104+
let lu = ata.lu();
105+
let params_x = lu.solve(&atb_x)?;
106+
let params_y = lu.solve(&atb_y)?;
107+
108+
Some(CalibrationMatrix {
109+
a: params_x[0],
110+
b: params_x[1],
111+
c: params_x[2],
112+
d: params_y[0],
113+
e: params_y[1],
114+
f: params_y[2],
115+
})
116+
}
117+
118+
fn restart(&mut self) {
119+
self.points.iter_mut().for_each(|p| p.touch = None);
120+
}
121+
}
122+
123+
#[derive(Debug)]
124+
struct CalibrationMatrix {
125+
a: f32,
126+
b: f32,
127+
c: f32,
128+
d: f32,
129+
e: f32,
130+
f: f32,
131+
}
132+
133+
fn main() {
134+
let num_points = std::env::args()
135+
.nth(1)
136+
.and_then(|arg| arg.parse::<usize>().ok())
137+
.unwrap_or(5);
138+
139+
let num_points = match num_points {
140+
3 | 4 | 5 => num_points,
141+
_ => {
142+
eprintln!("Invalid number of points. Using default (5).");
143+
eprintln!("Valid options: 3, 4, or 5");
144+
5
145+
}
146+
};
147+
148+
let main_window = MainWindow::new().unwrap();
149+
150+
// Get window dimensions for aspect-ratio aware target positioning
151+
let size = main_window.window().size();
152+
let width = size.width as f32;
153+
let height = size.height as f32;
154+
155+
let calibrator = Rc::new(RefCell::new(Calibrator::new(num_points, width, height)));
156+
let current_point = Rc::new(RefCell::new(0_usize));
157+
158+
// Setup point-clicked callback
159+
{
160+
let main_window_weak = main_window.as_weak();
161+
let calibrator = calibrator.clone();
162+
let current_point = current_point.clone();
163+
164+
main_window.on_point_clicked(move |x, y| {
165+
let main_window = main_window_weak.unwrap();
166+
let mut current = current_point.borrow_mut();
167+
let total = calibrator.borrow().points.len();
168+
169+
println!("Point {} clicked at ({:.3}, {:.3})", *current, x, y);
170+
171+
calibrator.borrow_mut().add_point(x, y, *current);
172+
*current += 1;
173+
main_window.set_current_point(*current as i32);
174+
175+
if *current >= total {
176+
main_window.set_calibration_complete(true);
177+
if let Some(matrix) = calibrator.borrow().calculate_calibration() {
178+
let result = format!(
179+
"{:.6} {:.6} {:.6}\n{:.6} {:.6} {:.6}",
180+
matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f
181+
);
182+
println!("\nCalibration values: {:.6} {:.6} {:.6} {:.6} {:.6} {:.6}",
183+
matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
184+
main_window.set_calibration_result(result.into());
185+
}
186+
} else if let Some((tx, ty)) = calibrator.borrow().get_target(*current) {
187+
main_window.set_target_x(tx);
188+
main_window.set_target_y(ty);
189+
main_window.set_progress_text(format!("Point {} / {}", *current + 1, total).into());
190+
}
191+
});
192+
}
193+
194+
// Setup restart callback
195+
{
196+
let main_window_weak = main_window.as_weak();
197+
let calibrator = calibrator.clone();
198+
let current_point = current_point.clone();
199+
200+
main_window.on_restart_calibration(move || {
201+
let main_window = main_window_weak.unwrap();
202+
let total = calibrator.borrow().points.len();
203+
204+
calibrator.borrow_mut().restart();
205+
*current_point.borrow_mut() = 0;
206+
207+
if let Some((tx, ty)) = calibrator.borrow().get_target(0) {
208+
main_window.set_target_x(tx);
209+
main_window.set_target_y(ty);
210+
}
211+
212+
main_window.set_calibration_complete(false);
213+
main_window.set_calibration_result("".into());
214+
main_window.set_current_point(0);
215+
main_window.set_progress_text(format!("Point 1 / {}", total).into());
216+
println!("\nCalibration restarted");
217+
});
218+
}
219+
220+
// Initialize UI
221+
let total = calibrator.borrow().points.len();
222+
main_window.set_total_points(total as i32);
223+
main_window.set_current_point(0);
224+
main_window.set_progress_text(format!("Point 1 / {}", total).into());
225+
if let Some((tx, ty)) = calibrator.borrow().get_target(0) {
226+
main_window.set_target_x(tx);
227+
main_window.set_target_y(ty);
228+
}
229+
230+
println!("Touch Calibration Tool ({}-point calibration)", num_points);
231+
println!("======================");
232+
println!("Touch each target as it appears on the screen.");
233+
println!();
234+
235+
main_window.run().unwrap();
236+
}

0 commit comments

Comments
 (0)