Skip to main content

projective_grid/topological/
trace.rs

1//! Serializable trace for the square topological detector.
2//!
3//! This diagnostic entry point is intentionally layered over the production
4//! [`crate::detect_grid_all`] facade so timing and recovery stay aligned with
5//! the detector path. It records the stable facts downstream diagnostics need:
6//! input corner usability, final labelled components, and summary counts.
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::detect::{
12    detect_grid_all, DetectionParams, DetectionRequest, Evidence, TopologicalParams,
13};
14use crate::feature::OrientedFeature;
15use crate::lattice::LatticeKind;
16
17/// One input corner as seen by the topological trace.
18#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
19#[non_exhaustive]
20pub struct TopologicalCornerTrace {
21    /// Index of this feature in the supplied slice.
22    pub index: usize,
23    /// Caller-owned source index.
24    pub source_index: usize,
25    /// Corner position `[x, y]` in image pixels.
26    pub position: [f32; 2],
27    /// Local axis angles in radians.
28    pub axis_angles_rad: [f32; 2],
29    /// Local axis uncertainties in radians. `None` means no uncertainty was supplied.
30    pub axis_sigmas_rad: [Option<f32>; 2],
31    /// Whether the feature survived the topological sigma/axis prefilter.
32    pub usable: bool,
33}
34
35/// One final `(u, v) -> source_index` label.
36#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct TopologicalLabelTrace {
39    /// First square-grid coordinate.
40    pub u: i32,
41    /// Second square-grid coordinate.
42    pub v: i32,
43    /// Source index of the labelled input feature.
44    pub source_index: usize,
45}
46
47impl TopologicalLabelTrace {
48    /// Build a label trace entry mapping grid `(u, v)` to a source index.
49    pub fn new(u: i32, v: i32, source_index: usize) -> Self {
50        Self { u, v, source_index }
51    }
52}
53
54/// One connected labelled component.
55#[derive(Clone, Debug, Serialize, Deserialize)]
56#[non_exhaustive]
57pub struct TopologicalComponentTrace {
58    /// Index of this component within the result.
59    pub index: usize,
60    /// The component's labels, sorted by `(v, u, source_index)`.
61    pub labels: Vec<TopologicalLabelTrace>,
62}
63
64/// Summary counters for the topological trace.
65#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
66#[non_exhaustive]
67pub struct TopologicalTraceDiagnostics {
68    /// Number of input features.
69    pub corners_in: usize,
70    /// Number of features that survived the axis usability filter.
71    pub corners_used: usize,
72    /// Number of labelled components returned by production detection.
73    pub components: usize,
74    /// Total number of labelled entries across all components.
75    pub labels: usize,
76}
77
78/// Full topological trace.
79#[derive(Clone, Debug, Serialize, Deserialize)]
80#[non_exhaustive]
81pub struct TopologicalTrace {
82    /// Parameters used by the topological stage.
83    pub params: TopologicalParams,
84    /// Every input corner with its usable flag.
85    pub corners: Vec<TopologicalCornerTrace>,
86    /// Labelled connected components.
87    pub components: Vec<TopologicalComponentTrace>,
88    /// Per-stage summary counters.
89    pub diagnostics: TopologicalTraceDiagnostics,
90}
91
92/// Errors emitted by [`build_grid_topological_trace`].
93#[derive(Clone, Debug, Error, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum TopologicalTraceError {
96    /// Fewer than three usable corners survived the topological prefilter.
97    #[error("not enough usable corners ({usable}) for Delaunay triangulation")]
98    NotEnoughCorners {
99        /// Number of usable corners.
100        usable: usize,
101    },
102    /// Production detection did not return a labelled component.
103    #[error("topological detection produced no labelled components")]
104    NoComponents,
105}
106
107/// Build a trace for the production square topological detector.
108pub fn build_grid_topological_trace(
109    features: &[OrientedFeature<2>],
110    params: TopologicalParams,
111) -> Result<TopologicalTrace, TopologicalTraceError> {
112    let corners: Vec<TopologicalCornerTrace> = features
113        .iter()
114        .enumerate()
115        .map(|(index, feature)| {
116            let usable = feature.axes.iter().any(|axis| {
117                axis.sigma_rad
118                    .map(|s| s < params.max_axis_sigma_rad)
119                    .unwrap_or(true)
120            });
121            TopologicalCornerTrace {
122                index,
123                source_index: feature.point.source_index,
124                position: [feature.point.position.x, feature.point.position.y],
125                axis_angles_rad: [feature.axes[0].angle_rad, feature.axes[1].angle_rad],
126                axis_sigmas_rad: [feature.axes[0].sigma_rad, feature.axes[1].sigma_rad],
127                usable,
128            }
129        })
130        .collect();
131    let corners_used = corners.iter().filter(|c| c.usable).count();
132    if corners_used < 3 {
133        return Err(TopologicalTraceError::NotEnoughCorners {
134            usable: corners_used,
135        });
136    }
137
138    let validate = crate::detect::ValidateParams::default()
139        .with_line_tol_rel(f32::INFINITY)
140        .with_local_h_tol_rel(f32::INFINITY)
141        .with_edge_length_band_rel(f32::INFINITY);
142    let request = DetectionRequest::new(
143        LatticeKind::Square,
144        Evidence::Oriented2(features),
145        None,
146        DetectionParams::default()
147            .with_topological(params)
148            .with_validate(validate)
149            .with_max_residual_px(f32::INFINITY),
150    );
151    let report = detect_grid_all(request).map_err(|_| TopologicalTraceError::NoComponents)?;
152    if report.solutions.is_empty() {
153        return Err(TopologicalTraceError::NoComponents);
154    }
155
156    let components: Vec<TopologicalComponentTrace> = report
157        .solutions
158        .iter()
159        .enumerate()
160        .map(|(index, solution)| {
161            let mut labels: Vec<TopologicalLabelTrace> = solution
162                .grid
163                .entries
164                .iter()
165                .map(|entry| TopologicalLabelTrace {
166                    u: entry.coord.u,
167                    v: entry.coord.v,
168                    source_index: entry.source_index,
169                })
170                .collect();
171            labels.sort_by_key(|label| (label.v, label.u, label.source_index));
172            TopologicalComponentTrace { index, labels }
173        })
174        .collect();
175    let labels = components
176        .iter()
177        .map(|component| component.labels.len())
178        .sum();
179    let diagnostics = TopologicalTraceDiagnostics {
180        corners_in: features.len(),
181        corners_used,
182        components: components.len(),
183        labels,
184    };
185    Ok(TopologicalTrace {
186        params,
187        corners,
188        components,
189        diagnostics,
190    })
191}