Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Regular Grid Detection Example

Reference: crates/projective-grid/examples/hello_grid.rs — the minimal, image-free story for the standalone projective-grid crate: a handful of oriented feature points go in, a labelled (i, j) grid comes out, with no image and no other workspace crate.


Quick run

cargo run -p projective-grid --example hello_grid

The example synthesizes its own input (no image files needed): a small square lattice with a mild perspective shear, so the cloud looks like a board photographed at a slight angle.


Walkthrough

1. Build oriented features

projective-grid knows nothing about images — for the square detector it works on OrientedFeature<2> values: a position plus two roughly-orthogonal local axis directions. The example builds a 3×3 grid by hand. In a real application these positions and axes would come from a corner detector, a blob detector with a local-orientation estimate, or a laser-dot extractor.

Each feature pairs a PointFeature (an image-frame position plus a stable, caller-owned source_index) with two LocalAxis directions (an angle in radians plus an optional angular uncertainty sigma):

#![allow(unused)]
fn main() {
use nalgebra::Point2;
use projective_grid::{LocalAxis, OrientedFeature, PointFeature};

let mut features: Vec<OrientedFeature<2>> = Vec::new();
for j in 0..3 {
    for i in 0..3 {
        // Image-frame position (origin top-left, x right, y down).
        // The `+ j * 6.0` term shears each successive row, so this is a
        // genuine projective grid, not a perfectly axis-aligned one.
        let x = 60.0 + i as f32 * 40.0 + j as f32 * 6.0;
        let y = 50.0 + j as f32 * 40.0;

        // `source_index` is a stable handle; the solution reports it back
        // so a recovered `(i, j)` label maps to the exact input feature.
        let point = PointFeature::new(features.len(), Point2::new(x, y));

        // Two undirected, roughly-orthogonal axes: horizontal (0 rad)
        // and vertical (pi/2 rad), each with a small angular sigma.
        let axes = [
            LocalAxis::new(0.0, Some(0.02)),
            LocalAxis::new(std::f32::consts::FRAC_PI_2, Some(0.02)),
        ];
        features.push(OrientedFeature::new(point, axes));
    }
}
}

2. One call: detect_grid

Wrap the features as Evidence::Oriented2, bundle them into a DetectionRequest for a Square lattice, and call detect_grid. Grid dimensions are omitted; the detector infers the extent.

#![allow(unused)]
fn main() {
use projective_grid::{detect_grid, DetectionRequest, Evidence, LatticeKind};

let request = DetectionRequest::new(
    LatticeKind::Square,
    Evidence::Oriented2(&features),
);
let detection = detect_grid(request)?;
assert_eq!(detection.grid().entries().len(), 9);
}

DetectionParams::default() carries a max_residual_px fit gate and runs the topological grid builder — the sole builder, so there is no algorithm to select. It runs a Delaunay triangulation over the corner cloud, classifies edges by axis match, merges triangle pairs into cells, and floods integer coordinates across the mesh, then fits a projective transform. See the Topological grid finder algorithm page for the full method.

3. Handle the Result

Detection returns Result<GridDetection, GridError>. GridError is #[non_exhaustive], so callers always need a wildcard arm. The variants worth matching:

  • UnsupportedCombination { task, lattice, evidence } — the requested (lattice, evidence) pair has no algorithm. The unsupported cells are (Square, Oriented3) and (Hex, Oriented1/Oriented2).
  • InsufficientEvidence — too few features to assemble a 2×2 seed.
  • DegenerateGeometry — coincident or collinear points; no usable lattice spread.
  • InconsistentInput(String) — input slices disagree or carry duplicate source_index handles.

4. Read the result

A successful detection is a GridDetection:

  • grid(): &LabelledGrid — the recovered component. entries() is one GridEntry per placed feature; bbox() is the inclusive coordinate bounding box; dimensions() echoes any caller-supplied GridDimensions.
  • fit(): &LatticeFit — the mandatory fitted model-plane-to-image projective transform (model_to_image) plus a residual summary (residuals.count, residuals.mean_px, residuals.max_px).

Rejected features and exact stage snapshots are available separately through the opt-in diagnostics feature, so they do not expand the mandatory result.

Each GridEntry carries:

  • coord: Coord — the (i, j) label as coord.u / coord.v, rebased so the labelled bounding box starts at (0, 0).
  • source_index: usize — back into the input slice.
  • image_position: Point2<f32> — the feature’s image-frame pixel-center position.
  • residual_px: Option<f32> — reprojection residual in pixels, present when a fit was computed.
#![allow(unused)]
fn main() {
for entry in detection.grid().entries() {
    // coord.u = i, coord.v = j; source_index maps back to the input.
    println!(
        "(i={}, j={}) <- feature {} at ({:.1}, {:.1})",
        entry.coord.u,
        entry.coord.v,
        entry.source_index,
        entry.image_position.x,
        entry.image_position.y,
    );
}
}

Running it labels all nine features (0,0) through (2,2) with a sub-pixel fit residual.


Going further

  • Multiple componentsdetect_grid_all returns a Vec<GridDetection>, ordered by labelled count descending. Use it when the lattice fragments into islands (for example by occlusion) and the secondary components matter. The topological algorithm may yield several components.
  • Checking caller-supplied labels — when (i, j) labels already exist (for instance from a marker decode), check_consistency scores them against a single projective fit instead of recovering them from scratch. The runnable version is crates/projective-grid/examples/check_square_consistency.rs.
  • A larger detection runcrates/projective-grid/examples/detect_square_oriented2.rs exercises the same Evidence::Oriented2 path on a bigger synthetic grid.

See the projective-grid chapter for the full model — the two lattice families, the Evidence shapes, and the topological algorithm.