projective_grid/check/
mod.rs1use std::collections::{HashMap, HashSet};
4
5use nalgebra::Point2;
6
7use crate::error::{GridError, Result};
8use crate::feature::{CoordinateHypothesis, PointFeature};
9use crate::geometry::{apply_projective, estimate_projective};
10use crate::lattice::{GridDimensions, LatticeKind};
11use crate::result::{
12 ConsistencyReport, GridEntry, LabelledGrid, LatticeFit, RejectedFeature, RejectionReason,
13 ResidualSummary,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
18#[non_exhaustive]
19pub struct ConsistencyParams {
20 pub max_residual_px: f32,
22}
23
24impl Default for ConsistencyParams {
25 fn default() -> Self {
26 Self {
27 max_residual_px: 2.0,
28 }
29 }
30}
31
32impl ConsistencyParams {
33 pub fn new(max_residual_px: f32) -> Self {
35 Self { max_residual_px }
36 }
37}
38
39#[derive(Clone, Copy, Debug)]
41#[non_exhaustive]
42pub struct ConsistencyRequest<'a> {
43 pub lattice: LatticeKind,
45 pub features: &'a [PointFeature],
47 pub hypotheses: &'a [CoordinateHypothesis],
49 pub dimensions: Option<GridDimensions>,
51 pub params: ConsistencyParams,
53}
54
55impl<'a> ConsistencyRequest<'a> {
56 pub fn new(
58 lattice: LatticeKind,
59 features: &'a [PointFeature],
60 hypotheses: &'a [CoordinateHypothesis],
61 dimensions: Option<GridDimensions>,
62 params: ConsistencyParams,
63 ) -> Self {
64 Self {
65 lattice,
66 features,
67 hypotheses,
68 dimensions,
69 params,
70 }
71 }
72}
73
74pub fn check_consistency(request: ConsistencyRequest<'_>) -> Result<ConsistencyReport> {
77 let positions_by_source = validate_features(request.features)?;
78 validate_hypotheses(request.hypotheses, &positions_by_source)?;
79
80 if request.hypotheses.len() < 4 {
81 return Err(GridError::InsufficientEvidence);
82 }
83
84 let mut model_points = Vec::with_capacity(request.hypotheses.len());
85 let mut image_points = Vec::with_capacity(request.hypotheses.len());
86 for hypothesis in request.hypotheses {
87 model_points.push(request.lattice.model_point(hypothesis.coord));
88 image_points.push(positions_by_source[&hypothesis.source_index]);
89 }
90
91 let model_to_image = estimate_projective(&model_points, &image_points)?;
92
93 let mut entries = Vec::with_capacity(request.hypotheses.len());
94 let mut rejected = Vec::new();
95 let mut residual_sum = 0.0_f32;
96 let mut residual_max = 0.0_f32;
97
98 for hypothesis in request.hypotheses {
99 let model = request.lattice.model_point(hypothesis.coord);
100 let actual = positions_by_source[&hypothesis.source_index];
101 let predicted =
102 apply_projective(&model_to_image, model).ok_or(GridError::DegenerateGeometry)?;
103 let residual = distance(actual, predicted);
104 residual_sum += residual;
105 if residual > residual_max {
106 residual_max = residual;
107 }
108 if residual > request.params.max_residual_px {
109 rejected.push(RejectedFeature::new(
110 hypothesis.source_index,
111 Some(hypothesis.coord),
112 Some(residual),
113 RejectionReason::ResidualTooHigh,
114 ));
115 }
116 entries.push(GridEntry::new(
117 hypothesis.coord,
118 hypothesis.source_index,
119 actual,
120 Some(residual),
121 ));
122 }
123
124 entries.sort_by_key(|entry| (entry.coord, entry.source_index));
125 rejected.sort_by_key(|entry| (entry.coord, entry.source_index));
126
127 let count = entries.len();
128 let residuals = ResidualSummary::new(count, residual_sum / count as f32, residual_max);
129 let fit = LatticeFit::new(model_to_image, residuals);
130 let grid = LabelledGrid::new(request.lattice, entries, request.dimensions);
131 let passed = rejected.is_empty();
132 Ok(ConsistencyReport::new(passed, grid, fit, rejected))
133}
134
135fn validate_features(features: &[PointFeature]) -> Result<HashMap<usize, Point2<f32>>> {
136 let mut out = HashMap::with_capacity(features.len());
137 for feature in features {
138 if !feature.position.x.is_finite() || !feature.position.y.is_finite() {
139 return Err(GridError::InconsistentInput(format!(
140 "feature {} has non-finite position",
141 feature.source_index
142 )));
143 }
144 if out.insert(feature.source_index, feature.position).is_some() {
145 return Err(GridError::InconsistentInput(format!(
146 "duplicate feature source_index {}",
147 feature.source_index
148 )));
149 }
150 }
151 Ok(out)
152}
153
154fn validate_hypotheses(
155 hypotheses: &[CoordinateHypothesis],
156 positions_by_source: &HashMap<usize, Point2<f32>>,
157) -> Result<()> {
158 let mut seen_sources = HashSet::with_capacity(hypotheses.len());
159 let mut seen_coords = HashSet::with_capacity(hypotheses.len());
160 for hypothesis in hypotheses {
161 if !positions_by_source.contains_key(&hypothesis.source_index) {
162 return Err(GridError::InconsistentInput(format!(
163 "hypothesis references missing feature source_index {}",
164 hypothesis.source_index
165 )));
166 }
167 if !seen_sources.insert(hypothesis.source_index) {
168 return Err(GridError::InconsistentInput(format!(
169 "duplicate hypothesis for feature source_index {}",
170 hypothesis.source_index
171 )));
172 }
173 if !seen_coords.insert(hypothesis.coord) {
174 return Err(GridError::InconsistentInput(format!(
175 "duplicate hypothesis for coordinate ({}, {})",
176 hypothesis.coord.u, hypothesis.coord.v
177 )));
178 }
179 }
180 Ok(())
181}
182
183fn distance(a: Point2<f32>, b: Point2<f32>) -> f32 {
184 let dx = a.x - b.x;
185 let dy = a.y - b.y;
186 (dx * dx + dy * dy).sqrt()
187}