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
91pub fn set_default_image_threshold(value: f64) -> Result<()> {
93 store_threshold(&DEFAULT_IMAGE_THRESHOLD, value)
94}
95
96pub fn set_default_color_threshold(value: f64) -> Result<()> {
98 store_threshold(&DEFAULT_COLOR_THRESHOLD, value)
99}
100
101pub fn default_image_threshold() -> f64 {
103 load_threshold(&DEFAULT_IMAGE_THRESHOLD)
104}
105
106pub fn default_color_threshold() -> f64 {
108 load_threshold(&DEFAULT_COLOR_THRESHOLD)
109}
110
111#[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#[cfg(feature = "ocr")]
124pub fn default_ocr_threshold() -> f32 {
125 load_threshold(&DEFAULT_OCR_THRESHOLD) as f32
126}
127
128#[cfg(feature = "ocr")]
131pub fn set_default_ocr_case_sensitive(value: bool) {
132 DEFAULT_OCR_CASE_SENSITIVE.store(value, Ordering::Relaxed);
133}
134
135#[cfg(feature = "ocr")]
137pub fn default_ocr_case_sensitive() -> bool {
138 DEFAULT_OCR_CASE_SENSITIVE.load(Ordering::Relaxed)
139}
140
141#[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#[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 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 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}