1use super::cost_function::PointPlaneDistance;
2use super::icp_params::IcpParams;
3use crate::{
4 extra_math,
5 kdtree::R3dTree,
6 optim::GaussNewton,
7 pointcloud::PointCloud,
8 transform::{LieGroup, Transform},
9};
10use itertools::izip;
11use num::Float;
12
13pub struct Icp<'target> {
16 pub params: IcpParams,
18 pub initial_transform: Transform,
20 target: &'target PointCloud,
21 kdtree: R3dTree,
22}
23
24impl<'target> Icp<'target> {
25 pub fn new(params: IcpParams, target: &'target PointCloud) -> Self {
32 Self {
33 params,
34 initial_transform: Transform::eye(),
35 target,
36 kdtree: R3dTree::new(&target.points.view()),
37 }
38 }
39
40 pub fn align(&self, source: &PointCloud) -> Transform {
50 let target_normals = self
51 .target
52 .normals
53 .as_ref()
54 .expect("Please, the target point cloud should have normals.");
55 let source_normals = source
56 .normals
57 .as_ref()
58 .expect("Please, the source point cloud should have normals.");
59 let mut optim_transform = Transform::eye();
60 let mut optimizer = GaussNewton::<6>::new();
61 let geom_cost = PointPlaneDistance {};
62
63 let max_distance_sqr = self.params.max_distance * self.params.max_distance;
64
65 let mut best_residual = Float::infinity();
66 let mut best_transform = optim_transform.clone();
67 for _ in 0..self.params.max_iterations {
68 for (source_point, source_normal) in izip!(source.points.iter(), source_normals.iter())
69 {
70 let source_point = optim_transform.transform_vector(source_point);
71 let source_normal = optim_transform.transform_normal(source_normal);
72
73 let (found_index, found_sqr_distance) = self.kdtree.nearest(&source_point);
74 if found_sqr_distance > max_distance_sqr {
75 continue;
76 }
77
78 let target_normal = target_normals[found_index];
79
80 if extra_math::angle_between_normals(&source_normal, &target_normal)
81 > self.params.max_normal_angle
82 {
83 continue;
84 }
85
86 let target_point = self.target.points[found_index];
87
88 let (residual, jacobian) =
89 geom_cost.jacobian(&source_point, &target_point, &target_normal);
90
91 optimizer.step(residual, &jacobian);
92 }
93
94 let residual = optimizer.mean_squared_residual();
95 optimizer.weight(self.params.weight);
96 let update = optimizer.solve().unwrap();
97 optim_transform = &Transform::exp(&LieGroup::Se3(update)) * &optim_transform;
98 optimizer.reset();
99
100 if residual < best_residual {
101 best_residual = residual;
102 best_transform = optim_transform.clone();
103 }
104 }
105
106 best_transform
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use rstest::*;
114
115 use crate::{
116 metrics::TransformMetrics,
117 unit_test::{sample_pcl_ds1, TestPclDataset},
118 };
119
120 #[rstest]
122 fn test_icp(sample_pcl_ds1: TestPclDataset) {
123 let target_pcl = sample_pcl_ds1.get(0);
124 let source_pcl = sample_pcl_ds1.get(1);
125
126 let actual = Icp::new(
127 IcpParams {
128 max_iterations: 5,
129 ..Default::default()
130 },
131 &target_pcl,
132 )
133 .align(&source_pcl);
134 let gt_transform = sample_pcl_ds1.get_ground_truth(1, 0);
135 assert!(TransformMetrics::new(&actual, >_transform).angle.abs() < 0.1);
136 }
137}