use nalgebra::{Matrix3, Point2};
use projective_grid::{
detect_regular_grid, ExtensionStrategy, RegularGridDetector, RegularGridError,
RegularGridParams,
};
fn perspective_warped_grid(rows: i32, cols: i32, scale: f32) -> Vec<Point2<f32>> {
let h = Matrix3::new(
1.0_f32 * scale,
0.10 * scale,
50.0,
0.05 * scale,
1.0 * scale,
50.0,
2e-4,
1e-4,
1.0,
);
let mut out = Vec::with_capacity((rows * cols) as usize);
for j in 0..rows {
for i in 0..cols {
let x = i as f32;
let y = j as f32;
let w = h[(2, 0)] * x + h[(2, 1)] * y + h[(2, 2)];
let xp = (h[(0, 0)] * x + h[(0, 1)] * y + h[(0, 2)]) / w;
let yp = (h[(1, 0)] * x + h[(1, 1)] * y + h[(1, 2)]) / w;
out.push(Point2::new(xp, yp));
}
}
out
}
fn axis_aligned_grid(rows: i32, cols: i32, s: f32, ox: f32, oy: f32) -> Vec<Point2<f32>> {
let mut out = Vec::new();
for j in 0..rows {
for i in 0..cols {
out.push(Point2::new(i as f32 * s + ox, j as f32 * s + oy));
}
}
out
}
#[test]
fn clean_axis_aligned_grid_is_fully_labelled() {
let pts = axis_aligned_grid(7, 7, 25.0, 60.0, 60.0);
let grid = detect_regular_grid(&pts).expect("clean grid detects");
assert_eq!(grid.points.len(), 49, "every corner must be labelled");
let min_i = grid.points.iter().map(|p| p.grid.0).min().unwrap();
let min_j = grid.points.iter().map(|p| p.grid.1).min().unwrap();
assert_eq!((min_i, min_j), (0, 0));
for p in &grid.points {
assert_eq!(p.position, pts[p.source_index]);
}
}
#[test]
fn rotated_grid_is_recovered() {
let theta = 37.0_f32.to_radians();
let (c, s) = (theta.cos(), theta.sin());
let mut pts = Vec::new();
for j in 0..6 {
for i in 0..6 {
let (x, y) = (i as f32 * 22.0, j as f32 * 22.0);
pts.push(Point2::new(x * c - y * s + 150.0, x * s + y * c + 150.0));
}
}
let grid = detect_regular_grid(&pts).expect("rotated grid detects");
assert!(
grid.points.len() >= 30,
"expected most of the rotated grid; got {}",
grid.points.len()
);
}
#[test]
fn perspective_warped_grid_is_recovered() {
let pts = perspective_warped_grid(7, 7, 30.0);
let grid = detect_regular_grid(&pts).expect("warped grid detects");
assert!(
grid.points.len() >= 40,
"expected the bulk of the warped grid; got {}",
grid.points.len()
);
}
#[test]
fn missing_interior_points_do_not_break_detection() {
let full = axis_aligned_grid(7, 7, 25.0, 60.0, 60.0);
let cols = 7;
let drop: [(i32, i32); 4] = [(2, 2), (3, 3), (4, 2), (3, 4)];
let pts: Vec<Point2<f32>> = full
.iter()
.enumerate()
.filter(|(idx, _)| {
let i = (*idx as i32) % cols;
let j = (*idx as i32) / cols;
!drop.contains(&(i, j))
})
.map(|(_, &p)| p)
.collect();
assert_eq!(pts.len(), 45);
let grid = detect_regular_grid(&pts).expect("holey grid detects");
assert!(grid.points.len() <= 45);
assert!(
grid.points.len() >= 40,
"expected most of the holey grid; got {}",
grid.points.len()
);
for p in &grid.points {
assert_eq!(p.position, pts[p.source_index]);
}
}
#[test]
fn spurious_off_grid_points_are_pruned() {
let mut pts = axis_aligned_grid(6, 6, 25.0, 80.0, 80.0);
let on_grid = pts.len();
pts.push(Point2::new(7.0, 9.0));
pts.push(Point2::new(400.0, 11.0));
pts.push(Point2::new(13.0, 380.0));
pts.push(Point2::new(500.0, 500.0));
pts.push(Point2::new(250.0, 12.0));
let grid = detect_regular_grid(&pts).expect("grid with noise detects");
assert!(
grid.points.len() <= on_grid,
"spurious points must not be labelled: got {} (on-grid {})",
grid.points.len(),
on_grid
);
assert!(grid.points.len() >= 30, "got {}", grid.points.len());
}
#[test]
fn two_disjoint_components_are_returned_by_detect_all() {
let mut pts = axis_aligned_grid(5, 5, 20.0, 100.0, 100.0);
pts.extend(axis_aligned_grid(4, 4, 20.0, 700.0, 700.0));
let detector = RegularGridDetector::default();
let detections = detector.detect_all(&pts);
assert_eq!(
detections.len(),
2,
"expected two components, got {}",
detections.len()
);
let total: usize = detections.iter().map(|d| d.points.len()).sum();
assert_eq!(total, pts.len(), "every corner accounted for");
let mut seen = std::collections::HashSet::new();
for d in &detections {
for p in &d.points {
assert!(
seen.insert(p.source_index),
"index reused across components"
);
}
}
}
#[test]
fn top_left_canonicalization_orients_axes() {
let mut pts = Vec::new();
for j in 0..5 {
for i in 0..5 {
let x = 300.0 - j as f32 * 20.0;
let y = 100.0 + i as f32 * 20.0;
pts.push(Point2::new(x, y));
}
}
let grid = detect_regular_grid(&pts).expect("detects");
assert!(grid.stats.canonicalized);
let at = |gi: i32, gj: i32| {
grid.points
.iter()
.find(|p| p.grid == (gi, gj))
.map(|p| p.position)
};
let p00 = at(0, 0).expect("(0,0)");
let p10 = at(1, 0).expect("(1,0)");
let p01 = at(0, 1).expect("(0,1)");
assert!(p10.x > p00.x, "+i must point right after canonicalisation");
assert!(p01.y > p00.y, "+j must point down after canonicalisation");
assert!(grid.axis_i.x > 0.0, "axis_i should point +x");
assert!(grid.axis_j.y > 0.0, "axis_j should point +y");
}
#[test]
fn canonicalization_can_be_disabled() {
let pts = axis_aligned_grid(5, 5, 25.0, 50.0, 50.0);
let params = RegularGridParams::default().with_canonicalize_top_left(false);
let grid = RegularGridDetector::new(params)
.detect(&pts)
.expect("detects");
assert!(!grid.stats.canonicalized);
assert_eq!(grid.points.len(), 25);
}
#[test]
fn extension_disabled_still_recovers_clean_grid() {
let pts = axis_aligned_grid(6, 6, 25.0, 50.0, 50.0);
let params = RegularGridParams::default().with_extension(ExtensionStrategy::Disabled);
let grid = RegularGridDetector::new(params)
.detect(&pts)
.expect("detects");
assert_eq!(grid.points.len(), 36);
}
#[test]
fn too_few_points_returns_err() {
let pts = vec![Point2::new(0.0, 0.0), Point2::new(10.0, 0.0)];
assert_eq!(
detect_regular_grid(&pts).unwrap_err(),
RegularGridError::TooFewPoints { found: 2 }
);
}
#[test]
fn collinear_cloud_yields_no_grid_found() {
let pts: Vec<Point2<f32>> = (0..8).map(|i| Point2::new(i as f32 * 12.0, 0.0)).collect();
assert_eq!(
detect_regular_grid(&pts).unwrap_err(),
RegularGridError::NoGridFound
);
}
#[test]
fn coincident_cloud_is_degenerate() {
let pts = vec![Point2::new(5.0, 5.0); 6];
assert_eq!(
detect_regular_grid(&pts).unwrap_err(),
RegularGridError::DegeneratePointCloud
);
}