Skip to main content

mcv_rs/mcv/
frame.rs

1use super::{
2    error::{McvError, Result},
3    MatchResult, Roi,
4};
5
6#[cfg(feature = "opencv")]
7use opencv::{core::Mat, imgproc};
8
9/// In-memory RGBA image source used by MCV templates.
10///
11/// This type is the boundary object between capture backends and recognition.
12/// Callers provide width/height/RGBA bytes once; individual templates decide
13/// whether they need grayscale, BGR, or an OCR image internally.
14pub struct RgbaFrame<'a> {
15    width: u32,
16    height: u32,
17    bytes: &'a [u8],
18    #[cfg(feature = "opencv")]
19    gray: Option<Mat>,
20    #[cfg(feature = "opencv")]
21    bgr: Option<Mat>,
22    #[cfg(feature = "ocr")]
23    dynamic_image: Option<::image::DynamicImage>,
24}
25
26impl<'a> RgbaFrame<'a> {
27    pub fn new(width: u32, height: u32, bytes: &'a [u8]) -> Result<Self> {
28        if width == 0 || height == 0 {
29            return Err(McvError::InvalidImage(format!(
30                "RGBA frame dimensions must be positive, got {width}x{height}"
31            )));
32        }
33        let expected = (width as usize)
34            .checked_mul(height as usize)
35            .and_then(|value| value.checked_mul(4))
36            .ok_or_else(|| {
37                McvError::InvalidImage(format!(
38                    "RGBA frame dimensions are too large: {width}x{height}"
39                ))
40            })?;
41        if bytes.len() != expected {
42            return Err(McvError::InvalidImage(format!(
43                "RGBA buffer length mismatch: got {}, expected {}",
44                bytes.len(),
45                expected
46            )));
47        }
48        Ok(Self {
49            width,
50            height,
51            bytes,
52            #[cfg(feature = "opencv")]
53            gray: None,
54            #[cfg(feature = "opencv")]
55            bgr: None,
56            #[cfg(feature = "ocr")]
57            dynamic_image: None,
58        })
59    }
60
61    pub fn width(&self) -> u32 {
62        self.width
63    }
64
65    pub fn height(&self) -> u32 {
66        self.height
67    }
68
69    pub fn bytes(&self) -> &'a [u8] {
70        self.bytes
71    }
72
73    /// Returns a cached grayscale OpenCV Mat, creating it from RGBA on demand.
74    #[cfg(feature = "opencv")]
75    pub fn gray(&mut self) -> Result<&Mat> {
76        if self.gray.is_none() {
77            let rgba = self.rgba_mat()?;
78            let mut gray = Mat::default();
79            imgproc::cvt_color_def(&rgba, &mut gray, imgproc::COLOR_RGBA2GRAY)?;
80            self.gray = Some(gray);
81        }
82        Ok(self.gray.as_ref().expect("gray image is initialized"))
83    }
84
85    /// Returns a cached BGR OpenCV Mat, creating it from RGBA on demand.
86    #[cfg(feature = "opencv")]
87    pub fn bgr(&mut self) -> Result<&Mat> {
88        if self.bgr.is_none() {
89            let rgba = self.rgba_mat()?;
90            let mut bgr = Mat::default();
91            imgproc::cvt_color_def(&rgba, &mut bgr, imgproc::COLOR_RGBA2BGR)?;
92            self.bgr = Some(bgr);
93        }
94        Ok(self.bgr.as_ref().expect("BGR image is initialized"))
95    }
96
97    /// Returns a cached image crate DynamicImage for OCR, creating it from RGBA
98    /// on demand.
99    #[cfg(feature = "ocr")]
100    pub fn dynamic_image(&mut self) -> Result<&::image::DynamicImage> {
101        if self.dynamic_image.is_none() {
102            let buffer = ::image::RgbaImage::from_raw(self.width, self.height, self.bytes.to_vec())
103                .ok_or_else(|| {
104                    McvError::InvalidImage(format!(
105                        "failed to create RGBA image {}x{}",
106                        self.width, self.height
107                    ))
108                })?;
109            self.dynamic_image = Some(::image::DynamicImage::ImageRgba8(buffer));
110        }
111        Ok(self
112            .dynamic_image
113            .as_ref()
114            .expect("DynamicImage is initialized"))
115    }
116
117    #[cfg(feature = "opencv")]
118    fn rgba_mat(&self) -> Result<opencv::boxed_ref::BoxedRef<'_, Mat>> {
119        Ok(Mat::new_rows_cols_with_bytes::<opencv::core::Vec4b>(
120            self.height as i32,
121            self.width as i32,
122            self.bytes,
123        )?)
124    }
125}
126
127/// Unified template interface for in-memory recognition.
128///
129/// Image templates, color templates, and OCR text templates all implement this
130/// trait so automation loops can keep the same calling code when switching
131/// template types.
132pub trait VisionTemplate {
133    /// Finds this template using its default ROI, if one was configured.
134    fn find(&mut self, image: &mut RgbaFrame<'_>) -> Result<Option<MatchResult>> {
135        self.find_with_roi(image, None)
136    }
137
138    /// Finds this template with a per-call ROI override.
139    ///
140    /// Passing `Some(roi)` searches that ROI for this call only. Passing `None`
141    /// falls back to the template's default ROI, if any.
142    fn find_with_roi(
143        &mut self,
144        image: &mut RgbaFrame<'_>,
145        roi: Option<Roi>,
146    ) -> Result<Option<MatchResult>>;
147}