1use nalgebra::{Matrix2, Matrix2x3, Matrix3, Rotation2, SVector, Vector2, Vector3};
13use nearest_neighbor::KdTree;
14
15const HUBER_K: f64 = 1.345;
18
19fn huber_rho(e: f64, k: f64) -> f64 {
20 let k_sq = k * k;
21 if e <= k_sq {
22 e
23 } else {
24 2.0 * k * e.sqrt() - k_sq
25 }
26}
27
28fn huber_drho(e: f64, k: f64) -> f64 {
29 let k_sq = k * k;
30 if e <= k_sq {
31 1.0
32 } else {
33 k / e.sqrt()
34 }
35}
36
37#[derive(Copy, Clone, Debug)]
41pub struct Transform2D {
42 pub rot: Rotation2<f64>,
43 pub t: Vector2<f64>,
44}
45
46impl Transform2D {
47 pub fn from_param(param: &Vector3<f64>) -> Self {
49 let theta = param[2];
50 let cos = theta.cos();
51 let sin = theta.sin();
52 let rot = Rotation2::from_matrix_unchecked(Matrix2::new(cos, -sin, sin, cos));
53
54 let t = if theta.abs() < 1e-10 {
55 Vector2::new(param[0], param[1])
56 } else {
57 Vector2::new(
58 (sin * param[0] - (1.0 - cos) * param[1]) / theta,
59 ((1.0 - cos) * param[0] + sin * param[1]) / theta,
60 )
61 };
62 Self { rot, t }
63 }
64
65 pub fn identity() -> Self {
66 Self {
67 rot: Rotation2::identity(),
68 t: Vector2::zeros(),
69 }
70 }
71
72 pub fn transform(&self, p: &Vector2<f64>) -> Vector2<f64> {
73 self.rot * p + self.t
74 }
75
76 pub fn compose(&self, rhs: &Self) -> Self {
78 Self {
79 rot: self.rot * rhs.rot,
80 t: self.rot * rhs.t + self.t,
81 }
82 }
83}
84
85pub struct RobustICPResult {
89 pub transform: Transform2D,
90 pub iterations: usize,
91 pub final_error: f64,
92}
93
94pub struct RobustIcp2D<'a> {
96 kdtree: KdTree<'a, f64, 2>,
97 dst: &'a [SVector<f64, 2>],
98}
99
100impl<'a> RobustIcp2D<'a> {
101 pub fn new(dst: &'a [SVector<f64, 2>]) -> Self {
103 Self {
104 kdtree: KdTree::new(dst, 2),
105 dst,
106 }
107 }
108
109 pub fn estimate(
111 &self,
112 src: &[SVector<f64, 2>],
113 initial: &Transform2D,
114 max_iter: usize,
115 ) -> RobustICPResult {
116 let mut transform = *initial;
117 let mut prev_error = f64::MAX;
118 let mut iterations = 0;
119
120 for iter in 0..max_iter {
121 iterations = iter + 1;
122
123 let src_transformed: Vec<Vector2<f64>> =
125 src.iter().map(|sp| transform.transform(sp)).collect();
126
127 let nearest_dsts: Vec<Vector2<f64>> = src_transformed
129 .iter()
130 .map(|sp| {
131 let query = SVector::<f64, 2>::from([sp[0], sp[1]]);
132 let (index, _dist) = self.kdtree.search(&query);
133 self.dst[index.unwrap()]
134 })
135 .collect();
136
137 let error: f64 = src_transformed
139 .iter()
140 .zip(nearest_dsts.iter())
141 .map(|(s, d)| {
142 let r = s - d;
143 huber_rho(r.dot(&r), HUBER_K)
144 })
145 .sum();
146
147 if error > prev_error {
148 break;
149 }
150 prev_error = error;
151
152 let Some(delta) = weighted_gauss_newton(&transform, &src_transformed, &nearest_dsts)
154 else {
155 break;
156 };
157
158 if delta.dot(&delta) < 1e-12 {
159 break;
160 }
161
162 let dtransform = Transform2D::from_param(&delta);
163 transform = dtransform.compose(&transform);
164 }
165
166 RobustICPResult {
167 transform,
168 iterations,
169 final_error: prev_error,
170 }
171 }
172}
173
174fn jacobian(rot: &Rotation2<f64>, landmark: &Vector2<f64>) -> Matrix2x3<f64> {
176 let a = Vector2::new(-landmark[1], landmark[0]);
177 let r = rot.matrix();
178 let b = rot * a;
179 Matrix2x3::new(r[(0, 0)], r[(0, 1)], b[0], r[(1, 0)], r[(1, 1)], b[1])
180}
181
182fn calc_stddevs(residuals: &[Vector2<f64>]) -> Option<[f64; 2]> {
184 if residuals.is_empty() {
185 return None;
186 }
187 let ppf34 = 1.482602218505602; let mut result = [0.0; 2];
189
190 for j in 0..2 {
191 let mut vals: Vec<f64> = residuals.iter().map(|r| r[j]).collect();
192 let median = median_of(&mut vals)?;
193 let mut abs_devs: Vec<f64> = vals.iter().map(|v| (v - median).abs()).collect();
194 let mad = median_of(&mut abs_devs)?;
195 result[j] = ppf34 * mad;
196 }
197 Some(result)
198}
199
200fn median_of(data: &mut [f64]) -> Option<f64> {
201 let n = data.len();
202 if n == 0 {
203 return None;
204 }
205 let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
206 if n % 2 == 1 {
207 data.select_nth_unstable_by(n / 2, cmp);
208 Some(data[n / 2])
209 } else {
210 data.select_nth_unstable_by(n / 2 - 1, cmp);
211 let a = data[n / 2 - 1];
212 data.select_nth_unstable_by(n / 2, cmp);
213 let b = data[n / 2];
214 Some((a + b) / 2.0)
215 }
216}
217
218fn inverse3x3(m: &Matrix3<f64>) -> Option<Matrix3<f64>> {
220 let det = m[(0, 0)] * (m[(2, 2)] * m[(1, 1)] - m[(2, 1)] * m[(1, 2)])
221 - m[(1, 0)] * (m[(2, 2)] * m[(0, 1)] - m[(2, 1)] * m[(0, 2)])
222 + m[(2, 0)] * (m[(1, 2)] * m[(0, 1)] - m[(1, 1)] * m[(0, 2)]);
223 if det.abs() < 1e-30 {
224 return None;
225 }
226 #[rustfmt::skip]
227 let inv = Matrix3::new(
228 m[(2,2)]*m[(1,1)] - m[(2,1)]*m[(1,2)], -(m[(2,2)]*m[(0,1)] - m[(2,1)]*m[(0,2)]), m[(1,2)]*m[(0,1)] - m[(1,1)]*m[(0,2)],
229 -(m[(2,2)]*m[(1,0)] - m[(2,0)]*m[(1,2)]), m[(2,2)]*m[(0,0)] - m[(2,0)]*m[(0,2)], -(m[(1,2)]*m[(0,0)] - m[(1,0)]*m[(0,2)]),
230 m[(2,1)]*m[(1,0)] - m[(2,0)]*m[(1,1)], -(m[(2,1)]*m[(0,0)] - m[(2,0)]*m[(0,1)]), m[(1,1)]*m[(0,0)] - m[(1,0)]*m[(0,1)],
231 );
232 Some(inv / det)
233}
234
235fn weighted_gauss_newton(
237 transform: &Transform2D,
238 src: &[Vector2<f64>],
239 dst: &[Vector2<f64>],
240) -> Option<Vector3<f64>> {
241 if src.len() < 2 {
242 return None;
243 }
244
245 let residuals: Vec<Vector2<f64>> = src
246 .iter()
247 .zip(dst.iter())
248 .map(|(s, d)| transform.transform(s) - d)
249 .collect();
250
251 let stddevs = calc_stddevs(&residuals)?;
252
253 let mut jtr = Vector3::<f64>::zeros();
254 let mut jtj = Matrix3::<f64>::zeros();
255
256 for (s, r) in src.iter().zip(residuals.iter()) {
257 let j = jacobian(&transform.rot, s);
258 for (dim, jacobian_row) in j.row_iter().enumerate() {
259 if stddevs[dim] == 0.0 {
260 continue;
261 }
262 let g = 1.0 / stddevs[dim];
263 let r_dim = r[dim];
264 let w = huber_drho(r_dim * r_dim, HUBER_K);
265
266 jtr += w * g * jacobian_row.transpose() * r_dim;
267 jtj += w * g * jacobian_row.transpose() * jacobian_row;
268 }
269 }
270
271 let inv = inverse3x3(&jtj)?;
272 Some(-inv * jtr)
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 fn make_points(coords: &[[f64; 2]]) -> Vec<SVector<f64, 2>> {
280 coords.iter().map(|c| SVector::from(*c)).collect()
281 }
282
283 #[test]
284 fn test_robust_icp_identity() {
285 let dst = make_points(&[
286 [0.0, 0.0],
287 [1.0, 0.0],
288 [0.0, 1.0],
289 [1.0, 1.0],
290 [0.5, 0.5],
291 [0.3, 0.7],
292 [0.7, 0.3],
293 ]);
294 let src = dst.clone();
295
296 let icp = RobustIcp2D::new(&dst);
297 let result = icp.estimate(&src, &Transform2D::identity(), 20);
298
299 assert!(result.final_error < 1e-6);
300 }
301
302 #[test]
303 fn test_robust_icp_translation() {
304 let dst = make_points(&[
306 [-2.0, -2.0],
307 [-2.0, 0.0],
308 [-2.0, 2.0],
309 [0.0, -2.0],
310 [0.0, 0.0],
311 [0.0, 2.0],
312 [2.0, -2.0],
313 [2.0, 0.0],
314 [2.0, 2.0],
315 ]);
316
317 let true_param = Vector3::new(0.1, 0.05, 0.05);
318 let true_transform = Transform2D::from_param(&true_param);
319
320 let src: Vec<SVector<f64, 2>> = dst
321 .iter()
322 .map(|p| {
323 let tp = true_transform.transform(p);
324 SVector::from([tp[0], tp[1]])
325 })
326 .collect();
327
328 let icp = RobustIcp2D::new(&dst);
329 let result = icp.estimate(&src, &Transform2D::identity(), 50);
330
331 for (sp, dp) in src.iter().zip(dst.iter()) {
333 let transformed = result.transform.transform(sp);
334 let error = (transformed - dp).norm();
335 assert!(error < 0.3, "Point error too large: {}", error);
336 }
337 }
338
339 #[test]
340 fn test_huber_loss() {
341 assert_eq!(huber_rho(0.5, 1.0), 0.5);
343 let rho = huber_rho(4.0, 1.0);
345 assert!((rho - 3.0).abs() < 1e-10); }
347}