use std::ops::ControlFlow;
use nalgebra::{Matrix6, Vector3, Vector6};
use rayon::prelude::*;
use crate::cloud::PointCloud;
use crate::icp::kernel::Kernel;
use crate::icp::residual::point_to_plane_row;
use crate::lie::Se3;
use crate::neighbors::{Neighbor, NeighborSearch};
const REDUCTION_CHUNK: usize = 4_096;
#[derive(Debug, Clone, Copy)]
pub struct Surface<'a> {
pub cloud: &'a PointCloud,
pub normals: &'a [Vector3<f64>],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IcpConfig {
pub max_iterations: usize,
pub max_correspondence_distance: f64,
pub min_normal_cosine: f64,
pub kernel: Kernel,
pub translation_tolerance: f64,
pub rotation_tolerance: f64,
pub initial_damping: f64,
}
impl Default for IcpConfig {
fn default() -> Self {
Self {
max_iterations: 50,
max_correspondence_distance: 1.0,
min_normal_cosine: 0.8,
kernel: Kernel::Huber(0.1),
translation_tolerance: 1e-8,
rotation_tolerance: 1e-8,
initial_damping: 1e-4,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct IterationReport {
pub iteration: usize,
pub pose: Se3,
pub rmse: f64,
pub correspondences: usize,
}
#[derive(Debug, Clone)]
pub struct IcpResult {
pub pose: Se3,
pub iterations: usize,
pub converged: bool,
pub rmse: f64,
pub correspondences: usize,
pub information: Matrix6<f64>,
}
#[derive(Debug, Clone, Copy)]
struct SystemBlock {
hessian: Matrix6<f64>,
gradient: Vector6<f64>,
cost: f64,
squared_residual: f64,
count: usize,
}
impl SystemBlock {
fn zero() -> Self {
Self {
hessian: Matrix6::zeros(),
gradient: Vector6::zeros(),
cost: 0.0,
squared_residual: 0.0,
count: 0,
}
}
fn absorb(&mut self, other: &Self) {
self.hessian += other.hessian;
self.gradient += other.gradient;
self.cost += other.cost;
self.squared_residual += other.squared_residual;
self.count += other.count;
}
fn mean_cost(&self) -> f64 {
if self.count == 0 {
f64::INFINITY
} else {
self.cost / self.count as f64
}
}
}
fn assemble<S>(
source: &Surface<'_>,
target: &Surface<'_>,
search: &S,
pose: &Se3,
config: &IcpConfig,
) -> SystemBlock
where
S: NeighborSearch + Sync,
{
let count = source.cloud.len();
if count == 0 {
return SystemBlock::zero();
}
let rotation = *pose.rotation().matrix();
let max_distance_squared =
config.max_correspondence_distance * config.max_correspondence_distance;
let chunks = count.div_ceil(REDUCTION_CHUNK);
let blocks: Vec<SystemBlock> = (0..chunks)
.into_par_iter()
.map(|chunk| {
let begin = chunk * REDUCTION_CHUNK;
let end = ((chunk + 1) * REDUCTION_CHUNK).min(count);
let mut block = SystemBlock::zero();
let mut found: Vec<Neighbor> = Vec::with_capacity(1);
for index in begin..end {
let transformed = pose.transform_point(&source.cloud.point(index));
search.knn_into(&transformed, 1, &mut found);
let Some(nearest) = found.first() else {
continue;
};
if nearest.distance_squared > max_distance_squared {
continue;
}
let matched = nearest.index as usize;
let target_normal = target.normals[matched];
let source_normal = rotation * source.normals[index];
if source_normal.dot(&target_normal).abs() < config.min_normal_cosine {
continue;
}
let residual = target_normal.dot(&(transformed - target.cloud.point(matched)));
let weight = config.kernel.weight(residual);
let row = point_to_plane_row(&transformed, &target_normal);
block.hessian += (row * row.transpose()) * weight;
block.gradient += row * (weight * residual);
block.cost += config.kernel.loss(residual);
block.squared_residual += residual * residual;
block.count += 1;
}
block
})
.collect();
let mut total = SystemBlock::zero();
for block in &blocks {
total.absorb(block);
}
total
}
fn solve_step(
hessian: &Matrix6<f64>,
gradient: &Vector6<f64>,
damping: f64,
) -> Option<Vector6<f64>> {
const DIAGONAL_FLOOR: f64 = 1e-6;
let diagonal = hessian.diagonal();
let largest = diagonal.max();
if !largest.is_finite() || largest <= 0.0 {
return None;
}
let floor = largest * DIAGONAL_FLOOR;
let mut damped = *hessian;
for axis in 0..6 {
damped[(axis, axis)] += damping * diagonal[axis].max(floor);
}
nalgebra::Cholesky::new(damped).map(|factorisation| factorisation.solve(&(-gradient)))
}
pub fn register<S>(
source: &Surface<'_>,
target: &Surface<'_>,
search: &S,
initial: Se3,
config: &IcpConfig,
) -> IcpResult
where
S: NeighborSearch + Sync,
{
register_observed(source, target, search, initial, config, |_| {
ControlFlow::Continue(())
})
}
pub fn register_observed<S, F>(
source: &Surface<'_>,
target: &Surface<'_>,
search: &S,
initial: Se3,
config: &IcpConfig,
mut observer: F,
) -> IcpResult
where
S: NeighborSearch + Sync,
F: FnMut(&IterationReport) -> ControlFlow<()>,
{
assert_eq!(
source.cloud.len(),
source.normals.len(),
"the source has a different number of normals than points"
);
assert_eq!(
target.cloud.len(),
target.normals.len(),
"the target has a different number of normals than points"
);
let mut pose = initial;
let mut damping = config.initial_damping;
let mut current = assemble(source, target, search, &pose, config);
let mut iterations = 0;
let mut converged = false;
while iterations < config.max_iterations {
iterations += 1;
let Some(step) = solve_step(¤t.hessian, ¤t.gradient, damping) else {
damping *= 10.0;
if damping > 1e12 {
break;
}
continue;
};
let candidate_pose = Se3::exp(&step) * pose;
let candidate = assemble(source, target, search, &candidate_pose, config);
if candidate.mean_cost() <= current.mean_cost() {
pose = candidate_pose;
current = candidate;
damping = (damping * 0.1).max(1e-12);
let flow = observer(&IterationReport {
iteration: iterations,
pose,
rmse: if current.count == 0 {
f64::INFINITY
} else {
(current.squared_residual / current.count as f64).sqrt()
},
correspondences: current.count,
});
let translation_step = step.fixed_rows::<3>(0).norm();
let rotation_step = step.fixed_rows::<3>(3).norm();
if translation_step < config.translation_tolerance
&& rotation_step < config.rotation_tolerance
{
converged = true;
break;
}
if flow.is_break() {
break;
}
} else {
damping *= 10.0;
if damping > 1e12 {
break;
}
}
}
let rmse = if current.count == 0 {
f64::INFINITY
} else {
(current.squared_residual / current.count as f64).sqrt()
};
IcpResult {
pose,
iterations,
converged,
rmse,
correspondences: current.count,
information: current.hessian,
}
}
pub fn surface<'a>(cloud: &'a PointCloud, normals: &'a [Vector3<f64>]) -> Surface<'a> {
Surface { cloud, normals }
}