mcv-rs 0.1.0

MCV computer-vision primitives
Documentation
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);

/// Sets the default threshold used by `ImageTemplateOptions::default()`.
pub fn set_default_image_threshold(value: f64) -> Result<()> {
    store_threshold(&DEFAULT_IMAGE_THRESHOLD, value)
}

/// Sets the default threshold used by `MultiColorTemplateOptions::default()`.
pub fn set_default_color_threshold(value: f64) -> Result<()> {
    store_threshold(&DEFAULT_COLOR_THRESHOLD, value)
}

/// Returns the current default image-template threshold.
pub fn default_image_threshold() -> f64 {
    load_threshold(&DEFAULT_IMAGE_THRESHOLD)
}

/// Returns the current default multi-color threshold.
pub fn default_color_threshold() -> f64 {
    load_threshold(&DEFAULT_COLOR_THRESHOLD)
}

/// Sets the default OCR threshold used when `OcrEngineOptions::threshold` is `None`.
#[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(())
}

/// Returns the current default OCR threshold.
#[cfg(feature = "ocr")]
pub fn default_ocr_threshold() -> f32 {
    load_threshold(&DEFAULT_OCR_THRESHOLD) as f32
}

/// Sets the default case-sensitivity used when OCR matcher options do not
/// provide an explicit value.
#[cfg(feature = "ocr")]
pub fn set_default_ocr_case_sensitive(value: bool) {
    DEFAULT_OCR_CASE_SENSITIVE.store(value, Ordering::Relaxed);
}

/// Returns the current default OCR case-sensitivity.
#[cfg(feature = "ocr")]
pub fn default_ocr_case_sensitive() -> bool {
    DEFAULT_OCR_CASE_SENSITIVE.load(Ordering::Relaxed)
}

/// Sets the default OCR worker-thread count.
#[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(())
}

/// Returns the current default OCR worker-thread count.
#[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)
    }

    /// Returns the inclusive bottom-right coordinate
    /// `(x + width - 1, y + height - 1)`.
    pub fn bottom_right(&self) -> (i32, i32) {
        (
            self.bbox.x + self.bbox.width - 1,
            self.bbox.y + self.bbox.height - 1,
        )
    }

    /// Returns the exclusive bottom-right coordinate `(x + width, y + height)`.
    ///
    /// Use this for half-open rectangle math such as ROI slicing and IoU.
    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)
}