Implement clip filter, and fix having no optimizations due to sending a all as single line

This commit is contained in:
Ruben van de Ven 2025-07-08 21:17:22 +02:00
parent ad9c8d6d37
commit 744a3b3dfa
5 changed files with 384 additions and 32 deletions

160
Cargo.lock generated
View file

@ -2158,6 +2158,16 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "earcutr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01"
dependencies = [
"itertools 0.11.0",
"num-traits",
]
[[package]] [[package]]
name = "ecolor" name = "ecolor"
version = "0.23.0" version = "0.23.0"
@ -2597,6 +2607,46 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "geo"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1"
dependencies = [
"earcutr",
"float_next_after 1.0.0",
"geo-types",
"geographiclib-rs",
"i_overlay",
"log",
"num-traits",
"robust",
"rstar",
"spade",
]
[[package]]
name = "geo-types"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224"
dependencies = [
"approx 0.5.1",
"num-traits",
"rayon",
"rstar",
"serde",
]
[[package]]
name = "geographiclib-rs"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "gethostname" name = "gethostname"
version = "0.4.3" version = "0.4.3"
@ -2901,6 +2951,15 @@ dependencies = [
"svg_fmt", "svg_fmt",
] ]
[[package]]
name = "hash32"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.12.3" version = "0.12.3"
@ -2924,6 +2983,8 @@ version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@ -2942,6 +3003,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "heapless"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
dependencies = [
"hash32",
"stable_deref_trait",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -3000,6 +3071,50 @@ dependencies = [
"sample-consensus", "sample-consensus",
] ]
[[package]]
name = "i_float"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343"
dependencies = [
"serde",
]
[[package]]
name = "i_key_sort"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd"
[[package]]
name = "i_overlay"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49"
dependencies = [
"i_float",
"i_key_sort",
"i_shape",
"i_tree",
"rayon",
]
[[package]]
name = "i_shape"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce"
dependencies = [
"i_float",
"serde",
]
[[package]]
name = "i_tree"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.63" version = "0.1.63"
@ -3149,6 +3264,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@ -5118,6 +5242,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "robust"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
[[package]] [[package]]
name = "rodio" name = "rodio"
version = "0.19.0" version = "0.19.0"
@ -5147,6 +5277,17 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rstar"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
dependencies = [
"heapless",
"num-traits",
"smallvec",
]
[[package]] [[package]]
name = "rusb" name = "rusb"
version = "0.7.0" version = "0.7.0"
@ -5544,6 +5685,18 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "spade"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c"
dependencies = [
"hashbrown 0.15.3",
"num-traits",
"robust",
"smallvec",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.2.0+1.5.4" version = "0.2.0+1.5.4"
@ -5563,6 +5716,12 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "stackfuture" name = "stackfuture"
version = "0.3.0" version = "0.3.0"
@ -5984,6 +6143,7 @@ dependencies = [
"bevy", "bevy",
"bevy_nannou", "bevy_nannou",
"cv-core", "cv-core",
"geo",
"homography", "homography",
"iyes_perf_ui", "iyes_perf_ui",
"nalgebra 0.30.1", "nalgebra 0.30.1",

View file

@ -23,6 +23,7 @@ serde_repr = "0.1.20"
homography = { git = "https://github.com/azazdeaz/homography" } homography = { git = "https://github.com/azazdeaz/homography" }
nalgebra = "0.30.0" nalgebra = "0.30.0"
cv-core = "0.15.0" cv-core = "0.15.0"
geo = "0.30.0"
[dev-dependencies] [dev-dependencies]

View file

@ -15,7 +15,7 @@ use serde_json::Result;
use trap_rust::trap::filters::PointFilters; use trap_rust::trap::filters::PointFilters;
use trap_rust::trap::laser::{shape_rect, LaserPoints, LaserSpace, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX, Corner}; use trap_rust::trap::laser::{shape_rect, LaserPoints, LaserSpace, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX, Corner};
use trap_rust::trap::tracks::CoordinateSpace; use trap_rust::trap::tracks::CoordinateSpace;
use trap_rust::trap::utils::closest_edge; use trap_rust::trap::utils::{closest_edge, split_on_blank};
use trap_rust::trap::{laser::{python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H, DacConfig}, tracks::{RenderableLines}}; use trap_rust::trap::{laser::{python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H, DacConfig}, tracks::{RenderableLines}};
use zmq::Socket; use zmq::Socket;
use std::sync::{mpsc, Arc}; use std::sync::{mpsc, Arc};
@ -420,7 +420,12 @@ fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){
// println!("Cropped Points {} (was: {})", new_laser_points.len(), pointno); // println!("Cropped Points {} (was: {})", new_laser_points.len(), pointno);
// } // }
frame.add_lines(new_laser_points); // split by blanked points
let lines = split_on_blank(new_laser_points);
for line in lines {
frame.add_lines(line);
}
return; return;
} }
@ -742,6 +747,21 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
} }
if ui
.checkbox(&mut selected_config.filters.crop.enabled ,"Crop before corrections (recommended)")
.changed()
{
let enabled = selected_config.filters.crop.enabled;
if let Some(stream) = selected_laser_stream {
stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.crop.enabled = enabled;
}).unwrap();
}
}
// Pincushion / Pillow / Barrel distortion. Generally, only needed for the x-axis // Pincushion / Pillow / Barrel distortion. Generally, only needed for the x-axis
if ui if ui
@ -794,13 +814,13 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
if ui if ui
.checkbox(&mut selected_config.filters.crop.enabled ,"Crop") .checkbox(&mut selected_config.filters.clip.enabled ,"Apply clip mask")
.changed() .changed()
{ {
let enabled = selected_config.filters.crop.enabled; let enabled = selected_config.filters.clip.enabled;
if let Some(stream) = selected_laser_stream { if let Some(stream) = selected_laser_stream {
stream.send(move |laser_model: &mut LaserModel| { stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.crop.enabled = enabled; laser_model.config.filters.clip.enabled = enabled;
}).unwrap(); }).unwrap();
} }
} }
@ -929,7 +949,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) {
let draw = app.draw(); let draw = app.draw();
draw.background().color(BLACK); draw.background().color(srgba(0.3,0.3,0.3,1.));
let win = app.window_rect(); let win = app.window_rect();
@ -938,7 +958,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) {
let hh = h / 2.; let hh = h / 2.;
let hw = w / 2.; let hw = w / 2.;
let thickness = 2.0; let thickness = 3.0;
let win_rect = app.main_window().rect().pad(20.0); let win_rect = app.main_window().rect().pad(20.0);
@ -979,7 +999,7 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) {
} }
draw.polygon() draw.polygon()
.color(srgba(1.,1.,1.,3.)) .color(srgba(0.3, 0.3, 0.3, 1.))
.stroke(PINK) .stroke(PINK)
.stroke_weight(thickness) .stroke_weight(thickness)
.join_round() .join_round()
@ -999,14 +1019,24 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) {
let pointno = points.points.len(); let pointno = points.points.len();
let new_points = config.filters.apply(&points); let new_points = config.filters.apply(&points);
let new_laser_points = new_points.points;
// draw as distinct lines (this is how it is send to post-processing)
// TODO: alternatively, if the optimisation becomes an actual filter
// this should be drawn as a single line, and we can have an option to
// visualise the intermediate lines to make the draw order apparent
let lines = split_on_blank(new_laser_points);
for line in lines {
// similar to map code: // similar to map code:
let vertices = new_points.points.iter().map(|p| { let vertices = line.iter().map(|p| {
let color = srgba(p.color[0], p.color[1], p.color[0], 1.); let color = srgba(p.color[0], p.color[1], p.color[0], 1.);
let pos = [p.position[0] * hw, p.position[1] * hh]; let pos = [p.position[0] * hw, p.position[1] * hh];
(pos, color) (pos, color)
}); });
draw.polyline() draw.polyline()
@ -1014,6 +1044,8 @@ fn view_laser_preview(app: &App, model: &GuiModel, frame: Frame) {
.join_round() .join_round()
.points_colored(vertices); .points_colored(vertices);
} }
}
} }
draw.to_frame(app, &frame).unwrap(); draw.to_frame(app, &frame).unwrap();
@ -1124,8 +1156,21 @@ fn laser_mouse_moved(app: &App, model: &mut GuiModel, pos: Point2) {
// config.filters.clip. // config.filters.clip.
let point = config.filters.clip.mask.get_mut(*point_idx).unwrap(); let point = config.filters.clip.mask.get_mut(*point_idx).unwrap();
// set new position
*point = [laser_x, laser_y]; *point = [laser_x, laser_y];
// 3. update config in laser stream threat
let mask = config.filters.clip.mask.clone();
let selected_laser_stream = model.laser_streams.get(&dac_id);
if let Some(stream) = selected_laser_stream {
stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.clip.mask = mask;
}).unwrap();
}
} }
fn laser_mouse_released(_app: &App, model: &mut GuiModel, _button: MouseButton) { fn laser_mouse_released(_app: &App, model: &mut GuiModel, _button: MouseButton) {

View file

@ -1,6 +1,6 @@
use bevy::prelude::*; // for glam::f32::Mat3 use bevy::prelude::*; // for glam::f32::Mat3
use crate::trap::{laser::{apply_homography_matrix, Corner, LaserPoints}, tracks::CoordinateSpace}; use crate::trap::{laser::{apply_homography_matrix, Corner, LaserPoints}, tracks::CoordinateSpace, utils::clip_lines};
use nannou_laser::{self as laser, Point}; use nannou_laser::{self as laser, Point};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -87,8 +87,9 @@ impl PointFilters {
let mut p = self.dim.apply(points); let mut p = self.dim.apply(points);
p = self.homography.apply(&p); p = self.homography.apply(&p);
p = self.scale.apply(&p); p = self.scale.apply(&p);
p = self.pincushion.apply(&p);
p = self.crop.apply(&p); p = self.crop.apply(&p);
p = self.pincushion.apply(&p);
p = self.clip.apply(&p);
p p
} }
@ -135,7 +136,7 @@ impl Default for PointFilters {
scale: ScaleFilter { factor: 1. }, scale: ScaleFilter { factor: 1. },
pincushion: PincushionFilter{k_x: 0.,k_x2: 0., k_y: 0., k_y2: 0.}, pincushion: PincushionFilter{k_x: 0.,k_x2: 0., k_y: 0., k_y2: 0.},
crop: CropFilter{ enabled: true }, crop: CropFilter{ enabled: true },
clip: ClipFilter{ enabled: false, mask: Corner::in_laser_space() }, clip: ClipFilter{ enabled: true, mask: Corner::in_laser_space() },
} }
} }
} }
@ -351,11 +352,9 @@ impl Filter for ClipFilter {
}; };
} }
// TODO
return LaserPoints{ clip_lines(&self.mask, points)
points: points.points.clone(),
space: points.space,
};
} }
fn reverse(&self, points: &LaserPoints) -> LaserPoints{ fn reverse(&self, points: &LaserPoints) -> LaserPoints{

View file

@ -1,5 +1,9 @@
// GPT generated code is isolated to this file // GPT generated code is isolated to this file
use geo::{Coordinate, Line, Point, Polygon, prelude::*}; use geo::{Coord, Line, Point, Polygon, prelude::*};
use geo::line_intersection::{line_intersection, LineIntersection};
use nannou_laser::{self as laser};
use crate::trap::laser::LaserPoints;
/////////////////////// ///////////////////////
// Clip filter related // Clip filter related
@ -50,3 +54,146 @@ pub fn closest_edge(mask: &[[f32; 2]], target: [f32; 2]) -> Option<usize> {
} }
fn interpolate_color(c1: [f32; 3], c2: [f32; 3], t: f32) -> [f32; 3] {
[
c1[0] + (c2[0] - c1[0]) * t as f32,
c1[1] + (c2[1] - c1[1]) * t as f32,
c1[2] + (c2[2] - c1[2]) * t as f32,
]
}
fn interpolate_point(p1: Coord<f32>, p2: Coord<f32>, t: f32) -> Coord<f32> {
Coord {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t,
}
}
fn clip_colored_path(points: &Vec<laser::Point>, bounds: &Polygon<f32>) -> Vec<laser::Point> {
let mut result = Vec::new();
for pair in points.windows(2) {
let p1 = &pair[0];
let p2 = &pair[1];
let inside1 = bounds.contains(&Point::from(p1.position));
let inside2 = bounds.contains(&Point::from(p2.position));
let line = Line::new(p1.position, p2.position);
match (inside1, inside2) {
(true, true) => {
// Both inside: keep both
if result.last().map(|r: &laser::Point| r.position != p1.position).unwrap_or(true) {
result.push(p1.clone());
}
result.push(p2.clone());
}
(true, false) | (false, true) => {
// Crossing the boundary
if let Some(intersection) = line_intersect_polygon(line, bounds) {
let t = line_fraction(p1.position, p2.position, intersection);
let boundary_color = interpolate_color(p1.color, p2.color, t);
let boundary_point = laser::Point {
position: intersection.into(),
color: boundary_color,
weight: p1.weight,
};
let blanked_boundary_point = boundary_point.blanked();
if inside1 {
if result.last().map(|r| r.position != p1.position).unwrap_or(true) {
result.push(p1.clone());
}
result.push(boundary_point);
result.push(blanked_boundary_point);
} else {
result.push(blanked_boundary_point);
result.push(boundary_point);
result.push(p2.clone());
}
}
}
(false, false) => {
// Both outside: discard
}
}
}
result
}
fn line_intersect_polygon(line: Line<f32>, polygon: &Polygon<f32>) -> Option<Coord<f32>> {
for edge in polygon.exterior().lines() {
// if let Some(point) = line.intersection(&edge) {
if let Some(point) = line_intersection(line, edge) {
return match point {
LineIntersection::SinglePoint {intersection, is_proper} => Some(intersection.into()),
LineIntersection::Collinear { intersection } => None,
};
}
}
None
}
fn line_fraction(start: [f32;2], end: [f32;2], pt: Coord<f32>) -> f32 {
let total = ((end[0] - start[0]).powi(2) + (end[1] - start[1]).powi(2)).sqrt();
let partial = ((pt.x - start[0]).powi(2) + (pt.y - start[1]).powi(2)).sqrt();
if total == 0.0 { 0.0 } else { partial / total }
}
pub fn clip_lines(mask: &Vec<[f32; 2]>, laser_points: &LaserPoints) -> LaserPoints {
let path = &laser_points.points;
let bounds = Polygon::new(
mask.clone()
.into(),
vec![],
);
let clipped = clip_colored_path(path, &bounds);
LaserPoints{
points: clipped,
space: laser_points.space,
}
// for p in clipped {
// println!("Point: {:?}, Color: {:?}", p.position, p.color);
// }
}
// split a Vec of laser::Points on blank'ed points, so that the optimiser can do its thing.
pub fn split_on_blank(points: Vec<laser::Point>) -> Vec<Vec<laser::Point>> {
let mut lines = Vec::new();
let mut current_line = Vec::new();
for point in points {
if point.is_blank() {
if !current_line.is_empty() {
lines.push(current_line);
current_line = Vec::new();
}
} else {
current_line.push(point);
}
}
// Push the last line if not empty
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}