use nalgebra::Point2;
use crate::detect::DetectionParams;
use crate::error::{GridError, Result};
use crate::feature::OrientedFeature;
use crate::geometry::{apply_projective, estimate_projective};
use crate::lattice::{Coord, LatticeKind};
use crate::result::{GridEntry, LatticeFit, RejectedFeature, RejectionReason, ResidualSummary};
pub(crate) struct FitComponentResult {
pub(crate) entries: Vec<GridEntry>,
pub(crate) fit: LatticeFit,
pub(crate) over_threshold: Vec<RejectedFeature>,
}
pub(crate) fn fit_component(
labelled: &[(Coord, usize)],
features: &[OrientedFeature<2>],
positions: &[Point2<f32>],
lattice: LatticeKind,
params: &DetectionParams,
) -> Result<FitComponentResult> {
if labelled.len() < 4 {
return Err(GridError::InsufficientEvidence);
}
let mut model_pts: Vec<Point2<f32>> = Vec::with_capacity(labelled.len());
let mut image_pts: Vec<Point2<f32>> = Vec::with_capacity(labelled.len());
for &(coord, idx) in labelled {
model_pts.push(lattice.model_point(coord));
image_pts.push(positions[idx]);
}
let model_to_image = estimate_projective(&model_pts, &image_pts)?;
let mut entries_out: Vec<GridEntry> = Vec::with_capacity(labelled.len());
let mut residual_sum = 0.0_f32;
let mut residual_max = 0.0_f32;
let mut over_threshold: Vec<RejectedFeature> = Vec::new();
for &(coord, idx) in labelled {
let predicted = apply_projective(&model_to_image, lattice.model_point(coord))
.ok_or(GridError::DegenerateGeometry)?;
let position = positions[idx];
let dx = position.x - predicted.x;
let dy = position.y - predicted.y;
let residual = (dx * dx + dy * dy).sqrt();
residual_sum += residual;
if residual > residual_max {
residual_max = residual;
}
let source_index = features[idx].point.source_index;
if residual > params.max_residual_px {
over_threshold.push(RejectedFeature::new(
source_index,
Some(coord),
Some(residual),
RejectionReason::ResidualTooHigh,
));
}
entries_out.push(GridEntry::new(
coord,
source_index,
position,
Some(residual),
));
}
let mean = residual_sum / labelled.len() as f32;
let summary = ResidualSummary::new(labelled.len(), mean, residual_max);
entries_out.sort_by_key(|e| (e.coord, e.source_index));
Ok(FitComponentResult {
entries: entries_out,
fit: LatticeFit::new(model_to_image, summary),
over_threshold,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feature::{LocalAxis, PointFeature};
use nalgebra::{Matrix3, Vector3};
fn feat(idx: usize, p: Point2<f32>) -> OrientedFeature<2> {
OrientedFeature::<2>::new(
PointFeature::new(idx, p),
[LocalAxis::new(0.0, None), LocalAxis::new(1.0, None)],
)
}
#[test]
fn fit_recovers_hex_component_under_homography() {
let h = Matrix3::new(
1.0, 0.12, 0.0, 0.03, 1.0, 0.0, 0.0006, 0.0004, 1.0,
);
let coords = [
Coord::new(0, 0),
Coord::new(1, 0),
Coord::new(0, 1),
Coord::new(1, 1),
Coord::new(-1, 1),
Coord::new(1, -1),
Coord::new(2, 0),
];
let mut features = Vec::new();
let mut positions = Vec::new();
let mut labelled: Vec<(Coord, usize)> = Vec::new();
for (idx, &c) in coords.iter().enumerate() {
let m = LatticeKind::Hex.model_point(c);
let v = h * Vector3::new(m.x * 30.0 + 100.0, m.y * 30.0 + 100.0, 1.0);
let p = Point2::new(v.x / v.z, v.y / v.z);
features.push(feat(idx, p));
positions.push(p);
labelled.push((c, idx));
}
let params = DetectionParams::default();
let res = fit_component(&labelled, &features, &positions, LatticeKind::Hex, ¶ms)
.expect("hex fit");
assert_eq!(res.entries.len(), coords.len());
assert!(
res.fit.residuals.max_px < 1e-2,
"hex fit residual {} px too high",
res.fit.residuals.max_px
);
assert!(res.over_threshold.is_empty());
}
}