Compare commits

...

5 commits

Author SHA1 Message Date
Ruben van de Ven
e8fec78250 Clean tryouts 2025-07-02 11:54:11 +02:00
Ruben van de Ven
7c3968d2dc Implement pillow distortion correction 2025-07-02 11:49:49 +02:00
Ruben van de Ven
150b3b7fd8 Test patterns, and glitchy pillow distortion 2025-07-02 11:25:29 +02:00
Ruben van de Ven
b25fc93997 Test patterns, and glitchy pillow distortion 2025-07-02 10:54:43 +02:00
Ruben van de Ven
188420c4ce Check for draw order 2025-07-01 20:08:27 +02:00
3 changed files with 303 additions and 93 deletions

View file

@ -12,7 +12,7 @@ use nannou_laser::DacId;
use nannou_laser::{self as laser};
use serde_json::Result;
use trap_rust::trap::filters::PointFilters;
use trap_rust::trap::laser::{LaserPoints, TMP_DESK_CLUBMAX};
use trap_rust::trap::laser::{LaserPoints, StreamSource, STREAM_SOURCES, TMP_DESK_CLUBMAX};
use trap_rust::trap::tracks::CoordinateSpace;
use trap_rust::trap::{laser::{python_cv_h_into_mat3, LaserModel, TMP_PYTHON_LASER_H, DacConfig}, tracks::{RenderableLines}};
use zmq::Socket;
@ -20,6 +20,8 @@ use std::sync::{mpsc, Arc};
use std::time::{Instant, Duration};
use std::collections::HashMap;
// use egui_dropdown::DropDownBox;
fn main() {
nannou::app(model).update(update).run();
@ -62,6 +64,7 @@ struct LaserSettings {
latency_points: u32,
frame_hz: u32,
enable_optimisations: bool,
enable_draw_reorder: bool,
distance_per_point: f32,
blank_delay_points: u32,
radians_per_point: f32,
@ -81,6 +84,7 @@ impl Default for LaserSettings {
) * 4,
frame_hz: 35, //stream::DEFAULT_FRAME_HZ,
enable_optimisations: true,
enable_draw_reorder: true,
distance_per_point: InterpolationConfig::DEFAULT_DISTANCE_PER_POINT,
blank_delay_points: InterpolationConfig::DEFAULT_BLANK_DELAY_POINTS,
radians_per_point: InterpolationConfig::DEFAULT_RADIANS_PER_POINT,
@ -158,9 +162,9 @@ fn zmq_receive(model: &mut GuiModel) {
for (_dac, stream_config) in (&model.laser_streams).into_iter() {
// let lines = get_laser_lines(version);
let lines_for_laser: RenderableLines = lines.clone();
let sending = stream_config.stream.send(move |laser| {
let sending = stream_config.stream.send(move |laser_model: &mut LaserModel| {
let laser_lines: RenderableLines = lines_for_laser;
laser.current_lines = laser_lines;
laser_model.current_lines = laser_lines;
});
if let Err(e) = sending {
println!("Error sending to laser! {e:?}");
@ -194,8 +198,7 @@ fn get_dac_configs() -> DacConfigMap{
DacId::Helios { id: 926298163 },
DacConfig{
name: "Helios#1".into(),
homography: python_cv_h_into_mat3(TMP_PYTHON_LASER_H),
filters: PointFilters::default(),
.. DacConfig::default()
}
);
dac_configs.insert(
@ -211,8 +214,7 @@ fn get_dac_configs() -> DacConfigMap{
},
DacConfig{
name: "ED - 192.168.8.101".into(),
homography: python_cv_h_into_mat3(TMP_DESK_CLUBMAX),
filters: PointFilters::default(),
.. DacConfig::default()
}
);
dac_configs.insert(
@ -228,8 +230,8 @@ fn get_dac_configs() -> DacConfigMap{
},
DacConfig{
name: "ED - 192.168.9.101".into(),
homography: python_cv_h_into_mat3(TMP_DESK_CLUBMAX),
filters: PointFilters::default(),
.. DacConfig::default()
// filters: PointFilters::default(),
}
);
dac_configs.insert(
@ -245,8 +247,7 @@ fn get_dac_configs() -> DacConfigMap{
},
DacConfig{
name: "Emulator".into(),
homography: python_cv_h_into_mat3(TMP_DESK_CLUBMAX),
filters: PointFilters::default(),
.. DacConfig::default()
}
);
dac_configs
@ -355,8 +356,35 @@ fn model(app: &App) -> GuiModel {
fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){
let points: LaserPoints = (&model.current_lines).into();
let current_points: LaserPoints = (&model.current_lines).into();
// let points = LaserPoints { points: vec!(
// laser::Point{
// position:[ 9.4, 7.2],
// color: [1.,1.,0.],
// weight: 0,
// },
// laser::Point{
// position:[ 12.4, 7.2],
// color: [1.,1.,0.],
// weight: 0,
// },
// laser::Point{
// position:[ 12.4, 4.2],
// color: [1.,1.,0.],
// weight: 0,
// },
// laser::Point{
// position:[ 9.4, 4.2],
// color: [1.,1.,0.],
// weight: 0,
// },
// ), space: CoordinateSpace::World };
let space = &model.current_lines.space;
// check which source should be used, and get points accordingly.
// potentially ignoring the points coming from the stream
let points = model.config.source.get_shape(current_points);
let pointno = points.points.len();
let new_points = model.config.filters.apply(&points);
@ -365,6 +393,11 @@ fn laser_frame_producer(model: &mut LaserModel, frame: &mut laser::Frame){
println!("Cropped Points {} (was: {})", new_laser_points.len(), pointno);
}
// on reconnect gives Unknown
// dbg!(&model.config);
// dbg!(&points.points[0]);
// dbg!(&new_laser_points[0]);
frame.add_lines(new_laser_points);
return;
}
@ -391,6 +424,7 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
.detected_dac(dac.clone())
.build()
.expect("failed to establish stream with newly detected DAC");
// dbg!(stream.enable_draw_reorder());
model.laser_streams.insert(dac.id(), StreamConfig{ stream, config: config.clone() });
}
@ -425,14 +459,25 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
// TODO: keeps looping on disconnect.
println!("attempting to restart stream with DAC {:?}", dac.id());
let dac_id = dac.id();
let config = match model.per_laser_config.contains_key(&dac.id()) {
true => &model.per_laser_config[&dac.id()],
false => {
println!("Found unknown DAC, try to register it in get_dac_configs()");
dbg!(&dac.id());
&DacConfig::default()
},
};
match model
.laser_api
.new_frame_stream(model.laser_model.clone(), laser_frame_producer)
.new_frame_stream(model.laser_model.with_config(config), laser_frame_producer)
.detected_dac(dac)
.build()
{
Err(err) => eprintln!("failed to restart stream: {}", err),
Ok(stream) => {model.laser_streams.insert(dac_id, StreamConfig{stream, config: stream_config.config});},
Ok(stream) => {
println!("Reinsert stream. {:?}", dac_id);
model.laser_streams.insert(dac_id, StreamConfig{stream, config: stream_config.config});
},
}
}
@ -517,8 +562,23 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
.ok();
}
}
if ui
.add(
.add_enabled(laser_settings.enable_optimisations,
egui::Checkbox::new(&mut laser_settings.enable_draw_reorder,"Reorder paths")
)
// .checkbox(&mut laser_settings.enable_draw_reorder, "Reorder paths")
.changed()
{
for (_dac_id, stream_config) in laser_streams.iter() {
stream_config.stream
.enable_draw_reorder(laser_settings.enable_draw_reorder)
.ok();
}
}
if ui
.add_enabled(laser_settings.enable_optimisations,
egui::Slider::new(&mut laser_settings.distance_per_point, 0.01..=1.0)
.text("Distance Per Point"),
)
@ -530,7 +590,7 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
}
}
if ui
.add(
.add_enabled(laser_settings.enable_optimisations,
egui::Slider::new(&mut laser_settings.blank_delay_points, 0..=32)
.text("Blank Delay (Points)"),
)
@ -543,7 +603,9 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
}
let mut degrees = rad_to_deg(laser_settings.radians_per_point);
if ui
.add(egui::Slider::new(&mut degrees, 1.0..=180.0).text("Degrees Per Point"))
.add_enabled(laser_settings.enable_optimisations,
egui::Slider::new(&mut degrees, 1.0..=180.0).text("Degrees Per Point")
)
.changed()
{
let radians = deg_to_rad(degrees);
@ -577,6 +639,73 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
let stream_config: &mut StreamConfig = laser_streams.get_mut(&selected_stream_value).expect("Selected stream not found in configs");
let source = &mut stream_config.config.source;
// for source_option in STREAM_SOURCES {
// if ui.radio_value(source, source_option.clone(), format!("{:?}", &source_option)).changed() {
// println!("Clicked!")
// };
// }
egui::ComboBox::from_label("Source")
.selected_text(format!("{source:?}"))
.show_ui(ui, |ui| {
for source_option in STREAM_SOURCES {
if ui.selectable_value(source, source_option.clone(), format!("{:?}", &source_option)).clicked() {
// let source = source_option;
stream_config.stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.source = source_option;
}).unwrap();
};
}
// ui.selectable_value(source, StreamSource::CurrentLines, "Zmq Stream");
// ui.selectable_value(source, StreamSource::Rectangle, "Rectangle");
// ui.selectable_value(source, StreamSource::Grid, "Grid");
});
// ui.radio_value(source, StreamSource::Rectangle, "Rectangle");
// ui.radio_value(source, StreamSource::Grid, "Grid");
// if ui.add(DropDownBox::from_iter(
// vec!(StreamSource::CurrentLines, StreamSource::Rectangle, StreamSource::Grid(5)),
// "test_dropbox",
// &mut stream_config.config.source,
// |ui, text| ui.selectable_label(false, text)
// )).changed() {
// println!("Changed source! {:?}", stream_config.config.source);
// let source = stream_config.config.source;
// stream_config.stream.send(move |laser_model: &mut LaserModel| {
// laser_model.config.source = source;
// }).unwrap();
// }
// if ui.add(
// egui::ComboBox::from_label("Source")
// .selected_text(format!("{source:?}"))
// // .show_ui(ui, |ui| {
// // ui.selectable_value(source, StreamSource::CurrentLines, "Zmq Stream");
// // ui.selectable_value(source, StreamSource::Rectangle, "Rectangle");
// // ui.selectable_value(source, StreamSource::Grid(5), "Grid");
// // })
// ).changed()
// {
// let source = stream_config.config.source;
// stream_config.stream.send(move |laser_model: &mut LaserModel| {
// laser_model.config.source = source;
// }).unwrap();
// }
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.dim.intensity, 0.0..=1.).text("Dimming"))
.changed()
{
let factor = stream_config.config.filters.dim.intensity;
stream_config.stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.dim.intensity = factor;
}).unwrap();
}
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.dim.intensity, 0.0..=1.).text("Dimming"))
.changed()
@ -598,8 +727,10 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
}
// Pincushion / Pillow / Barrel distortion. Generally, only needed for the x-axis
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_x, -1.0..=1.).text("Pincushion x"))
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_x, -0.5..=0.5).text("Pincushion x"))
.changed()
{
let factor = stream_config.config.filters.pincushion.k_x;
@ -609,7 +740,17 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
}
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_y, -1.0..=1.).text("Pincushion y"))
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_x2, -0.2..=0.2).text("Higher order pincushion x"))
.changed()
{
let factor = stream_config.config.filters.pincushion.k_x2;
stream_config.stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.pincushion.k_x2 = factor;
}).unwrap();
}
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_y, -0.5..=0.5).text("Pincushion y"))
.changed()
{
let factor = stream_config.config.filters.pincushion.k_y;
@ -618,6 +759,16 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
}).unwrap();
}
if ui
.add(egui::Slider::new(&mut stream_config.config.filters.pincushion.k_y2, -0.2..=0.2).text("Higher order pincushion y"))
.changed()
{
let factor = stream_config.config.filters.pincushion.k_y2;
stream_config.stream.send(move |laser_model: &mut LaserModel| {
laser_model.config.filters.pincushion.k_y2 = factor;
}).unwrap();
}
if ui
.checkbox(&mut stream_config.config.filters.crop.enabled ,"Crop")
@ -631,53 +782,6 @@ fn update(_app: &App, model: &mut GuiModel, update: Update) {
// ui.heading("Connected DACs");
// if laser_streams.is_empty() {
// ui.label("no streams");
// }
// for (_dac_id, stream_config) in laser_streams.iter_mut() {
// let dac: laser::DetectedDac = stream_config.stream
// .dac()
// .expect("`dac` returned `None` even though one was specified during stream creation");
// ui.add(egui::Label::new(format!("{:?}", dac.id())));
// if ui
// // todo : from custom dac config:
// .add(egui::Slider::new(&mut stream_config.config.filters.dim.intensity, 0.0..=1.).text("Dimming"))
// .changed()
// {
// for (_dac_id, laser_stream) in laser_streams.iter() {
// let factor = model.laser_model.dimming;
// // let lines = get_laser_lines(version);
// laser_stream.stream.send(move |laser| {
// // laser: LaserModel
// laser.config.filters.dim.intensity = factor;
// // laser.dimming = factor;
// }).unwrap();
// }
// }
// }
//if egui::ComboBox::from_label("Homography")
// .selected_text(format!("{radio:?}"))
// .show_ui(ui, |ui| {
// ui.selectable_value(radio, Enum::First, "First");
// ui.selectable_value(radio, Enum::Second, "Second");
// ui.selectable_value(radio, Enum::Third, "Third");
// })
// .changed() {
// let sending = laser_stream.send(move |laser| {
// let laser_lines: RenderableLines = lines_for_laser;
// laser.current_lines = laser_lines;
// });
// if let Err(e) = sending {
// println!("Error sending to laser! {e:?}");
// }
// };
} else {
ui.label("Select a DAC");
}

View file

@ -11,45 +11,47 @@ pub trait Filter {
fn apply(&self, points: &LaserPoints) -> LaserPoints;
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct HomographyFilter {
pub homography_matrix: Mat3
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CropFilter {
pub enabled: bool
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DimFilter {
pub intensity: f32
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ScaleFilter {
pub factor: f32
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PincushionFilter {
pub k_x: f32,
pub k_y: f32
pub k_x2: f32,
pub k_y: f32,
pub k_y2: f32,
}
#[derive(Serialize, Deserialize, Clone)]
// TODO consider moving to struct?
pub enum PointFilter {
Homography(HomographyFilter),
Dim(DimFilter),
Scale(ScaleFilter),
Pincushion(PincushionFilter),
Crop(CropFilter),
}
// #[derive(Serialize, Deserialize, Clone, Debug)]
// // TODO consider moving to struct?
// pub enum PointFilter {
// Homography(HomographyFilter),
// Dim(DimFilter),
// Scale(ScaleFilter),
// Pincushion(PincushionFilter),
// Crop(CropFilter),
// }
pub struct PointFilterList(Vec<PointFilter>); // deprecated
// pub struct PointFilterList(Vec<PointFilter>); // deprecated
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PointFilters{
pub dim: DimFilter,
pub homography: HomographyFilter,
@ -95,7 +97,7 @@ impl Default for PointFilters {
homography: HomographyFilter::default(),
dim: DimFilter{intensity: 0.5},
scale: ScaleFilter { factor: 1. },
pincushion: PincushionFilter{k_x: 0., k_y: 0.},
pincushion: PincushionFilter{k_x: 0.,k_x2: 0., k_y: 0., k_y2: 0.},
crop: CropFilter{ enabled: true },
}
}
@ -313,9 +315,17 @@ impl Filter for PincushionFilter {
let projected_positions: Vec<laser::Point> = points.points.iter().map(|point| {
let p = point.position;
// Apply Brown-Conrady model of distortion
// https://en.wikipedia.org/wiki/Distortion_(optics)#Software_correction
// calculate radius (though we might be able to get away, just using p[1] and p[0]
// as we calculate axes independently)
let radius = (p[0].powi(2) + p[1].powi(2)).sqrt();
let new_position = [
p[0] * (1. + self.k_x * p[0].powi(2)),
p[1] * (1. + self.k_y * p[1].powi(2))
p[0] * (1. + self.k_x * radius.powi(2)+ self.k_x2 * radius.powi(4)),
p[1] * (1. + self.k_y * radius.powi(2)+ self.k_y2 * radius.powi(4))
];
laser::Point {

View file

@ -2,10 +2,11 @@ use bevy::prelude::*;
use nannou_laser as laser;
use std::time::Instant;
use serde::{Deserialize, Serialize};
use crate::trap::{filters::{PointFilter, PointFilters}, tracks::CoordinateSpace};
use crate::trap::{filters::{PointFilters}, tracks::CoordinateSpace};
use super::tracks::{RenderableLines};
pub struct LaserPoints{
pub points: Vec<laser::Point>,
pub space: CoordinateSpace
@ -46,7 +47,7 @@ pub fn apply_homography_matrix(h: Mat3, p: &[f32; 2]) -> [f32; 2]{
pub struct LaserModel{
pub t: Instant, // register start time, so that animations can be moving
pub current_lines: RenderableLines,
pub dimming: f32,
// pub dimming: f32,
pub config: DacConfig, // per dac configuration
}
@ -55,7 +56,7 @@ impl LaserModel{
Self{
t: Instant::now(),
current_lines: RenderableLines::new(),
dimming: 0.3,
// dimming: 0.3,
config: DacConfig::default(),
}
}
@ -81,15 +82,29 @@ pub struct LaserTimer {
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DacConfig{
// #[serde(with = "DacIdSerializable")]
// id: DacId,
pub name: String,
pub homography: Mat3,
pub filters: PointFilters
// pub homography: Mat3,
pub source: StreamSource,
pub filters: PointFilters,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum StreamSource {
CurrentLines,
Rectangle,
Grid, // lines
Circle, // segments
Spiral,
}
// usefull to create pull downs with an iterator
pub const STREAM_SOURCES: [StreamSource; 5] = [StreamSource::CurrentLines, StreamSource::Rectangle, StreamSource::Grid, StreamSource::Circle, StreamSource::Spiral];
const LASER_H: Mat3 = python_cv_h_into_mat3(TMP_PYTHON_LASER_H);
const LASER_H_CM: Mat3 = python_cv_h_into_mat3(TMP_DESK_CLUBMAX);
@ -98,6 +113,87 @@ impl Default for DacConfig{
fn default() -> DacConfig{
//DacConfig { name: "Unknown".into(), homography: Mat3::IDENTITY }
// DacConfig { name: "Unknown".into(), homography: LASER_H_CM }
DacConfig { name: "Unknown".into(), homography: LASER_H, filters: PointFilters::default().with_homography(LASER_H) }
DacConfig { name: "Unknown".into(), source: StreamSource::CurrentLines, filters: PointFilters::default().with_homography(LASER_H) }
}
}
impl Default for LaserPoints {
fn default() -> LaserPoints {
LaserPoints { points: Vec::new(), space: CoordinateSpace::World }
}
}
// the different shapes that override the provided lines if needed
impl StreamSource{
pub fn get_shape(&self, current_lines: LaserPoints) -> LaserPoints {
match self {
Self::CurrentLines => current_lines,
Self::Rectangle => LaserPoints { points: vec!(
laser::Point{
position:[0xFFF as f32, 0xFFF as f32],
color: [1.,1.,1.],
weight: 0,
},
laser::Point{
position:[0xFFF as f32, 0.0],
color: [1.,1.,1.],
weight: 0,
},
laser::Point{
position:[0.0, 0.0],
color: [1.,1.,1.],
weight: 0,
},
laser::Point{
position:[0.0, 0xFFF as f32],
color: [1.,1.,1.],
weight: 0,
},
laser::Point{
position:[0xFFF as f32, 0xFFF as f32],
color: [1.,1.,1.],
weight: 0,
},
), space: CoordinateSpace::Laser },
Self::Grid => {
let mut points = Vec::new();
for i in (0..=0xFFF).step_by(0xFFF / 5) {
points.push(laser::Point{
position:[i as f32, 0.],
color: [0., 0., 0.],
weight: 0,
});
for j in (0..=0xFFF).step_by(0xFFF / 10) {
points.push(laser::Point{
position:[i as f32, j as f32],
color: [1.,1.,1.],
weight: 0,
});
}
points.push(points[points.len()-1].blanked());
}
for i in (0..=0xFFF).step_by(0xFFF / 5) {
points.push(laser::Point{
position:[0., i as f32],
color: [0., 0., 0.],
weight: 0,
});
for j in (0..=0xFFF).step_by(0xFFF / 10) {
points.push(laser::Point{
position:[j as f32, i as f32],
color: [1.,1.,1.],
weight: 0,
});
}
points.push(points[points.len()-1].blanked());
}
LaserPoints { points, space: CoordinateSpace::Laser }
},
_ => LaserPoints::default(),
}
}
}