Skip to main content

mcv_rs/mcv/
core.rs

1use serde::{Deserialize, Serialize};
2use std::sync::atomic::{AtomicU32, Ordering};
3
4#[cfg(feature = "ocr")]
5use std::sync::atomic::AtomicBool;
6
7use super::error::{McvError, Result};
8
9#[cfg(feature = "template-matching")]
10use opencv::core::Rect;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "camelCase")]
14pub struct Roi {
15    pub x: i32,
16    pub y: i32,
17    pub width: i32,
18    pub height: i32,
19}
20
21impl Roi {
22    pub fn full(width: i32, height: i32) -> Self {
23        Self {
24            x: 0,
25            y: 0,
26            width,
27            height,
28        }
29    }
30
31    pub fn clamp(self, image_width: i32, image_height: i32) -> Option<Self> {
32        if image_width <= 0 || image_height <= 0 || self.width <= 0 || self.height <= 0 {
33            return None;
34        }
35        let x1 = self.x.clamp(0, image_width);
36        let y1 = self.y.clamp(0, image_height);
37        let x2 = (self.x.saturating_add(self.width)).clamp(0, image_width);
38        let y2 = (self.y.saturating_add(self.height)).clamp(0, image_height);
39        let width = x2 - x1;
40        let height = y2 - y1;
41        (width > 0 && height > 0).then_some(Self {
42            x: x1,
43            y: y1,
44            width,
45            height,
46        })
47    }
48
49    pub fn from_center(center_x: i32, center_y: i32, width: i32, height: i32) -> Self {
50        Self {
51            x: center_x - width / 2,
52            y: center_y - height / 2,
53            width,
54            height,
55        }
56    }
57
58    pub fn to_tuple(self) -> (i32, i32, i32, i32) {
59        (self.x, self.y, self.width, self.height)
60    }
61
62    #[cfg(feature = "template-matching")]
63    pub(crate) fn as_rect(self) -> Rect {
64        Rect::new(self.x, self.y, self.width, self.height)
65    }
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct MatchResult {
71    pub bbox: Roi,
72}
73
74#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct MatchPoint {
77    pub x: f32,
78    pub y: f32,
79}
80
81const THRESHOLD_SCALE: f64 = 1_000_000.0;
82static DEFAULT_IMAGE_THRESHOLD: AtomicU32 = AtomicU32::new(800_000);
83static DEFAULT_COLOR_THRESHOLD: AtomicU32 = AtomicU32::new(1_000_000);
84#[cfg(feature = "ocr")]
85static DEFAULT_OCR_THRESHOLD: AtomicU32 = AtomicU32::new(500_000);
86#[cfg(feature = "ocr")]
87static DEFAULT_OCR_CASE_SENSITIVE: AtomicBool = AtomicBool::new(true);
88#[cfg(feature = "ocr")]
89static DEFAULT_OCR_THREAD_COUNT: AtomicU32 = AtomicU32::new(4);
90
91/// Sets the default threshold used by `ImageTemplateOptions::default()`.
92pub fn set_default_image_threshold(value: f64) -> Result<()> {
93    store_threshold(&DEFAULT_IMAGE_THRESHOLD, value)
94}
95
96/// Sets the default threshold used by `MultiColorTemplateOptions::default()`.
97pub fn set_default_color_threshold(value: f64) -> Result<()> {
98    store_threshold(&DEFAULT_COLOR_THRESHOLD, value)
99}
100
101/// Returns the current default image-template threshold.
102pub fn default_image_threshold() -> f64 {
103    load_threshold(&DEFAULT_IMAGE_THRESHOLD)
104}
105
106/// Returns the current default multi-color threshold.
107pub fn default_color_threshold() -> f64 {
108    load_threshold(&DEFAULT_COLOR_THRESHOLD)
109}
110
111/// Sets the default OCR threshold used when `OcrEngineOptions::threshold` is `None`.
112#[cfg(feature = "ocr")]
113pub fn set_default_ocr_threshold(value: f32) -> Result<()> {
114    validate_threshold_value(f64::from(value))?;
115    DEFAULT_OCR_THRESHOLD.store(
116        (f64::from(value) * THRESHOLD_SCALE).round() as u32,
117        Ordering::Relaxed,
118    );
119    Ok(())
120}
121
122/// Returns the current default OCR threshold.
123#[cfg(feature = "ocr")]
124pub fn default_ocr_threshold() -> f32 {
125    load_threshold(&DEFAULT_OCR_THRESHOLD) as f32
126}
127
128/// Sets the default case-sensitivity used when OCR matcher options do not
129/// provide an explicit value.
130#[cfg(feature = "ocr")]
131pub fn set_default_ocr_case_sensitive(value: bool) {
132    DEFAULT_OCR_CASE_SENSITIVE.store(value, Ordering::Relaxed);
133}
134
135/// Returns the current default OCR case-sensitivity.
136#[cfg(feature = "ocr")]
137pub fn default_ocr_case_sensitive() -> bool {
138    DEFAULT_OCR_CASE_SENSITIVE.load(Ordering::Relaxed)
139}
140
141/// Sets the default OCR worker-thread count.
142#[cfg(feature = "ocr")]
143pub fn set_default_ocr_thread_count(value: i32) -> Result<()> {
144    if value <= 0 {
145        return Err(McvError::InvalidThreadCount);
146    }
147    DEFAULT_OCR_THREAD_COUNT.store(value as u32, Ordering::Relaxed);
148    Ok(())
149}
150
151/// Returns the current default OCR worker-thread count.
152#[cfg(feature = "ocr")]
153pub fn default_ocr_thread_count() -> i32 {
154    DEFAULT_OCR_THREAD_COUNT.load(Ordering::Relaxed) as i32
155}
156
157fn store_threshold(target: &AtomicU32, value: f64) -> Result<()> {
158    validate_threshold_value(value)?;
159    target.store((value * THRESHOLD_SCALE).round() as u32, Ordering::Relaxed);
160    Ok(())
161}
162
163fn load_threshold(target: &AtomicU32) -> f64 {
164    f64::from(target.load(Ordering::Relaxed)) / THRESHOLD_SCALE
165}
166
167impl MatchResult {
168    pub fn from_box(x: i32, y: i32, width: i32, height: i32) -> Self {
169        Self {
170            bbox: Roi {
171                x,
172                y,
173                width,
174                height,
175            },
176        }
177    }
178
179    pub fn from_point(x: i32, y: i32) -> Self {
180        Self::from_box(x, y, 1, 1)
181    }
182
183    pub fn is_point(&self) -> bool {
184        self.bbox.width == 1 && self.bbox.height == 1
185    }
186
187    pub fn point(&self) -> (i32, i32) {
188        self.center()
189    }
190
191    pub fn top_left(&self) -> (i32, i32) {
192        (self.bbox.x, self.bbox.y)
193    }
194
195    /// Returns the inclusive bottom-right coordinate
196    /// `(x + width - 1, y + height - 1)`.
197    pub fn bottom_right(&self) -> (i32, i32) {
198        (
199            self.bbox.x + self.bbox.width - 1,
200            self.bbox.y + self.bbox.height - 1,
201        )
202    }
203
204    /// Returns the exclusive bottom-right coordinate `(x + width, y + height)`.
205    ///
206    /// Use this for half-open rectangle math such as ROI slicing and IoU.
207    pub fn bottom_right_exclusive(&self) -> (i32, i32) {
208        (
209            self.bbox.x + self.bbox.width,
210            self.bbox.y + self.bbox.height,
211        )
212    }
213
214    pub fn center(&self) -> (i32, i32) {
215        (
216            self.bbox.x + self.bbox.width / 2,
217            self.bbox.y + self.bbox.height / 2,
218        )
219    }
220
221    pub fn width(&self) -> i32 {
222        self.bbox.width
223    }
224
225    pub fn height(&self) -> i32 {
226        self.bbox.height
227    }
228
229    pub fn roi_around(&self, width: i32, height: i32) -> Roi {
230        let center = self.center();
231        Roi::from_center(center.0, center.1, width, height)
232    }
233}
234
235pub(crate) fn validate_threshold_value(value: f64) -> Result<()> {
236    if (0.0..=1.0).contains(&value) {
237        Ok(())
238    } else {
239        Err(McvError::InvalidThreshold(value))
240    }
241}
242
243#[cfg(any(feature = "template-matching", feature = "multi-color"))]
244pub(crate) fn validate_threshold(value: f64) -> Result<()> {
245    validate_threshold_value(value)
246}