1use visual_cortex_capture::FrameView;
2
3use crate::ocr::TextSpan;
4
5#[derive(Debug, Clone)]
7#[non_exhaustive]
8pub enum DetectorOutput {
9 Bool(bool),
11 Text(String),
13 Number(f64),
19 Score(f32),
22 Spans(Vec<TextSpan>),
30 None,
32}
33
34impl PartialEq for DetectorOutput {
37 fn eq(&self, other: &Self) -> bool {
38 match (self, other) {
39 (DetectorOutput::Bool(a), DetectorOutput::Bool(b)) => a == b,
40 (DetectorOutput::Text(a), DetectorOutput::Text(b)) => a == b,
41 (DetectorOutput::Number(a), DetectorOutput::Number(b)) => a == b,
42 (DetectorOutput::Score(a), DetectorOutput::Score(b)) => a == b,
43 (DetectorOutput::Spans(a), DetectorOutput::Spans(b)) => {
44 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.text == y.text)
45 }
46 (DetectorOutput::None, DetectorOutput::None) => true,
47 _ => false,
48 }
49 }
50}
51
52impl DetectorOutput {
53 pub fn as_number(&self) -> Option<f64> {
56 match self {
57 DetectorOutput::Number(n) => Some(*n),
58 DetectorOutput::Score(s) => Some(*s as f64),
59 _ => None,
60 }
61 }
62}
63
64#[derive(Debug, thiserror::Error)]
65pub enum DetectorError {
66 #[error("ocr error: {0}")]
67 Ocr(String),
68 #[error("{0}")]
69 Other(String),
70}
71
72pub trait Detector: Send + 'static {
75 fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError>;
76
77 fn is_heavy(&self) -> bool {
81 false
82 }
83}
84
85pub struct FnDetector<F>(F);
87
88impl<F> Detector for FnDetector<F>
89where
90 F: FnMut(&FrameView<'_>) -> Result<DetectorOutput, DetectorError> + Send + 'static,
91{
92 fn evaluate(&mut self, view: &FrameView<'_>) -> Result<DetectorOutput, DetectorError> {
93 (self.0)(view)
94 }
95}
96
97pub fn from_fn<F>(f: F) -> FnDetector<F>
99where
100 F: FnMut(&FrameView<'_>) -> Result<DetectorOutput, DetectorError> + Send + 'static,
101{
102 FnDetector(f)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use visual_cortex_capture::{Frame, PxRect};
109
110 #[test]
111 fn from_fn_adapts_a_closure() {
112 let mut det =
113 from_fn(|view: &FrameView<'_>| Ok(DetectorOutput::Number(view.width() as f64)));
114 let frame = Frame::solid(8, 8, [0, 0, 0, 255]);
115 let view = frame
116 .view(PxRect {
117 x: 0,
118 y: 0,
119 w: 4,
120 h: 4,
121 })
122 .unwrap();
123 assert_eq!(det.evaluate(&view).unwrap(), DetectorOutput::Number(4.0));
124 }
125
126 #[test]
127 fn spans_equality_is_text_only() {
128 use crate::ocr::TextSpan;
129 use visual_cortex_capture::PxRect;
130 let span = |text: &str, x: u32, conf: f32| TextSpan {
131 text: text.to_string(),
132 confidence: conf,
133 bbox: PxRect {
134 x,
135 y: 0,
136 w: 10,
137 h: 8,
138 },
139 };
140 let a = DetectorOutput::Spans(vec![span("Doombringer", 2, 0.98)]);
142 let b = DetectorOutput::Spans(vec![span("Doombringer", 3, 0.91)]);
143 assert_eq!(a, b, "bbox/confidence jitter must not register as change");
144 let c = DetectorOutput::Spans(vec![span("Stormclaw", 2, 0.98)]);
146 assert_ne!(a, c);
147 let d = DetectorOutput::Spans(vec![span("Doombringer", 2, 0.98), span("Sword", 2, 0.98)]);
149 assert_ne!(a, d);
150 assert_ne!(a, DetectorOutput::Text("Doombringer".to_string()));
152 assert_eq!(a.as_number(), None);
153 }
154
155 #[test]
156 fn non_spans_equality_semantics_unchanged() {
157 assert_eq!(DetectorOutput::Bool(true), DetectorOutput::Bool(true));
158 assert_ne!(DetectorOutput::Bool(true), DetectorOutput::Bool(false));
159 assert_eq!(
160 DetectorOutput::Text("x".to_string()),
161 DetectorOutput::Text("x".to_string())
162 );
163 assert_eq!(DetectorOutput::Number(3.5), DetectorOutput::Number(3.5));
164 assert_ne!(DetectorOutput::Number(3.5), DetectorOutput::Score(3.5));
165 assert_eq!(DetectorOutput::None, DetectorOutput::None);
166 }
167
168 #[test]
169 fn as_number_extracts_numeric_outputs() {
170 assert_eq!(DetectorOutput::Number(3.5).as_number(), Some(3.5));
171 assert_eq!(DetectorOutput::Score(0.5).as_number(), Some(0.5));
172 assert_eq!(DetectorOutput::Bool(true).as_number(), None);
173 assert_eq!(DetectorOutput::Text("7".into()).as_number(), None);
174 }
175
176 #[test]
177 fn detector_trait_is_object_safe() {
178 let mut det: Box<dyn Detector> = Box::new(from_fn(|_| Ok(DetectorOutput::Bool(true))));
179 let frame = Frame::solid(2, 2, [0, 0, 0, 255]);
180 let view = frame
181 .view(PxRect {
182 x: 0,
183 y: 0,
184 w: 2,
185 h: 2,
186 })
187 .unwrap();
188 assert_eq!(det.evaluate(&view).unwrap(), DetectorOutput::Bool(true));
189 }
190}