1use spatialrust_image::ImageView;
4use spatialrust_math::{Vec2, Vec3};
5
6use crate::{
7 estimate_essential_ransac, recover_relative_pose, solve_pnp_ransac, track_points_lucas_kanade,
8 AbsolutePose, CameraMatrix3, Keypoint2, LucasKanadeOptions, ObjectImageCorrespondence,
9 PointCorrespondence2, RelativePose, RobustEstimationOptions, VisionError, VisionResult,
10};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct GridSelectionOptions {
15 pub cell_width: usize,
17 pub cell_height: usize,
19 pub max_per_cell: usize,
21}
22
23impl Default for GridSelectionOptions {
24 fn default() -> Self {
25 Self { cell_width: 32, cell_height: 32, max_per_cell: 4 }
26 }
27}
28
29pub fn select_keypoints_grid(
34 keypoints: &[Keypoint2],
35 width: usize,
36 height: usize,
37 options: GridSelectionOptions,
38) -> VisionResult<Vec<Keypoint2>> {
39 if width == 0
40 || height == 0
41 || options.cell_width == 0
42 || options.cell_height == 0
43 || options.max_per_cell == 0
44 {
45 return Err(VisionError::InvalidParameter(
46 "grid selection dimensions and limits must be positive".into(),
47 ));
48 }
49 let columns = width.div_ceil(options.cell_width);
50 let rows = height.div_ceil(options.cell_height);
51 let mut cells = vec![Vec::<(usize, Keypoint2)>::new(); columns * rows];
52 for (index, &keypoint) in keypoints.iter().enumerate() {
53 if keypoint.x() < 0.0
54 || keypoint.y() < 0.0
55 || keypoint.x() >= width as f32
56 || keypoint.y() >= height as f32
57 {
58 continue;
59 }
60 let column = keypoint.x() as usize / options.cell_width;
61 let row = keypoint.y() as usize / options.cell_height;
62 cells[row * columns + column].push((index, keypoint));
63 }
64 let mut selected = Vec::new();
65 for cell in &mut cells {
66 cell.sort_by(|(left_index, left), (right_index, right)| {
67 right.response().total_cmp(&left.response()).then(left_index.cmp(right_index))
68 });
69 selected.extend(cell.iter().take(options.max_per_cell).map(|(_, point)| *point));
70 }
71 Ok(selected)
72}
73
74#[derive(Clone, Copy, Debug, PartialEq)]
76pub struct RobustTrackOptions {
77 pub lucas_kanade: LucasKanadeOptions,
79 pub max_forward_backward_error: f64,
81}
82
83impl Default for RobustTrackOptions {
84 fn default() -> Self {
85 Self { lucas_kanade: LucasKanadeOptions::default(), max_forward_backward_error: 1.0 }
86 }
87}
88
89#[derive(Clone, Debug, PartialEq)]
91pub struct RobustTracks {
92 next_points: Vec<Vec2<f64>>,
93 status: Vec<bool>,
94 forward_backward_errors: Vec<f64>,
95}
96
97impl RobustTracks {
98 pub fn next_points(&self) -> &[Vec2<f64>] {
100 &self.next_points
101 }
102 pub fn status(&self) -> &[bool] {
104 &self.status
105 }
106 pub fn forward_backward_errors(&self) -> &[f64] {
108 &self.forward_backward_errors
109 }
110 pub fn accepted_count(&self) -> usize {
112 self.status.iter().filter(|&&value| value).count()
113 }
114}
115
116pub fn track_points_forward_backward<T: crate::PixelComponent>(
118 previous: ImageView<'_, T, 1>,
119 next: ImageView<'_, T, 1>,
120 points: &[Vec2<f64>],
121 options: RobustTrackOptions,
122) -> VisionResult<RobustTracks> {
123 if !options.max_forward_backward_error.is_finite() || options.max_forward_backward_error < 0.0 {
124 return Err(VisionError::InvalidParameter(
125 "forward/backward threshold must be finite and non-negative".into(),
126 ));
127 }
128 let forward = track_points_lucas_kanade(previous, next, points, options.lucas_kanade)?;
129 let backward =
130 track_points_lucas_kanade(next, previous, forward.next_points(), options.lucas_kanade)?;
131 let mut status = Vec::with_capacity(points.len());
132 let mut errors = Vec::with_capacity(points.len());
133 for (index, point) in points.iter().enumerate() {
134 let valid = forward.status()[index] && backward.status()[index];
135 let error = if valid {
136 let dx = backward.next_points()[index].x - point.x;
137 let dy = backward.next_points()[index].y - point.y;
138 dx.hypot(dy)
139 } else {
140 f64::INFINITY
141 };
142 status.push(valid && error <= options.max_forward_backward_error);
143 errors.push(error);
144 }
145 Ok(RobustTracks {
146 next_points: forward.next_points().to_vec(),
147 status,
148 forward_backward_errors: errors,
149 })
150}
151
152#[derive(Clone, Debug, PartialEq)]
154pub struct MonocularOdometryEstimate {
155 pub pose: RelativePose,
157 pub inliers: Vec<bool>,
159 pub positive_depth_count: usize,
161}
162
163pub fn estimate_monocular_odometry(
165 correspondences: &[PointCorrespondence2],
166 camera: CameraMatrix3,
167 options: RobustEstimationOptions,
168) -> VisionResult<MonocularOdometryEstimate> {
169 let estimate = estimate_essential_ransac(correspondences, camera, camera, options)?;
170 let inlier_pairs = correspondences
171 .iter()
172 .zip(estimate.inliers())
173 .filter_map(|(&pair, &inlier)| inlier.then_some(pair))
174 .collect::<Vec<_>>();
175 if inlier_pairs.len() < 8 {
176 return Err(VisionError::InvalidParameter(
177 "monocular odometry requires at least eight essential inliers".into(),
178 ));
179 }
180 let recovered = recover_relative_pose(*estimate.model(), &inlier_pairs, camera, camera)?;
181 Ok(MonocularOdometryEstimate {
182 pose: recovered.pose(),
183 inliers: estimate.inliers().to_vec(),
184 positive_depth_count: recovered.positive_depth_count(),
185 })
186}
187
188#[derive(Clone, Copy, Debug, PartialEq)]
190pub struct RgbdOdometryOptions {
191 pub depth_scale: f64,
193 pub min_depth: f64,
195 pub max_depth: f64,
197 pub robust: RobustEstimationOptions,
199}
200
201impl Default for RgbdOdometryOptions {
202 fn default() -> Self {
203 Self {
204 depth_scale: 1.0,
205 min_depth: 0.1,
206 max_depth: 100.0,
207 robust: RobustEstimationOptions { threshold: 1.0, ..Default::default() },
208 }
209 }
210}
211
212#[derive(Clone, Debug, PartialEq)]
214pub struct RgbdOdometryEstimate {
215 pub pose: AbsolutePose,
217 pub inliers: Vec<bool>,
219 pub rejected_depth_count: usize,
221}
222
223pub fn estimate_rgbd_odometry<T: crate::PixelComponent>(
225 previous_depth: ImageView<'_, T, 1>,
226 correspondences: &[PointCorrespondence2],
227 camera: CameraMatrix3,
228 options: RgbdOdometryOptions,
229) -> VisionResult<RgbdOdometryEstimate> {
230 if !options.depth_scale.is_finite()
231 || options.depth_scale <= 0.0
232 || !options.min_depth.is_finite()
233 || options.min_depth <= 0.0
234 || options.max_depth.is_nan()
235 || options.max_depth < options.min_depth
236 {
237 return Err(VisionError::InvalidParameter("invalid RGB-D odometry depth range".into()));
238 }
239 let mut pairs = Vec::new();
240 let mut rejected = 0;
241 for &pair in correspondences {
242 let pixel = pair.source();
243 let x = pixel.x.round() as isize;
244 let y = pixel.y.round() as isize;
245 let Some(sample) =
246 (x >= 0 && y >= 0).then(|| previous_depth.get(x as usize, y as usize)).flatten()
247 else {
248 rejected += 1;
249 continue;
250 };
251 let depth = sample[0].to_f64() * options.depth_scale;
252 if !depth.is_finite() || depth < options.min_depth || depth > options.max_depth {
253 rejected += 1;
254 continue;
255 }
256 let ray = camera.normalize_pixel(pixel);
257 pairs.push(ObjectImageCorrespondence::try_new(
258 Vec3::new(ray.x * depth, ray.y * depth, depth),
259 pair.target(),
260 )?);
261 }
262 if pairs.len() < 6 {
263 return Err(VisionError::InvalidParameter(
264 "RGB-D odometry requires at least six tracks with valid depth".into(),
265 ));
266 }
267 let estimate = solve_pnp_ransac(&pairs, camera, options.robust)?;
268 Ok(RgbdOdometryEstimate {
269 pose: *estimate.model(),
270 inliers: estimate.inliers().to_vec(),
271 rejected_depth_count: rejected,
272 })
273}
274
275#[cfg(test)]
276mod tests {
277 use super::{
278 estimate_rgbd_odometry, select_keypoints_grid, GridSelectionOptions, RgbdOdometryOptions,
279 };
280 use crate::{CameraMatrix3, Keypoint2, PointCorrespondence2, RobustEstimationOptions};
281 use spatialrust_camera::CameraIntrinsics;
282 use spatialrust_image::Image;
283 use spatialrust_math::Vec2;
284
285 #[test]
286 fn grid_selection_keeps_strongest_per_cell() {
287 let points = vec![
288 Keypoint2::try_new(2.0, 2.0, 1.0).unwrap(),
289 Keypoint2::try_new(3.0, 3.0, 4.0).unwrap(),
290 Keypoint2::try_new(12.0, 2.0, 2.0).unwrap(),
291 ];
292 let selected = select_keypoints_grid(
293 &points,
294 20,
295 10,
296 GridSelectionOptions { cell_width: 10, cell_height: 10, max_per_cell: 1 },
297 )
298 .unwrap();
299 assert_eq!(selected.len(), 2);
300 assert_eq!(selected[0].response(), 4.0);
301 assert_eq!(selected[1].response(), 2.0);
302 }
303
304 #[test]
305 fn rgbd_odometry_recovers_metric_translation() {
306 let camera = CameraMatrix3::from_intrinsics(
307 CameraIntrinsics::try_new(100.0, 100.0, 50.0, 40.0, 100, 80).unwrap(),
308 );
309 let mut depth = Image::<f32, 1>::from_pixel(100, 80, [f32::NAN]).unwrap();
310 let mut pairs = Vec::new();
311 for (index, (x, y)) in [
312 (20, 20),
313 (35, 20),
314 (50, 20),
315 (65, 20),
316 (80, 20),
317 (20, 35),
318 (35, 35),
319 (50, 35),
320 (65, 35),
321 (80, 35),
322 (25, 55),
323 (45, 55),
324 (65, 55),
325 (80, 55),
326 ]
327 .into_iter()
328 .enumerate()
329 {
330 let z = 1.5 + index as f64 * 0.07;
331 depth.get_mut(x, y).unwrap()[0] = z as f32;
332 let source = Vec2 { x: x as f64, y: y as f64 };
333 let target = Vec2 { x: source.x + 100.0 * 0.1 / z, y: source.y };
334 pairs.push(PointCorrespondence2::try_new(source, target).unwrap());
335 }
336 let estimate = estimate_rgbd_odometry(
337 depth.view(),
338 &pairs,
339 camera,
340 RgbdOdometryOptions {
341 robust: RobustEstimationOptions {
342 threshold: 0.05,
343 max_iterations: 500,
344 ..Default::default()
345 },
346 ..Default::default()
347 },
348 )
349 .unwrap();
350 assert!((estimate.pose.translation().x - 0.1).abs() < 1e-4);
351 assert!(estimate.pose.translation().y.abs() < 1e-4);
352 assert!(estimate.pose.translation().z.abs() < 1e-4);
353 assert_eq!(estimate.inliers.iter().filter(|&&value| value).count(), pairs.len());
354 }
355}