projective_grid/lib.rs
1//! Target-agnostic projective grid recovery primitives.
2//!
3//! This crate is intentionally small at the public boundary. It models two
4//! lattice families, two tasks, and explicit evidence shapes. Target-specific
5//! identifiers and detector classes belong in caller crates and should be
6//! converted into generic point features or coordinate hypotheses before
7//! entering this crate.
8//!
9//! # Design references
10//!
11//! The crate is organised around three orthogonal axes — lattice family,
12//! recovery strategy, and input-feature kind. The architecture and the
13//! orientation-as-an-optional-cue model are documented under `docs/` in the
14//! crate source tree:
15//!
16//! - `docs/DESIGN.md` — the three design axes, the shared pipeline back-half,
17//! and how the lattice family extends to hex.
18//! - `docs/ORIENTATION.md` — where each strategy consumes per-corner
19//! orientation and how each can run orientation-free (the dot-grid path).
20//!
21//! The per-strategy stage maps live in `docs/algorithms/topological-grid-detection.md`
22//! (repo root) and `calib-targets-chessboard/docs/PIPELINE.md`.
23//!
24//! # Two public tiers
25//!
26//! The crate ships an intentional two-tier public API. Both tiers are
27//! supported; they differ in audience and in how fast the surface is allowed
28//! to move.
29//!
30//! ## Stable tier — the facade (ordinary users)
31//!
32//! The items re-exported at the crate root are the supported surface for an
33//! ordinary caller who wants to *detect a grid* and read the labels back:
34//!
35//! * the entry points [`detect_grid`] / [`detect_grid_all`] and the
36//! consistency check [`check_consistency`];
37//! * the request types [`Evidence`] / [`DetectionParams`] /
38//! [`DetectionRequest`] (and the consistency-task [`ConsistencyParams`] /
39//! [`ConsistencyRequest`]);
40//! * the result types [`GridSolution`] / [`LabelledGrid`] /
41//! [`DetectionReport`];
42//! * the lattice model [`Lattice`] / [`LatticeKind`] / [`Coord`] and the
43//! feature-evidence types ([`PointFeature`], [`OrientedFeature`],
44//! [`LocalAxis`]);
45//! * the orientation-synthesis helpers ([`synthesize_oriented2`] and friends)
46//! and the [`cluster_axes`] global-direction prior with its
47//! [`AxisClusterCenters`] / [`AxisAssignment`] types.
48//!
49//! This tier carries normal semver guarantees: breaking changes here go
50//! through the usual deprecation cycle.
51//!
52//! ## Advanced tier — the composition API (build your own detector)
53//!
54//! [`shared`], [`topological`], [`lattice`], [`orient`], and [`cluster`]
55//! expose the assembly engine the facade is built from, as a deliberate
56//! **composition API**. It exists so a consumer with its own per-pattern
57//! invariants — the in-workspace chessboard detector is the reference example,
58//! but the contract is written for external consumers too — can drive the same
59//! grid-growth and recovery machinery under its own policy instead of going
60//! through the one-size-fits-all facade. This is intended product, not a
61//! private engine that merely happens to be `pub`.
62//!
63//! The composition contract — what a consumer supplies and what the engine
64//! guarantees in return:
65//!
66//! * **You supply a [`shared::grow::SquareAttachPolicy`].** This is the seam
67//! between the geometry-only growth machinery and your pattern's rules. The
68//! policy answers four questions per candidate — `is_eligible`,
69//! `required_label_at`, `accept_candidate`, `edge_ok` — letting you veto an
70//! attachment that is geometrically plausible but illegal for your pattern
71//! (e.g. a chessboard's alternating-colour parity, or a per-corner
72//! blacklist). The engine never relabels behind your back: every attachment
73//! it proposes has already passed your policy.
74//! * **You drive the growth / recovery engine.** The geometry-only primitives
75//! live under [`shared`]: [`shared::grow`] (candidate search + ambiguity
76//! resolution + the per-edge cardinal gate), [`shared::fill`] (interior
77//! hole fill), [`shared::extension`] / [`shared::grow_extend`] (boundary
78//! extension by local homography then cardinal BFS), and
79//! [`shared::recovery_schedule`] — the [`RecoverySchedule`]
80//! fixed-point that composes extension + fill + revalidation + drop filters
81//! into one post-convergence pass. Tune it with [`RecoveryParams`].
82//! * **You compose the shared back-half.** After your front-half has built
83//! integer-labelled components, the lattice-parameterised back-half reunites
84//! them with local geometry only ([`shared::merge`]), drops outliers with the
85//! structural-cue gate ([`shared::validate`]), and fits the model→image
86//! projective transform. The whole back-half is written against the
87//! [`Lattice`] trait, so it serves any implemented family.
88//! * **The guarantees.** Every stage is *drop-only* with respect to labels: a
89//! corner whose geometry does not cohere is removed, never relabelled. The
90//! precision contract of the facade — **zero wrong `(i, j)` labels**, misses
91//! acceptable — therefore holds for a consumer that composes the engine
92//! under its own policy, because the policy gates and the validate/drop
93//! filters run on every attachment exactly as they do on the facade path.
94//! Returned components are rebased so the labelled bounding-box minimum is
95//! `(0, 0)`; quad / homography corner order is TL, TR, BR, BL.
96//!
97//! **Stability of the advanced tier.** This surface is *advanced, and may
98//! evolve*: its shape tracks the engine's internal structure, so an item may
99//! change between minor releases when the engine is refactored. That is a
100//! deliberate product choice — the tier trades a slower-moving contract for
101//! direct access to the engine — not a disclaimer that it is really private.
102//! If you only need to detect a grid, depend on the facade; reach for the
103//! composition tier when you are building a *new kind of detector* on top of
104//! the engine. Engine items with no external consumer stay `pub(crate)`, so
105//! the advanced surface is kept no wider than what a consumer actually
106//! composes.
107//!
108//! # Core vs. extended surface
109//!
110//! Orthogonal to the stable/advanced split is a second distinction worth
111//! making explicit, because the two halves are exercised very differently in
112//! practice:
113//!
114//! * **Core path (exercised in production).** The square lattice
115//! ([`LatticeKind::Square`]) driven by two-axis evidence
116//! ([`Evidence::Oriented2`]). This is the path every in-workspace detector —
117//! chessboard, ChArUco, puzzleboard, marker board — actually runs, and it is
118//! the most heavily regression-tested surface in the crate.
119//! * **Extended path (library-only breadth).** The hexagonal lattice
120//! ([`Hex`], the [`topological`] hex arm, [`D6_TRANSFORMS`]) and the
121//! orientation-free / single-axis synthesis ([`orient`],
122//! [`shared::recovery_schedule`], [`Evidence::Positions`] /
123//! [`Evidence::Oriented1`]). This breadth is intended external product — it
124//! is published precisely so a downstream user can detect a dot grid or a
125//! hex target — but no in-workspace detector exercises it today; it is
126//! covered by this crate's own tests. See `docs/DESIGN.md` for the full
127//! core-vs-extended map.
128
129#![warn(missing_docs)]
130
131pub mod check;
132pub mod cluster;
133pub mod detect;
134pub mod error;
135pub mod feature;
136pub mod float;
137pub mod geometry;
138pub mod lattice;
139pub mod orient;
140pub mod result;
141
142pub use crate::check::{check_consistency, ConsistencyParams, ConsistencyRequest};
143pub use crate::cluster::{
144 cluster_axes, AxisAssignment, AxisClusterCenters, AxisClusterDebug, AxisFeature,
145 AxisObservation, ClusterParams,
146};
147pub use crate::detect::{
148 detect_grid, detect_grid_all, DetectionParams, DetectionReport, DetectionRequest, Evidence,
149 RecoveryParams, RecoverySchedule, TopologicalParams,
150};
151pub use crate::error::{EvidenceKind, GridError, GridTask};
152pub use crate::feature::{CoordinateHypothesis, LocalAxis, OrientedFeature, PointFeature};
153pub use crate::float::Float;
154pub use crate::lattice::{
155 Coord, GridDimensions, GridTransform, Hex, Lattice, LatticeKind, Square, D4_TRANSFORMS,
156 D6_TRANSFORMS, HEX_AXIAL_OFFSETS, SQUARE_CARDINAL_OFFSETS,
157};
158pub use crate::orient::{
159 synthesize_oriented2, synthesize_oriented2_from_oriented1, synthesize_oriented3,
160};
161pub use crate::result::{
162 ConsistencyReport, GridEntry, GridSolution, LabelledGrid, LatticeFit, RejectedFeature,
163 RejectionReason, ResidualSummary,
164};
165
166pub mod shared;
167pub mod topological;