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, GridSolution, LabelledGrid, LatticeFit, RejectedFeature,
13 RejectionReason, 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 let solution = GridSolution::new(grid, Some(fit), rejected);
133 Ok(ConsistencyReport::new(passed, solution))
134}
135
136fn validate_features(features: &[PointFeature]) -> Result<HashMap<usize, Point2<f32>>> {
137 let mut out = HashMap::with_capacity(features.len());
138 for feature in features {
139 if !feature.position.x.is_finite() || !feature.position.y.is_finite() {
140 return Err(GridError::InconsistentInput(format!(
141 "feature {} has non-finite position",
142 feature.source_index
143 )));
144 }
145 if out.insert(feature.source_index, feature.position).is_some() {
146 return Err(GridError::InconsistentInput(format!(
147 "duplicate feature source_index {}",
148 feature.source_index
149 )));
150 }
151 }
152 Ok(out)
153}
154
155fn validate_hypotheses(
156 hypotheses: &[CoordinateHypothesis],
157 positions_by_source: &HashMap<usize, Point2<f32>>,
158) -> Result<()> {
159 let mut seen_sources = HashSet::with_capacity(hypotheses.len());
160 let mut seen_coords = HashSet::with_capacity(hypotheses.len());
161 for hypothesis in hypotheses {
162 if !positions_by_source.contains_key(&hypothesis.source_index) {
163 return Err(GridError::InconsistentInput(format!(
164 "hypothesis references missing feature source_index {}",
165 hypothesis.source_index
166 )));
167 }
168 if !seen_sources.insert(hypothesis.source_index) {
169 return Err(GridError::InconsistentInput(format!(
170 "duplicate hypothesis for feature source_index {}",
171 hypothesis.source_index
172 )));
173 }
174 if !seen_coords.insert(hypothesis.coord) {
175 return Err(GridError::InconsistentInput(format!(
176 "duplicate hypothesis for coordinate ({}, {})",
177 hypothesis.coord.u, hypothesis.coord.v
178 )));
179 }
180 }
181 Ok(())
182}
183
184fn distance(a: Point2<f32>, b: Point2<f32>) -> f32 {
185 let dx = a.x - b.x;
186 let dy = a.y - b.y;
187 (dx * dx + dy * dy).sqrt()
188}