use crate::error::{EvidenceKind, GridError, GridTask, Result};
use crate::feature::{OrientedFeature, PointFeature};
use crate::lattice::{GridDimensions, LatticeKind};
use crate::result::{GridDetection, GridSolution};
use std::collections::HashSet;
use crate::shared::recovery_schedule::{RecoverySchedule, SquareAxisProvenance};
use crate::shared::validate::ValidationParams;
use crate::topological::TopologicalParams;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Evidence<'a> {
Positions(&'a [PointFeature]),
Oriented1(&'a [OrientedFeature<1>]),
Oriented2(&'a [OrientedFeature<2>]),
Oriented3(&'a [OrientedFeature<3>]),
}
impl Evidence<'_> {
pub fn kind(&self) -> EvidenceKind {
match self {
Self::Positions(_) => EvidenceKind::Positions,
Self::Oriented1(_) => EvidenceKind::Oriented1,
Self::Oriented2(_) => EvidenceKind::Oriented2,
Self::Oriented3(_) => EvidenceKind::Oriented3,
}
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DetectionParams {
max_residual_px: f32,
advanced: Option<Box<DetectionTuning>>,
}
impl Default for DetectionParams {
fn default() -> Self {
Self {
max_residual_px: 2.0,
advanced: None,
}
}
}
impl DetectionParams {
pub fn new(max_residual_px: f32) -> Self {
Self {
max_residual_px,
..Self::default()
}
}
#[must_use]
pub fn with_max_residual_px(mut self, max_residual_px: f32) -> Self {
self.max_residual_px = max_residual_px;
self
}
#[must_use]
pub fn with_advanced(mut self, tuning: DetectionTuning) -> Self {
self.advanced = Some(Box::new(tuning));
self
}
pub fn max_residual_px(&self) -> f32 {
self.max_residual_px
}
pub(crate) fn tuning(&self) -> &DetectionTuning {
self.advanced.as_deref().unwrap_or(&DEFAULT_TUNING)
}
}
static DEFAULT_TUNING: std::sync::LazyLock<DetectionTuning> =
std::sync::LazyLock::new(DetectionTuning::default);
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct DetectionTuning {
pub topological: TopologicalParams,
pub validation: ValidationParams,
pub recovery: RecoverySchedule,
}
impl DetectionTuning {
#[must_use]
pub fn with_topological(mut self, value: TopologicalParams) -> Self {
self.topological = value;
self
}
#[must_use]
pub fn with_validation(mut self, value: ValidationParams) -> Self {
self.validation = value;
self
}
#[must_use]
pub fn with_recovery(mut self, value: RecoverySchedule) -> Self {
self.recovery = value;
self
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DetectionRequest<'a> {
lattice: LatticeKind,
evidence: Evidence<'a>,
dimensions: Option<GridDimensions>,
params: DetectionParams,
}
impl<'a> DetectionRequest<'a> {
pub fn new(lattice: LatticeKind, evidence: Evidence<'a>) -> Self {
Self {
lattice,
evidence,
dimensions: None,
params: DetectionParams::default(),
}
}
#[must_use]
pub fn with_dimensions(mut self, dimensions: GridDimensions) -> Self {
self.dimensions = Some(dimensions);
self
}
#[must_use]
pub fn with_params(mut self, params: DetectionParams) -> Self {
self.params = params;
self
}
pub(crate) fn lattice(&self) -> LatticeKind {
self.lattice
}
pub(crate) fn evidence(&self) -> Evidence<'a> {
self.evidence
}
pub(crate) fn dimensions(&self) -> Option<GridDimensions> {
self.dimensions
}
pub(crate) fn params(&self) -> &DetectionParams {
&self.params
}
}
pub fn detect_grid(request: DetectionRequest<'_>) -> Result<GridDetection> {
let mut detections = detect_grid_all(request)?;
if detections.is_empty() {
Err(GridError::InsufficientEvidence)
} else {
Ok(detections.remove(0))
}
}
fn run_square_oriented2(
features: &[OrientedFeature<2>],
request: &DetectionRequest<'_>,
axis_provenance: SquareAxisProvenance,
) -> Result<Vec<GridSolution>> {
crate::topological::detect_square_oriented2_all(
features,
request.dimensions(),
request.params(),
axis_provenance,
)
}
fn run_hex_oriented3(
features: &[OrientedFeature<3>],
request: &DetectionRequest<'_>,
) -> Result<Vec<GridSolution>> {
crate::topological::detect_hex_oriented3_topological_all(
features,
request.dimensions(),
request.params(),
)
}
pub fn detect_grid_all(request: DetectionRequest<'_>) -> Result<Vec<GridDetection>> {
Ok(detect_grid_all_internal(request)?
.into_iter()
.map(|solution| solution.detection)
.collect())
}
pub(crate) fn detect_grid_all_internal(request: DetectionRequest<'_>) -> Result<Vec<GridSolution>> {
validate_request(&request)?;
let solutions = match (request.lattice(), request.evidence()) {
(LatticeKind::Square, Evidence::Oriented2(features)) => {
run_square_oriented2(features, &request, SquareAxisProvenance::FullyMeasured)?
}
(LatticeKind::Square, Evidence::Positions(features)) => {
let oriented = crate::orient::synthesize_oriented2(features);
run_square_oriented2(
&oriented,
&request,
SquareAxisProvenance::IncludesSynthesized,
)?
}
(LatticeKind::Square, Evidence::Oriented1(features)) => {
let oriented = crate::orient::synthesize_oriented2_from_oriented1(features);
run_square_oriented2(
&oriented,
&request,
SquareAxisProvenance::IncludesSynthesized,
)?
}
(LatticeKind::Hex, Evidence::Oriented3(features)) => {
run_hex_oriented3(features, &request)?
}
(LatticeKind::Hex, Evidence::Positions(features)) => {
let oriented = crate::orient::synthesize_oriented3(features);
run_hex_oriented3(&oriented, &request)?
}
(LatticeKind::Hex, Evidence::Oriented1(features)) => {
let oriented = crate::orient::synthesize_oriented3_from_oriented1(features);
run_hex_oriented3(&oriented, &request)?
}
_ => {
return Err(GridError::UnsupportedCombination {
task: GridTask::Detection,
lattice: request.lattice(),
evidence: request.evidence().kind(),
})
}
};
Ok(solutions)
}
pub(crate) fn validate_request(request: &DetectionRequest<'_>) -> Result<()> {
if let Some(dimensions) = request.dimensions() {
if dimensions.width == 0 || dimensions.height == 0 {
return Err(GridError::InconsistentInput(
"grid dimensions count feature positions and must be non-zero".to_owned(),
));
}
}
let residual = request.params().max_residual_px();
if residual.is_nan() || residual < 0.0 {
return Err(GridError::InconsistentInput(
"max_residual_px must be non-negative or +infinity".to_owned(),
));
}
validate_tuning(request.params().tuning())?;
match request.evidence() {
Evidence::Positions(features) => validate_points(features.iter()),
Evidence::Oriented1(features) => validate_oriented(features),
Evidence::Oriented2(features) => validate_oriented(features),
Evidence::Oriented3(features) => validate_oriented(features),
}
}
fn validate_points<'a>(points: impl Iterator<Item = &'a PointFeature>) -> Result<()> {
let mut source_indices = HashSet::new();
for point in points {
if !point.position.x.is_finite() || !point.position.y.is_finite() {
return Err(GridError::InconsistentInput(format!(
"feature {} has a non-finite image position",
point.source_index
)));
}
if !source_indices.insert(point.source_index) {
return Err(GridError::InconsistentInput(format!(
"duplicate feature source_index {}",
point.source_index
)));
}
}
Ok(())
}
fn validate_oriented<const N: usize>(features: &[OrientedFeature<N>]) -> Result<()> {
validate_points(features.iter().map(|feature| &feature.point))?;
for feature in features {
for (slot, axis) in feature.axes.iter().enumerate() {
if !axis.angle_rad.is_finite() {
return Err(GridError::InconsistentInput(format!(
"feature {} axis {slot} has a non-finite angle",
feature.point.source_index
)));
}
if axis
.sigma_rad
.is_some_and(|sigma| !sigma.is_finite() || sigma < 0.0)
{
return Err(GridError::InconsistentInput(format!(
"feature {} axis {slot} has an invalid sigma",
feature.point.source_index
)));
}
}
}
Ok(())
}
fn validate_tuning(tuning: &DetectionTuning) -> Result<()> {
let topo = &tuning.topological;
let finite_positive = |value: f32| value.is_finite() && value > 0.0;
if !finite_positive(topo.axis_align_tol_rad)
|| !finite_positive(topo.max_axis_sigma_rad)
|| !finite_positive(topo.cluster_axis_tol_rad)
|| !topo.opposing_edge_ratio_max.is_finite()
|| topo.opposing_edge_ratio_max < 1.0
|| !topo.edge_length_min_rel.is_finite()
|| topo.edge_length_min_rel < 0.0
|| topo.edge_length_max_rel.is_nan()
|| topo.edge_length_max_rel <= 0.0
|| topo.edge_length_min_rel > topo.edge_length_max_rel
|| topo.min_corners_for_component < 4
|| topo.min_quads_per_component == 0
|| topo
.axis_cluster_centers
.is_some_and(|centers| centers.iter().any(|center| !center.is_finite()))
{
return Err(GridError::InconsistentInput(
"invalid expert topological tuning".to_owned(),
));
}
let validation = &tuning.validation;
let non_negative_or_inf = |value: f32| !value.is_nan() && value >= 0.0;
if !non_negative_or_inf(validation.line_tol_rel)
|| validation.line_min_members < 2
|| !non_negative_or_inf(validation.local_h_tol_rel)
|| !validation.step_deviation_thresh_rel.is_finite()
|| validation.step_deviation_thresh_rel < 0.0
{
return Err(GridError::InconsistentInput(
"invalid expert validation tuning".to_owned(),
));
}
Ok(())
}