use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU32, Ordering};
#[cfg(feature = "ocr")]
use std::sync::atomic::AtomicBool;
use super::error::{McvError, Result};
#[cfg(feature = "template-matching")]
use opencv::core::Rect;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Roi {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl Roi {
pub fn full(width: i32, height: i32) -> Self {
Self {
x: 0,
y: 0,
width,
height,
}
}
pub fn clamp(self, image_width: i32, image_height: i32) -> Option<Self> {
if image_width <= 0 || image_height <= 0 || self.width <= 0 || self.height <= 0 {
return None;
}
let x1 = self.x.clamp(0, image_width);
let y1 = self.y.clamp(0, image_height);
let x2 = (self.x.saturating_add(self.width)).clamp(0, image_width);
let y2 = (self.y.saturating_add(self.height)).clamp(0, image_height);
let width = x2 - x1;
let height = y2 - y1;
(width > 0 && height > 0).then_some(Self {
x: x1,
y: y1,
width,
height,
})
}
pub fn from_center(center_x: i32, center_y: i32, width: i32, height: i32) -> Self {
Self {
x: center_x - width / 2,
y: center_y - height / 2,
width,
height,
}
}
pub fn to_tuple(self) -> (i32, i32, i32, i32) {
(self.x, self.y, self.width, self.height)
}
#[cfg(feature = "template-matching")]
pub(crate) fn as_rect(self) -> Rect {
Rect::new(self.x, self.y, self.width, self.height)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MatchResult {
pub bbox: Roi,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MatchPoint {
pub x: f32,
pub y: f32,
}
const THRESHOLD_SCALE: f64 = 1_000_000.0;
static DEFAULT_IMAGE_THRESHOLD: AtomicU32 = AtomicU32::new(800_000);
static DEFAULT_COLOR_THRESHOLD: AtomicU32 = AtomicU32::new(1_000_000);
#[cfg(feature = "ocr")]
static DEFAULT_OCR_THRESHOLD: AtomicU32 = AtomicU32::new(500_000);
#[cfg(feature = "ocr")]
static DEFAULT_OCR_CASE_SENSITIVE: AtomicBool = AtomicBool::new(true);
#[cfg(feature = "ocr")]
static DEFAULT_OCR_THREAD_COUNT: AtomicU32 = AtomicU32::new(4);
pub fn set_default_image_threshold(value: f64) -> Result<()> {
store_threshold(&DEFAULT_IMAGE_THRESHOLD, value)
}
pub fn set_default_color_threshold(value: f64) -> Result<()> {
store_threshold(&DEFAULT_COLOR_THRESHOLD, value)
}
pub fn default_image_threshold() -> f64 {
load_threshold(&DEFAULT_IMAGE_THRESHOLD)
}
pub fn default_color_threshold() -> f64 {
load_threshold(&DEFAULT_COLOR_THRESHOLD)
}
#[cfg(feature = "ocr")]
pub fn set_default_ocr_threshold(value: f32) -> Result<()> {
validate_threshold_value(f64::from(value))?;
DEFAULT_OCR_THRESHOLD.store(
(f64::from(value) * THRESHOLD_SCALE).round() as u32,
Ordering::Relaxed,
);
Ok(())
}
#[cfg(feature = "ocr")]
pub fn default_ocr_threshold() -> f32 {
load_threshold(&DEFAULT_OCR_THRESHOLD) as f32
}
#[cfg(feature = "ocr")]
pub fn set_default_ocr_case_sensitive(value: bool) {
DEFAULT_OCR_CASE_SENSITIVE.store(value, Ordering::Relaxed);
}
#[cfg(feature = "ocr")]
pub fn default_ocr_case_sensitive() -> bool {
DEFAULT_OCR_CASE_SENSITIVE.load(Ordering::Relaxed)
}
#[cfg(feature = "ocr")]
pub fn set_default_ocr_thread_count(value: i32) -> Result<()> {
if value <= 0 {
return Err(McvError::InvalidThreadCount);
}
DEFAULT_OCR_THREAD_COUNT.store(value as u32, Ordering::Relaxed);
Ok(())
}
#[cfg(feature = "ocr")]
pub fn default_ocr_thread_count() -> i32 {
DEFAULT_OCR_THREAD_COUNT.load(Ordering::Relaxed) as i32
}
fn store_threshold(target: &AtomicU32, value: f64) -> Result<()> {
validate_threshold_value(value)?;
target.store((value * THRESHOLD_SCALE).round() as u32, Ordering::Relaxed);
Ok(())
}
fn load_threshold(target: &AtomicU32) -> f64 {
f64::from(target.load(Ordering::Relaxed)) / THRESHOLD_SCALE
}
impl MatchResult {
pub fn from_box(x: i32, y: i32, width: i32, height: i32) -> Self {
Self {
bbox: Roi {
x,
y,
width,
height,
},
}
}
pub fn from_point(x: i32, y: i32) -> Self {
Self::from_box(x, y, 1, 1)
}
pub fn is_point(&self) -> bool {
self.bbox.width == 1 && self.bbox.height == 1
}
pub fn point(&self) -> (i32, i32) {
self.center()
}
pub fn top_left(&self) -> (i32, i32) {
(self.bbox.x, self.bbox.y)
}
pub fn bottom_right(&self) -> (i32, i32) {
(
self.bbox.x + self.bbox.width - 1,
self.bbox.y + self.bbox.height - 1,
)
}
pub fn bottom_right_exclusive(&self) -> (i32, i32) {
(
self.bbox.x + self.bbox.width,
self.bbox.y + self.bbox.height,
)
}
pub fn center(&self) -> (i32, i32) {
(
self.bbox.x + self.bbox.width / 2,
self.bbox.y + self.bbox.height / 2,
)
}
pub fn width(&self) -> i32 {
self.bbox.width
}
pub fn height(&self) -> i32 {
self.bbox.height
}
pub fn roi_around(&self, width: i32, height: i32) -> Roi {
let center = self.center();
Roi::from_center(center.0, center.1, width, height)
}
}
pub(crate) fn validate_threshold_value(value: f64) -> Result<()> {
if (0.0..=1.0).contains(&value) {
Ok(())
} else {
Err(McvError::InvalidThreshold(value))
}
}
#[cfg(any(feature = "template-matching", feature = "multi-color"))]
pub(crate) fn validate_threshold(value: f64) -> Result<()> {
validate_threshold_value(value)
}