use visual_cortex_capture::FrameView;
use crate::ocr::TextSpan;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DetectorOutput {
Bool(bool),
Text(String),
Number(f64),
Score(f32),
Spans(Vec<TextSpan>),
None,
}
impl PartialEq for DetectorOutput {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(DetectorOutput::Bool(a), DetectorOutput::Bool(b)) => a == b,
(DetectorOutput::Text(a), DetectorOutput::Text(b)) => a == b,
(DetectorOutput::Number(a), DetectorOutput::Number(b)) => a == b,
(DetectorOutput::Score(a), DetectorOutput::Score(b)) => a == b,
(DetectorOutput::Spans(a), DetectorOutput::Spans(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.text == y.text)
}
(DetectorOutput::None, DetectorOutput::None) => true,
_ => false,
}
}
}
impl DetectorOutput {
pub fn as_number(&self) -> Option<f64> {
match self {
DetectorOutput::Number(n) => Some(*n),
DetectorOutput::Score(s) => Some(*s as f64),
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum DetectorError {
#[error("ocr error: {0}")]
Ocr(String),
#[error("{0}")]
Other(String),
}
pub trait Detector: Send + 'static {
fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError>;
fn is_heavy(&self) -> bool {
false
}
}
pub struct FnDetector<F>(F);
impl<F> Detector for FnDetector<F>
where
F: FnMut(&FrameView<'_>) -> Result<DetectorOutput, DetectorError> + Send + 'static,
{
fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError> {
(self.0)(view)
}
}
pub fn from_fn<F>(f: F) -> FnDetector<F>
where
F: FnMut(&FrameView<'_>) -> Result<DetectorOutput, DetectorError> + Send + 'static,
{
FnDetector(f)
}
#[cfg(test)]
mod tests {
use super::*;
use visual_cortex_capture::{Frame, PxRect};
#[test]
fn from_fn_adapts_a_closure() {
let mut det =
from_fn(|view: &FrameView<'_>| Ok(DetectorOutput::Number(view.width() as f64)));
let frame = Frame::solid(8, 8, [0, 0, 0, 255]);
let view = frame
.view(PxRect {
x: 0,
y: 0,
w: 4,
h: 4,
})
.unwrap();
assert_eq!(det.evaluate(&view).unwrap(), DetectorOutput::Number(4.0));
}
#[test]
fn spans_equality_is_text_only() {
use crate::ocr::TextSpan;
use visual_cortex_capture::PxRect;
let span = |text: &str, x: u32, conf: f32| TextSpan {
text: text.to_string(),
confidence: conf,
bbox: PxRect {
x,
y: 0,
w: 10,
h: 8,
},
};
let a = DetectorOutput::Spans(vec![span("Doombringer", 2, 0.98)]);
let b = DetectorOutput::Spans(vec![span("Doombringer", 3, 0.91)]);
assert_eq!(a, b, "bbox/confidence jitter must not register as change");
let c = DetectorOutput::Spans(vec![span("Stormclaw", 2, 0.98)]);
assert_ne!(a, c);
let d = DetectorOutput::Spans(vec![span("Doombringer", 2, 0.98), span("Sword", 2, 0.98)]);
assert_ne!(a, d);
assert_ne!(a, DetectorOutput::Text("Doombringer".to_string()));
assert_eq!(a.as_number(), None);
}
#[test]
fn non_spans_equality_semantics_unchanged() {
assert_eq!(DetectorOutput::Bool(true), DetectorOutput::Bool(true));
assert_ne!(DetectorOutput::Bool(true), DetectorOutput::Bool(false));
assert_eq!(
DetectorOutput::Text("x".to_string()),
DetectorOutput::Text("x".to_string())
);
assert_eq!(DetectorOutput::Number(3.5), DetectorOutput::Number(3.5));
assert_ne!(DetectorOutput::Number(3.5), DetectorOutput::Score(3.5));
assert_eq!(DetectorOutput::None, DetectorOutput::None);
}
#[test]
fn as_number_extracts_numeric_outputs() {
assert_eq!(DetectorOutput::Number(3.5).as_number(), Some(3.5));
assert_eq!(DetectorOutput::Score(0.5).as_number(), Some(0.5));
assert_eq!(DetectorOutput::Bool(true).as_number(), None);
assert_eq!(DetectorOutput::Text("7".into()).as_number(), None);
}
#[test]
fn detector_trait_is_object_safe() {
let mut det: Box<dyn Detector> = Box::new(from_fn(|_| Ok(DetectorOutput::Bool(true))));
let frame = Frame::solid(2, 2, [0, 0, 0, 255]);
let view = frame
.view(PxRect {
x: 0,
y: 0,
w: 2,
h: 2,
})
.unwrap();
assert_eq!(det.evaluate(&view).unwrap(), DetectorOutput::Bool(true));
}
}