Skip to main content

projective_grid/
grid_rectify.rs

1//! Single global homography from grid corners.
2//!
3//! Computes a global projective mapping between a rectified coordinate system
4//! (uniform grid spacing) and the original image. Suitable when lens distortion
5//! is negligible. For distorted images, prefer [`GridHomographyMesh`](crate::GridHomographyMesh).
6
7use crate::grid_index::GridIndex;
8use crate::homography::{estimate_homography, Homography};
9use nalgebra::Point2;
10use std::collections::HashMap;
11
12#[derive(thiserror::Error, Debug)]
13pub enum GridRectifyError {
14    #[error("not enough grid corners with positions (need >= 4, got {got})")]
15    NotEnoughPoints { got: usize },
16    #[error("homography estimation failed")]
17    HomographyFailed,
18    #[error("homography not invertible")]
19    NonInvertible,
20}
21
22/// A global homography mapping between rectified grid space and image space.
23#[derive(Clone, Debug)]
24pub struct GridHomography {
25    /// Maps rectified coordinates to image coordinates.
26    pub h_img_from_rect: Homography,
27    /// Maps image coordinates to rectified coordinates.
28    pub h_rect_from_img: Homography,
29    /// Grid bounding box (with margin) in corner index space.
30    pub min_i: i32,
31    pub min_j: i32,
32    pub max_i: i32,
33    pub max_j: i32,
34    /// Rectified pixels per grid cell.
35    pub px_per_cell: f32,
36    /// Rectified image dimensions.
37    pub rect_width: usize,
38    pub rect_height: usize,
39}
40
41impl GridHomography {
42    /// Compute a global homography from grid corners to a rectified coordinate system.
43    ///
44    /// - `corners`: map from grid index to image position.
45    /// - `px_per_cell`: rectified pixels per grid cell.
46    /// - `margin_cells`: extra margin around the grid bounding box (in cell units).
47    pub fn from_corners(
48        corners: &HashMap<GridIndex, Point2<f32>>,
49        px_per_cell: f32,
50        margin_cells: f32,
51    ) -> Result<Self, GridRectifyError> {
52        if corners.len() < 4 {
53            return Err(GridRectifyError::NotEnoughPoints { got: corners.len() });
54        }
55
56        let (mut min_i, mut min_j) = (i32::MAX, i32::MAX);
57        let (mut max_i, mut max_j) = (i32::MIN, i32::MIN);
58        for g in corners.keys() {
59            min_i = min_i.min(g.i);
60            min_j = min_j.min(g.j);
61            max_i = max_i.max(g.i);
62            max_j = max_j.max(g.j);
63        }
64
65        let mi = (min_i as f32 - margin_cells).floor() as i32;
66        let mj = (min_j as f32 - margin_cells).floor() as i32;
67        let ma = (max_i as f32 + margin_cells).ceil() as i32;
68        let mb = (max_j as f32 + margin_cells).ceil() as i32;
69
70        let rect_width = ((ma - mi) as f32 * px_per_cell).round().max(1.0) as usize;
71        let rect_height = ((mb - mj) as f32 * px_per_cell).round().max(1.0) as usize;
72
73        let mut rect_pts = Vec::with_capacity(corners.len());
74        let mut img_pts = Vec::with_capacity(corners.len());
75        for (g, &pos) in corners {
76            let x = (g.i - mi) as f32 * px_per_cell;
77            let y = (g.j - mj) as f32 * px_per_cell;
78            rect_pts.push(Point2::new(x, y));
79            img_pts.push(pos);
80        }
81
82        let h_img_from_rect =
83            estimate_homography(&rect_pts, &img_pts).ok_or(GridRectifyError::HomographyFailed)?;
84
85        let h_rect_from_img = h_img_from_rect
86            .inverse()
87            .ok_or(GridRectifyError::NonInvertible)?;
88
89        Ok(Self {
90            h_img_from_rect,
91            h_rect_from_img,
92            min_i: mi,
93            min_j: mj,
94            max_i: ma,
95            max_j: mb,
96            px_per_cell,
97            rect_width,
98            rect_height,
99        })
100    }
101
102    /// Map a point from rectified space to image space.
103    pub fn rect_to_img(&self, p_rect: Point2<f32>) -> Point2<f32> {
104        self.h_img_from_rect.apply(p_rect)
105    }
106
107    /// Map a point from image space to rectified space.
108    pub fn img_to_rect(&self, p_img: Point2<f32>) -> Point2<f32> {
109        self.h_rect_from_img.apply(p_img)
110    }
111}