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 a2×2seed.DegenerateGeometry— coincident or collinear points; no usable lattice spread.InconsistentInput(String)— input slices disagree or carry duplicatesource_indexhandles.
4. Read the result
A successful detection is a GridDetection:
grid(): &LabelledGrid— the recovered component.entries()is oneGridEntryper placed feature;bbox()is the inclusive coordinate bounding box;dimensions()echoes any caller-suppliedGridDimensions.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 ascoord.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 components —
detect_grid_allreturns aVec<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_consistencyscores them against a single projective fit instead of recovering them from scratch. The runnable version iscrates/projective-grid/examples/check_square_consistency.rs. - A larger detection run —
crates/projective-grid/examples/detect_square_oriented2.rsexercises the sameEvidence::Oriented2path 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.