1use crate::features::{extract_fpfh_features_with_normals, FpfhConfig, FPFH_DIM};
14use crate::normals::estimate_normals;
15use crate::registration::{icp_point_to_point, ICPResult};
16use nalgebra::{Matrix3, Rotation3, Translation3, UnitQuaternion, Vector3};
17use rand::Rng;
18use rayon::prelude::*;
19use threecrate_core::{Error, Isometry3, Point3f, PointCloud, Result};
20
21#[derive(Debug, Clone)]
27pub struct GlobalRegistrationConfig {
28 pub ransac_iterations: usize,
30 pub distance_threshold: f32,
32 pub inlier_ratio: f32,
34 pub fpfh_radius: f32,
36 pub fpfh_k_neighbors: usize,
38 pub normal_k_neighbors: usize,
40 pub refine_with_icp: bool,
42 pub icp_max_iterations: usize,
44 pub icp_distance_threshold: Option<f32>,
46}
47
48impl Default for GlobalRegistrationConfig {
49 fn default() -> Self {
50 Self {
51 ransac_iterations: 50_000,
52 distance_threshold: 0.05,
53 inlier_ratio: 0.25,
54 fpfh_radius: 0.25,
55 fpfh_k_neighbors: 10,
56 normal_k_neighbors: 10,
57 refine_with_icp: true,
58 icp_max_iterations: 50,
59 icp_distance_threshold: None,
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
70pub struct GlobalRegistrationResult {
71 pub transformation: Isometry3<f32>,
73 pub inlier_count: usize,
75 pub inlier_ratio: f32,
77 pub icp_result: Option<ICPResult>,
79}
80
81#[inline]
87fn fpfh_dist_sq(a: &[f32; FPFH_DIM], b: &[f32; FPFH_DIM]) -> f32 {
88 a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum()
89}
90
91fn find_feature_correspondences(
95 src_descs: &[[f32; FPFH_DIM]],
96 tgt_descs: &[[f32; FPFH_DIM]],
97) -> Vec<(usize, usize)> {
98 src_descs
99 .par_iter()
100 .enumerate()
101 .filter_map(|(i, sd)| {
102 let best = tgt_descs.iter().enumerate().min_by(|(_, ta), (_, tb)| {
103 fpfh_dist_sq(sd, ta)
104 .partial_cmp(&fpfh_dist_sq(sd, tb))
105 .unwrap_or(std::cmp::Ordering::Equal)
106 })?;
107 Some((i, best.0))
108 })
109 .collect()
110}
111
112fn estimate_transform_svd(src_pts: &[Point3f], tgt_pts: &[Point3f]) -> Option<Isometry3<f32>> {
114 let n = src_pts.len();
115 if n < 3 {
116 return None;
117 }
118 let scale = 1.0 / n as f32;
119
120 let src_centroid = src_pts.iter().fold(Vector3::zeros(), |a, p| a + p.coords) * scale;
121 let tgt_centroid = tgt_pts.iter().fold(Vector3::zeros(), |a, p| a + p.coords) * scale;
122
123 let mut h = Matrix3::<f32>::zeros();
124 for (s, t) in src_pts.iter().zip(tgt_pts.iter()) {
125 let ds = s.coords - src_centroid;
126 let dt = t.coords - tgt_centroid;
127 h += ds * dt.transpose();
128 }
129
130 let svd = h.svd(true, true);
131 let u = svd.u?;
132 let vt = svd.v_t?;
133 let mut r = vt.transpose() * u.transpose();
134
135 if r.determinant() < 0.0 {
137 let mut vt_fix = vt;
138 vt_fix.row_mut(2).neg_mut();
139 r = vt_fix.transpose() * u.transpose();
140 }
141
142 let rotation = UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(r));
143 let t = tgt_centroid - rotation * src_centroid;
144 Some(Isometry3::from_parts(Translation3::from(t), rotation))
145}
146
147fn count_inliers(
149 corrs: &[(usize, usize)],
150 src_pts: &[Point3f],
151 tgt_pts: &[Point3f],
152 transform: &Isometry3<f32>,
153 threshold: f32,
154) -> usize {
155 let thr_sq = threshold * threshold;
156 corrs
157 .iter()
158 .filter(|&&(si, ti)| {
159 let tp = transform * src_pts[si];
160 (tp - tgt_pts[ti]).magnitude_squared() <= thr_sq
161 })
162 .count()
163}
164
165pub fn global_registration(
186 source: &PointCloud<Point3f>,
187 target: &PointCloud<Point3f>,
188 config: &GlobalRegistrationConfig,
189) -> Result<GlobalRegistrationResult> {
190 if source.is_empty() {
191 return Err(Error::Algorithm("Source point cloud is empty".into()));
192 }
193 if target.is_empty() {
194 return Err(Error::Algorithm("Target point cloud is empty".into()));
195 }
196
197 let src_normals = estimate_normals(source, config.normal_k_neighbors)?;
198 let tgt_normals = estimate_normals(target, config.normal_k_neighbors)?;
199
200 global_registration_with_normals(&src_normals, &tgt_normals, source, target, config)
201}
202
203pub fn global_registration_with_normals(
214 source_n: &PointCloud<threecrate_core::NormalPoint3f>,
215 target_n: &PointCloud<threecrate_core::NormalPoint3f>,
216 source: &PointCloud<Point3f>,
217 target: &PointCloud<Point3f>,
218 config: &GlobalRegistrationConfig,
219) -> Result<GlobalRegistrationResult> {
220 if source_n.is_empty() || target_n.is_empty() {
221 return Err(Error::Algorithm("Source or target cloud is empty".into()));
222 }
223
224 let fpfh_cfg = FpfhConfig {
226 search_radius: config.fpfh_radius,
227 k_neighbors: config.fpfh_k_neighbors,
228 };
229 let src_descs = extract_fpfh_features_with_normals(source_n, &fpfh_cfg)?;
230 let tgt_descs = extract_fpfh_features_with_normals(target_n, &fpfh_cfg)?;
231
232 let corrs = find_feature_correspondences(&src_descs, &tgt_descs);
234
235 if corrs.len() < 3 {
236 return Err(Error::Algorithm(
237 "Too few feature correspondences for RANSAC (need ≥ 3)".into(),
238 ));
239 }
240
241 let src_pts = &source_n
242 .points
243 .iter()
244 .map(|p| p.position)
245 .collect::<Vec<_>>();
246 let tgt_pts = &target_n
247 .points
248 .iter()
249 .map(|p| p.position)
250 .collect::<Vec<_>>();
251
252 let mut best_transform = Isometry3::identity();
254 let mut best_inliers = 0usize;
255 let early_exit_count = ((config.inlier_ratio * corrs.len() as f32).ceil() as usize).max(3);
256 let mut rng = rand::rng();
257 let n_corrs = corrs.len();
258
259 for _ in 0..config.ransac_iterations {
260 let i0 = rng.random_range(0..n_corrs);
262 let mut i1 = rng.random_range(0..n_corrs - 1);
263 if i1 >= i0 {
264 i1 += 1;
265 }
266 let mut i2 = rng.random_range(0..n_corrs - 2);
267 if i2 >= i0.min(i1) {
268 i2 += 1;
269 }
270 if i2 >= i0.max(i1) {
271 i2 += 1;
272 }
273 let sample = [i0, i1, i2];
274
275 let s_pts: Vec<Point3f> = sample.iter().map(|&i| src_pts[corrs[i].0]).collect();
276 let t_pts: Vec<Point3f> = sample.iter().map(|&i| tgt_pts[corrs[i].1]).collect();
277
278 let transform = match estimate_transform_svd(&s_pts, &t_pts) {
279 Some(t) => t,
280 None => continue,
281 };
282
283 let inliers = count_inliers(
284 &corrs,
285 src_pts,
286 tgt_pts,
287 &transform,
288 config.distance_threshold,
289 );
290
291 if inliers > best_inliers {
292 best_inliers = inliers;
293 best_transform = transform;
294
295 if inliers >= early_exit_count {
296 break;
297 }
298 }
299 }
300
301 let total_corrs = corrs.len();
302 let inlier_ratio = if total_corrs > 0 {
303 best_inliers as f32 / total_corrs as f32
304 } else {
305 0.0
306 };
307
308 let icp_result = if config.refine_with_icp {
310 Some(icp_point_to_point(
311 source,
312 target,
313 best_transform,
314 config.icp_max_iterations,
315 1e-6,
316 config.icp_distance_threshold,
317 )?)
318 } else {
319 None
320 };
321
322 let final_transform = icp_result
323 .as_ref()
324 .map(|r| r.transformation)
325 .unwrap_or(best_transform);
326
327 Ok(GlobalRegistrationResult {
328 transformation: final_transform,
329 inlier_count: best_inliers,
330 inlier_ratio,
331 icp_result,
332 })
333}
334
335#[cfg(test)]
340mod tests {
341 use super::*;
342 use nalgebra::{Isometry3, Translation3, UnitQuaternion};
343 use threecrate_core::{Point3f, PointCloud};
344
345 fn grid_cloud(nx: usize, ny: usize, nz: usize, scale: f32) -> PointCloud<Point3f> {
346 let mut pts = Vec::new();
347 for ix in 0..nx {
348 for iy in 0..ny {
349 for iz in 0..nz {
350 pts.push(Point3f::new(
351 ix as f32 * scale,
352 iy as f32 * scale,
353 iz as f32 * scale,
354 ));
355 }
356 }
357 }
358 PointCloud { points: pts }
359 }
360
361 fn apply(cloud: &PointCloud<Point3f>, iso: &Isometry3<f32>) -> PointCloud<Point3f> {
362 PointCloud {
363 points: cloud.points.iter().map(|p| iso * p).collect(),
364 }
365 }
366
367 #[test]
368 fn test_global_reg_empty_source() {
369 let empty: PointCloud<Point3f> = PointCloud { points: vec![] };
370 let target = grid_cloud(4, 4, 4, 1.0);
371 let cfg = GlobalRegistrationConfig::default();
372 assert!(global_registration(&empty, &target, &cfg).is_err());
373 }
374
375 #[test]
376 fn test_global_reg_empty_target() {
377 let source = grid_cloud(4, 4, 4, 1.0);
378 let empty: PointCloud<Point3f> = PointCloud { points: vec![] };
379 let cfg = GlobalRegistrationConfig::default();
380 assert!(global_registration(&source, &empty, &cfg).is_err());
381 }
382
383 #[test]
384 fn test_global_reg_identity() {
385 let cloud = grid_cloud(4, 4, 4, 1.0);
386 let cfg = GlobalRegistrationConfig {
387 ransac_iterations: 200,
388 distance_threshold: 0.5,
389 fpfh_radius: 3.0,
390 refine_with_icp: false,
391 ..Default::default()
392 };
393 let result = global_registration(&cloud, &cloud, &cfg).unwrap();
394 assert!(result.inlier_count > 0);
396 assert!(result.inlier_ratio > 0.0);
397 }
398
399 #[test]
400 fn test_global_reg_returns_valid_isometry() {
401 let cloud = grid_cloud(4, 4, 4, 1.0);
402 let cfg = GlobalRegistrationConfig {
403 ransac_iterations: 100,
404 distance_threshold: 0.5,
405 fpfh_radius: 3.0,
406 refine_with_icp: false,
407 ..Default::default()
408 };
409 let result = global_registration(&cloud, &cloud, &cfg).unwrap();
410 let qnorm = result.transformation.rotation.norm();
412 assert!((qnorm - 1.0).abs() < 1e-5);
413 }
414
415 #[test]
416 fn test_global_reg_with_icp() {
417 let target = grid_cloud(4, 4, 4, 1.0);
418 let t = Isometry3::from_parts(Translation3::new(0.3, 0.2, 0.1), UnitQuaternion::identity());
419 let source = apply(&target, &t);
420 let cfg = GlobalRegistrationConfig {
421 ransac_iterations: 500,
422 distance_threshold: 1.0,
423 fpfh_radius: 3.0,
424 refine_with_icp: true,
425 icp_max_iterations: 30,
426 icp_distance_threshold: Some(2.0),
427 ..Default::default()
428 };
429 let result = global_registration(&source, &target, &cfg).unwrap();
430 assert!(result.icp_result.is_some());
431 }
432
433 #[test]
434 fn test_estimate_transform_svd_three_points() {
435 let src = vec![
436 Point3f::new(0.0, 0.0, 0.0),
437 Point3f::new(1.0, 0.0, 0.0),
438 Point3f::new(0.0, 1.0, 0.0),
439 ];
440 let shift = Vector3::new(1.0f32, 2.0, 3.0);
441 let tgt: Vec<Point3f> = src
442 .iter()
443 .map(|p| Point3f::from(p.coords + shift))
444 .collect();
445 let iso = estimate_transform_svd(&src, &tgt).unwrap();
446 let t = iso.translation.vector;
447 assert!((t.x - 1.0).abs() < 1e-4);
448 assert!((t.y - 2.0).abs() < 1e-4);
449 assert!((t.z - 3.0).abs() < 1e-4);
450 }
451
452 #[test]
453 fn test_config_defaults() {
454 let cfg = GlobalRegistrationConfig::default();
455 assert!(cfg.ransac_iterations > 0);
456 assert!(cfg.distance_threshold > 0.0);
457 assert!(cfg.inlier_ratio > 0.0 && cfg.inlier_ratio < 1.0);
458 assert!(cfg.fpfh_radius > 0.0);
459 }
460}