1use serde::{Deserialize, Serialize};
12
13pub(crate) const MAX_POINTS: u64 = 100_000_000;
15pub(crate) const MAX_ABS_COORDINATE: f64 = 1e6;
18pub(crate) const MAX_COARSENINGS: u32 = 16;
20
21#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24pub struct ScanRegion {
25 pub min: [f64; 3],
26 pub max: [f64; 3],
27}
28
29#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
31pub struct ScanSegmentationOptions {
32 pub voxel_size_metres: f64,
34 pub max_voxels: u32,
40 pub normal_neighbor_rings: u32,
43 pub min_neighbors: u32,
45 pub max_seed_curvature: f64,
50 pub max_normal_angle_degrees: f64,
53 pub max_plane_distance_metres: f64,
56 pub mad_scale: f64,
60 pub max_bend_per_metre: f64,
65 pub min_plane_area_square_metres: f64,
67 pub classification_angle_degrees: f64,
70 pub up_axis: [f64; 3],
73 pub scanner_position: Option<[f64; 3]>,
75 pub origin: [f64; 3],
77 pub region: Option<ScanRegion>,
79 pub max_planes: u32,
81 pub detect_cylinders: bool,
84 pub min_cylinder_radius_metres: Option<f64>,
91 pub max_cylinder_radius_metres: f64,
92 pub min_cylinder_inlier_fraction: f64,
95 pub min_cylinder_arc_degrees: f64,
98 pub min_cylinder_length_metres: f64,
100 pub cylinder_draws: u32,
102 pub cylinder_score_sample: u32,
105 pub max_cylinder_groups: u32,
107}
108
109impl Default for ScanSegmentationOptions {
110 fn default() -> Self {
111 Self {
112 voxel_size_metres: 0.03,
113 max_voxels: 1_500_000,
114 normal_neighbor_rings: 1,
115 min_neighbors: 6,
116 max_seed_curvature: 0.02,
117 max_normal_angle_degrees: 10.,
118 max_plane_distance_metres: 0.02,
119 mad_scale: 2.5,
120 max_bend_per_metre: 1.,
121 min_plane_area_square_metres: 0.25,
122 classification_angle_degrees: 10.,
123 up_axis: [0., 0., 1.],
124 scanner_position: None,
125 origin: [0.; 3],
126 region: None,
127 max_planes: 10_000,
128 detect_cylinders: true,
129 min_cylinder_radius_metres: None,
130 max_cylinder_radius_metres: 2.,
131 min_cylinder_inlier_fraction: 0.6,
132 min_cylinder_arc_degrees: 90.,
133 min_cylinder_length_metres: 0.3,
134 cylinder_draws: 256,
135 cylinder_score_sample: 2_048,
136 max_cylinder_groups: 1_024,
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
143pub(crate) struct Params {
144 pub base_size_quanta: i64,
145 pub max_voxels: usize,
146 pub rings: i32,
147 pub min_support: usize,
148 pub max_seed_curvature: f64,
149 pub cos_angle: f64,
150 pub distance: f64,
151 pub mad_scale: f64,
152 pub max_bend: f64,
153 pub min_area: f64,
154 pub cos_class: f64,
155 pub sin_class: f64,
156 pub up: [f64; 3],
157 pub scanner: Option<[f64; 3]>,
158 pub origin: [f64; 3],
159 pub region: Option<ScanRegion>,
160 pub max_planes: usize,
161 pub cylinders: Option<CylinderParams>,
162}
163
164#[derive(Debug, Clone)]
165pub(crate) struct CylinderParams {
166 pub min_radius: Option<f64>,
168 pub max_radius: f64,
169 pub min_fraction: f64,
170 pub min_arc: f64,
171 pub min_length: f64,
172 pub draws: usize,
173 pub sample: usize,
174 pub max_groups: usize,
175}
176
177fn within(value: f64, lo: f64, hi: f64) -> bool {
178 value.is_finite() && value >= lo && value <= hi
179}
180
181fn above(value: f64, lo: f64, hi: f64) -> bool {
183 value > lo && value <= hi
184}
185
186impl ScanSegmentationOptions {
187 pub(crate) fn validate(&self) -> Result<Params, String> {
188 let o = self;
189 let fail = |what: &str| Err(format!("Scan segmentation option {what}"));
190 if !within(o.voxel_size_metres, 0.005, 1.) {
191 return fail("voxelSizeMetres must be within 0.005..=1");
192 }
193 if !(1_000..=8_000_000).contains(&o.max_voxels) {
194 return fail("maxVoxels must be within 1,000..=8,000,000");
195 }
196 let rings = o.normal_neighbor_rings;
197 let neighbourhood = (2 * rings + 1).pow(3);
198 if !(1..=2).contains(&rings) || o.min_neighbors < 4 || o.min_neighbors > neighbourhood {
199 return fail("normalNeighborRings must be 1 or 2 and minNeighbors 4..=(2*rings+1)^3");
200 }
201 if !above(o.max_seed_curvature, 0., 1. / 3.) {
202 return fail("maxSeedCurvature must be within (0, 1/3]");
203 }
204 if !above(o.max_normal_angle_degrees, 0., 45.) || !above(o.classification_angle_degrees, 0., 45.) {
205 return fail("angles must be within (0, 45] degrees");
206 }
207 if !above(o.max_plane_distance_metres, 0., 1.)
208 || !above(o.mad_scale, 0., 10.)
209 || !above(o.max_bend_per_metre, 0., 1e3)
210 || !within(o.min_plane_area_square_metres, 0., 1e6)
211 {
212 return fail("distance (0, 1] m, madScale (0, 10], maxBendPerMetre (0, 1000] and area 0..=1e6 m^2 are required");
213 }
214 let up_length = o.up_axis.iter().map(|v| v * v).sum::<f64>().sqrt();
215 if !up_length.is_finite() || up_length < 1e-9 {
216 return fail("upAxis must be a finite non-zero vector");
217 }
218 let bounded = |p: &[f64; 3], limit: f64| p.iter().all(|v| within(*v, -limit, limit));
219 if o.scanner_position.is_some_and(|p| !bounded(&p, MAX_ABS_COORDINATE)) || !bounded(&o.origin, 1e12) {
220 return fail("scannerPosition must lie within 1e6 m and origin within 1e12 m");
221 }
222 if let Some(r) = o.region {
223 if !bounded(&r.min, 1e12) || !bounded(&r.max, 1e12) || (0..3).any(|a| r.min[a] > r.max[a]) {
224 return fail("region needs finite min <= max");
225 }
226 }
227 if !(1..=100_000).contains(&o.max_planes) {
228 return fail("maxPlanes must be within 1..=100,000");
229 }
230 let cylinders = o.cylinder_params()?;
231 let class = o.classification_angle_degrees.to_radians();
232 Ok(Params {
233 base_size_quanta: (o.voxel_size_metres * super::voxel::QUANTA_PER_METRE).round() as i64,
234 max_voxels: o.max_voxels as usize,
235 rings: rings as i32,
236 min_support: o.min_neighbors as usize,
237 max_seed_curvature: o.max_seed_curvature,
238 cos_angle: o.max_normal_angle_degrees.to_radians().cos(),
239 distance: o.max_plane_distance_metres,
240 mad_scale: o.mad_scale,
241 max_bend: o.max_bend_per_metre,
242 min_area: o.min_plane_area_square_metres,
243 cos_class: class.cos(),
244 sin_class: class.sin(),
245 up: o.up_axis.map(|v| v / up_length),
246 scanner: o.scanner_position,
247 origin: o.origin,
248 region: o.region,
249 max_planes: o.max_planes as usize,
250 cylinders,
251 })
252 }
253
254 fn cylinder_params(&self) -> Result<Option<CylinderParams>, String> {
255 let o = self;
256 if !o.detect_cylinders {
257 return Ok(None);
258 }
259 let min = o.min_cylinder_radius_metres.unwrap_or(0.005);
260 if !within(min, 0.005, 10.) || !within(o.max_cylinder_radius_metres, min, 10.) {
261 return Err("Scan segmentation option cylinder radii must satisfy 0.005 <= min <= max <= 10 m".into());
262 }
263 if !above(o.min_cylinder_inlier_fraction, 0., 1.)
264 || !within(o.min_cylinder_arc_degrees, 0., 360.)
265 || !within(o.min_cylinder_length_metres, 0., 1e3)
266 {
267 return Err("Scan segmentation option cylinder fraction (0, 1], arc 0..=360 and length 0..=1000 m are required".into());
268 }
269 if !(1..=4_096).contains(&o.cylinder_draws)
270 || !(16..=65_536).contains(&o.cylinder_score_sample)
271 || !(1..=100_000).contains(&o.max_cylinder_groups)
272 {
273 return Err("Scan segmentation option cylinderDraws 1..=4,096, cylinderScoreSample 16..=65,536 and maxCylinderGroups 1..=100,000 are required".into());
274 }
275 Ok(Some(CylinderParams {
276 min_radius: o.min_cylinder_radius_metres,
277 max_radius: o.max_cylinder_radius_metres,
278 min_fraction: o.min_cylinder_inlier_fraction,
279 min_arc: o.min_cylinder_arc_degrees,
280 min_length: o.min_cylinder_length_metres,
281 draws: o.cylinder_draws as usize,
282 sample: o.cylinder_score_sample as usize,
283 max_groups: o.max_cylinder_groups as usize,
284 }))
285 }
286}