mcv-rs 0.1.0

MCV computer-vision primitives
Documentation
use super::{
    error::{McvError, Result},
    MatchResult, Roi,
};

#[cfg(feature = "opencv")]
use opencv::{core::Mat, imgproc};

/// In-memory RGBA image source used by MCV templates.
///
/// This type is the boundary object between capture backends and recognition.
/// Callers provide width/height/RGBA bytes once; individual templates decide
/// whether they need grayscale, BGR, or an OCR image internally.
pub struct RgbaFrame<'a> {
    width: u32,
    height: u32,
    bytes: &'a [u8],
    #[cfg(feature = "opencv")]
    gray: Option<Mat>,
    #[cfg(feature = "opencv")]
    bgr: Option<Mat>,
    #[cfg(feature = "ocr")]
    dynamic_image: Option<::image::DynamicImage>,
}

impl<'a> RgbaFrame<'a> {
    pub fn new(width: u32, height: u32, bytes: &'a [u8]) -> Result<Self> {
        if width == 0 || height == 0 {
            return Err(McvError::InvalidImage(format!(
                "RGBA frame dimensions must be positive, got {width}x{height}"
            )));
        }
        let expected = (width as usize)
            .checked_mul(height as usize)
            .and_then(|value| value.checked_mul(4))
            .ok_or_else(|| {
                McvError::InvalidImage(format!(
                    "RGBA frame dimensions are too large: {width}x{height}"
                ))
            })?;
        if bytes.len() != expected {
            return Err(McvError::InvalidImage(format!(
                "RGBA buffer length mismatch: got {}, expected {}",
                bytes.len(),
                expected
            )));
        }
        Ok(Self {
            width,
            height,
            bytes,
            #[cfg(feature = "opencv")]
            gray: None,
            #[cfg(feature = "opencv")]
            bgr: None,
            #[cfg(feature = "ocr")]
            dynamic_image: None,
        })
    }

    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn bytes(&self) -> &'a [u8] {
        self.bytes
    }

    /// Returns a cached grayscale OpenCV Mat, creating it from RGBA on demand.
    #[cfg(feature = "opencv")]
    pub fn gray(&mut self) -> Result<&Mat> {
        if self.gray.is_none() {
            let rgba = self.rgba_mat()?;
            let mut gray = Mat::default();
            imgproc::cvt_color_def(&rgba, &mut gray, imgproc::COLOR_RGBA2GRAY)?;
            self.gray = Some(gray);
        }
        Ok(self.gray.as_ref().expect("gray image is initialized"))
    }

    /// Returns a cached BGR OpenCV Mat, creating it from RGBA on demand.
    #[cfg(feature = "opencv")]
    pub fn bgr(&mut self) -> Result<&Mat> {
        if self.bgr.is_none() {
            let rgba = self.rgba_mat()?;
            let mut bgr = Mat::default();
            imgproc::cvt_color_def(&rgba, &mut bgr, imgproc::COLOR_RGBA2BGR)?;
            self.bgr = Some(bgr);
        }
        Ok(self.bgr.as_ref().expect("BGR image is initialized"))
    }

    /// Returns a cached image crate DynamicImage for OCR, creating it from RGBA
    /// on demand.
    #[cfg(feature = "ocr")]
    pub fn dynamic_image(&mut self) -> Result<&::image::DynamicImage> {
        if self.dynamic_image.is_none() {
            let buffer = ::image::RgbaImage::from_raw(self.width, self.height, self.bytes.to_vec())
                .ok_or_else(|| {
                    McvError::InvalidImage(format!(
                        "failed to create RGBA image {}x{}",
                        self.width, self.height
                    ))
                })?;
            self.dynamic_image = Some(::image::DynamicImage::ImageRgba8(buffer));
        }
        Ok(self
            .dynamic_image
            .as_ref()
            .expect("DynamicImage is initialized"))
    }

    #[cfg(feature = "opencv")]
    fn rgba_mat(&self) -> Result<opencv::boxed_ref::BoxedRef<'_, Mat>> {
        Ok(Mat::new_rows_cols_with_bytes::<opencv::core::Vec4b>(
            self.height as i32,
            self.width as i32,
            self.bytes,
        )?)
    }
}

/// Unified template interface for in-memory recognition.
///
/// Image templates, color templates, and OCR text templates all implement this
/// trait so automation loops can keep the same calling code when switching
/// template types.
pub trait VisionTemplate {
    /// Finds this template using its default ROI, if one was configured.
    fn find(&mut self, image: &mut RgbaFrame<'_>) -> Result<Option<MatchResult>> {
        self.find_with_roi(image, None)
    }

    /// Finds this template with a per-call ROI override.
    ///
    /// Passing `Some(roi)` searches that ROI for this call only. Passing `None`
    /// falls back to the template's default ROI, if any.
    fn find_with_roi(
        &mut self,
        image: &mut RgbaFrame<'_>,
        roi: Option<Roi>,
    ) -> Result<Option<MatchResult>>;
}