threecrate_reconstruction/
delaunay.rs1use crate::parallel;
7use nalgebra::Matrix3;
8use spade::{DelaunayTriangulation, Point2, Triangulation};
9use threecrate_core::{Error, Point3f, PointCloud, Result, TriangleMesh};
10
11#[derive(Debug, Clone)]
13pub struct DelaunayConfig {
14 pub projection: ProjectionMethod,
16 pub validate: bool,
18 pub min_triangle_area: f32,
20 pub max_edge_length: Option<f32>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum ProjectionMethod {
27 XY,
29 XZ,
31 YZ,
33 PCA,
35 BestFitPlane,
37}
38
39impl Default for DelaunayConfig {
40 fn default() -> Self {
41 Self {
42 projection: ProjectionMethod::PCA,
43 validate: true,
44 min_triangle_area: 1e-8,
45 max_edge_length: None,
46 }
47 }
48}
49
50pub fn delaunay_triangulation_2d(points: &[Point2<f64>]) -> Result<Vec<[usize; 3]>> {
52 if points.len() < 3 {
53 return Err(Error::InvalidData(
54 "Need at least 3 points for triangulation".to_string(),
55 ));
56 }
57
58 let mut triangulation: DelaunayTriangulation<Point2<f64>> = DelaunayTriangulation::new();
59
60 for point in points {
62 triangulation.insert(*point).map_err(|e| {
63 Error::Algorithm(format!(
64 "Failed to insert point in Delaunay triangulation: {:?}",
65 e
66 ))
67 })?;
68 }
69
70 let mut triangles = Vec::new();
72 for face in triangulation.inner_faces() {
73 let vertices = face.vertices();
74 let positions: Vec<Point2<f64>> = vertices.iter().map(|v| v.position()).collect();
75
76 let mut indices = Vec::new();
78 for pos in positions {
79 if let Some(idx) = points
80 .iter()
81 .position(|p| (p.x - pos.x).abs() < 1e-10 && (p.y - pos.y).abs() < 1e-10)
82 {
83 indices.push(idx);
84 } else {
85 return Err(Error::Algorithm(
86 "Failed to match triangle vertex to original point".to_string(),
87 ));
88 }
89 }
90
91 if indices.len() == 3 {
92 triangles.push([indices[0], indices[1], indices[2]]);
93 }
94 }
95
96 Ok(triangles)
97}
98
99pub fn project_3d_to_2d(points: &[Point3f], method: &ProjectionMethod) -> Result<Vec<Point2<f64>>> {
101 if points.is_empty() {
102 return Ok(Vec::new());
103 }
104
105 match method {
106 ProjectionMethod::XY => Ok(parallel::parallel_map(points, |p| {
107 Point2::new(p.x as f64, p.y as f64)
108 })),
109 ProjectionMethod::XZ => Ok(parallel::parallel_map(points, |p| {
110 Point2::new(p.x as f64, p.z as f64)
111 })),
112 ProjectionMethod::YZ => Ok(parallel::parallel_map(points, |p| {
113 Point2::new(p.y as f64, p.z as f64)
114 })),
115 ProjectionMethod::PCA => project_using_pca(points),
116 ProjectionMethod::BestFitPlane => project_using_best_fit_plane(points),
117 }
118}
119
120fn project_using_pca(points: &[Point3f]) -> Result<Vec<Point2<f64>>> {
122 if points.len() < 3 {
123 return Err(Error::InvalidData(
124 "Need at least 3 points for PCA projection".to_string(),
125 ));
126 }
127
128 let mut centroid = Point3f::origin();
130 for point in points {
131 centroid = Point3f::from(centroid.coords + point.coords);
132 }
133 centroid = Point3f::from(centroid.coords / points.len() as f32);
134
135 let mut covariance = Matrix3::zeros();
137 for point in points {
138 let diff = point - centroid;
139 covariance += diff * diff.transpose();
140 }
141
142 let eigen = covariance.symmetric_eigen();
144
145 let mut eigen_pairs: Vec<(f32, nalgebra::Vector3<f32>)> = eigen
147 .eigenvalues
148 .iter()
149 .zip(eigen.eigenvectors.column_iter())
150 .map(|(val, vec)| (*val, vec.clone_owned()))
151 .collect();
152
153 eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
155
156 let u = eigen_pairs[0].1.normalize();
157 let v = eigen_pairs[1].1.normalize();
158
159 let projected: Vec<Point2<f64>> = parallel::parallel_map(points, |point| {
161 let diff = point - centroid;
162 let x = diff.dot(&u) as f64;
163 let y = diff.dot(&v) as f64;
164 Point2::new(x, y)
165 });
166
167 Ok(projected)
168}
169
170fn project_using_best_fit_plane(points: &[Point3f]) -> Result<Vec<Point2<f64>>> {
172 if points.len() < 3 {
173 return Err(Error::InvalidData(
174 "Need at least 3 points for best-fit plane projection".to_string(),
175 ));
176 }
177
178 let p1 = points[0];
180 let mut p2 = None;
181 let mut p3 = None;
182
183 for point in points.iter().skip(1) {
185 if (point - p1).magnitude() > 1e-6 {
186 p2 = Some(*point);
187 break;
188 }
189 }
190
191 let p2 =
192 p2.ok_or_else(|| Error::InvalidData("All points are too close together".to_string()))?;
193
194 for point in points.iter().skip(2) {
196 let v1 = p2 - p1;
197 let v2 = *point - p1;
198 if v1.cross(&v2).magnitude() > 1e-6 {
199 p3 = Some(*point);
200 break;
201 }
202 }
203
204 let p3 = p3.ok_or_else(|| Error::InvalidData("All points are collinear".to_string()))?;
205
206 let u = (p2 - p1).normalize();
208 let w = (p3 - p1).cross(&u).normalize();
209 let v = w.cross(&u).normalize();
210
211 let projected: Vec<Point2<f64>> = parallel::parallel_map(points, |point| {
213 let diff = *point - p1;
214 let x = diff.dot(&u) as f64;
215 let y = diff.dot(&v) as f64;
216 Point2::new(x, y)
217 });
218
219 Ok(projected)
220}
221
222fn is_triangle_valid(p1: &Point3f, p2: &Point3f, p3: &Point3f, config: &DelaunayConfig) -> bool {
224 let v1 = p2 - p1;
226 let v2 = p3 - p1;
227 let cross = v1.cross(&v2);
228 let area = cross.magnitude() * 0.5;
229
230 if area < config.min_triangle_area {
231 return false;
232 }
233
234 if let Some(max_len) = config.max_edge_length {
236 let edge1 = (p2 - p1).magnitude();
237 let edge2 = (p3 - p2).magnitude();
238 let edge3 = (p1 - p3).magnitude();
239
240 if edge1 > max_len || edge2 > max_len || edge3 > max_len {
241 return false;
242 }
243 }
244
245 true
246}
247
248pub fn delaunay_triangulation(cloud: &PointCloud<Point3f>) -> Result<TriangleMesh> {
250 delaunay_triangulation_with_config(cloud, &DelaunayConfig::default())
251}
252
253pub fn delaunay_triangulation_with_config(
255 cloud: &PointCloud<Point3f>,
256 config: &DelaunayConfig,
257) -> Result<TriangleMesh> {
258 if cloud.is_empty() {
259 return Err(Error::InvalidData("Point cloud is empty".to_string()));
260 }
261
262 if cloud.points.len() < 3 {
263 return Err(Error::InvalidData(
264 "Need at least 3 points for triangulation".to_string(),
265 ));
266 }
267
268 let projected_points = project_3d_to_2d(&cloud.points, &config.projection)?;
270
271 let triangle_indices = delaunay_triangulation_2d(&projected_points)?;
273
274 let mut valid_triangles = Vec::new();
276 for &[i, j, k] in &triangle_indices {
277 if i < cloud.points.len() && j < cloud.points.len() && k < cloud.points.len() {
278 let p1 = &cloud.points[i];
279 let p2 = &cloud.points[j];
280 let p3 = &cloud.points[k];
281
282 if !config.validate || is_triangle_valid(p1, p2, p3, config) {
283 valid_triangles.push([i, j, k]);
284 }
285 }
286 }
287
288 if valid_triangles.is_empty() {
289 return Err(Error::Algorithm("No valid triangles generated".to_string()));
290 }
291
292 let mesh = TriangleMesh::from_vertices_and_faces(cloud.points.clone(), valid_triangles);
294
295 Ok(mesh)
296}
297
298pub fn auto_select_projection(points: &[Point3f]) -> ProjectionMethod {
300 if points.len() < 10 {
301 return ProjectionMethod::BestFitPlane;
302 }
303
304 let mut min_vals = [f32::INFINITY; 3];
306 let mut max_vals = [f32::NEG_INFINITY; 3];
307
308 for point in points {
309 min_vals[0] = min_vals[0].min(point.x);
310 min_vals[1] = min_vals[1].min(point.y);
311 min_vals[2] = min_vals[2].min(point.z);
312 max_vals[0] = max_vals[0].max(point.x);
313 max_vals[1] = max_vals[1].max(point.y);
314 max_vals[2] = max_vals[2].max(point.z);
315 }
316
317 let extents = [
318 max_vals[0] - min_vals[0],
319 max_vals[1] - min_vals[1],
320 max_vals[2] - min_vals[2],
321 ];
322
323 let min_extent_idx = extents
325 .iter()
326 .enumerate()
327 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
328 .map(|(idx, _)| idx)
329 .unwrap();
330
331 let min_extent = extents[min_extent_idx];
333 let max_extent = extents.iter().fold(0.0f32, |acc, &x| acc.max(x));
334
335 if min_extent < max_extent * 0.1 {
336 match min_extent_idx {
337 0 => ProjectionMethod::YZ, 1 => ProjectionMethod::XZ, 2 => ProjectionMethod::XY, _ => ProjectionMethod::PCA,
341 }
342 } else {
343 ProjectionMethod::PCA
345 }
346}
347
348pub fn delaunay_triangulation_auto(cloud: &PointCloud<Point3f>) -> Result<TriangleMesh> {
350 let projection = auto_select_projection(&cloud.points);
351 let config = DelaunayConfig {
352 projection,
353 ..Default::default()
354 };
355 delaunay_triangulation_with_config(cloud, &config)
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use nalgebra::Point3;
362
363 #[test]
364 fn test_delaunay_config_default() {
365 let config = DelaunayConfig::default();
366 assert_eq!(config.projection, ProjectionMethod::PCA);
367 assert!(config.validate);
368 assert_eq!(config.min_triangle_area, 1e-8);
369 assert_eq!(config.max_edge_length, None);
370 }
371
372 #[test]
373 fn test_project_3d_to_2d_xy() {
374 let points = vec![
375 Point3f::new(0.0, 0.0, 1.0),
376 Point3f::new(1.0, 1.0, 2.0),
377 Point3f::new(2.0, 0.0, 3.0),
378 ];
379
380 let projected = project_3d_to_2d(&points, &ProjectionMethod::XY).unwrap();
381
382 assert_eq!(projected.len(), 3);
383 assert_eq!(projected[0], Point2::new(0.0, 0.0));
384 assert_eq!(projected[1], Point2::new(1.0, 1.0));
385 assert_eq!(projected[2], Point2::new(2.0, 0.0));
386 }
387
388 #[test]
389 fn test_delaunay_triangulation_2d_simple() {
390 let points = vec![
391 Point2::new(0.0, 0.0),
392 Point2::new(1.0, 0.0),
393 Point2::new(0.5, 1.0),
394 ];
395
396 let triangles = delaunay_triangulation_2d(&points).unwrap();
397
398 assert_eq!(triangles.len(), 1);
399 assert_eq!(triangles[0].len(), 3);
401 }
402
403 #[test]
404 fn test_delaunay_triangulation_empty() {
405 let cloud = PointCloud::new();
406 let result = delaunay_triangulation(&cloud);
407 assert!(result.is_err());
408 }
409
410 #[test]
411 fn test_delaunay_triangulation_too_few_points() {
412 let points = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)];
413 let cloud = PointCloud::from_points(points);
414
415 let result = delaunay_triangulation(&cloud);
416 assert!(result.is_err());
417 }
418
419 #[test]
420 fn test_delaunay_triangulation_simple() {
421 let points = vec![
422 Point3::new(0.0, 0.0, 0.0),
423 Point3::new(1.0, 0.0, 0.0),
424 Point3::new(0.5, 1.0, 0.0),
425 Point3::new(0.0, 1.0, 0.0),
426 ];
427 let cloud = PointCloud::from_points(points);
428
429 let result = delaunay_triangulation(&cloud);
430
431 match result {
432 Ok(mesh) => {
433 assert!(!mesh.is_empty());
434 assert!(mesh.vertex_count() <= 4);
435 assert!(mesh.face_count() >= 1);
436 }
437 Err(_) => {
438 }
440 }
441 }
442
443 #[test]
444 fn test_auto_select_projection() {
445 let points = vec![
447 Point3f::new(0.0, 0.0, 0.0),
448 Point3f::new(1.0, 0.0, 0.01),
449 Point3f::new(0.5, 1.0, 0.02),
450 Point3f::new(0.0, 1.0, 0.01),
451 ];
452
453 let projection = auto_select_projection(&points);
454 assert!(matches!(projection, ProjectionMethod::BestFitPlane));
457
458 let many_points: Vec<Point3f> = (0..20)
460 .map(|i| Point3f::new(i as f32 * 0.1, (i % 5) as f32 * 0.1, 0.001))
461 .collect();
462
463 let projection = auto_select_projection(&many_points);
464 assert!(matches!(
466 projection,
467 ProjectionMethod::XY | ProjectionMethod::PCA
468 ));
469 }
470}