use std::collections::HashMap;
use nalgebra::{Matrix3, Point2, Vector3};
use projective_grid::{
detect_grid, Coord, DetectionParams, DetectionRequest, Evidence, LatticeKind, LocalAxis,
OrientedFeature, PointFeature,
};
type Truth = HashMap<usize, (i32, i32)>;
fn perspective_grid(
rows: i32,
cols: i32,
s: f32,
origin: f32,
h: &Matrix3<f32>,
) -> (Vec<Point2<f32>>, Truth) {
let mut pts = Vec::new();
let mut truth = HashMap::new();
let mut idx = 0usize;
for j in 0..rows {
for i in 0..cols {
let g = Vector3::new(i as f32 * s + origin, j as f32 * s + origin, 1.0);
let p = h * g;
pts.push(Point2::new(p.x / p.z, p.y / p.z));
truth.insert(idx, (i, j));
idx += 1;
}
}
(pts, truth)
}
fn fold_pi(theta: f32) -> f32 {
let pi = std::f32::consts::PI;
let mut t = theta % pi;
if t < 0.0 {
t += pi;
}
t
}
fn local_u_axis(pts: &[Point2<f32>], cols: usize, i: usize, j: usize) -> f32 {
let here = pts[j * cols + i];
let nb = if i + 1 < cols {
pts[j * cols + i + 1]
} else {
pts[j * cols + i - 1]
};
fold_pi((nb.y - here.y).atan2(nb.x - here.x))
}
fn build_features(
pts: &[Point2<f32>],
cols: usize,
rows: usize,
) -> (Vec<OrientedFeature<1>>, Vec<OrientedFeature<2>>) {
let mut o1 = Vec::with_capacity(pts.len());
let mut o2 = Vec::with_capacity(pts.len());
for j in 0..rows {
for i in 0..cols {
let flat = j * cols + i;
let point = PointFeature::new(flat, pts[flat]);
let u = local_u_axis(pts, cols, i, j);
let here = pts[flat];
let vnb = if j + 1 < rows {
pts[(j + 1) * cols + i]
} else {
pts[(j - 1) * cols + i]
};
let v = fold_pi((vnb.y - here.y).atan2(vnb.x - here.x));
o1.push(OrientedFeature::<1>::new(point, [LocalAxis::new(u, None)]));
o2.push(OrientedFeature::<2>::new(
point,
[LocalAxis::new(u, None), LocalAxis::new(v, None)],
));
}
}
(o1, o2)
}
fn assert_labels_consistent_with_truth(entries: &[(usize, Coord)], truth: &Truth, ctx: &str) {
assert!(entries.len() >= 4, "{ctx}: too few labelled corners");
let pairs: Vec<((i32, i32), (i32, i32))> = entries
.iter()
.map(|(src, c)| ((c.u, c.v), truth[src]))
.collect();
const D4: [[[i32; 2]; 2]; 8] = [
[[1, 0], [0, 1]],
[[0, -1], [1, 0]],
[[-1, 0], [0, -1]],
[[0, 1], [-1, 0]],
[[-1, 0], [0, 1]],
[[1, 0], [0, -1]],
[[0, 1], [1, 0]],
[[0, -1], [-1, 0]],
];
let apply = |m: &[[i32; 2]; 2], (u, v): (i32, i32)| {
(m[0][0] * u + m[0][1] * v, m[1][0] * u + m[1][1] * v)
};
let found = D4.iter().any(|m| {
let (mu0, mv0) = apply(m, pairs[0].0);
let t = (pairs[0].1 .0 - mu0, pairs[0].1 .1 - mv0);
pairs.iter().all(|(d, tc)| {
let (mu, mv) = apply(m, *d);
(mu + t.0, mv + t.1) == *tc
})
});
assert!(
found,
"{ctx}: labels are NOT a consistent lattice map of ground truth"
);
}
fn entries(sol: &projective_grid::GridSolution) -> Vec<(usize, Coord)> {
sol.grid
.entries
.iter()
.map(|e| (e.source_index, e.coord))
.collect()
}
fn h_perspective() -> Matrix3<f32> {
Matrix3::new(
1.0, 0.16, 0.0, 0.05, 1.0, 0.0, 0.0011, 0.0007, 1.0,
)
}
fn detect_o1(feats: &[OrientedFeature<1>]) -> projective_grid::GridSolution {
detect_grid(DetectionRequest::new(
LatticeKind::Square,
Evidence::Oriented1(feats),
None,
DetectionParams::default(),
))
.expect("oriented1 detect")
}
fn detect_o2(feats: &[OrientedFeature<2>]) -> projective_grid::GridSolution {
detect_grid(DetectionRequest::new(
LatticeKind::Square,
Evidence::Oriented2(feats),
None,
DetectionParams::default(),
))
.expect("oriented2 detect")
}
#[test]
fn oriented1_topological_parity_and_zero_wrong() {
let (pts, truth) = perspective_grid(8, 8, 28.0, 50.0, &h_perspective());
let (o1, o2) = build_features(&pts, 8, 8);
let sol1 = detect_o1(&o1);
let sol2 = detect_o2(&o2);
assert_labels_consistent_with_truth(&entries(&sol1), &truth, "oriented1 topological");
assert_labels_consistent_with_truth(&entries(&sol2), &truth, "oriented2 topological");
let n1 = sol1.grid.entries.len();
let n2 = sol2.grid.entries.len();
assert!(n2 >= n1, "oriented2 should be the upper bound: {n2} < {n1}");
assert!(
n1 >= 60,
"oriented1 topological recovered only {n1}/64 (recovery-schedule recall floor 60)"
);
}