use std::path::{Path, PathBuf};
use image::GrayImage;
use crate::DetectionResult;
use crate::api::{DetectError, Detector};
use crate::detector::DetectConfig;
use crate::target::TargetLayout;
use super::recipe::TargetRecipe;
#[derive(Debug)]
pub enum DetectRunError {
Io {
path: PathBuf,
source: std::io::Error,
},
Image {
path: PathBuf,
message: String,
},
TargetLoad {
path: PathBuf,
message: String,
},
Config(String),
Detect(DetectError),
}
impl std::fmt::Display for DetectRunError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => write!(f, "{}: {source}", path.display()),
Self::Image { path, message } => {
write!(f, "failed to read image {}: {message}", path.display())
}
Self::TargetLoad { path, message } => {
write!(f, "failed to load target {}: {message}", path.display())
}
Self::Config(message) => write!(f, "invalid detect config: {message}"),
Self::Detect(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for DetectRunError {}
pub fn load_target(path: &Path) -> Result<TargetLayout, DetectRunError> {
let text = std::fs::read_to_string(path).map_err(|source| DetectRunError::Io {
path: path.to_path_buf(),
source,
})?;
let is_toml = path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("toml"));
if is_toml {
let recipe = TargetRecipe::parse_toml(&text).map_err(|e| DetectRunError::TargetLoad {
path: path.to_path_buf(),
message: e.to_string(),
})?;
return recipe.to_target().map_err(|e| DetectRunError::TargetLoad {
path: path.to_path_buf(),
message: e.to_string(),
});
}
match TargetLayout::from_json_str(&text) {
Ok(target) => Ok(target),
Err(spec_err) => match TargetRecipe::parse_json(&text).ok().map(|r| r.to_target()) {
Some(Ok(target)) => Ok(target),
_ => Err(DetectRunError::TargetLoad {
path: path.to_path_buf(),
message: spec_err.to_string(),
}),
},
}
}
pub fn build_detector(
target: TargetLayout,
marker_diameter_px: Option<f32>,
strict: bool,
overlay: Option<serde_json::Value>,
) -> Result<Detector, DetectRunError> {
let mut config = match marker_diameter_px {
Some(d) => DetectConfig::from_target_and_marker_diameter(target, d),
None => DetectConfig::from_target(target),
};
if let Some(overlay) = overlay {
config = config
.with_json_overlay(overlay)
.map_err(|e| DetectRunError::Config(e.to_string()))?;
}
config.require_complete_board = config.require_complete_board || strict;
Ok(Detector::with_config(config))
}
pub fn detect_image(
detector: &Detector,
image: &GrayImage,
has_diameter_hint: bool,
) -> Result<DetectionResult, DetectError> {
if has_diameter_hint {
detector.detect(image)
} else {
detector.detect_adaptive(image)
}
}
pub fn detect_file(
detector: &Detector,
image_path: &Path,
has_diameter_hint: bool,
) -> Result<DetectionResult, DetectRunError> {
let image = image::open(image_path)
.map_err(|e| DetectRunError::Image {
path: image_path.to_path_buf(),
message: e.to_string(),
})?
.to_luma8();
detect_image(detector, &image, has_diameter_hint).map_err(DetectRunError::Detect)
}
#[derive(Debug)]
pub struct BatchOutcome {
pub image: PathBuf,
pub result: Option<DetectionResult>,
pub error: Option<String>,
}
pub fn image_files_in_dir(dir: &Path) -> Result<Vec<PathBuf>, DetectRunError> {
let mut files = Vec::new();
let entries = std::fs::read_dir(dir).map_err(|source| DetectRunError::Io {
path: dir.to_path_buf(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| DetectRunError::Io {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
let is_image = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.is_some_and(|e| matches!(e.as_str(), "png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff"));
if is_image {
files.push(path);
}
}
files.sort();
Ok(files)
}
pub fn run_batch(
detector: &Detector,
images: &[PathBuf],
has_diameter_hint: bool,
) -> Vec<BatchOutcome> {
images
.iter()
.map(
|path| match detect_file(detector, path, has_diameter_hint) {
Ok(result) => BatchOutcome {
image: path.clone(),
result: Some(result),
error: None,
},
Err(e) => BatchOutcome {
image: path.clone(),
result: None,
error: Some(e.to_string()),
},
},
)
.collect()
}
pub fn decoded_count(result: &DetectionResult) -> usize {
result
.detected_markers
.iter()
.filter(|m| m.id.is_some())
.count()
}