mod common;
pub mod global;
pub mod local;
pub use global::extend_via_global_homography;
pub use local::extend_via_local_homography;
use crate::geometry::HomographyQuality;
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct ExtensionCommonParams {
pub search_rel: f32,
pub ambiguity_factor: f32,
pub max_iters: u32,
pub max_residual_rel: f32,
}
impl Default for ExtensionCommonParams {
fn default() -> Self {
Self {
search_rel: 0.40,
ambiguity_factor: 2.5,
max_iters: 5,
max_residual_rel: 0.30,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct ExtensionParams {
pub common: ExtensionCommonParams,
pub min_labels_for_h: usize,
pub max_median_residual_rel: f32,
}
impl Default for ExtensionParams {
fn default() -> Self {
Self {
common: ExtensionCommonParams::default(),
min_labels_for_h: 12,
max_median_residual_rel: 0.10,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct LocalExtensionParams {
pub common: ExtensionCommonParams,
pub k_nearest: usize,
pub min_k: usize,
pub extend_depth: u32,
}
impl Default for LocalExtensionParams {
fn default() -> Self {
Self {
common: ExtensionCommonParams {
max_iters: 8, ..ExtensionCommonParams::default()
},
k_nearest: 12,
min_k: 6,
extend_depth: 3,
}
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default)]
pub struct ExtensionStats {
pub iterations: usize,
pub h_quality: Option<HomographyQuality<f32>>,
pub h_residual_median_px: Option<f32>,
pub h_residual_max_px: Option<f32>,
pub h_trusted: bool,
pub attached: usize,
pub rejected_no_candidate: usize,
pub rejected_ambiguous: usize,
pub rejected_label: usize,
pub rejected_policy: usize,
pub rejected_edge: usize,
pub attached_indices: Vec<usize>,
pub attached_cells: Vec<(i32, i32)>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::grow::{Admit, GrowResult, LabelledNeighbour, SquareAttachPolicy};
use nalgebra::Point2;
use std::collections::HashMap;
struct OpenValidator;
impl SquareAttachPolicy for OpenValidator {
fn is_eligible(&self, _idx: usize) -> bool {
true
}
fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
None
}
fn label_of(&self, _idx: usize) -> Option<u8> {
None
}
fn accept_candidate(
&self,
_idx: usize,
_at: (i32, i32),
_prediction: Point2<f32>,
_neighbours: &[LabelledNeighbour],
) -> Admit {
Admit::Accept
}
}
struct AlternatingLabelPolicy {
labels: Vec<u8>,
}
impl SquareAttachPolicy for AlternatingLabelPolicy {
fn is_eligible(&self, _idx: usize) -> bool {
true
}
fn required_label_at(&self, i: i32, j: i32) -> Option<u8> {
Some(((i + j).rem_euclid(2)) as u8)
}
fn label_of(&self, idx: usize) -> Option<u8> {
self.labels.get(idx).copied()
}
fn accept_candidate(
&self,
_idx: usize,
_at: (i32, i32),
_prediction: Point2<f32>,
_neighbours: &[LabelledNeighbour],
) -> Admit {
Admit::Accept
}
}
struct EdgeRejectingValidator {
forbid_idx: usize,
}
impl SquareAttachPolicy for EdgeRejectingValidator {
fn is_eligible(&self, _idx: usize) -> bool {
true
}
fn required_label_at(&self, _i: i32, _j: i32) -> Option<u8> {
None
}
fn label_of(&self, _idx: usize) -> Option<u8> {
None
}
fn accept_candidate(
&self,
_idx: usize,
_at: (i32, i32),
_prediction: Point2<f32>,
_neighbours: &[LabelledNeighbour],
) -> Admit {
Admit::Accept
}
fn edge_ok(
&self,
candidate_idx: usize,
neighbour_idx: usize,
_at_candidate: (i32, i32),
_at_neighbour: (i32, i32),
) -> bool {
candidate_idx != self.forbid_idx && neighbour_idx != self.forbid_idx
}
}
fn synthetic_grid(rows: i32, cols: i32, scale: f32) -> Vec<Point2<f32>> {
let mut pts = Vec::with_capacity((rows * cols) as usize);
for j in 0..rows {
for i in 0..cols {
pts.push(Point2::new(
i as f32 * scale + 100.0,
j as f32 * scale + 50.0,
));
}
}
pts
}
fn label_subgrid(
positions: &[Point2<f32>],
cols: i32,
i_range: std::ops::Range<i32>,
j_range: std::ops::Range<i32>,
) -> GrowResult {
let mut labelled = HashMap::new();
let mut by_corner = HashMap::new();
for j in j_range {
for i in i_range.clone() {
let idx = (j * cols + i) as usize;
labelled.insert((i, j), idx);
by_corner.insert(idx, (i, j));
}
}
let _ = positions;
GrowResult {
labelled,
by_corner,
..Default::default()
}
}
#[test]
fn extends_clean_perspective_grid() {
let cols = 6_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow = label_subgrid(&positions, cols, 1..5, 1..3);
let starting_count = grow.labelled.len();
assert_eq!(starting_count, 8);
let stats = extend_via_global_homography(
&positions,
&mut grow,
scale,
&ExtensionParams {
min_labels_for_h: 4,
..Default::default()
},
&OpenValidator,
);
assert!(stats.h_trusted, "H must be trusted on a clean affine grid");
assert!(
grow.labelled.len() > starting_count,
"extension should add corners on a clean grid"
);
}
#[test]
fn refuses_to_extend_when_residuals_too_high() {
let cols = 4_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let mut positions = synthetic_grid(rows, cols, scale);
positions[(cols + 1) as usize].x += scale * 0.5;
let mut grow = label_subgrid(&positions, cols, 0..4, 0..4);
let stats = extend_via_global_homography(
&positions,
&mut grow,
scale,
&ExtensionParams {
min_labels_for_h: 4,
common: ExtensionCommonParams {
max_residual_rel: 0.30,
..ExtensionCommonParams::default()
},
..Default::default()
},
&OpenValidator,
);
assert!(!stats.h_trusted);
assert_eq!(stats.attached, 0);
}
#[test]
fn no_op_when_too_few_labels() {
let cols = 4_i32;
let rows = 4_i32;
let positions = synthetic_grid(rows, cols, 50.0);
let mut grow = label_subgrid(&positions, cols, 0..2, 0..2);
let stats = extend_via_global_homography(
&positions,
&mut grow,
50.0,
&ExtensionParams::default(),
&OpenValidator,
);
assert_eq!(stats.attached, 0);
assert!(stats.h_quality.is_none());
}
#[test]
fn rejects_wrong_alternating_label_at_h_prediction() {
let cols = 4_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
let labels: Vec<u8> = (0..(rows * cols))
.map(|k| {
let i = k % cols;
let j = k / cols;
((i + j).rem_euclid(2)) as u8
})
.collect();
let bad_idx = cols as usize;
let mut labels = labels;
labels[bad_idx] = 0;
let policy = AlternatingLabelPolicy { labels };
let stats = extend_via_global_homography(
&positions,
&mut grow,
scale,
&ExtensionParams {
min_labels_for_h: 4,
..Default::default()
},
&policy,
);
assert!(stats.h_trusted);
assert!(!grow.labelled.contains_key(&(0, 1)) || grow.labelled[&(0, 1)] != bad_idx);
assert!(stats.rejected_label >= 1);
}
#[test]
fn rejects_bad_edge_via_edge_ok_gate() {
let cols = 4_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
let bad_candidate = cols as usize;
let policy = EdgeRejectingValidator {
forbid_idx: bad_candidate,
};
let stats = extend_via_global_homography(
&positions,
&mut grow,
scale,
&ExtensionParams {
min_labels_for_h: 4,
..Default::default()
},
&policy,
);
assert!(stats.h_trusted);
assert!(stats.rejected_edge >= 1);
assert!(!grow.labelled.contains_key(&(0, 1)));
}
#[test]
fn single_claim_prevents_double_attach() {
let scale = 50.0_f32;
let mut positions = Vec::new();
for j in 0..3_i32 {
for i in 0..3_i32 {
positions.push(Point2::new(
i as f32 * scale + 100.0,
j as f32 * scale + 50.0,
));
}
}
positions.push(Point2::new(250.0, 125.0));
let mut labelled = HashMap::new();
let mut by_corner = HashMap::new();
for j in 0..3_i32 {
for i in 0..3_i32 {
let idx = (j * 3 + i) as usize;
labelled.insert((i, j), idx);
by_corner.insert(idx, (i, j));
}
}
let mut grow = GrowResult {
labelled,
by_corner,
..Default::default()
};
let stats = extend_via_global_homography(
&positions,
&mut grow,
scale,
&ExtensionParams {
min_labels_for_h: 4,
common: ExtensionCommonParams {
search_rel: 1.5,
ambiguity_factor: 1.01,
..ExtensionCommonParams::default()
},
..Default::default()
},
&OpenValidator,
);
assert!(stats.h_trusted);
let attached_for_idx_9: Vec<&(i32, i32)> = grow
.labelled
.iter()
.filter_map(|(k, &v)| if v == 9 { Some(k) } else { None })
.collect();
assert!(
attached_for_idx_9.len() <= 1,
"corner index 9 attached to {} cells: {:?}",
attached_for_idx_9.len(),
attached_for_idx_9
);
for (&cell, &idx) in &grow.labelled {
assert_eq!(grow.by_corner.get(&idx), Some(&cell));
}
}
#[test]
fn local_h_extends_clean_perspective_grid() {
let cols = 6_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow = label_subgrid(&positions, cols, 1..5, 1..3);
let starting_count = grow.labelled.len();
assert_eq!(starting_count, 8);
let stats = extend_via_local_homography(
&positions,
&mut grow,
scale,
&LocalExtensionParams {
min_k: 4,
k_nearest: 8,
..Default::default()
},
&OpenValidator,
);
assert!(stats.h_trusted);
assert!(
grow.labelled.len() > starting_count,
"local-H extension should add corners on a clean grid"
);
}
#[test]
fn local_h_reaches_further_than_global() {
let cols = 8_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow_local = label_subgrid(&positions, cols, 2..6, 0..rows);
assert_eq!(grow_local.labelled.len(), 16);
let stats = extend_via_local_homography(
&positions,
&mut grow_local,
scale,
&LocalExtensionParams {
min_k: 4,
..Default::default()
},
&OpenValidator,
);
assert!(stats.iterations >= 2, "expected >= 2 iters");
assert_eq!(
grow_local.labelled.len(),
(rows * cols) as usize,
"local-H should reach every cell on a clean grid: {} of {}",
grow_local.labelled.len(),
rows * cols,
);
}
#[test]
fn local_h_no_op_when_too_few_labels() {
let cols = 4_i32;
let rows = 4_i32;
let positions = synthetic_grid(rows, cols, 50.0);
let mut grow = label_subgrid(&positions, cols, 0..2, 0..2);
let stats = extend_via_local_homography(
&positions,
&mut grow,
50.0,
&LocalExtensionParams {
min_k: 8,
..Default::default()
},
&OpenValidator,
);
assert_eq!(stats.attached, 0);
assert!(!stats.h_trusted);
}
#[test]
fn local_h_rejects_wrong_alternating_label() {
let cols = 4_i32;
let rows = 4_i32;
let scale = 50.0_f32;
let positions = synthetic_grid(rows, cols, scale);
let mut grow = label_subgrid(&positions, cols, 1..3, 1..3);
let labels: Vec<u8> = (0..(rows * cols))
.map(|k| {
let i = k % cols;
let j = k / cols;
((i + j).rem_euclid(2)) as u8
})
.collect();
let bad_idx = cols as usize;
let mut labels = labels;
labels[bad_idx] = 0;
let policy = AlternatingLabelPolicy { labels };
let stats = extend_via_local_homography(
&positions,
&mut grow,
scale,
&LocalExtensionParams {
min_k: 4,
..Default::default()
},
&policy,
);
assert!(!grow.labelled.contains_key(&(0, 1)) || grow.labelled[&(0, 1)] != bad_idx);
assert!(stats.rejected_label >= 1);
}
}