projective-grid 0.9.0

Generic 2D projective grid graph construction, traversal, and homography tools
Documentation
use crate::square::index::GridCoords;
use serde::{Deserialize, Serialize};

/// Integer 2D grid transform (a 2×2 matrix) for aligning detected grids to a board model.
///
/// Represents a linear transform on integer coordinates:
/// `(i', j') = (a*i + b*j, c*i + d*j)`.
///
/// For most calibration targets, valid transforms are the 8 elements of the dihedral group `D4`
/// (rotations/reflections on the square grid). Those are provided in [`GRID_TRANSFORMS_D4`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GridTransform {
    /// Row-0, column-0 entry — the `i`-contribution to the new `i`.
    pub a: i32,
    /// Row-0, column-1 entry — the `j`-contribution to the new `i`.
    pub b: i32,
    /// Row-1, column-0 entry — the `i`-contribution to the new `j`.
    pub c: i32,
    /// Row-1, column-1 entry — the `j`-contribution to the new `j`.
    pub d: i32,
}

impl GridTransform {
    /// The identity transform — leaves `(i, j)` unchanged.
    pub const IDENTITY: GridTransform = GridTransform {
        a: 1,
        b: 0,
        c: 0,
        d: 1,
    };

    /// Apply the transform to `(i, j)`, returning the result as [`GridCoords`].
    #[inline]
    pub fn apply(&self, i: i32, j: i32) -> GridCoords {
        GridCoords {
            i: self.a * i + self.b * j,
            j: self.c * i + self.d * j,
        }
    }

    /// Invert the transform if it is unimodular (det = ±1).
    pub fn inverse(&self) -> Option<GridTransform> {
        let det = self.a * self.d - self.b * self.c;
        if det != 1 && det != -1 {
            return None;
        }
        Some(GridTransform {
            a: self.d / det,
            b: -self.b / det,
            c: -self.c / det,
            d: self.a / det,
        })
    }
}

/// A grid alignment: `dst = transform(src) + translation`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GridAlignment {
    /// Linear part — applied to the source coordinates before translation.
    pub transform: GridTransform,
    /// Integer `[Δi, Δj]` offset added after `transform`.
    pub translation: [i32; 2],
}

impl GridAlignment {
    /// The identity alignment — identity transform with zero translation.
    pub const IDENTITY: GridAlignment = GridAlignment {
        transform: GridTransform::IDENTITY,
        translation: [0, 0],
    };

    /// Map grid coordinates `(i, j)` using this alignment, returning [`GridCoords`].
    #[inline]
    pub fn map(&self, i: i32, j: i32) -> GridCoords {
        let g = self.transform.apply(i, j);
        GridCoords {
            i: g.i + self.translation[0],
            j: g.j + self.translation[1],
        }
    }

    /// Invert the alignment if its linear part is unimodular (det = ±1).
    pub fn inverse(&self) -> Option<GridAlignment> {
        let inv = self.transform.inverse()?;
        let [tx, ty] = self.translation;
        let g = inv.apply(-tx, -ty);
        Some(GridAlignment {
            transform: inv,
            translation: [g.i, g.j],
        })
    }
}

/// The 8 dihedral transforms `D4` on the integer grid.
pub const GRID_TRANSFORMS_D4: [GridTransform; 8] = [
    // rotations: 0°, 90°, 180°, 270°
    GridTransform {
        a: 1,
        b: 0,
        c: 0,
        d: 1,
    },
    GridTransform {
        a: 0,
        b: 1,
        c: -1,
        d: 0,
    },
    GridTransform {
        a: -1,
        b: 0,
        c: 0,
        d: -1,
    },
    GridTransform {
        a: 0,
        b: -1,
        c: 1,
        d: 0,
    },
    // reflections (and combinations)
    GridTransform {
        a: -1,
        b: 0,
        c: 0,
        d: 1,
    },
    GridTransform {
        a: 1,
        b: 0,
        c: 0,
        d: -1,
    },
    GridTransform {
        a: 0,
        b: 1,
        c: 1,
        d: 0,
    },
    GridTransform {
        a: 0,
        b: -1,
        c: -1,
        d: 0,
    },
];