1use super::{delta_from_solution, update_magnitude, GicpRegistration};
2use crate::RegistrationResult;
3use spatialrust_core::{
4 HasPositions3, PointCloud, SpatialError, SpatialResult, TransferDirection, TransferStats,
5};
6use spatialrust_gpu::{GpuGicpEquations, GpuGicpInput, WgpuRuntime};
7use spatialrust_math::{solve_linear_system, Isometry3, LeastSquaresResult};
8use spatialrust_search::KdTree;
9
10#[derive(Clone, Debug, PartialEq)]
12pub struct GpuGicpIteration {
13 pub iteration: usize,
15 pub correspondences: u32,
17 pub evaluated_correspondences: u32,
19 pub fitness: f64,
21 pub update_magnitude: f64,
23}
24
25#[derive(Clone, Debug, PartialEq)]
27pub struct GpuGicpReductionDiagnostics {
28 pub result: RegistrationResult,
30 pub history: Vec<GpuGicpIteration>,
32 pub transfers: TransferStats,
34 pub evaluation_batches: usize,
36 pub covariance_batches: usize,
38}
39
40fn invalid() -> SpatialError {
41 SpatialError::InvalidArgument("GPU GICP reduction requires at least one iteration, minimum six correspondences, finite positive covariance epsilon/distance square, finite nonnegative stopping thresholds and a finite unit initial pose".to_owned())
42}
43fn valid_pose(pose: Isometry3<f32>) -> bool {
44 let q = pose.rotation();
45 let t = pose.translation();
46 let norm = f64::from(q.x).hypot(f64::from(q.y)).hypot(f64::from(q.z)).hypot(f64::from(q.w));
47 t.x.is_finite()
48 && t.y.is_finite()
49 && t.z.is_finite()
50 && norm.is_finite()
51 && (norm - 1.0).abs() <= 1e-4
52}
53fn record(total: &mut TransferStats, part: TransferStats) {
54 total.record(TransferDirection::HostToDevice, part.host_to_device_bytes());
55 total.record(TransferDirection::DeviceToHost, part.device_to_host_bytes());
56}
57fn fitness(equations: &GpuGicpEquations) -> f64 {
58 if equations.fitness_correspondences == 0 {
59 f64::MAX
60 } else {
61 equations.squared_error_sum / f64::from(equations.fitness_correspondences)
62 }
63}
64
65#[derive(Clone, Copy)]
66enum CovariancePreparation {
67 Legacy,
68 ResidentKnn,
69 ResidentRadius(u32),
70 ResidentGrid(u32),
71}
72
73type TreeBuilder = fn(&[f32], &[f32], &[f32]) -> KdTree;
74
75#[path = "gicp_covariance_preparation.rs"]
76mod covariance_preparation;
77
78impl GicpRegistration {
79 pub fn align_with_gpu_reduction(
92 &self,
93 runtime: &WgpuRuntime,
94 source: &PointCloud,
95 target: &PointCloud,
96 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
97 self.align_reduction_impl(runtime, source, target, CovariancePreparation::Legacy)
98 }
99
100 pub fn align_with_gpu_resident_covariances(
110 &self,
111 runtime: &WgpuRuntime,
112 source: &PointCloud,
113 target: &PointCloud,
114 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
115 self.align_reduction_impl(runtime, source, target, CovariancePreparation::ResidentKnn)
116 }
117
118 pub fn align_with_gpu_resident_radius_covariances(
130 &self,
131 runtime: &WgpuRuntime,
132 source: &PointCloud,
133 target: &PointCloud,
134 neighbor_capacity: u32,
135 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
136 self.align_reduction_impl(
137 runtime,
138 source,
139 target,
140 CovariancePreparation::ResidentRadius(neighbor_capacity),
141 )
142 }
143
144 pub fn align_with_gpu_resident_grid_covariances(
154 &self,
155 runtime: &WgpuRuntime,
156 source: &PointCloud,
157 target: &PointCloud,
158 neighbor_capacity: u32,
159 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
160 self.align_reduction_impl(
161 runtime,
162 source,
163 target,
164 CovariancePreparation::ResidentGrid(neighbor_capacity),
165 )
166 }
167
168 fn align_reduction_impl(
169 &self,
170 runtime: &WgpuRuntime,
171 source: &PointCloud,
172 target: &PointCloud,
173 preparation: CovariancePreparation,
174 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
175 self.align_reduction_with_tree_builder(
176 runtime,
177 source,
178 target,
179 preparation,
180 KdTree::from_slices_parallel,
181 )
182 }
183
184 fn align_reduction_with_tree_builder(
185 &self,
186 runtime: &WgpuRuntime,
187 source: &PointCloud,
188 target: &PointCloud,
189 preparation: CovariancePreparation,
190 build_tree: TreeBuilder,
191 ) -> SpatialResult<GpuGicpReductionDiagnostics> {
192 self.validate_inputs(source, target)?;
193 let c = self.config;
194 let distance_squared = c.max_correspondence_distance * c.max_correspondence_distance;
195 let epsilon = c.epsilon as f32;
196 if matches!(preparation, CovariancePreparation::ResidentKnn)
197 && (c.covariance_radius.is_some()
198 || c.k_neighbors > spatialrust_gpu::MAX_GPU_K_NEIGHBORS as usize
199 || !(1e-4..=1.0).contains(&epsilon))
200 {
201 return Err(SpatialError::InvalidArgument("resident GPU GICP covariances require covariance_radius=None, k_neighbors=3..=64 and epsilon (as f32) in 1e-4..=1".to_owned()));
202 }
203 if let CovariancePreparation::ResidentRadius(capacity)
204 | CovariancePreparation::ResidentGrid(capacity) = preparation
205 {
206 if capacity == 0
207 || !(1e-4..=1.0).contains(&epsilon)
208 || c.covariance_radius
209 .map_or(true, |r| !r.is_finite() || r <= 0.0 || !(r * r).is_normal())
210 {
211 return Err(SpatialError::InvalidArgument("resident GPU GICP radius covariances require a positive capacity including self, covariance_radius=Some(finite positive radius) with a normal f32 square and epsilon (as f32) in 1e-4..=1".to_owned()));
212 }
213 }
214 if c.max_iterations == 0
215 || c.min_correspondences < 6
216 || !c.epsilon.is_finite()
217 || epsilon <= 0.0
218 || !epsilon.is_finite()
219 || !c.max_correspondence_distance.is_finite()
220 || c.max_correspondence_distance <= 0.0
221 || !distance_squared.is_finite()
222 || distance_squared == 0.0
223 || !c.transformation_epsilon.is_finite()
224 || c.transformation_epsilon < 0.0
225 || !c.fitness_epsilon.is_finite()
226 || c.fitness_epsilon < 0.0
227 || !valid_pose(c.initial_guess)
228 {
229 return Err(invalid());
230 }
231 let (sx, sy, sz) = source.positions3()?;
232 let (tx, ty, tz) = target.positions3()?;
233 if sx.iter().chain(sy).chain(sz).chain(tx).chain(ty).chain(tz).any(|v| !v.is_finite()) {
234 return Err(SpatialError::InvalidArgument("GPU GICP requires finite XYZ".to_owned()));
235 }
236 let tree = build_tree(tx, ty, tz);
237 let mut transfers = TransferStats::default();
238 let mut covariance_batches = 0;
239 let input = if let CovariancePreparation::ResidentGrid(capacity) = preparation {
240 covariance_batches = 2;
241 GpuGicpInput::upload_with_gpu_grid_covariances(
242 runtime,
243 (sx, sy, sz),
244 &tree,
245 c.covariance_radius.unwrap(),
246 capacity,
247 epsilon,
248 )?
249 } else if !matches!(preparation, CovariancePreparation::Legacy) {
250 let source_tree = build_tree(sx, sy, sz);
251 covariance_batches = 2;
252 match preparation {
253 CovariancePreparation::ResidentKnn => {
254 let k = u32::try_from(c.k_neighbors).map_err(|_| invalid())?;
255 GpuGicpInput::upload_with_gpu_covariances(
256 runtime,
257 &source_tree,
258 &tree,
259 k,
260 epsilon,
261 )?
262 }
263 CovariancePreparation::ResidentRadius(capacity) => {
264 GpuGicpInput::upload_with_gpu_radius_covariances(
265 runtime,
266 &source_tree,
267 &tree,
268 c.covariance_radius.unwrap(),
269 capacity,
270 epsilon,
271 )?
272 }
273 CovariancePreparation::Legacy | CovariancePreparation::ResidentGrid(_) => {
274 unreachable!()
275 }
276 }
277 } else {
278 let mut prepare = |x: &[f32],
279 y: &[f32],
280 z: &[f32],
281 tree: Option<&KdTree>|
282 -> SpatialResult<Vec<[f32; 6]>> {
283 if let Some(radius) = c.covariance_radius {
284 let (covariance, receipt) =
285 spatialrust_gpu::estimate_plane_covariances_grid_gpu_with_receipt(
286 runtime, x, y, z, radius, epsilon,
287 )?;
288 record(&mut transfers, receipt);
289 covariance_batches += 1;
290 Ok(covariance)
291 } else {
292 let built;
293 let tree = if let Some(tree) = tree {
294 tree
295 } else {
296 built = build_tree(x, y, z);
297 &built
298 };
299 Ok(covariance_preparation::prepare(x, y, z, tree, c.k_neighbors, c.epsilon))
300 }
301 };
302 let source_cov = prepare(sx, sy, sz, None)?;
303 let target_cov = prepare(tx, ty, tz, Some(&tree))?;
304 let positions: Vec<_> = (0..source.len()).map(|i| [sx[i], sy[i], sz[i]]).collect();
305 GpuGicpInput::upload(runtime, &positions, &source_cov, &tree, &target_cov)?
306 };
307 record(&mut transfers, input.upload_transfers());
308 let mut evaluation_batches = 0;
309 let mut evaluate = |pose| -> SpatialResult<GpuGicpEquations> {
310 let equations = input
311 .equations_resident(runtime, pose, c.max_correspondence_distance)?
312 .download(runtime)?;
313 record(&mut transfers, equations.transfers);
314 evaluation_batches += 1;
315 Ok(equations)
316 };
317 let mut transform = c.initial_guess;
318 let mut equations = evaluate(transform)?;
319 let mut converged = false;
320 let mut history = Vec::new();
321 for iteration in 1..=c.max_iterations {
322 let count = equations.correspondences;
323 if (count as usize) < c.min_correspondences {
324 return Err(SpatialError::InvalidArgument(format!(
325 "GICP found only {count} correspondences, minimum is {}",
326 c.min_correspondences
327 )));
328 }
329 let rows = equations.hessian.iter().map(|row| row.to_vec()).collect();
330 let gradient = equations.gradient.iter().map(|g| -g).collect();
331 let LeastSquaresResult::Solved(solution) = solve_linear_system(rows, gradient) else {
332 return Err(SpatialError::InvalidArgument(
333 "GICP normal equations were singular".to_owned(),
334 ));
335 };
336 let solution: [f64; 6] = solution.try_into().map_err(|_| invalid())?;
337 let world = equations.world_step(solution)?;
338 transform = delta_from_solution(&world).compose(transform);
339 if !valid_pose(transform) {
340 return Err(invalid());
341 }
342 equations = evaluate(transform)?;
343 let fitness = fitness(&equations);
344 let update_magnitude = update_magnitude(&world);
345 history.push(GpuGicpIteration {
346 iteration,
347 correspondences: count,
348 evaluated_correspondences: equations.fitness_correspondences,
349 fitness,
350 update_magnitude,
351 });
352 if update_magnitude < c.transformation_epsilon || fitness < c.fitness_epsilon {
353 converged = true;
354 break;
355 }
356 }
357 Ok(GpuGicpReductionDiagnostics {
358 result: RegistrationResult {
359 transform,
360 fitness: fitness(&equations),
361 iterations: history.len(),
362 converged,
363 },
364 history,
365 transfers,
366 evaluation_batches,
367 covariance_batches,
368 })
369 }
370}
371
372#[cfg(all(test, not(target_arch = "wasm32")))]
373#[path = "gicp_tree_build_tests.rs"]
374mod tree_build_tests;