use threecrate_core::{PointCloud, Result, Point3f, Vector3f, Error, Isometry3};
use nalgebra::{Matrix3, UnitQuaternion, Translation3};
use rayon::prelude::*;
#[derive(Debug, Clone)]
pub struct ICPResult {
pub transformation: Isometry3<f32>,
pub mse: f32,
pub iterations: usize,
pub converged: bool,
pub correspondences: Vec<(usize, usize)>,
}
fn find_correspondences(
source: &[Point3f],
target: &[Point3f],
max_distance: Option<f32>,
) -> Vec<Option<(usize, f32)>> {
source
.par_iter()
.map(|source_point| {
let mut best_distance = f32::INFINITY;
let mut best_idx = None;
for (target_idx, target_point) in target.iter().enumerate() {
let distance = (source_point - target_point).magnitude();
if distance < best_distance {
best_distance = distance;
best_idx = Some(target_idx);
}
}
if let Some(max_dist) = max_distance {
if best_distance > max_dist {
return None;
}
}
best_idx.map(|idx| (idx, best_distance))
})
.collect()
}
fn compute_transformation(
source_points: &[Point3f],
target_points: &[Point3f],
) -> Result<Isometry3<f32>> {
if source_points.len() != target_points.len() || source_points.is_empty() {
return Err(Error::InvalidData("Point correspondence mismatch".to_string()));
}
let n = source_points.len() as f32;
let source_centroid = source_points.iter().fold(Point3f::origin(), |acc, p| acc + p.coords) / n;
let target_centroid = target_points.iter().fold(Point3f::origin(), |acc, p| acc + p.coords) / n;
let mut h = Matrix3::zeros();
for (src, tgt) in source_points.iter().zip(target_points.iter()) {
let p = src - source_centroid;
let q = tgt - target_centroid;
h += p * q.transpose();
}
let svd = h.svd(true, true);
let u = svd.u.ok_or_else(|| Error::Algorithm("SVD U matrix not available".to_string()))?;
let v_t = svd.v_t.ok_or_else(|| Error::Algorithm("SVD V^T matrix not available".to_string()))?;
let mut r = v_t.transpose() * u.transpose();
if r.determinant() < 0.0 {
let mut v_t_corrected = v_t;
v_t_corrected.set_row(2, &(-v_t.row(2)));
r = v_t_corrected.transpose() * u.transpose();
}
let rotation = UnitQuaternion::from_matrix(&r);
let translation = target_centroid - rotation * source_centroid;
Ok(Isometry3::from_parts(
Translation3::new(translation.x, translation.y, translation.z),
rotation,
))
}
fn compute_mse(
source_points: &[Point3f],
target_points: &[Point3f],
) -> f32 {
if source_points.is_empty() {
return 0.0;
}
let sum_squared_error: f32 = source_points
.iter()
.zip(target_points.iter())
.map(|(src, tgt)| (src - tgt).magnitude_squared())
.sum();
sum_squared_error / source_points.len() as f32
}
pub fn icp(
source: &PointCloud<Point3f>,
target: &PointCloud<Point3f>,
init: Isometry3<f32>,
max_iters: usize,
) -> Isometry3<f32> {
match icp_detailed(source, target, init, max_iters, None, 1e-6) {
Ok(result) => result.transformation,
Err(_) => init, }
}
pub fn icp_detailed(
source: &PointCloud<Point3f>,
target: &PointCloud<Point3f>,
init: Isometry3<f32>,
max_iters: usize,
max_correspondence_distance: Option<f32>,
convergence_threshold: f32,
) -> Result<ICPResult> {
if source.is_empty() || target.is_empty() {
return Err(Error::InvalidData("Source or target point cloud is empty".to_string()));
}
if max_iters == 0 {
return Err(Error::InvalidData("Max iterations must be positive".to_string()));
}
let mut current_transform = init;
let mut previous_mse = f32::INFINITY;
let mut final_correspondences = Vec::new();
for iteration in 0..max_iters {
let transformed_source: Vec<Point3f> = source
.points
.iter()
.map(|point| current_transform * point)
.collect();
let correspondences = find_correspondences(
&transformed_source,
&target.points,
max_correspondence_distance,
);
let mut valid_source_points = Vec::new();
let mut valid_target_points = Vec::new();
let mut corr_pairs = Vec::new();
for (src_idx, correspondence) in correspondences.iter().enumerate() {
if let Some((tgt_idx, _distance)) = correspondence {
valid_source_points.push(transformed_source[src_idx]);
valid_target_points.push(target.points[*tgt_idx]);
corr_pairs.push((src_idx, *tgt_idx));
}
}
if valid_source_points.len() < 3 {
return Err(Error::Algorithm("Insufficient correspondences found".to_string()));
}
let delta_transform = compute_transformation(&valid_source_points, &valid_target_points)?;
current_transform = delta_transform * current_transform;
let current_mse = compute_mse(&valid_source_points, &valid_target_points);
let mse_change = (previous_mse - current_mse).abs();
if mse_change < convergence_threshold {
return Ok(ICPResult {
transformation: current_transform,
mse: current_mse,
iterations: iteration + 1,
converged: true,
correspondences: corr_pairs,
});
}
previous_mse = current_mse;
final_correspondences = corr_pairs;
}
let transformed_source: Vec<Point3f> = source
.points
.iter()
.map(|point| current_transform * point)
.collect();
let final_mse = if !final_correspondences.is_empty() {
let valid_source: Vec<Point3f> = final_correspondences
.iter()
.map(|(src_idx, _)| transformed_source[*src_idx])
.collect();
let valid_target: Vec<Point3f> = final_correspondences
.iter()
.map(|(_, tgt_idx)| target.points[*tgt_idx])
.collect();
compute_mse(&valid_source, &valid_target)
} else {
previous_mse
};
Ok(ICPResult {
transformation: current_transform,
mse: final_mse,
iterations: max_iters,
converged: false,
correspondences: final_correspondences,
})
}
#[deprecated(note = "Use icp instead which matches the standard API")]
pub fn icp_legacy(
source: &PointCloud<Point3f>,
target: &PointCloud<Point3f>,
max_iterations: usize,
threshold: f32,
) -> Result<(threecrate_core::Transform3D, f32)> {
let init = Isometry3::identity();
let result = icp_detailed(source, target, init, max_iterations, Some(threshold), 1e-6)?;
let transform = threecrate_core::Transform3D::from(result.transformation);
Ok((transform, result.mse))
}
pub fn icp_point_to_plane(
source: &PointCloud<Point3f>,
target: &PointCloud<Point3f>,
_target_normals: &[Vector3f],
init: Isometry3<f32>,
max_iters: usize,
) -> Result<ICPResult> {
icp_detailed(source, target, init, max_iters, None, 1e-6)
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::UnitQuaternion;
#[test]
fn test_icp_identity_transformation() {
let mut source = PointCloud::new();
let mut target = PointCloud::new();
for i in 0..10 {
let point = Point3f::new(i as f32, (i * 2) as f32, (i * 3) as f32);
source.push(point);
target.push(point);
}
let init = Isometry3::identity();
let result = icp_detailed(&source, &target, init, 10, None, 1e-6).unwrap();
assert!(result.converged);
assert!(result.mse < 1e-6);
assert!(result.iterations <= 3);
}
#[test]
fn test_icp_translation() {
let mut source = PointCloud::new();
let mut target = PointCloud::new();
let translation = Vector3f::new(1.0, 2.0, 3.0);
for i in 0..10 {
let source_point = Point3f::new(i as f32, (i * 2) as f32, (i * 3) as f32);
let target_point = source_point + translation;
source.push(source_point);
target.push(target_point);
}
let init = Isometry3::identity();
let result = icp_detailed(&source, &target, init, 50, None, 1e-6).unwrap();
let computed_translation = result.transformation.translation.vector;
assert!(computed_translation.magnitude() > 0.05, "Translation magnitude too small: {}", computed_translation.magnitude());
assert!(result.mse < 2.0); }
#[test]
fn test_icp_rotation() {
let mut source = PointCloud::new();
let mut target = PointCloud::new();
let rotation = UnitQuaternion::from_axis_angle(&Vector3f::z_axis(), std::f32::consts::FRAC_PI_4);
for i in 0..20 {
let source_point = Point3f::new(i as f32, (i % 5) as f32, 0.0);
let target_point = rotation * source_point;
source.push(source_point);
target.push(target_point);
}
let init = Isometry3::identity();
let result = icp_detailed(&source, &target, init, 100, None, 1e-6).unwrap();
assert!(result.mse < 1.0, "MSE too high: {}", result.mse);
}
#[test]
fn test_icp_insufficient_points() {
let mut source = PointCloud::new();
let mut target = PointCloud::new();
source.push(Point3f::new(0.0, 0.0, 0.0));
target.push(Point3f::new(1.0, 1.0, 1.0));
let init = Isometry3::identity();
let result = icp_detailed(&source, &target, init, 10, None, 1e-6);
assert!(result.is_err());
}
#[test]
fn test_icp_api_compatibility() {
let mut source = PointCloud::new();
let mut target = PointCloud::new();
for i in 0..5 {
let point = Point3f::new(i as f32, i as f32, 0.0);
source.push(point);
target.push(point + Vector3f::new(1.0, 0.0, 0.0));
}
let init = Isometry3::identity();
let transform = icp(&source, &target, init, 20);
assert!(transform.translation.vector.magnitude() > 0.5);
}
#[test]
fn test_correspondence_finding() {
let source = vec![
Point3f::new(0.0, 0.0, 0.0),
Point3f::new(1.0, 0.0, 0.0),
Point3f::new(0.0, 1.0, 0.0),
];
let target = vec![
Point3f::new(0.1, 0.1, 0.0),
Point3f::new(1.1, 0.1, 0.0),
Point3f::new(0.1, 1.1, 0.0),
];
let correspondences = find_correspondences(&source, &target, None);
assert_eq!(correspondences.len(), 3);
assert!(correspondences[0].is_some());
assert!(correspondences[1].is_some());
assert!(correspondences[2].is_some());
}
}