use std::cmp::Ordering;
use super::consistency::{consistency_evidence_for_id, should_clear_by_consistency};
use super::index::dist2;
use super::vote::{VoteOutcome, gather_trusted_neighbors_local_scale, vote_for_candidate};
use super::workspace::{IdCorrectionWorkspace, is_soft_locked_assignment, marker_center_is_finite};
pub(super) fn estimate_adjacent_spacing_px(ws: &IdCorrectionWorkspace<'_>) -> Option<f64> {
let mut dists = Vec::<f64>::new();
for i in 0..ws.markers.len() {
let Some(id_i) = ws.markers[i].id else {
continue;
};
if !ws.board_index.id_to_xy.contains_key(&id_i) || !marker_center_is_finite(&ws.markers[i])
{
continue;
}
for j in (i + 1)..ws.markers.len() {
let Some(id_j) = ws.markers[j].id else {
continue;
};
if !ws.board_index.id_to_xy.contains_key(&id_j)
|| !marker_center_is_finite(&ws.markers[j])
{
continue;
}
if !ws.board_index.are_neighbors(id_i, id_j) {
continue;
}
let d = dist2(ws.markers[i].center, ws.markers[j].center).sqrt();
if d.is_finite() && d > 1.0 {
dists.push(d);
}
}
}
if dists.is_empty() {
return None;
}
dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let mid = dists.len() / 2;
Some(if dists.len().is_multiple_of(2) {
0.5 * (dists[mid - 1] + dists[mid])
} else {
dists[mid]
})
}
pub(super) fn diagnose_unverified_reasons(
ws: &mut IdCorrectionWorkspace<'_>,
final_outer_mul: f64,
) {
let tolerance_mm = ws.board_index.pitch_mm * 0.6;
for i in 0..ws.markers.len() {
if ws.trust[i].is_trusted() || !marker_center_is_finite(&ws.markers[i]) {
continue;
}
let neighbors = gather_trusted_neighbors_local_scale(
i,
ws.markers,
&ws.trust,
&ws.board_index,
&ws.outer_radii_px,
final_outer_mul,
);
if neighbors.is_empty() {
ws.stats.n_unverified_no_neighbors += 1;
continue;
}
let effective_min_votes = ws.config.effective_min_votes(ws.markers[i].id.is_some());
let out = vote_for_candidate(
ws.markers[i].center,
ws.outer_radii_px[i],
&neighbors,
&ws.board_index,
tolerance_mm,
effective_min_votes,
ws.config.min_vote_weight_frac,
);
match out {
VoteOutcome::NoVotes | VoteOutcome::InsufficientVotes { .. } => {
ws.stats.n_unverified_no_votes += 1;
}
VoteOutcome::GateRejected { .. } => {
ws.stats.n_unverified_gate_rejects += 1;
}
VoteOutcome::Candidate { .. } => {}
}
}
}
pub(super) fn count_inconsistent_remaining(ws: &IdCorrectionWorkspace<'_>) -> usize {
let mut n = 0usize;
for i in 0..ws.markers.len() {
let Some(id) = ws.markers[i].id else {
continue;
};
if !ws.board_index.id_to_xy.contains_key(&id) {
n += 1;
continue;
}
let evidence = consistency_evidence_for_id(ws, i, id);
let is_soft_locked = is_soft_locked_assignment(
&ws.markers[i],
ws.config.soft_lock_exact_decode,
ws.codebook_min_cyclic_dist,
);
if should_clear_by_consistency(evidence, is_soft_locked, ws.config) {
n += 1;
}
}
n
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::config::IdCorrectionConfig;
use crate::detector::id_correction::index::BoardIndex;
use crate::detector::marker_build::MarkerRecord;
use crate::marker::codec::Codebook;
use crate::target::TargetLayout;
fn marker(id: Option<usize>, center: [f64; 2]) -> MarkerRecord {
MarkerRecord {
id,
center,
confidence: 0.9,
..MarkerRecord::default()
}
}
fn min_cyclic() -> usize {
Codebook::default().min_cyclic_dist()
}
fn adjacent_pair(board_index: &BoardIndex) -> (usize, usize) {
let (&id_a, nbrs) = board_index
.board_neighbors
.iter()
.find(|(_, nbrs)| !nbrs.is_empty())
.expect("hex board has an adjacent pair");
(id_a, nbrs[0])
}
#[test]
fn adjacent_spacing_is_the_pair_center_distance() {
let board = TargetLayout::default_hex();
let cfg = IdCorrectionConfig::default();
let board_index = BoardIndex::build(&board);
let (id_a, id_b) = adjacent_pair(&board_index);
let mut markers = vec![
marker(Some(id_a), [10.0, 10.0]),
marker(Some(id_b), [50.0, 10.0]),
];
let ws = IdCorrectionWorkspace::new(&mut markers, &board, &cfg, min_cyclic());
let spacing = estimate_adjacent_spacing_px(&ws).expect("adjacent pair yields a spacing");
assert!((spacing - 40.0).abs() < 1e-9, "spacing = {spacing}");
}
#[test]
fn adjacent_spacing_is_none_without_adjacent_pairs() {
let board = TargetLayout::default_hex();
let cfg = IdCorrectionConfig::default();
let mut markers = vec![marker(Some(0), [10.0, 10.0])];
let ws = IdCorrectionWorkspace::new(&mut markers, &board, &cfg, min_cyclic());
assert!(estimate_adjacent_spacing_px(&ws).is_none());
}
#[test]
fn count_inconsistent_flags_off_board_ids() {
let board = TargetLayout::default_hex();
let cfg = IdCorrectionConfig::default();
let off_board = board.n_cells() + 100_000;
let mut markers = vec![marker(Some(off_board), [10.0, 10.0])];
let ws = IdCorrectionWorkspace::new(&mut markers, &board, &cfg, min_cyclic());
assert_eq!(count_inconsistent_remaining(&ws), 1);
}
#[test]
fn count_inconsistent_ignores_isolated_valid_marker() {
let board = TargetLayout::default_hex();
let cfg = IdCorrectionConfig::default();
let mut markers = vec![marker(Some(0), [10.0, 10.0])];
let ws = IdCorrectionWorkspace::new(&mut markers, &board, &cfg, min_cyclic());
assert_eq!(count_inconsistent_remaining(&ws), 0);
}
}