1use std::collections::HashMap;
15use std::f64::consts::PI;
16
17#[derive(Debug, Clone, Copy)]
19pub struct CorrelativeScanMatcherConfig {
20 pub linear_search_range: f64,
22 pub angular_search_range: f64,
24 pub linear_step: f64,
26 pub angular_step: f64,
28 pub grid_resolution: f64,
30}
31
32impl Default for CorrelativeScanMatcherConfig {
33 fn default() -> Self {
34 Self {
35 linear_search_range: 1.0,
36 angular_search_range: 0.2,
37 linear_step: 0.1,
38 angular_step: 0.02,
39 grid_resolution: 0.05,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct ScanMatchResult {
47 pub x: f64,
48 pub y: f64,
49 pub yaw: f64,
50 pub score: f64,
51 pub converged: bool,
52}
53
54pub fn correlative_scan_match(
56 reference_x: &[f64],
57 reference_y: &[f64],
58 query_x: &[f64],
59 query_y: &[f64],
60 initial_pose: (f64, f64, f64),
61 config: &CorrelativeScanMatcherConfig,
62) -> ScanMatchResult {
63 if reference_x.len() != reference_y.len()
64 || query_x.len() != query_y.len()
65 || reference_x.is_empty()
66 || query_x.is_empty()
67 || config.linear_step <= 0.0
68 || config.angular_step <= 0.0
69 || config.grid_resolution <= 0.0
70 {
71 return ScanMatchResult {
72 x: initial_pose.0,
73 y: initial_pose.1,
74 yaw: initial_pose.2,
75 score: 0.0,
76 converged: false,
77 };
78 }
79
80 let grid = build_lookup_table(reference_x, reference_y, config.grid_resolution);
81 let linear_offsets = enumerate_offsets(config.linear_search_range, config.linear_step);
82 let angular_offsets = enumerate_offsets(config.angular_search_range, config.angular_step);
83
84 let mut best = ScanMatchResult {
85 x: initial_pose.0,
86 y: initial_pose.1,
87 yaw: normalize_angle(initial_pose.2),
88 score: -1.0,
89 converged: false,
90 };
91 let mut best_penalty = f64::INFINITY;
92
93 for dx in &linear_offsets {
94 for dy in &linear_offsets {
95 for dyaw in &angular_offsets {
96 let candidate = (
97 initial_pose.0 + dx,
98 initial_pose.1 + dy,
99 normalize_angle(initial_pose.2 + dyaw),
100 );
101 let score =
102 score_candidate(&grid, query_x, query_y, candidate, config.grid_resolution);
103 let penalty = dx * dx + dy * dy + dyaw * dyaw;
104
105 if score > best.score || (score == best.score && penalty < best_penalty) {
106 best = ScanMatchResult {
107 x: candidate.0,
108 y: candidate.1,
109 yaw: candidate.2,
110 score,
111 converged: score > 0.0,
112 };
113 best_penalty = penalty;
114 }
115 }
116 }
117 }
118
119 best
120}
121
122fn enumerate_offsets(range: f64, step: f64) -> Vec<f64> {
123 let n_steps = (range / step).round() as i32;
124 (-n_steps..=n_steps)
125 .map(|index| index as f64 * step)
126 .collect()
127}
128
129fn build_lookup_table(
130 reference_x: &[f64],
131 reference_y: &[f64],
132 resolution: f64,
133) -> HashMap<(i32, i32), f64> {
134 let mut grid: HashMap<(i32, i32), f64> = HashMap::new();
135 let sigma = resolution;
136 let cutoff_radius = (3.0 * sigma / resolution).ceil() as i32;
137 let inv_two_sigma_sq = 0.5 / (sigma * sigma);
138
139 for (&x, &y) in reference_x.iter().zip(reference_y.iter()) {
140 let (cx, cy) = cell_index(x, y, resolution);
141 for ix in (cx - cutoff_radius)..=(cx + cutoff_radius) {
142 for iy in (cy - cutoff_radius)..=(cy + cutoff_radius) {
143 let gx = ix as f64 * resolution;
144 let gy = iy as f64 * resolution;
145 let squared_distance = (gx - x).powi(2) + (gy - y).powi(2);
146 let weight = (-squared_distance * inv_two_sigma_sq).exp();
147 if weight < 1.0e-6 {
148 continue;
149 }
150
151 grid.entry((ix, iy))
152 .and_modify(|score| *score = (*score).max(weight))
153 .or_insert(weight);
154 }
155 }
156 }
157
158 grid
159}
160
161fn score_candidate(
162 grid: &HashMap<(i32, i32), f64>,
163 query_x: &[f64],
164 query_y: &[f64],
165 pose: (f64, f64, f64),
166 resolution: f64,
167) -> f64 {
168 let cos_yaw = pose.2.cos();
169 let sin_yaw = pose.2.sin();
170 let mut score = 0.0;
171
172 for (&x, &y) in query_x.iter().zip(query_y.iter()) {
173 let world_x = cos_yaw * x - sin_yaw * y + pose.0;
174 let world_y = sin_yaw * x + cos_yaw * y + pose.1;
175 let cell = cell_index(world_x, world_y, resolution);
176 score += grid.get(&cell).copied().unwrap_or(0.0);
177 }
178
179 score
180}
181
182fn cell_index(x: f64, y: f64, resolution: f64) -> (i32, i32) {
183 (
184 (x / resolution).round() as i32,
185 (y / resolution).round() as i32,
186 )
187}
188
189fn normalize_angle(mut angle: f64) -> f64 {
190 while angle > PI {
191 angle -= 2.0 * PI;
192 }
193 while angle < -PI {
194 angle += 2.0 * PI;
195 }
196 angle
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 fn inverse_transform_points(
204 points: &[(f64, f64)],
205 pose: (f64, f64, f64),
206 ) -> (Vec<f64>, Vec<f64>) {
207 let cos_yaw = pose.2.cos();
208 let sin_yaw = pose.2.sin();
209 let mut xs = Vec::with_capacity(points.len());
210 let mut ys = Vec::with_capacity(points.len());
211
212 for &(x, y) in points {
213 let dx = x - pose.0;
214 let dy = y - pose.1;
215 xs.push(cos_yaw * dx + sin_yaw * dy);
216 ys.push(-sin_yaw * dx + cos_yaw * dy);
217 }
218
219 (xs, ys)
220 }
221
222 fn fixture_points() -> Vec<(f64, f64)> {
223 vec![
224 (0.0, 0.0),
225 (1.0, 0.0),
226 (2.0, 0.0),
227 (0.0, 1.0),
228 (0.0, 2.0),
229 (1.0, 1.0),
230 (1.5, 2.0),
231 ]
232 }
233
234 #[test]
235 fn test_identity_match() {
236 let points = fixture_points();
237 let (reference_x, reference_y): (Vec<_>, Vec<_>) = points.iter().copied().unzip();
238 let config = CorrelativeScanMatcherConfig::default();
239
240 let result = correlative_scan_match(
241 &reference_x,
242 &reference_y,
243 &reference_x,
244 &reference_y,
245 (0.0, 0.0, 0.0),
246 &config,
247 );
248
249 assert!(result.converged);
250 assert_eq!(result.x, 0.0);
251 assert_eq!(result.y, 0.0);
252 assert_eq!(result.yaw, 0.0);
253 assert_eq!(result.score, points.len() as f64);
254 }
255
256 #[test]
257 fn test_known_translation_recovery() {
258 let points = fixture_points();
259 let (reference_x, reference_y): (Vec<_>, Vec<_>) = points.iter().copied().unzip();
260 let true_pose = (0.4, -0.3, 0.0);
261 let (query_x, query_y) = inverse_transform_points(&points, true_pose);
262 let config = CorrelativeScanMatcherConfig {
263 linear_search_range: 0.6,
264 angular_search_range: 0.1,
265 linear_step: 0.1,
266 angular_step: 0.05,
267 grid_resolution: 0.05,
268 };
269
270 let result = correlative_scan_match(
271 &reference_x,
272 &reference_y,
273 &query_x,
274 &query_y,
275 (0.0, 0.0, 0.0),
276 &config,
277 );
278
279 assert!(result.converged);
280 assert!((result.x - true_pose.0).abs() < 1.0e-9);
281 assert!((result.y - true_pose.1).abs() < 1.0e-9);
282 assert!(result.yaw.abs() < 1.0e-9);
283 }
284
285 #[test]
286 fn test_known_rotation_recovery() {
287 let points = fixture_points();
288 let (reference_x, reference_y): (Vec<_>, Vec<_>) = points.iter().copied().unzip();
289 let true_pose = (0.0, 0.0, 0.16);
290 let (query_x, query_y) = inverse_transform_points(&points, true_pose);
291 let config = CorrelativeScanMatcherConfig {
292 linear_search_range: 0.2,
293 angular_search_range: 0.3,
294 linear_step: 0.1,
295 angular_step: 0.02,
296 grid_resolution: 0.05,
297 };
298
299 let result = correlative_scan_match(
300 &reference_x,
301 &reference_y,
302 &query_x,
303 &query_y,
304 (0.0, 0.0, 0.0),
305 &config,
306 );
307
308 assert!(result.converged);
309 assert!(result.x.abs() < 1.0e-9);
310 assert!(result.y.abs() < 1.0e-9);
311 assert!((result.yaw - true_pose.2).abs() < 1.0e-9);
312 }
313
314 #[test]
315 fn test_lookup_table_prefers_exact_alignment() {
316 let points = fixture_points();
317 let (reference_x, reference_y): (Vec<_>, Vec<_>) = points.iter().copied().unzip();
318 let resolution = 0.05;
319 let grid = build_lookup_table(&reference_x, &reference_y, resolution);
320
321 let aligned = score_candidate(
322 &grid,
323 &reference_x,
324 &reference_y,
325 (0.0, 0.0, 0.0),
326 resolution,
327 );
328 let translated = score_candidate(
329 &grid,
330 &reference_x,
331 &reference_y,
332 (0.15, 0.0, 0.0),
333 resolution,
334 );
335 let rotated = score_candidate(
336 &grid,
337 &reference_x,
338 &reference_y,
339 (0.0, 0.0, 0.15),
340 resolution,
341 );
342
343 assert!(aligned > translated);
344 assert!(aligned > rotated);
345 }
346}