use std::collections::HashMap;
use std::collections::HashSet;
use nalgebra::{Point2, Vector2};
use crate::square::extension::{
extend_via_global_homography, extend_via_local_homography, ExtensionParams, ExtensionStats,
LocalExtensionParams,
};
use crate::square::fill::{fill_grid_holes, FillParams, FillStats};
use crate::square::grow::{
bfs_grow, Admit, FillEdgeCtx, GrowParams, GrowResult, GrowValidator, LabelledNeighbour,
};
use crate::square::seed::finder::{find_quad, SeedQuadParams, SeedQuadValidator};
use crate::square::seed::{Seed, SeedOutput};
use crate::square::validate::{validate, LabelledEntry, ValidationParams, ValidationResult};
use crate::topological::AxisEstimate;
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub enum ExtensionStrategy {
Disabled,
Global(ExtensionParams),
Local(LocalExtensionParams),
}
impl Default for ExtensionStrategy {
fn default() -> Self {
ExtensionStrategy::Global(ExtensionParams::default())
}
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct SquareGridParams {
pub seed: SeedQuadParams,
pub grow: GrowParams,
pub extension: ExtensionStrategy,
pub fill: Option<FillParams>,
pub validate: Option<ValidationParams>,
}
impl Default for SquareGridParams {
fn default() -> Self {
Self {
seed: SeedQuadParams::default(),
grow: GrowParams::default(),
extension: ExtensionStrategy::default(),
fill: Some(FillParams::default()),
validate: Some(ValidationParams::default()),
}
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct SquareGridDetection {
pub labelled: HashMap<(i32, i32), usize>,
pub by_corner: HashMap<usize, (i32, i32)>,
pub axis_i: Vector2<f32>,
pub axis_j: Vector2<f32>,
pub cell_size: f32,
pub stats: SquareGridStats,
}
#[non_exhaustive]
#[derive(Debug, Default)]
pub struct SquareGridStats {
pub seed: Option<[usize; 4]>,
pub grown: usize,
pub extension: Option<ExtensionStats>,
pub fill: Option<FillStats>,
pub validation: Option<ValidationResult>,
pub dropped_by_validation: usize,
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "info",
skip_all,
fields(num_corners = positions.len()),
)
)]
pub fn detect_square_grid<S, G>(
positions: &[Point2<f32>],
seed_validator: &S,
grow_validator: &G,
params: &SquareGridParams,
) -> Option<SquareGridDetection>
where
S: SeedQuadValidator,
G: GrowValidator,
{
let mut stats = SquareGridStats::default();
let SeedOutput { seed, cell_size } = find_quad(seed_validator, ¶ms.seed)?;
stats.seed = Some([seed.a, seed.b, seed.c, seed.d]);
let mut grow_res: GrowResult =
bfs_grow(positions, seed, cell_size, ¶ms.grow, grow_validator);
stats.grown = grow_res.labelled.len().saturating_sub(4);
match ¶ms.extension {
ExtensionStrategy::Disabled => {}
ExtensionStrategy::Global(extension_params) => {
let ext = extend_via_global_homography(
positions,
&mut grow_res,
cell_size,
extension_params,
grow_validator,
);
stats.extension = Some(ext);
}
ExtensionStrategy::Local(extension_params) => {
let ext = extend_via_local_homography(
positions,
&mut grow_res,
cell_size,
extension_params,
grow_validator,
);
stats.extension = Some(ext);
}
}
if let Some(fill_params) = params.fill.as_ref() {
let fill = fill_grid_holes(
positions,
&mut grow_res,
cell_size,
fill_params,
grow_validator,
);
stats.fill = Some(fill);
}
if let Some(validate_params) = params.validate.as_ref() {
let entries: Vec<LabelledEntry> = grow_res
.labelled
.iter()
.map(|(&(i, j), &idx)| LabelledEntry {
idx,
pixel: positions[idx],
grid: (i, j),
})
.collect();
let result = validate(&entries, cell_size, validate_params);
for &idx in &result.blacklist {
if let Some(&cell) = grow_res.by_corner.get(&idx) {
grow_res.labelled.remove(&cell);
}
grow_res.by_corner.remove(&idx);
}
stats.dropped_by_validation = result.blacklist.len();
stats.validation = Some(result);
}
Some(SquareGridDetection {
labelled: grow_res.labelled,
by_corner: grow_res.by_corner,
axis_i: grow_res.axis_i,
axis_j: grow_res.axis_j,
cell_size,
stats,
})
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug)]
pub struct MultiComponentParams {
pub max_components: usize,
pub min_corners_per_component: usize,
}
impl Default for MultiComponentParams {
fn default() -> Self {
Self {
max_components: 4,
min_corners_per_component: 4,
}
}
}
pub fn detect_square_grid_all<S, G>(
positions: &[Point2<f32>],
seed_validator: &S,
grow_validator: &G,
params: &SquareGridParams,
multi: &MultiComponentParams,
) -> Vec<SquareGridDetection>
where
S: SeedQuadValidator,
G: GrowValidator,
{
let mut consumed: HashSet<usize> = HashSet::new();
let mut detections: Vec<SquareGridDetection> = Vec::new();
while detections.len() < multi.max_components {
let wrapped_seed = ExcludeConsumedSeed {
inner: seed_validator,
consumed: &consumed,
};
let wrapped_grow = ExcludeConsumedGrow {
inner: grow_validator,
consumed: &consumed,
};
let Some(det) = detect_square_grid(positions, &wrapped_seed, &wrapped_grow, params) else {
break;
};
if det.labelled.len() < multi.min_corners_per_component {
break;
}
for (_, &corner_idx) in det.labelled.iter() {
consumed.insert(corner_idx);
}
detections.push(det);
}
detections
}
struct ExcludeConsumedSeed<'a, S> {
inner: &'a S,
consumed: &'a HashSet<usize>,
}
impl<'a, S: SeedQuadValidator> SeedQuadValidator for ExcludeConsumedSeed<'a, S> {
fn position(&self, idx: usize) -> Point2<f32> {
self.inner.position(idx)
}
fn axes(&self, idx: usize) -> [AxisEstimate; 2] {
self.inner.axes(idx)
}
fn a_candidates(&self) -> Vec<usize> {
self.inner
.a_candidates()
.into_iter()
.filter(|i| !self.consumed.contains(i))
.collect()
}
fn bc_candidates(&self) -> Vec<usize> {
self.inner
.bc_candidates()
.into_iter()
.filter(|i| !self.consumed.contains(i))
.collect()
}
fn edge_ok(&self, from: usize, to: usize, axis_tol_rad: f32) -> bool {
self.inner.edge_ok(from, to, axis_tol_rad)
}
fn has_midpoint_violation(&self, seed: Seed, cell_size: f32) -> bool {
self.inner.has_midpoint_violation(seed, cell_size)
}
}
struct ExcludeConsumedGrow<'a, G> {
inner: &'a G,
consumed: &'a HashSet<usize>,
}
impl<'a, G: GrowValidator> GrowValidator for ExcludeConsumedGrow<'a, G> {
fn is_eligible(&self, idx: usize) -> bool {
!self.consumed.contains(&idx) && self.inner.is_eligible(idx)
}
fn required_label_at(&self, i: i32, j: i32) -> Option<u8> {
self.inner.required_label_at(i, j)
}
fn label_of(&self, idx: usize) -> Option<u8> {
self.inner.label_of(idx)
}
fn accept_candidate(
&self,
idx: usize,
at: (i32, i32),
prediction: Point2<f32>,
neighbours: &[LabelledNeighbour],
) -> Admit {
if self.consumed.contains(&idx) {
return Admit::Reject;
}
self.inner.accept_candidate(idx, at, prediction, neighbours)
}
fn edge_ok(
&self,
candidate_idx: usize,
neighbour_idx: usize,
at_candidate: (i32, i32),
at_neighbour: (i32, i32),
) -> bool {
self.inner
.edge_ok(candidate_idx, neighbour_idx, at_candidate, at_neighbour)
}
fn eligible_for_fill(&self, idx: usize) -> bool {
!self.consumed.contains(&idx) && self.inner.eligible_for_fill(idx)
}
fn fill_edge_ok(&self, ctx: FillEdgeCtx<'_>) -> bool {
self.inner.fill_edge_ok(ctx)
}
}