spatialrust_registration/
icp_reduction.rs1use super::{
2 transform_delta_magnitudes, validate_icp_transform, IcpDiagnostics, IcpIteration,
3 IcpRegistration, IcpStopReason,
4};
5use crate::{kabsch::estimate_rigid_transform_from_moments, RegistrationResult};
6use spatialrust_core::{
7 HasPositions3, PointCloud, SpatialError, SpatialResult, TransferDirection, TransferStats,
8};
9use spatialrust_gpu::{GpuIcpInput, GpuIcpTrimPrecision, GpuIcpTrimmedStatistics, WgpuRuntime};
10use spatialrust_search::KdTree;
11
12#[derive(Clone, Debug, PartialEq)]
14pub struct GpuIcpReductionDiagnostics {
15 pub diagnostics: IcpDiagnostics,
17 pub transfers: TransferStats,
19 pub evaluation_batches: usize,
21}
22
23impl IcpRegistration {
24 #[cfg(feature = "register-icp-gpu")]
34 pub fn align_with_gpu_reduction(
35 &self,
36 runtime: &WgpuRuntime,
37 source: &PointCloud,
38 target: &PointCloud,
39 ) -> SpatialResult<GpuIcpReductionDiagnostics> {
40 self.align_reduction_impl(runtime, source, target, false, GpuIcpTrimPrecision::DoubleSingle)
41 }
42
43 #[cfg(feature = "register-icp-gpu")]
51 pub fn align_with_gpu_trimmed_reduction(
52 &self,
53 runtime: &WgpuRuntime,
54 source: &PointCloud,
55 target: &PointCloud,
56 ) -> SpatialResult<GpuIcpReductionDiagnostics> {
57 self.align_with_gpu_trimmed_reduction_with_precision(
58 runtime,
59 source,
60 target,
61 GpuIcpTrimPrecision::DoubleSingle,
62 )
63 }
64
65 #[cfg(feature = "register-icp-gpu")]
71 pub fn align_with_gpu_trimmed_reduction_with_precision(
72 &self,
73 runtime: &WgpuRuntime,
74 source: &PointCloud,
75 target: &PointCloud,
76 precision: GpuIcpTrimPrecision,
77 ) -> SpatialResult<GpuIcpReductionDiagnostics> {
78 self.align_reduction_impl(runtime, source, target, true, precision)
79 }
80
81 fn align_reduction_impl(
82 &self,
83 runtime: &WgpuRuntime,
84 source: &PointCloud,
85 target: &PointCloud,
86 trimmed: bool,
87 precision: GpuIcpTrimPrecision,
88 ) -> SpatialResult<GpuIcpReductionDiagnostics> {
89 self.validate_inputs(source, target)?;
90 if !trimmed && self.trim_fraction != 1.0 {
91 return Err(SpatialError::InvalidArgument("GPU ICP reduction requires trim_fraction=1; use the explicit correspondence API for trimming".to_owned()));
92 }
93 let (sx, sy, sz) = source.positions3()?;
94 let (tx, ty, tz) = target.positions3()?;
95 let source_positions: Vec<_> = (0..source.len()).map(|i| [sx[i], sy[i], sz[i]]).collect();
96 let tree = KdTree::from_slices(tx, ty, tz);
97 let input = GpuIcpInput::upload(runtime, &source_positions, &tree)?;
98 let mut transfers = input.upload_transfers();
99 let mut evaluation_batches = 0;
100 let mut evaluate = |pose| -> SpatialResult<GpuIcpTrimmedStatistics> {
101 let statistics = input
102 .trimmed_moments_resident_with_precision(
103 runtime,
104 pose,
105 self.config.max_correspondence_distance,
106 self.trim_fraction,
107 precision,
108 )?
109 .download(runtime)?;
110 transfers.record(
111 TransferDirection::DeviceToHost,
112 statistics.moments.transfers.device_to_host_bytes(),
113 );
114 evaluation_batches += 1;
115 Ok(statistics)
116 };
117 let mut transform = self.config.initial_guess;
118 let mut moments = evaluate(transform)?;
119 let mut history = Vec::new();
120 let mut previous_fitness = None;
121 let mut converged = false;
122 let mut stop_reason = IcpStopReason::IterationLimit;
123 for iteration in 1..=self.config.max_iterations {
124 let evaluated = moments.evaluated_correspondences as usize;
125 let count = moments.moments.correspondences as usize;
126 if evaluated < self.config.min_correspondences {
127 return Err(SpatialError::InvalidArgument(format!(
128 "ICP found only {evaluated} correspondences, minimum is {}",
129 self.config.min_correspondences
130 )));
131 }
132 if count < self.config.min_correspondences {
133 return Err(SpatialError::InvalidArgument(format!(
134 "trim_fraction retains only {count} of {evaluated} correspondences, minimum is {}",
135 self.config.min_correspondences
136 )));
137 }
138 let delta = estimate_rigid_transform_from_moments(
139 count,
140 moments.moments.source_centroid,
141 moments.moments.target_centroid,
142 moments.moments.cross_covariance,
143 )
144 .ok_or_else(|| {
145 SpatialError::InvalidArgument("ICP failed to estimate a rigid transform".to_owned())
146 })?;
147 validate_icp_transform(delta)?;
148 transform = delta.compose(transform);
149 validate_icp_transform(transform)?;
150 moments = evaluate(transform)?;
151 let fitness = fitness(&moments);
152 let (translation_delta, rotation_delta_radians) = transform_delta_magnitudes(delta);
153 history.push(IcpIteration {
154 iteration,
155 correspondences: count,
156 evaluated_correspondences: moments.evaluated_correspondences as usize,
157 fitness,
158 fitness_change: previous_fitness.map(|previous| previous - fitness),
159 translation_delta,
160 rotation_delta_radians,
161 });
162 previous_fitness = Some(fitness);
163 if fitness < self.criteria.fitness_epsilon {
164 converged = true;
165 stop_reason = IcpStopReason::FitnessThreshold;
166 break;
167 }
168 if translation_delta < self.criteria.translation_epsilon
169 && rotation_delta_radians < self.criteria.rotation_epsilon
170 {
171 converged = true;
172 stop_reason = IcpStopReason::TransformThreshold;
173 break;
174 }
175 }
176 Ok(GpuIcpReductionDiagnostics {
177 diagnostics: IcpDiagnostics {
178 result: RegistrationResult {
179 transform,
180 fitness: fitness(&moments),
181 iterations: history.len(),
182 converged,
183 },
184 history,
185 stop_reason,
186 },
187 transfers,
188 evaluation_batches,
189 })
190 }
191}
192
193fn fitness(statistics: &GpuIcpTrimmedStatistics) -> f64 {
194 if statistics.evaluated_correspondences == 0 {
195 f64::MAX
196 } else {
197 statistics.moments.squared_error_sum / f64::from(statistics.evaluated_correspondences)
198 }
199}