Skip to main content

projective_grid/shared/grow/
params.rs

1//! Grow policy trait, tuning, and result types.
2//!
3//! This module owns the growth *contract*: the [`SquareAttachPolicy`] trait
4//! that lets a caller plug pattern-specific invariants into the otherwise
5//! pure-geometry candidate search, the per-candidate [`Admit`] /
6//! [`LabelledNeighbour`] / [`FillEdgeCtx`] data carriers, the [`GrowParams`]
7//! tolerances, and the [`GrowResult`] output container. It deliberately holds
8//! no algorithm — the candidate-search / ambiguity helpers live in
9//! [`super`](crate::shared::grow) and the prediction geometry in
10//! [`super::predict`] — so the policy contract can be read without wading
11//! through the search loop. Tier: advanced engine (semver-exempt pre-1.0).
12
13use nalgebra::{Point2, Vector2};
14use std::collections::{HashMap, HashSet};
15
16/// Per-candidate decision from a [`SquareAttachPolicy`].
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum Admit {
19    /// Accept this candidate at the given grid cell.
20    Accept,
21    /// Reject this candidate; the generic code may move on to the
22    /// next nearest (if any).
23    Reject,
24}
25
26/// Information about an existing labelled neighbour, passed to the
27/// policy during candidate evaluation.
28#[derive(Clone, Copy, Debug)]
29pub struct LabelledNeighbour {
30    /// Index of the neighbour corner in the caller's position array.
31    pub idx: usize,
32    /// The neighbour's `(i, j)` grid cell.
33    pub at: (i32, i32),
34    /// The neighbour's position in image pixels.
35    pub position: Point2<f32>,
36}
37
38/// Caller-supplied attachment policy for the square-lattice growth helpers.
39///
40/// Implementations typically hold references to the caller's feature
41/// data (axes, labels, strengths) plus tuning parameters, and use `idx`
42/// to look up the relevant per-feature record inside each callback.
43pub trait SquareAttachPolicy {
44    /// Is this corner index a possible candidate at all? Called
45    /// once per corner when the KD-tree is built.
46    fn is_eligible(&self, idx: usize) -> bool;
47
48    /// Optional caller-defined label required at grid cell `(i, j)`.
49    /// Return `None` for no constraint.
50    fn required_label_at(&self, i: i32, j: i32) -> Option<u8>;
51
52    /// Return the label of the corner at `idx`. Must agree with
53    /// `required_label_at` at attachment time. Called during
54    /// candidate filtering.
55    fn label_of(&self, idx: usize) -> Option<u8>;
56
57    /// Accept or reject a candidate for attachment at grid cell
58    /// `at` given its geometric prediction and existing labelled
59    /// neighbours. Called per candidate in order of increasing
60    /// distance to `prediction`.
61    fn accept_candidate(
62        &self,
63        idx: usize,
64        at: (i32, i32),
65        prediction: Point2<f32>,
66        neighbours: &[LabelledNeighbour],
67    ) -> Admit;
68
69    /// Soft per-edge check: is the induced edge between the just-
70    /// attached candidate and one of its cardinal-labelled neighbours
71    /// admissible? At least one cardinal edge must pass for the
72    /// attachment to stick; otherwise the position is marked a hole
73    /// and the candidate is rolled back.
74    ///
75    /// Default: accept all edges (no soft check).
76    fn edge_ok(
77        &self,
78        _candidate_idx: usize,
79        _neighbour_idx: usize,
80        _at_candidate: (i32, i32),
81        _at_neighbour: (i32, i32),
82    ) -> bool {
83        true
84    }
85
86    /// Optional widened eligibility used by the fill-pass booster.
87    ///
88    /// Defaults to [`Self::is_eligible`]; patterns whose precision
89    /// core admits only `Clustered` corners but want to admit a few
90    /// near-cluster corners during the booster pass override this to
91    /// expand the admissible set. The fill pass calls this when
92    /// building its KD-tree; the regular grow / boundary-extension
93    /// passes ignore it.
94    fn eligible_for_fill(&self, idx: usize) -> bool {
95        self.is_eligible(idx)
96    }
97
98    /// Optional fill-pass edge check that has access to the full
99    /// labelled set and the position table via [`FillEdgeCtx`].
100    ///
101    /// The default delegates to [`Self::edge_ok`], ignoring the extra
102    /// context. Pattern implementations that need a directional edge
103    /// metric (e.g., a strongly anisotropic component where the
104    /// horizontal pitch is much larger than the vertical pitch and a
105    /// scalar `cell_size` rejects legitimate vertical extrapolations)
106    /// override this to consult the labelled set when computing the
107    /// expected edge length.
108    ///
109    /// Only invoked by [`crate::shared::fill::fill_grid_holes`]; the
110    /// regular grow and boundary-extension passes call [`Self::edge_ok`]
111    /// directly.
112    fn fill_edge_ok(&self, ctx: FillEdgeCtx<'_>) -> bool {
113        self.edge_ok(
114            ctx.candidate_idx,
115            ctx.neighbour_idx,
116            ctx.at_candidate,
117            ctx.at_neighbour,
118        )
119    }
120}
121
122/// Context passed to [`SquareAttachPolicy::fill_edge_ok`].
123///
124/// Bundles every piece of state the policy needs to make a
125/// labelled-set-aware edge decision: the candidate + cardinal
126/// neighbour indices, their `(i, j)` cells, the full labelled map,
127/// the corner position array, and the scalar fallback cell size.
128#[non_exhaustive]
129#[derive(Clone, Copy)]
130pub struct FillEdgeCtx<'a> {
131    /// Index of the candidate corner being evaluated.
132    pub candidate_idx: usize,
133    /// Index of the already-labelled cardinal neighbour.
134    pub neighbour_idx: usize,
135    /// The candidate's prospective `(i, j)` cell.
136    pub at_candidate: (i32, i32),
137    /// The cardinal neighbour's `(i, j)` cell.
138    pub at_neighbour: (i32, i32),
139    /// The full `(i, j) → corner_idx` labelled map at this point in the grow.
140    pub labelled: &'a HashMap<(i32, i32), usize>,
141    /// Corner positions in image pixels, indexed by the values of `labelled`.
142    pub positions: &'a [Point2<f32>],
143    /// Scalar fallback cell size in pixels, used when no local estimate exists.
144    pub cell_size: f32,
145}
146
147/// Tolerances for the square-lattice growth helpers.
148#[non_exhaustive]
149#[derive(Clone, Copy, Debug)]
150pub struct GrowParams {
151    /// Candidate-search radius (fraction of `cell_size`) around each
152    /// prediction. Applies when the target is being **interpolated**
153    /// between labelled neighbours on opposite sides.
154    pub attach_search_rel: f32,
155    /// Ambiguity factor: if the second-nearest candidate is within
156    /// `factor × nearest_distance`, the attachment is skipped.
157    pub attach_ambiguity_factor: f32,
158    /// Multiplier on `attach_search_rel` when the target is being
159    /// **extrapolated** outward from the labelled set (every labelled
160    /// neighbour sits on the same side of the target along at least one
161    /// axis). Defaults to 2.0 — opens the search up enough to absorb
162    /// the perspective-foreshortening overshoot at the image edge while
163    /// still rejecting off-lattice candidates that sit several cell-
164    /// widths away.
165    pub boundary_search_factor: f32,
166}
167
168impl Default for GrowParams {
169    fn default() -> Self {
170        Self {
171            attach_search_rel: 0.35,
172            attach_ambiguity_factor: 1.5,
173            boundary_search_factor: 2.0,
174        }
175    }
176}
177
178impl GrowParams {
179    /// Construct grow parameters from the interpolation search radius and
180    /// ambiguity factor; `boundary_search_factor` keeps its default.
181    pub fn new(attach_search_rel: f32, attach_ambiguity_factor: f32) -> Self {
182        Self {
183            attach_search_rel,
184            attach_ambiguity_factor,
185            ..Self::default()
186        }
187    }
188}
189
190/// Outcome of a grow pass.
191#[derive(Debug, Default)]
192pub struct GrowResult {
193    /// `(i, j) → corner_index` map of accepted labels. Rebased so the
194    /// bounding-box minimum is `(0, 0)`.
195    pub labelled: HashMap<(i32, i32), usize>,
196    /// Inverse map.
197    pub by_corner: HashMap<usize, (i32, i32)>,
198    /// Positions with ≥ 2 candidates inside the ambiguity window.
199    pub ambiguous: HashSet<(i32, i32)>,
200    /// Positions with no accepted candidate.
201    pub holes: HashSet<(i32, i32)>,
202    /// Grid `i`-axis vector (pixels per cell) carried forward — overlays
203    /// and boosters use it.
204    pub axis_i: Vector2<f32>,
205    /// Grid `j`-axis vector (pixels per cell) carried forward — overlays
206    /// and boosters use it.
207    pub axis_j: Vector2<f32>,
208    /// Mod-2 `i` component of the coordinate shift removed by the
209    /// final rebase.
210    ///
211    /// `bfs_grow` walks in seed-local coordinates, then subtracts the
212    /// labelled bounding-box minimum so output coordinates start at
213    /// `(0, 0)`. Callers with an alternating label rule can add these
214    /// mod-2 components back when evaluating labels in post-rebase
215    /// coordinates. Callers without an alternating rule can ignore
216    /// these fields.
217    pub rebase_i_mod2: i32,
218    /// See [`Self::rebase_i_mod2`].
219    pub rebase_j_mod2: i32,
220}