1use std::ops::ControlFlow;
4
5use nalgebra::{Matrix6, Vector3, Vector6};
6use rayon::prelude::*;
7
8use crate::cloud::PointCloud;
9use crate::icp::kernel::Kernel;
10use crate::icp::residual::point_to_plane_row;
11use crate::lie::Se3;
12use crate::neighbors::{Neighbor, NeighborSearch};
13
14const REDUCTION_CHUNK: usize = 4_096;
25
26#[derive(Debug, Clone, Copy)]
28pub struct Surface<'a> {
29 pub cloud: &'a PointCloud,
31 pub normals: &'a [Vector3<f64>],
33}
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct IcpConfig {
38 pub max_iterations: usize,
40 pub max_correspondence_distance: f64,
42 pub min_normal_cosine: f64,
47 pub kernel: Kernel,
49 pub translation_tolerance: f64,
51 pub rotation_tolerance: f64,
53 pub initial_damping: f64,
55}
56
57impl Default for IcpConfig {
58 fn default() -> Self {
59 Self {
60 max_iterations: 50,
61 max_correspondence_distance: 1.0,
62 min_normal_cosine: 0.8,
63 kernel: Kernel::Huber(0.1),
64 translation_tolerance: 1e-8,
65 rotation_tolerance: 1e-8,
66 initial_damping: 1e-4,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
73pub struct IterationReport {
74 pub iteration: usize,
76 pub pose: Se3,
78 pub rmse: f64,
80 pub correspondences: usize,
82}
83
84#[derive(Debug, Clone)]
86pub struct IcpResult {
87 pub pose: Se3,
89 pub iterations: usize,
91 pub converged: bool,
93 pub rmse: f64,
95 pub correspondences: usize,
97 pub information: Matrix6<f64>,
104}
105
106#[derive(Debug, Clone, Copy)]
108struct SystemBlock {
109 hessian: Matrix6<f64>,
110 gradient: Vector6<f64>,
111 cost: f64,
112 squared_residual: f64,
113 count: usize,
114}
115
116impl SystemBlock {
117 fn zero() -> Self {
118 Self {
119 hessian: Matrix6::zeros(),
120 gradient: Vector6::zeros(),
121 cost: 0.0,
122 squared_residual: 0.0,
123 count: 0,
124 }
125 }
126
127 fn absorb(&mut self, other: &Self) {
128 self.hessian += other.hessian;
129 self.gradient += other.gradient;
130 self.cost += other.cost;
131 self.squared_residual += other.squared_residual;
132 self.count += other.count;
133 }
134
135 fn mean_cost(&self) -> f64 {
141 if self.count == 0 {
142 f64::INFINITY
143 } else {
144 self.cost / self.count as f64
145 }
146 }
147}
148
149fn assemble<S>(
150 source: &Surface<'_>,
151 target: &Surface<'_>,
152 search: &S,
153 pose: &Se3,
154 config: &IcpConfig,
155) -> SystemBlock
156where
157 S: NeighborSearch + Sync,
158{
159 let count = source.cloud.len();
160 if count == 0 {
161 return SystemBlock::zero();
162 }
163 let rotation = *pose.rotation().matrix();
164 let max_distance_squared =
165 config.max_correspondence_distance * config.max_correspondence_distance;
166 let chunks = count.div_ceil(REDUCTION_CHUNK);
167
168 let blocks: Vec<SystemBlock> = (0..chunks)
169 .into_par_iter()
170 .map(|chunk| {
171 let begin = chunk * REDUCTION_CHUNK;
172 let end = ((chunk + 1) * REDUCTION_CHUNK).min(count);
173 let mut block = SystemBlock::zero();
174 let mut found: Vec<Neighbor> = Vec::with_capacity(1);
175
176 for index in begin..end {
177 let transformed = pose.transform_point(&source.cloud.point(index));
178 search.knn_into(&transformed, 1, &mut found);
179 let Some(nearest) = found.first() else {
180 continue;
181 };
182 if nearest.distance_squared > max_distance_squared {
183 continue;
184 }
185
186 let matched = nearest.index as usize;
187 let target_normal = target.normals[matched];
188 let source_normal = rotation * source.normals[index];
189 if source_normal.dot(&target_normal).abs() < config.min_normal_cosine {
190 continue;
191 }
192
193 let residual = target_normal.dot(&(transformed - target.cloud.point(matched)));
194 let weight = config.kernel.weight(residual);
195 let row = point_to_plane_row(&transformed, &target_normal);
196
197 block.hessian += (row * row.transpose()) * weight;
198 block.gradient += row * (weight * residual);
199 block.cost += config.kernel.loss(residual);
200 block.squared_residual += residual * residual;
201 block.count += 1;
202 }
203 block
204 })
205 .collect();
206
207 let mut total = SystemBlock::zero();
208 for block in &blocks {
209 total.absorb(block);
210 }
211 total
212}
213
214fn solve_step(
227 hessian: &Matrix6<f64>,
228 gradient: &Vector6<f64>,
229 damping: f64,
230) -> Option<Vector6<f64>> {
231 const DIAGONAL_FLOOR: f64 = 1e-6;
232 let diagonal = hessian.diagonal();
233 let largest = diagonal.max();
234 if !largest.is_finite() || largest <= 0.0 {
237 return None;
238 }
239 let floor = largest * DIAGONAL_FLOOR;
240
241 let mut damped = *hessian;
242 for axis in 0..6 {
243 damped[(axis, axis)] += damping * diagonal[axis].max(floor);
244 }
245 nalgebra::Cholesky::new(damped).map(|factorisation| factorisation.solve(&(-gradient)))
246}
247
248pub fn register<S>(
258 source: &Surface<'_>,
259 target: &Surface<'_>,
260 search: &S,
261 initial: Se3,
262 config: &IcpConfig,
263) -> IcpResult
264where
265 S: NeighborSearch + Sync,
266{
267 register_observed(source, target, search, initial, config, |_| {
268 ControlFlow::Continue(())
269 })
270}
271
272pub fn register_observed<S, F>(
293 source: &Surface<'_>,
294 target: &Surface<'_>,
295 search: &S,
296 initial: Se3,
297 config: &IcpConfig,
298 mut observer: F,
299) -> IcpResult
300where
301 S: NeighborSearch + Sync,
302 F: FnMut(&IterationReport) -> ControlFlow<()>,
303{
304 assert_eq!(
305 source.cloud.len(),
306 source.normals.len(),
307 "the source has a different number of normals than points"
308 );
309 assert_eq!(
310 target.cloud.len(),
311 target.normals.len(),
312 "the target has a different number of normals than points"
313 );
314
315 let mut pose = initial;
316 let mut damping = config.initial_damping;
317 let mut current = assemble(source, target, search, &pose, config);
318 let mut iterations = 0;
319 let mut converged = false;
320
321 while iterations < config.max_iterations {
322 iterations += 1;
323
324 let Some(step) = solve_step(¤t.hessian, ¤t.gradient, damping) else {
325 damping *= 10.0;
326 if damping > 1e12 {
327 break;
328 }
329 continue;
330 };
331
332 let candidate_pose = Se3::exp(&step) * pose;
333 let candidate = assemble(source, target, search, &candidate_pose, config);
334
335 if candidate.mean_cost() <= current.mean_cost() {
336 pose = candidate_pose;
337 current = candidate;
338 damping = (damping * 0.1).max(1e-12);
339 let flow = observer(&IterationReport {
340 iteration: iterations,
341 pose,
342 rmse: if current.count == 0 {
343 f64::INFINITY
344 } else {
345 (current.squared_residual / current.count as f64).sqrt()
346 },
347 correspondences: current.count,
348 });
349
350 let translation_step = step.fixed_rows::<3>(0).norm();
353 let rotation_step = step.fixed_rows::<3>(3).norm();
354 if translation_step < config.translation_tolerance
355 && rotation_step < config.rotation_tolerance
356 {
357 converged = true;
358 break;
359 }
360 if flow.is_break() {
361 break;
362 }
363 } else {
364 damping *= 10.0;
365 if damping > 1e12 {
366 break;
367 }
368 }
369 }
370
371 let rmse = if current.count == 0 {
372 f64::INFINITY
373 } else {
374 (current.squared_residual / current.count as f64).sqrt()
375 };
376
377 IcpResult {
378 pose,
379 iterations,
380 converged,
381 rmse,
382 correspondences: current.count,
383 information: current.hessian,
384 }
385}
386
387pub fn surface<'a>(cloud: &'a PointCloud, normals: &'a [Vector3<f64>]) -> Surface<'a> {
392 Surface { cloud, normals }
393}