Skip to main content

projective_grid/square/
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 [`SquareGridHomographyMesh`](crate::SquareGridHomographyMesh).
6
7use crate::float_helpers::lit;
8use crate::homography::{estimate_homography, Homography};
9use crate::Float;
10use crate::GridCoords;
11use nalgebra::Point2;
12use std::collections::HashMap;
13
14/// Reason a [`SquareGridHomography`] could not be built.
15#[non_exhaustive]
16#[derive(thiserror::Error, Debug)]
17pub enum GridRectifyError {
18    /// Fewer than the 4 corners a homography needs were supplied.
19    #[error("not enough grid corners with positions (need >= 4, got {got})")]
20    NotEnoughPoints {
21        /// Number of corners actually supplied.
22        got: usize,
23    },
24    /// The DLT homography estimator failed (degenerate correspondences).
25    #[error("homography estimation failed")]
26    HomographyFailed,
27    /// The estimated homography could not be inverted.
28    #[error("homography not invertible")]
29    NonInvertible,
30}
31
32/// A global homography mapping between rectified grid space and image space.
33#[derive(Clone, Debug)]
34pub struct SquareGridHomography<F: Float = f32> {
35    /// Maps rectified coordinates to image coordinates.
36    pub h_img_from_rect: Homography<F>,
37    /// Maps image coordinates to rectified coordinates.
38    pub h_rect_from_img: Homography<F>,
39    /// Minimum grid `i` of the bounding box (with margin), corner-index space.
40    pub min_i: i32,
41    /// Minimum grid `j` of the bounding box (with margin), corner-index space.
42    pub min_j: i32,
43    /// Maximum grid `i` of the bounding box (with margin), corner-index space.
44    pub max_i: i32,
45    /// Maximum grid `j` of the bounding box (with margin), corner-index space.
46    pub max_j: i32,
47    /// Rectified pixels per grid cell.
48    pub px_per_cell: F,
49    /// Rectified image width in pixels.
50    pub rect_width: usize,
51    /// Rectified image height in pixels.
52    pub rect_height: usize,
53}
54
55impl<F: Float> SquareGridHomography<F> {
56    /// Compute a global homography from grid corners to a rectified coordinate system.
57    ///
58    /// - `corners`: map from grid index to image position.
59    /// - `px_per_cell`: rectified pixels per grid cell.
60    /// - `margin_cells`: extra margin around the grid bounding box (in cell units).
61    pub fn from_corners(
62        corners: &HashMap<GridCoords, Point2<F>>,
63        px_per_cell: F,
64        margin_cells: F,
65    ) -> Result<Self, GridRectifyError> {
66        if corners.len() < 4 {
67            return Err(GridRectifyError::NotEnoughPoints { got: corners.len() });
68        }
69
70        let (mut min_i, mut min_j) = (i32::MAX, i32::MAX);
71        let (mut max_i, mut max_j) = (i32::MIN, i32::MIN);
72        for g in corners.keys() {
73            min_i = min_i.min(g.i);
74            min_j = min_j.min(g.j);
75            max_i = max_i.max(g.i);
76            max_j = max_j.max(g.j);
77        }
78
79        let mi = nalgebra::try_convert::<F, f64>((lit::<F>(min_i as f64) - margin_cells).floor())
80            .unwrap_or(min_i as f64) as i32;
81        let mj = nalgebra::try_convert::<F, f64>((lit::<F>(min_j as f64) - margin_cells).floor())
82            .unwrap_or(min_j as f64) as i32;
83        let ma = nalgebra::try_convert::<F, f64>((lit::<F>(max_i as f64) + margin_cells).ceil())
84            .unwrap_or(max_i as f64) as i32;
85        let mb = nalgebra::try_convert::<F, f64>((lit::<F>(max_j as f64) + margin_cells).ceil())
86            .unwrap_or(max_j as f64) as i32;
87
88        let w = lit::<F>((ma - mi) as f64) * px_per_cell;
89        let h = lit::<F>((mb - mj) as f64) * px_per_cell;
90        let rect_width =
91            nalgebra::try_convert::<F, f64>(w.round().max(F::one())).unwrap_or(1.0) as usize;
92        let rect_height =
93            nalgebra::try_convert::<F, f64>(h.round().max(F::one())).unwrap_or(1.0) as usize;
94
95        let mut rect_pts = Vec::with_capacity(corners.len());
96        let mut img_pts = Vec::with_capacity(corners.len());
97        for (g, &pos) in corners {
98            let x = lit::<F>((g.i - mi) as f64) * px_per_cell;
99            let y = lit::<F>((g.j - mj) as f64) * px_per_cell;
100            rect_pts.push(Point2::new(x, y));
101            img_pts.push(pos);
102        }
103
104        let h_img_from_rect =
105            estimate_homography(&rect_pts, &img_pts).ok_or(GridRectifyError::HomographyFailed)?;
106
107        let h_rect_from_img = h_img_from_rect
108            .inverse()
109            .ok_or(GridRectifyError::NonInvertible)?;
110
111        Ok(Self {
112            h_img_from_rect,
113            h_rect_from_img,
114            min_i: mi,
115            min_j: mj,
116            max_i: ma,
117            max_j: mb,
118            px_per_cell,
119            rect_width,
120            rect_height,
121        })
122    }
123
124    /// Map a point from rectified space to image space.
125    pub fn rect_to_img(&self, p_rect: Point2<F>) -> Point2<F> {
126        self.h_img_from_rect.apply(p_rect)
127    }
128
129    /// Map a point from image space to rectified space.
130    pub fn img_to_rect(&self, p_img: Point2<F>) -> Point2<F> {
131        self.h_rect_from_img.apply(p_img)
132    }
133}