Skip to main content

edgefirst_image/
lib.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5
6## EdgeFirst HAL - Image Converter
7
8The `edgefirst_image` crate is part of the EdgeFirst Hardware Abstraction
9Layer (HAL) and provides functionality for converting images between
10different formats and sizes.  The crate is designed to work with hardware
11acceleration when available, but also provides a CPU-based fallback for
12environments where hardware acceleration is not present or not suitable.
13
14The main features of the `edgefirst_image` crate include:
15- Support for various image formats, including YUYV, RGB, RGBA, and GREY.
16- Support for source crop, destination crop, rotation, and flipping.
17- Image conversion using hardware acceleration (G2D, OpenGL) when available.
18- CPU-based image conversion as a fallback option.
19
20The crate defines a `TensorImage` struct that represents an image as a
21tensor, along with its format information. It also provides an
22`ImageProcessor` struct that manages the conversion process, selecting
23the appropriate conversion method based on the available hardware.
24
25## Examples
26
27```rust
28# use edgefirst_image::{ImageProcessor, TensorImage, RGBA, RGB, Rotation, Flip, Crop, ImageProcessorTrait};
29# fn main() -> Result<(), edgefirst_image::Error> {
30let image = include_bytes!("../../../testdata/zidane.jpg");
31let img = TensorImage::load(image, Some(RGBA), None)?;
32let mut converter = ImageProcessor::new()?;
33let mut dst = TensorImage::new(640, 480, RGB, None)?;
34converter.convert(&img, &mut dst, Rotation::None, Flip::None, Crop::default())?;
35# Ok(())
36# }
37```
38
39## Environment Variables
40The behavior of the `edgefirst_image::ImageProcessor` struct can be influenced by the
41following environment variables:
42- `EDGEFIRST_DISABLE_GL`: If set to `1`, disables the use of OpenGL for image
43  conversion, forcing the use of CPU or other available hardware methods.
44- `EDGEFIRST_DISABLE_G2D`: If set to `1`, disables the use of G2D for image
45  conversion, forcing the use of CPU or other available hardware methods.
46- `EDGEFIRST_DISABLE_CPU`: If set to `1`, disables the use of CPU for image
47  conversion, forcing the use of hardware acceleration methods. If no hardware
48  acceleration methods are available, an error will be returned when attempting
49  to create an `ImageProcessor`.
50
51Additionally the TensorMemory used by default allocations can be controlled using the
52`EDGEFIRST_TENSOR_FORCE_MEM` environment variable. If set to `1`, default tensor memory
53uses system memory. This will disable the use of specialized memory regions for tensors
54and hardware acceleration. However, this will increase the performance of the CPU converter.
55*/
56#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
57
58#[cfg(feature = "decoder")]
59use edgefirst_decoder::{DetectBox, Segmentation};
60use edgefirst_tensor::{Tensor, TensorMemory, TensorTrait as _};
61use enum_dispatch::enum_dispatch;
62use four_char_code::{four_char_code, FourCharCode};
63use std::{fmt::Display, time::Instant};
64use zune_jpeg::{
65    zune_core::{colorspace::ColorSpace, options::DecoderOptions},
66    JpegDecoder,
67};
68use zune_png::PngDecoder;
69
70pub use cpu::CPUProcessor;
71pub use error::{Error, Result};
72#[cfg(target_os = "linux")]
73pub use g2d::G2DProcessor;
74#[cfg(target_os = "linux")]
75#[cfg(feature = "opengl")]
76pub use opengl_headless::GLProcessorThreaded;
77
78mod cpu;
79mod error;
80mod g2d;
81mod opengl_headless;
82
83/// 8 bit interleaved YUV422, limited range
84pub const YUYV: FourCharCode = four_char_code!("YUYV");
85/// 8 bit planar YUV420, limited range
86pub const NV12: FourCharCode = four_char_code!("NV12");
87/// 8 bit planar YUV422, limited range
88pub const NV16: FourCharCode = four_char_code!("NV16");
89/// 8 bit RGBA
90pub const RGBA: FourCharCode = four_char_code!("RGBA");
91/// 8 bit RGB
92pub const RGB: FourCharCode = four_char_code!("RGB ");
93/// 8 bit grayscale, full range
94pub const GREY: FourCharCode = four_char_code!("Y800");
95
96// TODO: planar RGB is 8BPS? https://fourcc.org/8bps/
97pub const PLANAR_RGB: FourCharCode = four_char_code!("8BPS");
98
99// TODO: What fourcc code is planar RGBA?
100pub const PLANAR_RGBA: FourCharCode = four_char_code!("8BPA");
101
102/// An image represented as a tensor with associated format information.
103#[derive(Debug)]
104pub struct TensorImage {
105    tensor: Tensor<u8>,
106    fourcc: FourCharCode,
107    is_planar: bool,
108}
109
110impl TensorImage {
111    /// Creates a new `TensorImage` with the specified width, height, format,
112    /// and memory type.
113    ///
114    /// # Examples
115    /// ```rust
116    /// use edgefirst_image::{RGB, TensorImage};
117    /// use edgefirst_tensor::TensorMemory;
118    /// # fn main() -> Result<(), edgefirst_image::Error> {
119    /// let img = TensorImage::new(640, 480, RGB, Some(TensorMemory::Mem))?;
120    /// assert_eq!(img.width(), 640);
121    /// assert_eq!(img.height(), 480);
122    /// assert_eq!(img.fourcc(), RGB);
123    /// assert!(!img.is_planar());
124    /// # Ok(())
125    /// # }
126    /// ```
127    pub fn new(
128        width: usize,
129        height: usize,
130        fourcc: FourCharCode,
131        memory: Option<TensorMemory>,
132    ) -> Result<Self> {
133        let channels = fourcc_channels(fourcc)?;
134        let is_planar = fourcc_planar(fourcc)?;
135
136        // NV12 is semi-planar with Y plane (W×H) + UV plane (W×H/2)
137        // Total bytes = W × H × 1.5. Use shape [H*3/2, W] to encode this.
138        // Width = shape[1], Height = shape[0] * 2 / 3
139        if fourcc == NV12 {
140            let shape = vec![height * 3 / 2, width];
141            let tensor = Tensor::new(&shape, memory, None)?;
142
143            return Ok(Self {
144                tensor,
145                fourcc,
146                is_planar,
147            });
148        }
149
150        if is_planar {
151            let shape = vec![channels, height, width];
152            let tensor = Tensor::new(&shape, memory, None)?;
153
154            return Ok(Self {
155                tensor,
156                fourcc,
157                is_planar,
158            });
159        }
160
161        let shape = vec![height, width, channels];
162        let tensor = Tensor::new(&shape, memory, None)?;
163
164        Ok(Self {
165            tensor,
166            fourcc,
167            is_planar,
168        })
169    }
170
171    /// Creates a new `TensorImage` from an existing tensor and specified
172    /// format.
173    ///
174    /// # Examples
175    /// ```rust
176    /// use edgefirst_image::{RGB, TensorImage};
177    /// use edgefirst_tensor::Tensor;
178    ///  # fn main() -> Result<(), edgefirst_image::Error> {
179    /// let tensor = Tensor::new(&[720, 1280, 3], None, None)?;
180    /// let img = TensorImage::from_tensor(tensor, RGB)?;
181    /// assert_eq!(img.width(), 1280);
182    /// assert_eq!(img.height(), 720);
183    /// assert_eq!(img.fourcc(), RGB);
184    /// # Ok(())
185    /// # }
186    pub fn from_tensor(tensor: Tensor<u8>, fourcc: FourCharCode) -> Result<Self> {
187        // Validate tensor shape based on the fourcc and is_planar flag
188        let shape = tensor.shape();
189        if shape.len() != 3 {
190            return Err(Error::InvalidShape(format!(
191                "Tensor shape must have 3 dimensions, got {}: {:?}",
192                shape.len(),
193                shape
194            )));
195        }
196        let is_planar = fourcc_planar(fourcc)?;
197        let channels = if is_planar { shape[0] } else { shape[2] };
198
199        if fourcc_channels(fourcc)? != channels {
200            return Err(Error::InvalidShape(format!(
201                "Invalid tensor shape {:?} for format {}",
202                shape,
203                fourcc.to_string()
204            )));
205        }
206
207        Ok(Self {
208            tensor,
209            fourcc,
210            is_planar,
211        })
212    }
213
214    /// Loads an image from the given byte slice, attempting to decode it as
215    /// JPEG or PNG format. Exif orientation is supported. The default format is
216    /// RGB.
217    ///
218    /// # Examples
219    /// ```rust
220    /// use edgefirst_image::{RGBA, TensorImage};
221    /// use edgefirst_tensor::TensorMemory;
222    /// # fn main() -> Result<(), edgefirst_image::Error> {
223    /// let jpeg_bytes = include_bytes!("../../../testdata/zidane.png");
224    /// let img = TensorImage::load(jpeg_bytes, Some(RGBA), Some(TensorMemory::Mem))?;
225    /// assert_eq!(img.width(), 1280);
226    /// assert_eq!(img.height(), 720);
227    /// assert_eq!(img.fourcc(), RGBA);
228    /// # Ok(())
229    /// # }
230    /// ```
231    pub fn load(
232        image: &[u8],
233        format: Option<FourCharCode>,
234        memory: Option<TensorMemory>,
235    ) -> Result<Self> {
236        if let Ok(i) = Self::load_jpeg(image, format, memory) {
237            return Ok(i);
238        }
239        if let Ok(i) = Self::load_png(image, format, memory) {
240            return Ok(i);
241        }
242
243        Err(Error::NotSupported(
244            "Could not decode as jpeg or png".to_string(),
245        ))
246    }
247
248    /// Loads a JPEG image from the given byte slice. Supports EXIF orientation.
249    /// The default format is RGB.
250    ///
251    /// # Examples
252    /// ```rust
253    /// use edgefirst_image::{RGB, TensorImage};
254    /// use edgefirst_tensor::TensorMemory;
255    /// # fn main() -> Result<(), edgefirst_image::Error> {
256    /// let jpeg_bytes = include_bytes!("../../../testdata/zidane.jpg");
257    /// let img = TensorImage::load_jpeg(jpeg_bytes, Some(RGB), Some(TensorMemory::Mem))?;
258    /// assert_eq!(img.width(), 1280);
259    /// assert_eq!(img.height(), 720);
260    /// assert_eq!(img.fourcc(), RGB);
261    /// # Ok(())
262    /// # }
263    /// ```
264    pub fn load_jpeg(
265        image: &[u8],
266        format: Option<FourCharCode>,
267        memory: Option<TensorMemory>,
268    ) -> Result<Self> {
269        let colour = match format {
270            Some(RGB) => ColorSpace::RGB,
271            Some(RGBA) => ColorSpace::RGBA,
272            Some(GREY) => ColorSpace::Luma,
273            None => ColorSpace::RGB,
274            Some(f) => {
275                return Err(Error::NotSupported(format!(
276                    "Unsupported image format {}",
277                    f.display()
278                )));
279            }
280        };
281        let options = DecoderOptions::default().jpeg_set_out_colorspace(colour);
282        let mut decoder = JpegDecoder::new_with_options(image, options);
283        decoder.decode_headers()?;
284
285        let image_info = decoder.info().ok_or(Error::Internal(
286            "JPEG did not return decoded image info".to_string(),
287        ))?;
288
289        let converted_color_space = decoder
290            .get_output_colorspace()
291            .ok_or(Error::Internal("No output colorspace".to_string()))?;
292
293        let converted_color_space = match converted_color_space {
294            ColorSpace::RGB => RGB,
295            ColorSpace::RGBA => RGBA,
296            ColorSpace::Luma => GREY,
297            _ => {
298                return Err(Error::NotSupported(
299                    "Unsupported JPEG decoder output".to_string(),
300                ));
301            }
302        };
303
304        let dest_format = format.unwrap_or(converted_color_space);
305
306        let (rotation, flip) = decoder
307            .exif()
308            .map(|x| Self::read_exif_orientation(x))
309            .unwrap_or((Rotation::None, Flip::None));
310
311        if (rotation, flip) == (Rotation::None, Flip::None) {
312            let mut img = Self::new(
313                image_info.width as usize,
314                image_info.height as usize,
315                dest_format,
316                memory,
317            )?;
318
319            if converted_color_space != dest_format {
320                let tmp = Self::new(
321                    image_info.width as usize,
322                    image_info.height as usize,
323                    converted_color_space,
324                    Some(TensorMemory::Mem),
325                )?;
326
327                decoder.decode_into(&mut tmp.tensor.map()?)?;
328
329                CPUProcessor::convert_format(&tmp, &mut img)?;
330                return Ok(img);
331            }
332            decoder.decode_into(&mut img.tensor.map()?)?;
333            return Ok(img);
334        }
335
336        let mut tmp = Self::new(
337            image_info.width as usize,
338            image_info.height as usize,
339            dest_format,
340            Some(TensorMemory::Mem),
341        )?;
342
343        if converted_color_space != dest_format {
344            let tmp2 = Self::new(
345                image_info.width as usize,
346                image_info.height as usize,
347                converted_color_space,
348                Some(TensorMemory::Mem),
349            )?;
350
351            decoder.decode_into(&mut tmp2.tensor.map()?)?;
352
353            CPUProcessor::convert_format(&tmp2, &mut tmp)?;
354        } else {
355            decoder.decode_into(&mut tmp.tensor.map()?)?;
356        }
357
358        rotate_flip_to_tensor_image(&tmp, rotation, flip, memory)
359    }
360
361    /// Loads a PNG image from the given byte slice. Supports EXIF orientation.
362    /// The default format is RGB.
363    ///
364    /// # Examples
365    /// ```rust
366    /// use edgefirst_image::{RGB, TensorImage};
367    /// use edgefirst_tensor::TensorMemory;
368    /// # fn main() -> Result<(), edgefirst_image::Error> {
369    /// let png_bytes = include_bytes!("../../../testdata/zidane.png");
370    /// let img = TensorImage::load_png(png_bytes, Some(RGB), Some(TensorMemory::Mem))?;
371    /// assert_eq!(img.width(), 1280);
372    /// assert_eq!(img.height(), 720);
373    /// assert_eq!(img.fourcc(), RGB);
374    /// # Ok(())
375    /// # }
376    /// ```
377    pub fn load_png(
378        image: &[u8],
379        format: Option<FourCharCode>,
380        memory: Option<TensorMemory>,
381    ) -> Result<Self> {
382        let format = format.unwrap_or(RGB);
383        let alpha = match format {
384            RGB => false,
385            RGBA => true,
386            _ => {
387                return Err(Error::NotImplemented(
388                    "Unsupported image format".to_string(),
389                ));
390            }
391        };
392
393        let options = DecoderOptions::default()
394            .png_set_add_alpha_channel(alpha)
395            .png_set_decode_animated(false);
396        let mut decoder = PngDecoder::new_with_options(image, options);
397        decoder.decode_headers()?;
398        let image_info = decoder.get_info().ok_or(Error::Internal(
399            "PNG did not return decoded image info".to_string(),
400        ))?;
401
402        let (rotation, flip) = image_info
403            .exif
404            .as_ref()
405            .map(|x| Self::read_exif_orientation(x))
406            .unwrap_or((Rotation::None, Flip::None));
407
408        if (rotation, flip) == (Rotation::None, Flip::None) {
409            let img = Self::new(image_info.width, image_info.height, format, memory)?;
410            decoder.decode_into(&mut img.tensor.map()?)?;
411            return Ok(img);
412        }
413
414        let tmp = Self::new(
415            image_info.width,
416            image_info.height,
417            format,
418            Some(TensorMemory::Mem),
419        )?;
420        decoder.decode_into(&mut tmp.tensor.map()?)?;
421
422        rotate_flip_to_tensor_image(&tmp, rotation, flip, memory)
423    }
424
425    fn read_exif_orientation(exif_: &[u8]) -> (Rotation, Flip) {
426        let exifreader = exif::Reader::new();
427        let Ok(exif_) = exifreader.read_raw(exif_.to_vec()) else {
428            return (Rotation::None, Flip::None);
429        };
430        let Some(orientation) = exif_.get_field(exif::Tag::Orientation, exif::In::PRIMARY) else {
431            return (Rotation::None, Flip::None);
432        };
433        match orientation.value.get_uint(0) {
434            Some(1) => (Rotation::None, Flip::None),
435            Some(2) => (Rotation::None, Flip::Horizontal),
436            Some(3) => (Rotation::Rotate180, Flip::None),
437            Some(4) => (Rotation::Rotate180, Flip::Horizontal),
438            Some(5) => (Rotation::Clockwise90, Flip::Horizontal),
439            Some(6) => (Rotation::Clockwise90, Flip::None),
440            Some(7) => (Rotation::CounterClockwise90, Flip::Horizontal),
441            Some(8) => (Rotation::CounterClockwise90, Flip::None),
442            Some(v) => {
443                log::warn!("broken orientation EXIF value: {v}");
444                (Rotation::None, Flip::None)
445            }
446            None => (Rotation::None, Flip::None),
447        }
448    }
449
450    /// Saves the image as a JPEG file at the specified path with the given
451    /// quality. Only RGB and RGBA formats are supported.
452    ///
453    /// # Examples
454    /// ```rust
455    /// use edgefirst_image::{RGB, TensorImage};
456    /// use edgefirst_tensor::Tensor;
457    ///  # fn main() -> Result<(), edgefirst_image::Error> {
458    /// let tensor = Tensor::new(&[720, 1280, 3], None, None)?;
459    /// let img = TensorImage::from_tensor(tensor, RGB)?;
460    /// let save_path = "/tmp/output.jpg";
461    /// img.save_jpeg(save_path, 90)?;
462    /// # Ok(())
463    /// # }
464    pub fn save_jpeg(&self, path: &str, quality: u8) -> Result<()> {
465        if self.is_planar {
466            return Err(Error::NotImplemented(
467                "Saving planar images is not supported".to_string(),
468            ));
469        }
470
471        let colour = if self.fourcc == RGB {
472            jpeg_encoder::ColorType::Rgb
473        } else if self.fourcc == RGBA {
474            jpeg_encoder::ColorType::Rgba
475        } else {
476            return Err(Error::NotImplemented(
477                "Unsupported image format for saving".to_string(),
478            ));
479        };
480
481        let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
482        let tensor_map = self.tensor.map()?;
483
484        encoder.encode(
485            &tensor_map,
486            self.width() as u16,
487            self.height() as u16,
488            colour,
489        )?;
490
491        Ok(())
492    }
493
494    /// Returns a reference to the underlying tensor.
495    ///
496    /// # Examples
497    /// ```rust
498    /// use edgefirst_image::{RGB, TensorImage};
499    /// use edgefirst_tensor::{Tensor, TensorTrait};
500    ///  # fn main() -> Result<(), edgefirst_image::Error> {
501    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
502    /// let img = TensorImage::from_tensor(tensor, RGB)?;
503    /// let underlying_tensor = img.tensor();
504    /// assert_eq!(underlying_tensor.name(), "Tensor");
505    /// # Ok(())
506    /// # }
507    pub fn tensor(&self) -> &Tensor<u8> {
508        &self.tensor
509    }
510
511    /// Returns the FourCC code representing the image format.
512    ///
513    /// # Examples
514    /// ```rust
515    /// use edgefirst_image::{RGB, TensorImage};
516    /// use edgefirst_tensor::{Tensor, TensorTrait};
517    ///  # fn main() -> Result<(), edgefirst_image::Error> {
518    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
519    /// let img = TensorImage::from_tensor(tensor, RGB)?;
520    /// assert_eq!(img.fourcc(), RGB);
521    /// # Ok(())
522    /// # }
523    pub fn fourcc(&self) -> FourCharCode {
524        self.fourcc
525    }
526
527    /// # Examples
528    /// ```rust
529    /// use edgefirst_image::{RGB, TensorImage};
530    /// use edgefirst_tensor::{Tensor, TensorTrait};
531    ///  # fn main() -> Result<(), edgefirst_image::Error> {
532    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
533    /// let img = TensorImage::from_tensor(tensor, RGB)?;
534    /// assert!(!img.is_planar());
535    /// # Ok(())
536    /// # }
537    pub fn is_planar(&self) -> bool {
538        self.is_planar
539    }
540
541    /// # Examples
542    /// ```rust
543    /// use edgefirst_image::{RGB, TensorImage};
544    /// use edgefirst_tensor::{Tensor, TensorTrait};
545    ///  # fn main() -> Result<(), edgefirst_image::Error> {
546    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
547    /// let img = TensorImage::from_tensor(tensor, RGB)?;
548    /// assert_eq!(img.width(), 1280);
549    /// # Ok(())
550    /// # }
551    pub fn width(&self) -> usize {
552        // NV12 uses shape [H*3/2, W]
553        if self.fourcc == NV12 {
554            return self.tensor.shape()[1];
555        }
556        match self.is_planar {
557            true => self.tensor.shape()[2],
558            false => self.tensor.shape()[1],
559        }
560    }
561
562    /// # Examples
563    /// ```rust
564    /// use edgefirst_image::{RGB, TensorImage};
565    /// use edgefirst_tensor::{Tensor, TensorTrait};
566    ///  # fn main() -> Result<(), edgefirst_image::Error> {
567    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
568    /// let img = TensorImage::from_tensor(tensor, RGB)?;
569    /// assert_eq!(img.height(), 720);
570    /// # Ok(())
571    /// # }
572    pub fn height(&self) -> usize {
573        // NV12 uses shape [H*3/2, W], so height = shape[0] * 2 / 3
574        if self.fourcc == NV12 {
575            return self.tensor.shape()[0] * 2 / 3;
576        }
577        match self.is_planar {
578            true => self.tensor.shape()[1],
579            false => self.tensor.shape()[0],
580        }
581    }
582
583    /// # Examples
584    /// ```rust
585    /// use edgefirst_image::{RGB, TensorImage};
586    /// use edgefirst_tensor::{Tensor, TensorTrait};
587    ///  # fn main() -> Result<(), edgefirst_image::Error> {
588    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
589    /// let img = TensorImage::from_tensor(tensor, RGB)?;
590    /// assert_eq!(img.channels(), 3);
591    /// # Ok(())
592    /// # }
593    pub fn channels(&self) -> usize {
594        // NV12 uses 2D shape [H*3/2, W], conceptually has 2 components (Y + interleaved
595        // UV)
596        if self.fourcc == NV12 {
597            return 2;
598        }
599        match self.is_planar {
600            true => self.tensor.shape()[0],
601            false => self.tensor.shape()[2],
602        }
603    }
604
605    /// # Examples
606    /// ```rust
607    /// use edgefirst_image::{RGB, TensorImage};
608    /// use edgefirst_tensor::{Tensor, TensorTrait};
609    ///  # fn main() -> Result<(), edgefirst_image::Error> {
610    /// let tensor = Tensor::new(&[720, 1280, 3], None, Some("Tensor"))?;
611    /// let img = TensorImage::from_tensor(tensor, RGB)?;
612    /// assert_eq!(img.row_stride(), 1280*3);
613    /// # Ok(())
614    /// # }
615    pub fn row_stride(&self) -> usize {
616        match self.is_planar {
617            true => self.width(),
618            false => self.width() * self.channels(),
619        }
620    }
621}
622
623/// Trait for types that can be used as destination images for conversion.
624///
625/// This trait abstracts over the difference between owned (`TensorImage`) and
626/// borrowed (`TensorImageRef`) image buffers, enabling the same conversion code
627/// to work with both.
628pub trait TensorImageDst {
629    /// Returns a reference to the underlying tensor.
630    fn tensor(&self) -> &Tensor<u8>;
631    /// Returns a mutable reference to the underlying tensor.
632    fn tensor_mut(&mut self) -> &mut Tensor<u8>;
633    /// Returns the FourCC code representing the image format.
634    fn fourcc(&self) -> FourCharCode;
635    /// Returns whether the image is in planar format.
636    fn is_planar(&self) -> bool;
637    /// Returns the width of the image in pixels.
638    fn width(&self) -> usize;
639    /// Returns the height of the image in pixels.
640    fn height(&self) -> usize;
641    /// Returns the number of channels in the image.
642    fn channels(&self) -> usize;
643    /// Returns the row stride in bytes.
644    fn row_stride(&self) -> usize;
645}
646
647impl TensorImageDst for TensorImage {
648    fn tensor(&self) -> &Tensor<u8> {
649        &self.tensor
650    }
651
652    fn tensor_mut(&mut self) -> &mut Tensor<u8> {
653        &mut self.tensor
654    }
655
656    fn fourcc(&self) -> FourCharCode {
657        self.fourcc
658    }
659
660    fn is_planar(&self) -> bool {
661        self.is_planar
662    }
663
664    fn width(&self) -> usize {
665        TensorImage::width(self)
666    }
667
668    fn height(&self) -> usize {
669        TensorImage::height(self)
670    }
671
672    fn channels(&self) -> usize {
673        TensorImage::channels(self)
674    }
675
676    fn row_stride(&self) -> usize {
677        TensorImage::row_stride(self)
678    }
679}
680
681/// A borrowed view of an image tensor for zero-copy preprocessing.
682///
683/// `TensorImageRef` wraps a borrowed `&mut Tensor<u8>` instead of owning it,
684/// enabling zero-copy operations where the HAL writes directly into an external
685/// tensor (e.g., a model's pre-allocated input buffer).
686///
687/// # Examples
688/// ```rust,ignore
689/// // Create a borrowed tensor image wrapping the model's input tensor
690/// let mut dst = TensorImageRef::from_borrowed_tensor(
691///     model.input_tensor(0),
692///     PLANAR_RGB,
693/// )?;
694///
695/// // Preprocess directly into the model's input buffer
696/// processor.convert(&src_image, &mut dst, Rotation::None, Flip::None, Crop::default())?;
697///
698/// // Run inference - no copy needed!
699/// model.run()?;
700/// ```
701#[derive(Debug)]
702pub struct TensorImageRef<'a> {
703    pub(crate) tensor: &'a mut Tensor<u8>,
704    fourcc: FourCharCode,
705    is_planar: bool,
706}
707
708impl<'a> TensorImageRef<'a> {
709    /// Creates a `TensorImageRef` from a borrowed tensor reference.
710    ///
711    /// The tensor shape must match the expected format:
712    /// - For planar formats (e.g., PLANAR_RGB): shape is `[channels, height,
713    ///   width]`
714    /// - For interleaved formats (e.g., RGB, RGBA): shape is `[height, width,
715    ///   channels]`
716    ///
717    /// # Arguments
718    /// * `tensor` - A mutable reference to the tensor to wrap
719    /// * `fourcc` - The pixel format of the image
720    ///
721    /// # Returns
722    /// A `Result` containing the `TensorImageRef` or an error if the tensor
723    /// shape doesn't match the expected format.
724    pub fn from_borrowed_tensor(tensor: &'a mut Tensor<u8>, fourcc: FourCharCode) -> Result<Self> {
725        let shape = tensor.shape();
726        if shape.len() != 3 {
727            return Err(Error::InvalidShape(format!(
728                "Tensor shape must have 3 dimensions, got {}: {:?}",
729                shape.len(),
730                shape
731            )));
732        }
733        let is_planar = fourcc_planar(fourcc)?;
734        let channels = if is_planar { shape[0] } else { shape[2] };
735
736        if fourcc_channels(fourcc)? != channels {
737            return Err(Error::InvalidShape(format!(
738                "Invalid tensor shape {:?} for format {}",
739                shape,
740                fourcc.to_string()
741            )));
742        }
743
744        Ok(Self {
745            tensor,
746            fourcc,
747            is_planar,
748        })
749    }
750
751    /// Returns a reference to the underlying tensor.
752    pub fn tensor(&self) -> &Tensor<u8> {
753        self.tensor
754    }
755
756    /// Returns the FourCC code representing the image format.
757    pub fn fourcc(&self) -> FourCharCode {
758        self.fourcc
759    }
760
761    /// Returns whether the image is in planar format.
762    pub fn is_planar(&self) -> bool {
763        self.is_planar
764    }
765
766    /// Returns the width of the image in pixels.
767    pub fn width(&self) -> usize {
768        match self.is_planar {
769            true => self.tensor.shape()[2],
770            false => self.tensor.shape()[1],
771        }
772    }
773
774    /// Returns the height of the image in pixels.
775    pub fn height(&self) -> usize {
776        match self.is_planar {
777            true => self.tensor.shape()[1],
778            false => self.tensor.shape()[0],
779        }
780    }
781
782    /// Returns the number of channels in the image.
783    pub fn channels(&self) -> usize {
784        match self.is_planar {
785            true => self.tensor.shape()[0],
786            false => self.tensor.shape()[2],
787        }
788    }
789
790    /// Returns the row stride in bytes.
791    pub fn row_stride(&self) -> usize {
792        match self.is_planar {
793            true => self.width(),
794            false => self.width() * self.channels(),
795        }
796    }
797}
798
799impl TensorImageDst for TensorImageRef<'_> {
800    fn tensor(&self) -> &Tensor<u8> {
801        self.tensor
802    }
803
804    fn tensor_mut(&mut self) -> &mut Tensor<u8> {
805        self.tensor
806    }
807
808    fn fourcc(&self) -> FourCharCode {
809        self.fourcc
810    }
811
812    fn is_planar(&self) -> bool {
813        self.is_planar
814    }
815
816    fn width(&self) -> usize {
817        TensorImageRef::width(self)
818    }
819
820    fn height(&self) -> usize {
821        TensorImageRef::height(self)
822    }
823
824    fn channels(&self) -> usize {
825        TensorImageRef::channels(self)
826    }
827
828    fn row_stride(&self) -> usize {
829        TensorImageRef::row_stride(self)
830    }
831}
832
833/// Flips the image, and the rotates it.
834fn rotate_flip_to_tensor_image(
835    src: &TensorImage,
836    rotation: Rotation,
837    flip: Flip,
838    memory: Option<TensorMemory>,
839) -> Result<TensorImage, Error> {
840    let src_map = src.tensor.map()?;
841    let dst = match rotation {
842        Rotation::None | Rotation::Rotate180 => {
843            TensorImage::new(src.width(), src.height(), src.fourcc(), memory)?
844        }
845        Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
846            TensorImage::new(src.height(), src.width(), src.fourcc(), memory)?
847        }
848    };
849
850    let mut dst_map = dst.tensor.map()?;
851
852    CPUProcessor::flip_rotate_ndarray(&src_map, &mut dst_map, &dst, rotation, flip)?;
853
854    Ok(dst)
855}
856
857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
858pub enum Rotation {
859    None = 0,
860    Clockwise90 = 1,
861    Rotate180 = 2,
862    CounterClockwise90 = 3,
863}
864impl Rotation {
865    /// Creates a Rotation enum from an angle in degrees. The angle must be a
866    /// multiple of 90.
867    ///
868    /// # Panics
869    /// Panics if the angle is not a multiple of 90.
870    ///
871    /// # Examples
872    /// ```rust
873    /// # use edgefirst_image::Rotation;
874    /// let rotation = Rotation::from_degrees_clockwise(270);
875    /// assert_eq!(rotation, Rotation::CounterClockwise90);
876    /// ```
877    pub fn from_degrees_clockwise(angle: usize) -> Rotation {
878        match angle.rem_euclid(360) {
879            0 => Rotation::None,
880            90 => Rotation::Clockwise90,
881            180 => Rotation::Rotate180,
882            270 => Rotation::CounterClockwise90,
883            _ => panic!("rotation angle is not a multiple of 90"),
884        }
885    }
886}
887
888#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889pub enum Flip {
890    None = 0,
891    Vertical = 1,
892    Horizontal = 2,
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
896pub struct Crop {
897    pub src_rect: Option<Rect>,
898    pub dst_rect: Option<Rect>,
899    pub dst_color: Option<[u8; 4]>,
900}
901impl Crop {
902    // Creates a new Crop with default values (no cropping).
903    pub fn new() -> Self {
904        Crop::default()
905    }
906
907    // Sets the source rectangle for cropping.
908    pub fn with_src_rect(mut self, src_rect: Option<Rect>) -> Self {
909        self.src_rect = src_rect;
910        self
911    }
912
913    // Sets the destination rectangle for cropping.
914    pub fn with_dst_rect(mut self, dst_rect: Option<Rect>) -> Self {
915        self.dst_rect = dst_rect;
916        self
917    }
918
919    // Sets the destination color for areas outside the cropped region.
920    pub fn with_dst_color(mut self, dst_color: Option<[u8; 4]>) -> Self {
921        self.dst_color = dst_color;
922        self
923    }
924
925    // Creates a new Crop with no cropping.
926    pub fn no_crop() -> Self {
927        Crop::default()
928    }
929
930    // Checks if the crop rectangles are valid for the given source and
931    // destination images.
932    pub fn check_crop(&self, src: &TensorImage, dst: &TensorImage) -> Result<(), Error> {
933        let src = self.src_rect.is_none_or(|x| x.check_rect(src));
934        let dst = self.dst_rect.is_none_or(|x| x.check_rect(dst));
935        match (src, dst) {
936            (true, true) => Ok(()),
937            (true, false) => Err(Error::CropInvalid(format!(
938                "Dest crop invalid: {:?}",
939                self.dst_rect
940            ))),
941            (false, true) => Err(Error::CropInvalid(format!(
942                "Src crop invalid: {:?}",
943                self.src_rect
944            ))),
945            (false, false) => Err(Error::CropInvalid(format!(
946                "Dest and Src crop invalid: {:?} {:?}",
947                self.dst_rect, self.src_rect
948            ))),
949        }
950    }
951
952    // Checks if the crop rectangles are valid for the given source and
953    // destination images (using TensorImageRef for destination).
954    pub fn check_crop_ref(&self, src: &TensorImage, dst: &TensorImageRef<'_>) -> Result<(), Error> {
955        let src = self.src_rect.is_none_or(|x| x.check_rect(src));
956        let dst = self.dst_rect.is_none_or(|x| x.check_rect_dst(dst));
957        match (src, dst) {
958            (true, true) => Ok(()),
959            (true, false) => Err(Error::CropInvalid(format!(
960                "Dest crop invalid: {:?}",
961                self.dst_rect
962            ))),
963            (false, true) => Err(Error::CropInvalid(format!(
964                "Src crop invalid: {:?}",
965                self.src_rect
966            ))),
967            (false, false) => Err(Error::CropInvalid(format!(
968                "Dest and Src crop invalid: {:?} {:?}",
969                self.dst_rect, self.src_rect
970            ))),
971        }
972    }
973}
974
975#[derive(Debug, Clone, Copy, PartialEq, Eq)]
976pub struct Rect {
977    pub left: usize,
978    pub top: usize,
979    pub width: usize,
980    pub height: usize,
981}
982
983impl Rect {
984    // Creates a new Rect with the specified left, top, width, and height.
985    pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
986        Self {
987            left,
988            top,
989            width,
990            height,
991        }
992    }
993
994    // Checks if the rectangle is valid for the given image.
995    pub fn check_rect(&self, image: &TensorImage) -> bool {
996        self.left + self.width <= image.width() && self.top + self.height <= image.height()
997    }
998
999    // Checks if the rectangle is valid for the given destination image.
1000    pub fn check_rect_dst<D: TensorImageDst>(&self, image: &D) -> bool {
1001        self.left + self.width <= image.width() && self.top + self.height <= image.height()
1002    }
1003}
1004
1005#[enum_dispatch(ImageProcessor)]
1006pub trait ImageProcessorTrait {
1007    /// Converts the source image to the destination image format and size. The
1008    /// image is cropped first, then flipped, then rotated
1009    ///
1010    /// # Arguments
1011    ///
1012    /// * `dst` - The destination image to be converted to.
1013    /// * `src` - The source image to convert from.
1014    /// * `rotation` - The rotation to apply to the destination image.
1015    /// * `flip` - Flips the image
1016    /// * `crop` - An optional rectangle specifying the area to crop from the
1017    ///   source image
1018    ///
1019    /// # Returns
1020    ///
1021    /// A `Result` indicating success or failure of the conversion.
1022    fn convert(
1023        &mut self,
1024        src: &TensorImage,
1025        dst: &mut TensorImage,
1026        rotation: Rotation,
1027        flip: Flip,
1028        crop: Crop,
1029    ) -> Result<()>;
1030
1031    /// Converts the source image to a borrowed destination tensor for zero-copy
1032    /// preprocessing.
1033    ///
1034    /// This variant accepts a `TensorImageRef` as the destination, enabling
1035    /// direct writes into external buffers (e.g., model input tensors) without
1036    /// intermediate copies.
1037    ///
1038    /// # Arguments
1039    ///
1040    /// * `src` - The source image to convert from.
1041    /// * `dst` - A borrowed tensor image wrapping the destination buffer.
1042    /// * `rotation` - The rotation to apply to the destination image.
1043    /// * `flip` - Flips the image
1044    /// * `crop` - An optional rectangle specifying the area to crop from the
1045    ///   source image
1046    ///
1047    /// # Returns
1048    ///
1049    /// A `Result` indicating success or failure of the conversion.
1050    fn convert_ref(
1051        &mut self,
1052        src: &TensorImage,
1053        dst: &mut TensorImageRef<'_>,
1054        rotation: Rotation,
1055        flip: Flip,
1056        crop: Crop,
1057    ) -> Result<()>;
1058
1059    #[cfg(feature = "decoder")]
1060    fn render_to_image(
1061        &mut self,
1062        dst: &mut TensorImage,
1063        detect: &[DetectBox],
1064        segmentation: &[Segmentation],
1065    ) -> Result<()>;
1066
1067    #[cfg(feature = "decoder")]
1068    /// Sets the colors used for rendering segmentation masks. Up to 17 colors
1069    /// can be set.
1070    fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1071}
1072
1073/// Image converter that uses available hardware acceleration or CPU as a
1074/// fallback.
1075#[derive(Debug)]
1076pub struct ImageProcessor {
1077    /// CPU-based image converter as a fallback. This is only None if the
1078    /// EDGEFIRST_DISABLE_CPU environment variable is set.
1079    pub cpu: Option<CPUProcessor>,
1080
1081    #[cfg(target_os = "linux")]
1082    /// G2D-based image converter for Linux systems. This is only available if
1083    /// the EDGEFIRST_DISABLE_G2D environment variable is not set and libg2d.so
1084    /// is available.
1085    pub g2d: Option<G2DProcessor>,
1086    #[cfg(target_os = "linux")]
1087    #[cfg(feature = "opengl")]
1088    /// OpenGL-based image converter for Linux systems. This is only available
1089    /// if the EDGEFIRST_DISABLE_GL environment variable is not set and OpenGL
1090    /// ES is available.
1091    pub opengl: Option<GLProcessorThreaded>,
1092}
1093
1094unsafe impl Send for ImageProcessor {}
1095unsafe impl Sync for ImageProcessor {}
1096
1097impl ImageProcessor {
1098    /// Creates a new `ImageProcessor` instance, initializing available
1099    /// hardware converters based on the system capabilities and environment
1100    /// variables.
1101    ///
1102    /// # Examples
1103    /// ```rust
1104    /// # use edgefirst_image::{ImageProcessor, TensorImage, RGBA, RGB, Rotation, Flip, Crop, ImageProcessorTrait};
1105    /// # fn main() -> Result<(), edgefirst_image::Error> {
1106    /// let image = include_bytes!("../../../testdata/zidane.jpg");
1107    /// let img = TensorImage::load(image, Some(RGBA), None)?;
1108    /// let mut converter = ImageProcessor::new()?;
1109    /// let mut dst = TensorImage::new(640, 480, RGB, None)?;
1110    /// converter.convert(&img, &mut dst, Rotation::None, Flip::None, Crop::default())?;
1111    /// # Ok(())
1112    /// # }
1113    pub fn new() -> Result<Self> {
1114        #[cfg(target_os = "linux")]
1115        let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1116            .map(|x| x != "0" && x.to_lowercase() != "false")
1117            .unwrap_or(false)
1118        {
1119            log::debug!("EDGEFIRST_DISABLE_G2D is set");
1120            None
1121        } else {
1122            match G2DProcessor::new() {
1123                Ok(g2d_converter) => Some(g2d_converter),
1124                Err(err) => {
1125                    log::warn!("Failed to initialize G2D converter: {err:?}");
1126                    None
1127                }
1128            }
1129        };
1130
1131        #[cfg(target_os = "linux")]
1132        #[cfg(feature = "opengl")]
1133        let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1134            .map(|x| x != "0" && x.to_lowercase() != "false")
1135            .unwrap_or(false)
1136        {
1137            log::debug!("EDGEFIRST_DISABLE_GL is set");
1138            None
1139        } else {
1140            match GLProcessorThreaded::new() {
1141                Ok(gl_converter) => Some(gl_converter),
1142                Err(err) => {
1143                    log::warn!("Failed to initialize GL converter: {err:?}");
1144                    None
1145                }
1146            }
1147        };
1148
1149        let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1150            .map(|x| x != "0" && x.to_lowercase() != "false")
1151            .unwrap_or(false)
1152        {
1153            log::debug!("EDGEFIRST_DISABLE_CPU is set");
1154            None
1155        } else {
1156            Some(CPUProcessor::new())
1157        };
1158        Ok(Self {
1159            cpu,
1160            #[cfg(target_os = "linux")]
1161            g2d,
1162            #[cfg(target_os = "linux")]
1163            #[cfg(feature = "opengl")]
1164            opengl,
1165        })
1166    }
1167}
1168
1169impl ImageProcessorTrait for ImageProcessor {
1170    /// Converts the source image to the destination image format and size. The
1171    /// image is cropped first, then flipped, then rotated
1172    ///
1173    /// Prefer hardware accelerators when available, falling back to CPU if
1174    /// necessary.
1175    fn convert(
1176        &mut self,
1177        src: &TensorImage,
1178        dst: &mut TensorImage,
1179        rotation: Rotation,
1180        flip: Flip,
1181        crop: Crop,
1182    ) -> Result<()> {
1183        let start = Instant::now();
1184
1185        #[cfg(target_os = "linux")]
1186        if let Some(g2d) = self.g2d.as_mut() {
1187            log::trace!("image started with g2d in {:?}", start.elapsed());
1188            match g2d.convert(src, dst, rotation, flip, crop) {
1189                Ok(_) => {
1190                    log::trace!("image converted with g2d in {:?}", start.elapsed());
1191                    return Ok(());
1192                }
1193                Err(e) => {
1194                    log::trace!("image didn't convert with g2d: {e:?}")
1195                }
1196            }
1197        }
1198
1199        // if the image is just a copy without an resizing, the send it to the CPU and
1200        // skip OpenGL
1201        let src_shape = match crop.src_rect {
1202            Some(s) => (s.width, s.height),
1203            None => (src.width(), src.height()),
1204        };
1205        let dst_shape = match crop.dst_rect {
1206            Some(d) => (d.width, d.height),
1207            None => (dst.width(), dst.height()),
1208        };
1209
1210        // TODO: Check if still use CPU when rotation or flip is enabled
1211        if src_shape == dst_shape && flip == Flip::None && rotation == Rotation::None {
1212            if let Some(cpu) = self.cpu.as_mut() {
1213                match cpu.convert(src, dst, rotation, flip, crop) {
1214                    Ok(_) => {
1215                        log::trace!("image converted with cpu in {:?}", start.elapsed());
1216                        return Ok(());
1217                    }
1218                    Err(e) => {
1219                        log::trace!("image didn't convert with cpu: {e:?}");
1220                        return Err(e);
1221                    }
1222                }
1223            }
1224        }
1225
1226        #[cfg(target_os = "linux")]
1227        #[cfg(feature = "opengl")]
1228        if let Some(opengl) = self.opengl.as_mut() {
1229            log::trace!("image started with opengl in {:?}", start.elapsed());
1230            match opengl.convert(src, dst, rotation, flip, crop) {
1231                Ok(_) => {
1232                    log::trace!("image converted with opengl in {:?}", start.elapsed());
1233                    return Ok(());
1234                }
1235                Err(e) => {
1236                    log::trace!("image didn't convert with opengl: {e:?}")
1237                }
1238            }
1239        }
1240        log::trace!("image started with cpu in {:?}", start.elapsed());
1241        if let Some(cpu) = self.cpu.as_mut() {
1242            match cpu.convert(src, dst, rotation, flip, crop) {
1243                Ok(_) => {
1244                    log::trace!("image converted with cpu in {:?}", start.elapsed());
1245                    return Ok(());
1246                }
1247                Err(e) => {
1248                    log::trace!("image didn't convert with cpu: {e:?}");
1249                    return Err(e);
1250                }
1251            }
1252        }
1253        Err(Error::NoConverter)
1254    }
1255
1256    fn convert_ref(
1257        &mut self,
1258        src: &TensorImage,
1259        dst: &mut TensorImageRef<'_>,
1260        rotation: Rotation,
1261        flip: Flip,
1262        crop: Crop,
1263    ) -> Result<()> {
1264        let start = Instant::now();
1265
1266        // For TensorImageRef, we prefer CPU since hardware accelerators typically
1267        // don't support PLANAR_RGB output which is the common model input format.
1268        // The CPU path uses the generic conversion functions that work with any
1269        // TensorImageDst implementation.
1270        if let Some(cpu) = self.cpu.as_mut() {
1271            match cpu.convert_ref(src, dst, rotation, flip, crop) {
1272                Ok(_) => {
1273                    log::trace!("image converted with cpu (ref) in {:?}", start.elapsed());
1274                    return Ok(());
1275                }
1276                Err(e) => {
1277                    log::trace!("image didn't convert with cpu (ref): {e:?}");
1278                    return Err(e);
1279                }
1280            }
1281        }
1282
1283        Err(Error::NoConverter)
1284    }
1285
1286    #[cfg(feature = "decoder")]
1287    fn render_to_image(
1288        &mut self,
1289        dst: &mut TensorImage,
1290        detect: &[DetectBox],
1291        segmentation: &[Segmentation],
1292    ) -> Result<()> {
1293        let start = Instant::now();
1294
1295        if detect.is_empty() && segmentation.is_empty() {
1296            return Ok(());
1297        }
1298
1299        // skip G2D as it doesn't support rendering to image
1300
1301        #[cfg(target_os = "linux")]
1302        #[cfg(feature = "opengl")]
1303        if let Some(opengl) = self.opengl.as_mut() {
1304            log::trace!("image started with opengl in {:?}", start.elapsed());
1305            match opengl.render_to_image(dst, detect, segmentation) {
1306                Ok(_) => {
1307                    log::trace!("image rendered with opengl in {:?}", start.elapsed());
1308                    return Ok(());
1309                }
1310                Err(e) => {
1311                    log::trace!("image didn't render with opengl: {e:?}")
1312                }
1313            }
1314        }
1315        log::trace!("image started with cpu in {:?}", start.elapsed());
1316        if let Some(cpu) = self.cpu.as_mut() {
1317            match cpu.render_to_image(dst, detect, segmentation) {
1318                Ok(_) => {
1319                    log::trace!("image render with cpu in {:?}", start.elapsed());
1320                    return Ok(());
1321                }
1322                Err(e) => {
1323                    log::trace!("image didn't render with cpu: {e:?}");
1324                    return Err(e);
1325                }
1326            }
1327        }
1328        Err(Error::NoConverter)
1329    }
1330
1331    #[cfg(feature = "decoder")]
1332    fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
1333        let start = Instant::now();
1334
1335        // skip G2D as it doesn't support rendering to image
1336
1337        #[cfg(target_os = "linux")]
1338        #[cfg(feature = "opengl")]
1339        if let Some(opengl) = self.opengl.as_mut() {
1340            log::trace!("image started with opengl in {:?}", start.elapsed());
1341            match opengl.set_class_colors(colors) {
1342                Ok(_) => {
1343                    log::trace!("colors set with opengl in {:?}", start.elapsed());
1344                    return Ok(());
1345                }
1346                Err(e) => {
1347                    log::trace!("colors didn't set with opengl: {e:?}")
1348                }
1349            }
1350        }
1351        log::trace!("image started with cpu in {:?}", start.elapsed());
1352        if let Some(cpu) = self.cpu.as_mut() {
1353            match cpu.set_class_colors(colors) {
1354                Ok(_) => {
1355                    log::trace!("colors set with cpu in {:?}", start.elapsed());
1356                    return Ok(());
1357                }
1358                Err(e) => {
1359                    log::trace!("colors didn't set with cpu: {e:?}");
1360                    return Err(e);
1361                }
1362            }
1363        }
1364        Err(Error::NoConverter)
1365    }
1366}
1367
1368fn fourcc_channels(fourcc: FourCharCode) -> Result<usize> {
1369    match fourcc {
1370        RGBA => Ok(4), // RGBA has 4 channels (R, G, B, A)
1371        RGB => Ok(3),  // RGB has 3 channels (R, G, B)
1372        YUYV => Ok(2), // YUYV has 2 channels (Y and UV)
1373        GREY => Ok(1), // Y800 has 1 channel (Y)
1374        NV12 => Ok(2), // NV12 has 2 channel. 2nd channel is half empty
1375        NV16 => Ok(2), // NV16 has 2 channel. 2nd channel is full size
1376        PLANAR_RGB => Ok(3),
1377        PLANAR_RGBA => Ok(4),
1378        _ => Err(Error::NotSupported(format!(
1379            "Unsupported fourcc: {}",
1380            fourcc.to_string()
1381        ))),
1382    }
1383}
1384
1385fn fourcc_planar(fourcc: FourCharCode) -> Result<bool> {
1386    match fourcc {
1387        RGBA => Ok(false),       // RGBA has 4 channels (R, G, B, A)
1388        RGB => Ok(false),        // RGB has 3 channels (R, G, B)
1389        YUYV => Ok(false),       // YUYV has 2 channels (Y and UV)
1390        GREY => Ok(false),       // Y800 has 1 channel (Y)
1391        NV12 => Ok(true),        // Planar YUV
1392        NV16 => Ok(true),        // Planar YUV
1393        PLANAR_RGB => Ok(true),  // Planar RGB
1394        PLANAR_RGBA => Ok(true), // Planar RGBA
1395        _ => Err(Error::NotSupported(format!(
1396            "Unsupported fourcc: {}",
1397            fourcc.to_string()
1398        ))),
1399    }
1400}
1401
1402pub(crate) struct FunctionTimer<T: Display> {
1403    name: T,
1404    start: std::time::Instant,
1405}
1406
1407impl<T: Display> FunctionTimer<T> {
1408    pub fn new(name: T) -> Self {
1409        Self {
1410            name,
1411            start: std::time::Instant::now(),
1412        }
1413    }
1414}
1415
1416impl<T: Display> Drop for FunctionTimer<T> {
1417    fn drop(&mut self) {
1418        log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
1419    }
1420}
1421
1422#[cfg(feature = "decoder")]
1423const DEFAULT_COLORS: [[f32; 4]; 20] = [
1424    [0., 1., 0., 0.7],
1425    [1., 0.5568628, 0., 0.7],
1426    [0.25882353, 0.15294118, 0.13333333, 0.7],
1427    [0.8, 0.7647059, 0.78039216, 0.7],
1428    [0.3137255, 0.3137255, 0.3137255, 0.7],
1429    [0.1411765, 0.3098039, 0.1215686, 0.7],
1430    [1., 0.95686275, 0.5137255, 0.7],
1431    [0.3529412, 0.32156863, 0., 0.7],
1432    [0.4235294, 0.6235294, 0.6509804, 0.7],
1433    [0.5098039, 0.5098039, 0.7294118, 0.7],
1434    [0.00784314, 0.18823529, 0.29411765, 0.7],
1435    [0.0, 0.2706, 1.0, 0.7],
1436    [0.0, 0.0, 0.0, 0.7],
1437    [0.0, 0.5, 0.0, 0.7],
1438    [1.0, 0.0, 0.0, 0.7],
1439    [0.0, 0.0, 1.0, 0.7],
1440    [1.0, 0.5, 0.5, 0.7],
1441    [0.1333, 0.5451, 0.1333, 0.7],
1442    [0.1176, 0.4118, 0.8235, 0.7],
1443    [1., 1., 1., 0.7],
1444];
1445
1446#[cfg(feature = "decoder")]
1447const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
1448    let mut result = [[0; M]; N];
1449    let mut i = 0;
1450    while i < N {
1451        let mut j = 0;
1452        while j < M {
1453            result[i][j] = (a[i][j] * 255.0).round() as u8;
1454            j += 1;
1455        }
1456        i += 1;
1457    }
1458    result
1459}
1460
1461#[cfg(feature = "decoder")]
1462const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
1463
1464#[cfg(test)]
1465#[cfg_attr(coverage_nightly, coverage(off))]
1466mod image_tests {
1467    use super::*;
1468    use crate::{CPUProcessor, Rotation};
1469    #[cfg(target_os = "linux")]
1470    use edgefirst_tensor::is_dma_available;
1471    use edgefirst_tensor::{TensorMapTrait, TensorMemory};
1472    use image::buffer::ConvertBuffer;
1473
1474    #[ctor::ctor]
1475    fn init() {
1476        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
1477    }
1478
1479    macro_rules! function {
1480        () => {{
1481            fn f() {}
1482            fn type_name_of<T>(_: T) -> &'static str {
1483                std::any::type_name::<T>()
1484            }
1485            let name = type_name_of(f);
1486
1487            // Find and cut the rest of the path
1488            match &name[..name.len() - 3].rfind(':') {
1489                Some(pos) => &name[pos + 1..name.len() - 3],
1490                None => &name[..name.len() - 3],
1491            }
1492        }};
1493    }
1494
1495    #[test]
1496    fn test_invalid_crop() {
1497        let src = TensorImage::new(100, 100, RGB, None).unwrap();
1498        let dst = TensorImage::new(100, 100, RGB, None).unwrap();
1499
1500        let crop = Crop::new()
1501            .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1502            .with_dst_rect(Some(Rect::new(0, 0, 150, 150)));
1503
1504        let result = crop.check_crop(&src, &dst);
1505        assert!(matches!(
1506            result,
1507            Err(Error::CropInvalid(e)) if e.starts_with("Dest and Src crop invalid")
1508        ));
1509
1510        let crop = crop.with_src_rect(Some(Rect::new(0, 0, 10, 10)));
1511        let result = crop.check_crop(&src, &dst);
1512        assert!(matches!(
1513            result,
1514            Err(Error::CropInvalid(e)) if e.starts_with("Dest crop invalid")
1515        ));
1516
1517        let crop = crop
1518            .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1519            .with_dst_rect(Some(Rect::new(0, 0, 50, 50)));
1520        let result = crop.check_crop(&src, &dst);
1521        assert!(matches!(
1522            result,
1523            Err(Error::CropInvalid(e)) if e.starts_with("Src crop invalid")
1524        ));
1525
1526        let crop = crop.with_src_rect(Some(Rect::new(50, 50, 50, 50)));
1527
1528        let result = crop.check_crop(&src, &dst);
1529        assert!(result.is_ok());
1530    }
1531
1532    #[test]
1533    fn test_invalid_tensor() -> Result<(), Error> {
1534        let tensor = Tensor::new(&[720, 1280, 4, 1], None, None)?;
1535        let result = TensorImage::from_tensor(tensor, RGB);
1536        assert!(matches!(
1537            result,
1538            Err(Error::InvalidShape(e)) if e.starts_with("Tensor shape must have 3 dimensions, got")
1539        ));
1540
1541        let tensor = Tensor::new(&[720, 1280, 4], None, None)?;
1542        let result = TensorImage::from_tensor(tensor, RGB);
1543        assert!(matches!(
1544            result,
1545            Err(Error::InvalidShape(e)) if e.starts_with("Invalid tensor shape")
1546        ));
1547
1548        Ok(())
1549    }
1550
1551    #[test]
1552    fn test_invalid_image_file() -> Result<(), Error> {
1553        let result = TensorImage::load(&[123; 5000], None, None);
1554        assert!(matches!(
1555            result,
1556            Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
1557
1558        Ok(())
1559    }
1560
1561    #[test]
1562    fn test_invalid_jpeg_fourcc() -> Result<(), Error> {
1563        let result = TensorImage::load(&[123; 5000], Some(YUYV), None);
1564        assert!(matches!(
1565            result,
1566            Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
1567
1568        Ok(())
1569    }
1570
1571    #[test]
1572    fn test_load_resize_save() {
1573        let file = include_bytes!("../../../testdata/zidane.jpg");
1574        let img = TensorImage::load_jpeg(file, Some(RGBA), None).unwrap();
1575        assert_eq!(img.width(), 1280);
1576        assert_eq!(img.height(), 720);
1577
1578        let mut dst = TensorImage::new(640, 360, RGBA, None).unwrap();
1579        let mut converter = CPUProcessor::new();
1580        converter
1581            .convert(&img, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
1582            .unwrap();
1583        assert_eq!(dst.width(), 640);
1584        assert_eq!(dst.height(), 360);
1585
1586        dst.save_jpeg("zidane_resized.jpg", 80).unwrap();
1587
1588        let file = std::fs::read("zidane_resized.jpg").unwrap();
1589        let img = TensorImage::load_jpeg(&file, None, None).unwrap();
1590        assert_eq!(img.width(), 640);
1591        assert_eq!(img.height(), 360);
1592        assert_eq!(img.fourcc(), RGB);
1593    }
1594
1595    #[test]
1596    fn test_from_tensor_planar() -> Result<(), Error> {
1597        let tensor = Tensor::new(&[3, 720, 1280], None, None)?;
1598        tensor
1599            .map()?
1600            .copy_from_slice(include_bytes!("../../../testdata/camera720p.8bps"));
1601        let planar = TensorImage::from_tensor(tensor, PLANAR_RGB)?;
1602
1603        let rbga = load_bytes_to_tensor(
1604            1280,
1605            720,
1606            RGBA,
1607            None,
1608            include_bytes!("../../../testdata/camera720p.rgba"),
1609        )?;
1610        compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
1611
1612        Ok(())
1613    }
1614
1615    #[test]
1616    fn test_from_tensor_invalid_fourcc() {
1617        let tensor = Tensor::new(&[3, 720, 1280], None, None).unwrap();
1618        let result = TensorImage::from_tensor(tensor, four_char_code!("TEST"));
1619        matches!(result, Err(Error::NotSupported(e)) if e.starts_with("Unsupported fourcc : TEST"));
1620    }
1621
1622    #[test]
1623    #[should_panic(expected = "Failed to save planar RGB image")]
1624    fn test_save_planar() {
1625        let planar_img = load_bytes_to_tensor(
1626            1280,
1627            720,
1628            PLANAR_RGB,
1629            None,
1630            include_bytes!("../../../testdata/camera720p.8bps"),
1631        )
1632        .unwrap();
1633
1634        let save_path = "/tmp/planar_rgb.jpg";
1635        planar_img
1636            .save_jpeg(save_path, 90)
1637            .expect("Failed to save planar RGB image");
1638    }
1639
1640    #[test]
1641    #[should_panic(expected = "Failed to save YUYV image")]
1642    fn test_save_yuyv() {
1643        let planar_img = load_bytes_to_tensor(
1644            1280,
1645            720,
1646            YUYV,
1647            None,
1648            include_bytes!("../../../testdata/camera720p.yuyv"),
1649        )
1650        .unwrap();
1651
1652        let save_path = "/tmp/yuyv.jpg";
1653        planar_img
1654            .save_jpeg(save_path, 90)
1655            .expect("Failed to save YUYV image");
1656    }
1657
1658    #[test]
1659    fn test_rotation_angle() {
1660        assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
1661        assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
1662        assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
1663        assert_eq!(
1664            Rotation::from_degrees_clockwise(270),
1665            Rotation::CounterClockwise90
1666        );
1667        assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
1668        assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
1669        assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
1670        assert_eq!(
1671            Rotation::from_degrees_clockwise(630),
1672            Rotation::CounterClockwise90
1673        );
1674    }
1675
1676    #[test]
1677    #[should_panic(expected = "rotation angle is not a multiple of 90")]
1678    fn test_rotation_angle_panic() {
1679        Rotation::from_degrees_clockwise(361);
1680    }
1681
1682    #[test]
1683    fn test_disable_env_var() -> Result<(), Error> {
1684        #[cfg(target_os = "linux")]
1685        {
1686            let original = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1687            unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1688            let converter = ImageProcessor::new()?;
1689            match original {
1690                Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1691                None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1692            }
1693            assert!(converter.g2d.is_none());
1694        }
1695
1696        #[cfg(target_os = "linux")]
1697        #[cfg(feature = "opengl")]
1698        {
1699            let original = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1700            unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1701            let converter = ImageProcessor::new()?;
1702            match original {
1703                Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1704                None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1705            }
1706            assert!(converter.opengl.is_none());
1707        }
1708
1709        let original = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1710        unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1711        let converter = ImageProcessor::new()?;
1712        match original {
1713            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1714            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1715        }
1716        assert!(converter.cpu.is_none());
1717
1718        let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1719        unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1720        let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1721        unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1722        let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1723        unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1724        let mut converter = ImageProcessor::new()?;
1725
1726        let src = TensorImage::new(1280, 720, RGBA, None)?;
1727        let mut dst = TensorImage::new(640, 360, RGBA, None)?;
1728        let result = converter.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop());
1729        assert!(matches!(result, Err(Error::NoConverter)));
1730
1731        match original_cpu {
1732            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1733            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1734        }
1735        match original_gl {
1736            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1737            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1738        }
1739        match original_g2d {
1740            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1741            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1742        }
1743
1744        Ok(())
1745    }
1746
1747    #[test]
1748    fn test_unsupported_conversion() {
1749        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
1750        let mut dst = TensorImage::new(640, 360, NV12, None).unwrap();
1751        let mut converter = ImageProcessor::new().unwrap();
1752        let result = converter.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop());
1753        log::debug!("result: {:?}", result);
1754        assert!(matches!(
1755            result,
1756            Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
1757        ));
1758    }
1759
1760    #[test]
1761    fn test_load_grey() {
1762        let grey_img = TensorImage::load_jpeg(
1763            include_bytes!("../../../testdata/grey.jpg"),
1764            Some(RGBA),
1765            None,
1766        )
1767        .unwrap();
1768
1769        let grey_but_rgb_img = TensorImage::load_jpeg(
1770            include_bytes!("../../../testdata/grey-rgb.jpg"),
1771            Some(RGBA),
1772            None,
1773        )
1774        .unwrap();
1775
1776        compare_images(&grey_img, &grey_but_rgb_img, 0.99, function!());
1777    }
1778
1779    #[test]
1780    fn test_new_nv12() {
1781        let nv12 = TensorImage::new(1280, 720, NV12, None).unwrap();
1782        assert_eq!(nv12.height(), 720);
1783        assert_eq!(nv12.width(), 1280);
1784        assert_eq!(nv12.fourcc(), NV12);
1785        assert_eq!(nv12.channels(), 2);
1786        assert!(nv12.is_planar())
1787    }
1788
1789    #[test]
1790    #[cfg(target_os = "linux")]
1791    fn test_new_image_converter() {
1792        let dst_width = 640;
1793        let dst_height = 360;
1794        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1795        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1796
1797        let mut converter_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1798        let mut converter = ImageProcessor::new().unwrap();
1799        converter
1800            .convert(
1801                &src,
1802                &mut converter_dst,
1803                Rotation::None,
1804                Flip::None,
1805                Crop::no_crop(),
1806            )
1807            .unwrap();
1808
1809        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1810        let mut cpu_converter = CPUProcessor::new();
1811        cpu_converter
1812            .convert(
1813                &src,
1814                &mut cpu_dst,
1815                Rotation::None,
1816                Flip::None,
1817                Crop::no_crop(),
1818            )
1819            .unwrap();
1820
1821        compare_images(&converter_dst, &cpu_dst, 0.98, function!());
1822    }
1823
1824    #[test]
1825    fn test_crop_skip() {
1826        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1827        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1828
1829        let mut converter_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1830        let mut converter = ImageProcessor::new().unwrap();
1831        let crop = Crop::new()
1832            .with_src_rect(Some(Rect::new(0, 0, 640, 640)))
1833            .with_dst_rect(Some(Rect::new(0, 0, 640, 640)));
1834        converter
1835            .convert(&src, &mut converter_dst, Rotation::None, Flip::None, crop)
1836            .unwrap();
1837
1838        let mut cpu_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1839        let mut cpu_converter = CPUProcessor::new();
1840        cpu_converter
1841            .convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
1842            .unwrap();
1843
1844        compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
1845    }
1846
1847    #[test]
1848    fn test_invalid_fourcc() {
1849        let result = TensorImage::new(1280, 720, four_char_code!("TEST"), None);
1850        assert!(matches!(
1851            result,
1852            Err(Error::NotSupported(e)) if e == "Unsupported fourcc: TEST"
1853        ));
1854    }
1855
1856    // Helper function to check if G2D library is available (Linux/i.MX8 only)
1857    #[cfg(target_os = "linux")]
1858    static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1859
1860    #[cfg(target_os = "linux")]
1861    fn is_g2d_available() -> bool {
1862        *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
1863    }
1864
1865    #[cfg(target_os = "linux")]
1866    #[cfg(feature = "opengl")]
1867    static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1868
1869    #[cfg(target_os = "linux")]
1870    #[cfg(feature = "opengl")]
1871    // Helper function to check if OpenGL is available
1872    fn is_opengl_available() -> bool {
1873        #[cfg(all(target_os = "linux", feature = "opengl"))]
1874        {
1875            *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new().is_ok())
1876        }
1877
1878        #[cfg(not(all(target_os = "linux", feature = "opengl")))]
1879        {
1880            false
1881        }
1882    }
1883
1884    #[test]
1885    fn test_load_jpeg_with_exif() {
1886        let file = include_bytes!("../../../testdata/zidane_rotated_exif.jpg").to_vec();
1887        let loaded = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1888
1889        assert_eq!(loaded.height(), 1280);
1890        assert_eq!(loaded.width(), 720);
1891
1892        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1893        let cpu_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1894
1895        let (dst_width, dst_height) = (cpu_src.height(), cpu_src.width());
1896
1897        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1898        let mut cpu_converter = CPUProcessor::new();
1899
1900        cpu_converter
1901            .convert(
1902                &cpu_src,
1903                &mut cpu_dst,
1904                Rotation::Clockwise90,
1905                Flip::None,
1906                Crop::no_crop(),
1907            )
1908            .unwrap();
1909
1910        compare_images(&loaded, &cpu_dst, 0.98, function!());
1911    }
1912
1913    #[test]
1914    fn test_load_png_with_exif() {
1915        let file = include_bytes!("../../../testdata/zidane_rotated_exif_180.png").to_vec();
1916        let loaded = TensorImage::load_png(&file, Some(RGBA), None).unwrap();
1917
1918        assert_eq!(loaded.height(), 720);
1919        assert_eq!(loaded.width(), 1280);
1920
1921        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1922        let cpu_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1923
1924        let mut cpu_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1925        let mut cpu_converter = CPUProcessor::new();
1926
1927        cpu_converter
1928            .convert(
1929                &cpu_src,
1930                &mut cpu_dst,
1931                Rotation::Rotate180,
1932                Flip::None,
1933                Crop::no_crop(),
1934            )
1935            .unwrap();
1936
1937        compare_images(&loaded, &cpu_dst, 0.98, function!());
1938    }
1939
1940    #[test]
1941    #[cfg(target_os = "linux")]
1942    fn test_g2d_resize() {
1943        if !is_g2d_available() {
1944            eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
1945            return;
1946        }
1947        if !is_dma_available() {
1948            eprintln!(
1949                "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
1950            );
1951            return;
1952        }
1953
1954        let dst_width = 640;
1955        let dst_height = 360;
1956        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1957        let src = TensorImage::load_jpeg(&file, Some(RGBA), Some(TensorMemory::Dma)).unwrap();
1958
1959        let mut g2d_dst =
1960            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
1961        let mut g2d_converter = G2DProcessor::new().unwrap();
1962        g2d_converter
1963            .convert(
1964                &src,
1965                &mut g2d_dst,
1966                Rotation::None,
1967                Flip::None,
1968                Crop::no_crop(),
1969            )
1970            .unwrap();
1971
1972        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1973        let mut cpu_converter = CPUProcessor::new();
1974        cpu_converter
1975            .convert(
1976                &src,
1977                &mut cpu_dst,
1978                Rotation::None,
1979                Flip::None,
1980                Crop::no_crop(),
1981            )
1982            .unwrap();
1983
1984        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
1985    }
1986
1987    #[test]
1988    #[cfg(target_os = "linux")]
1989    #[cfg(feature = "opengl")]
1990    fn test_opengl_resize() {
1991        if !is_opengl_available() {
1992            eprintln!("SKIPPED: {} - OpenGL not available", function!());
1993            return;
1994        }
1995
1996        let dst_width = 640;
1997        let dst_height = 360;
1998        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1999        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2000
2001        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2002        let mut cpu_converter = CPUProcessor::new();
2003        cpu_converter
2004            .convert(
2005                &src,
2006                &mut cpu_dst,
2007                Rotation::None,
2008                Flip::None,
2009                Crop::no_crop(),
2010            )
2011            .unwrap();
2012        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2013        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2014
2015        for _ in 0..5 {
2016            gl_converter
2017                .convert(
2018                    &src,
2019                    &mut gl_dst,
2020                    Rotation::None,
2021                    Flip::None,
2022                    Crop::no_crop(),
2023                )
2024                .unwrap();
2025
2026            compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2027        }
2028
2029        drop(gl_dst);
2030    }
2031
2032    #[test]
2033    #[cfg(target_os = "linux")]
2034    #[cfg(feature = "opengl")]
2035    fn test_opengl_10_threads() {
2036        if !is_opengl_available() {
2037            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2038            return;
2039        }
2040
2041        let handles: Vec<_> = (0..10)
2042            .map(|i| {
2043                std::thread::Builder::new()
2044                    .name(format!("Thread {i}"))
2045                    .spawn(test_opengl_resize)
2046                    .unwrap()
2047            })
2048            .collect();
2049        handles.into_iter().for_each(|h| {
2050            if let Err(e) = h.join() {
2051                std::panic::resume_unwind(e)
2052            }
2053        });
2054    }
2055
2056    #[test]
2057    #[cfg(target_os = "linux")]
2058    #[cfg(feature = "opengl")]
2059    fn test_opengl_grey() {
2060        if !is_opengl_available() {
2061            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2062            return;
2063        }
2064
2065        let img = TensorImage::load_jpeg(
2066            include_bytes!("../../../testdata/grey.jpg"),
2067            Some(GREY),
2068            None,
2069        )
2070        .unwrap();
2071
2072        let mut gl_dst = TensorImage::new(640, 640, GREY, None).unwrap();
2073        let mut cpu_dst = TensorImage::new(640, 640, GREY, None).unwrap();
2074
2075        let mut converter = CPUProcessor::new();
2076
2077        converter
2078            .convert(
2079                &img,
2080                &mut cpu_dst,
2081                Rotation::None,
2082                Flip::None,
2083                Crop::no_crop(),
2084            )
2085            .unwrap();
2086
2087        let mut gl = GLProcessorThreaded::new().unwrap();
2088        gl.convert(
2089            &img,
2090            &mut gl_dst,
2091            Rotation::None,
2092            Flip::None,
2093            Crop::no_crop(),
2094        )
2095        .unwrap();
2096
2097        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2098    }
2099
2100    #[test]
2101    #[cfg(target_os = "linux")]
2102    fn test_g2d_src_crop() {
2103        if !is_g2d_available() {
2104            eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
2105            return;
2106        }
2107        if !is_dma_available() {
2108            eprintln!(
2109                "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2110            );
2111            return;
2112        }
2113
2114        let dst_width = 640;
2115        let dst_height = 640;
2116        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2117        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2118
2119        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2120        let mut cpu_converter = CPUProcessor::new();
2121        cpu_converter
2122            .convert(
2123                &src,
2124                &mut cpu_dst,
2125                Rotation::None,
2126                Flip::None,
2127                Crop {
2128                    src_rect: Some(Rect {
2129                        left: 0,
2130                        top: 0,
2131                        width: 640,
2132                        height: 360,
2133                    }),
2134                    dst_rect: None,
2135                    dst_color: None,
2136                },
2137            )
2138            .unwrap();
2139
2140        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2141        let mut g2d_converter = G2DProcessor::new().unwrap();
2142        g2d_converter
2143            .convert(
2144                &src,
2145                &mut g2d_dst,
2146                Rotation::None,
2147                Flip::None,
2148                Crop {
2149                    src_rect: Some(Rect {
2150                        left: 0,
2151                        top: 0,
2152                        width: 640,
2153                        height: 360,
2154                    }),
2155                    dst_rect: None,
2156                    dst_color: None,
2157                },
2158            )
2159            .unwrap();
2160
2161        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2162    }
2163
2164    #[test]
2165    #[cfg(target_os = "linux")]
2166    fn test_g2d_dst_crop() {
2167        if !is_g2d_available() {
2168            eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
2169            return;
2170        }
2171        if !is_dma_available() {
2172            eprintln!(
2173                "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2174            );
2175            return;
2176        }
2177
2178        let dst_width = 640;
2179        let dst_height = 640;
2180        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2181        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2182
2183        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2184        let mut cpu_converter = CPUProcessor::new();
2185        cpu_converter
2186            .convert(
2187                &src,
2188                &mut cpu_dst,
2189                Rotation::None,
2190                Flip::None,
2191                Crop {
2192                    src_rect: None,
2193                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2194                    dst_color: None,
2195                },
2196            )
2197            .unwrap();
2198
2199        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2200        let mut g2d_converter = G2DProcessor::new().unwrap();
2201        g2d_converter
2202            .convert(
2203                &src,
2204                &mut g2d_dst,
2205                Rotation::None,
2206                Flip::None,
2207                Crop {
2208                    src_rect: None,
2209                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2210                    dst_color: None,
2211                },
2212            )
2213            .unwrap();
2214
2215        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2216    }
2217
2218    #[test]
2219    #[cfg(target_os = "linux")]
2220    fn test_g2d_all_rgba() {
2221        if !is_g2d_available() {
2222            eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
2223            return;
2224        }
2225        if !is_dma_available() {
2226            eprintln!(
2227                "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2228            );
2229            return;
2230        }
2231
2232        let dst_width = 640;
2233        let dst_height = 640;
2234        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2235        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2236
2237        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2238        let mut cpu_converter = CPUProcessor::new();
2239        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2240        let mut g2d_converter = G2DProcessor::new().unwrap();
2241
2242        for rot in [
2243            Rotation::None,
2244            Rotation::Clockwise90,
2245            Rotation::Rotate180,
2246            Rotation::CounterClockwise90,
2247        ] {
2248            cpu_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2249            g2d_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2250            for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2251                cpu_converter
2252                    .convert(
2253                        &src,
2254                        &mut cpu_dst,
2255                        Rotation::None,
2256                        Flip::None,
2257                        Crop {
2258                            src_rect: Some(Rect::new(50, 120, 1024, 576)),
2259                            dst_rect: Some(Rect::new(100, 100, 512, 288)),
2260                            dst_color: None,
2261                        },
2262                    )
2263                    .unwrap();
2264
2265                g2d_converter
2266                    .convert(
2267                        &src,
2268                        &mut g2d_dst,
2269                        Rotation::None,
2270                        Flip::None,
2271                        Crop {
2272                            src_rect: Some(Rect::new(50, 120, 1024, 576)),
2273                            dst_rect: Some(Rect::new(100, 100, 512, 288)),
2274                            dst_color: None,
2275                        },
2276                    )
2277                    .unwrap();
2278
2279                compare_images(
2280                    &g2d_dst,
2281                    &cpu_dst,
2282                    0.98,
2283                    &format!("{} {:?} {:?}", function!(), rot, flip),
2284                );
2285            }
2286        }
2287    }
2288
2289    #[test]
2290    #[cfg(target_os = "linux")]
2291    #[cfg(feature = "opengl")]
2292    fn test_opengl_src_crop() {
2293        if !is_opengl_available() {
2294            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2295            return;
2296        }
2297
2298        let dst_width = 640;
2299        let dst_height = 360;
2300        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2301        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2302
2303        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2304        let mut cpu_converter = CPUProcessor::new();
2305        cpu_converter
2306            .convert(
2307                &src,
2308                &mut cpu_dst,
2309                Rotation::None,
2310                Flip::None,
2311                Crop {
2312                    src_rect: Some(Rect {
2313                        left: 320,
2314                        top: 180,
2315                        width: 1280 - 320,
2316                        height: 720 - 180,
2317                    }),
2318                    dst_rect: None,
2319                    dst_color: None,
2320                },
2321            )
2322            .unwrap();
2323
2324        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2325        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2326
2327        gl_converter
2328            .convert(
2329                &src,
2330                &mut gl_dst,
2331                Rotation::None,
2332                Flip::None,
2333                Crop {
2334                    src_rect: Some(Rect {
2335                        left: 320,
2336                        top: 180,
2337                        width: 1280 - 320,
2338                        height: 720 - 180,
2339                    }),
2340                    dst_rect: None,
2341                    dst_color: None,
2342                },
2343            )
2344            .unwrap();
2345
2346        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2347    }
2348
2349    #[test]
2350    #[cfg(target_os = "linux")]
2351    #[cfg(feature = "opengl")]
2352    fn test_opengl_dst_crop() {
2353        if !is_opengl_available() {
2354            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2355            return;
2356        }
2357
2358        let dst_width = 640;
2359        let dst_height = 640;
2360        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2361        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2362
2363        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2364        let mut cpu_converter = CPUProcessor::new();
2365        cpu_converter
2366            .convert(
2367                &src,
2368                &mut cpu_dst,
2369                Rotation::None,
2370                Flip::None,
2371                Crop {
2372                    src_rect: None,
2373                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2374                    dst_color: None,
2375                },
2376            )
2377            .unwrap();
2378
2379        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2380        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2381        gl_converter
2382            .convert(
2383                &src,
2384                &mut gl_dst,
2385                Rotation::None,
2386                Flip::None,
2387                Crop {
2388                    src_rect: None,
2389                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2390                    dst_color: None,
2391                },
2392            )
2393            .unwrap();
2394
2395        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2396    }
2397
2398    #[test]
2399    #[cfg(target_os = "linux")]
2400    #[cfg(feature = "opengl")]
2401    fn test_opengl_all_rgba() {
2402        if !is_opengl_available() {
2403            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2404            return;
2405        }
2406
2407        let dst_width = 640;
2408        let dst_height = 640;
2409        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2410
2411        let mut cpu_converter = CPUProcessor::new();
2412
2413        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2414
2415        let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
2416        if is_dma_available() {
2417            mem.push(Some(TensorMemory::Dma));
2418        }
2419        for m in mem {
2420            let src = TensorImage::load_jpeg(&file, Some(RGBA), m).unwrap();
2421
2422            for rot in [
2423                Rotation::None,
2424                Rotation::Clockwise90,
2425                Rotation::Rotate180,
2426                Rotation::CounterClockwise90,
2427            ] {
2428                for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2429                    let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, m).unwrap();
2430                    let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, m).unwrap();
2431                    cpu_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2432                    gl_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2433                    cpu_converter
2434                        .convert(
2435                            &src,
2436                            &mut cpu_dst,
2437                            Rotation::None,
2438                            Flip::None,
2439                            Crop {
2440                                src_rect: Some(Rect::new(50, 120, 1024, 576)),
2441                                dst_rect: Some(Rect::new(100, 100, 512, 288)),
2442                                dst_color: None,
2443                            },
2444                        )
2445                        .unwrap();
2446
2447                    gl_converter
2448                        .convert(
2449                            &src,
2450                            &mut gl_dst,
2451                            Rotation::None,
2452                            Flip::None,
2453                            Crop {
2454                                src_rect: Some(Rect::new(50, 120, 1024, 576)),
2455                                dst_rect: Some(Rect::new(100, 100, 512, 288)),
2456                                dst_color: None,
2457                            },
2458                        )
2459                        .map_err(|e| {
2460                            log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
2461                            e
2462                        })
2463                        .unwrap();
2464
2465                    compare_images(
2466                        &gl_dst,
2467                        &cpu_dst,
2468                        0.98,
2469                        &format!("{} {:?} {:?}", function!(), rot, flip),
2470                    );
2471                }
2472            }
2473        }
2474    }
2475
2476    #[test]
2477    #[cfg(target_os = "linux")]
2478    fn test_cpu_rotate() {
2479        for rot in [
2480            Rotation::Clockwise90,
2481            Rotation::Rotate180,
2482            Rotation::CounterClockwise90,
2483        ] {
2484            test_cpu_rotate_(rot);
2485        }
2486    }
2487
2488    #[cfg(target_os = "linux")]
2489    fn test_cpu_rotate_(rot: Rotation) {
2490        // This test rotates the image 4 times and checks that the image was returned to
2491        // be the same Currently doesn't check if rotations actually rotated in
2492        // right direction
2493        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2494
2495        let unchanged_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2496        let mut src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2497
2498        let (dst_width, dst_height) = match rot {
2499            Rotation::None | Rotation::Rotate180 => (src.width(), src.height()),
2500            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (src.height(), src.width()),
2501        };
2502
2503        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2504        let mut cpu_converter = CPUProcessor::new();
2505
2506        // After rotating 4 times, the image should be the same as the original
2507
2508        cpu_converter
2509            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2510            .unwrap();
2511
2512        cpu_converter
2513            .convert(&cpu_dst, &mut src, rot, Flip::None, Crop::no_crop())
2514            .unwrap();
2515
2516        cpu_converter
2517            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2518            .unwrap();
2519
2520        cpu_converter
2521            .convert(&cpu_dst, &mut src, rot, Flip::None, Crop::no_crop())
2522            .unwrap();
2523
2524        compare_images(&src, &unchanged_src, 0.98, function!());
2525    }
2526
2527    #[test]
2528    #[cfg(target_os = "linux")]
2529    #[cfg(feature = "opengl")]
2530    fn test_opengl_rotate() {
2531        if !is_opengl_available() {
2532            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2533            return;
2534        }
2535
2536        let size = (1280, 720);
2537        let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
2538
2539        if is_dma_available() {
2540            mem.push(Some(TensorMemory::Dma));
2541        }
2542        for m in mem {
2543            for rot in [
2544                Rotation::Clockwise90,
2545                Rotation::Rotate180,
2546                Rotation::CounterClockwise90,
2547            ] {
2548                test_opengl_rotate_(size, rot, m);
2549            }
2550        }
2551    }
2552
2553    #[cfg(target_os = "linux")]
2554    #[cfg(feature = "opengl")]
2555    fn test_opengl_rotate_(
2556        size: (usize, usize),
2557        rot: Rotation,
2558        tensor_memory: Option<TensorMemory>,
2559    ) {
2560        let (dst_width, dst_height) = match rot {
2561            Rotation::None | Rotation::Rotate180 => size,
2562            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2563        };
2564
2565        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2566        let src = TensorImage::load_jpeg(&file, Some(RGBA), tensor_memory).unwrap();
2567
2568        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2569        let mut cpu_converter = CPUProcessor::new();
2570
2571        cpu_converter
2572            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2573            .unwrap();
2574
2575        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, tensor_memory).unwrap();
2576        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2577
2578        for _ in 0..5 {
2579            gl_converter
2580                .convert(&src, &mut gl_dst, rot, Flip::None, Crop::no_crop())
2581                .unwrap();
2582            compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2583        }
2584    }
2585
2586    #[test]
2587    #[cfg(target_os = "linux")]
2588    fn test_g2d_rotate() {
2589        if !is_g2d_available() {
2590            eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
2591            return;
2592        }
2593        if !is_dma_available() {
2594            eprintln!(
2595                "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2596            );
2597            return;
2598        }
2599
2600        let size = (1280, 720);
2601        for rot in [
2602            Rotation::Clockwise90,
2603            Rotation::Rotate180,
2604            Rotation::CounterClockwise90,
2605        ] {
2606            test_g2d_rotate_(size, rot);
2607        }
2608    }
2609
2610    #[cfg(target_os = "linux")]
2611    fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
2612        let (dst_width, dst_height) = match rot {
2613            Rotation::None | Rotation::Rotate180 => size,
2614            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2615        };
2616
2617        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2618        let src = TensorImage::load_jpeg(&file, Some(RGBA), Some(TensorMemory::Dma)).unwrap();
2619
2620        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2621        let mut cpu_converter = CPUProcessor::new();
2622
2623        cpu_converter
2624            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2625            .unwrap();
2626
2627        let mut g2d_dst =
2628            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
2629        let mut g2d_converter = G2DProcessor::new().unwrap();
2630
2631        g2d_converter
2632            .convert(&src, &mut g2d_dst, rot, Flip::None, Crop::no_crop())
2633            .unwrap();
2634
2635        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2636    }
2637
2638    #[test]
2639    fn test_rgba_to_yuyv_resize_cpu() {
2640        let src = load_bytes_to_tensor(
2641            1280,
2642            720,
2643            RGBA,
2644            None,
2645            include_bytes!("../../../testdata/camera720p.rgba"),
2646        )
2647        .unwrap();
2648
2649        let (dst_width, dst_height) = (640, 360);
2650
2651        let mut dst = TensorImage::new(dst_width, dst_height, YUYV, None).unwrap();
2652
2653        let mut dst_through_yuyv = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2654        let mut dst_direct = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2655
2656        let mut cpu_converter = CPUProcessor::new();
2657
2658        cpu_converter
2659            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2660            .unwrap();
2661
2662        cpu_converter
2663            .convert(
2664                &dst,
2665                &mut dst_through_yuyv,
2666                Rotation::None,
2667                Flip::None,
2668                Crop::no_crop(),
2669            )
2670            .unwrap();
2671
2672        cpu_converter
2673            .convert(
2674                &src,
2675                &mut dst_direct,
2676                Rotation::None,
2677                Flip::None,
2678                Crop::no_crop(),
2679            )
2680            .unwrap();
2681
2682        compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
2683    }
2684
2685    #[test]
2686    #[cfg(target_os = "linux")]
2687    #[cfg(feature = "opengl")]
2688    #[ignore = "opengl doesn't support rendering to YUYV texture"]
2689    fn test_rgba_to_yuyv_resize_opengl() {
2690        if !is_opengl_available() {
2691            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2692            return;
2693        }
2694
2695        if !is_dma_available() {
2696            eprintln!(
2697                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
2698                function!()
2699            );
2700            return;
2701        }
2702
2703        let src = load_bytes_to_tensor(
2704            1280,
2705            720,
2706            RGBA,
2707            None,
2708            include_bytes!("../../../testdata/camera720p.rgba"),
2709        )
2710        .unwrap();
2711
2712        let (dst_width, dst_height) = (640, 360);
2713
2714        let mut dst =
2715            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2716
2717        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2718
2719        gl_converter
2720            .convert(
2721                &src,
2722                &mut dst,
2723                Rotation::None,
2724                Flip::None,
2725                Crop::new()
2726                    .with_dst_rect(Some(Rect::new(100, 100, 100, 100)))
2727                    .with_dst_color(Some([255, 255, 255, 255])),
2728            )
2729            .unwrap();
2730
2731        std::fs::write(
2732            "rgba_to_yuyv_opengl.yuyv",
2733            dst.tensor().map().unwrap().as_slice(),
2734        )
2735        .unwrap();
2736        let mut cpu_dst =
2737            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2738        CPUProcessor::new()
2739            .convert(
2740                &src,
2741                &mut cpu_dst,
2742                Rotation::None,
2743                Flip::None,
2744                Crop::no_crop(),
2745            )
2746            .unwrap();
2747
2748        compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
2749    }
2750
2751    #[test]
2752    #[cfg(target_os = "linux")]
2753    fn test_rgba_to_yuyv_resize_g2d() {
2754        if !is_g2d_available() {
2755            eprintln!(
2756                "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
2757            );
2758            return;
2759        }
2760        if !is_dma_available() {
2761            eprintln!(
2762                "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2763            );
2764            return;
2765        }
2766
2767        let src = load_bytes_to_tensor(
2768            1280,
2769            720,
2770            RGBA,
2771            Some(TensorMemory::Dma),
2772            include_bytes!("../../../testdata/camera720p.rgba"),
2773        )
2774        .unwrap();
2775
2776        let (dst_width, dst_height) = (1280, 720);
2777
2778        let mut cpu_dst =
2779            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2780
2781        let mut g2d_dst =
2782            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2783
2784        let mut g2d_converter = G2DProcessor::new().unwrap();
2785
2786        g2d_dst.tensor.map().unwrap().as_mut_slice().fill(128);
2787        g2d_converter
2788            .convert(
2789                &src,
2790                &mut g2d_dst,
2791                Rotation::None,
2792                Flip::None,
2793                Crop {
2794                    src_rect: None,
2795                    dst_rect: Some(Rect::new(100, 100, 2, 2)),
2796                    dst_color: None,
2797                },
2798            )
2799            .unwrap();
2800
2801        cpu_dst.tensor.map().unwrap().as_mut_slice().fill(128);
2802        CPUProcessor::new()
2803            .convert(
2804                &src,
2805                &mut cpu_dst,
2806                Rotation::None,
2807                Flip::None,
2808                Crop {
2809                    src_rect: None,
2810                    dst_rect: Some(Rect::new(100, 100, 2, 2)),
2811                    dst_color: None,
2812                },
2813            )
2814            .unwrap();
2815
2816        compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
2817    }
2818
2819    #[test]
2820    fn test_yuyv_to_rgba_cpu() {
2821        let file = include_bytes!("../../../testdata/camera720p.yuyv").to_vec();
2822        let src = TensorImage::new(1280, 720, YUYV, None).unwrap();
2823        src.tensor()
2824            .map()
2825            .unwrap()
2826            .as_mut_slice()
2827            .copy_from_slice(&file);
2828
2829        let mut dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
2830        let mut cpu_converter = CPUProcessor::new();
2831
2832        cpu_converter
2833            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2834            .unwrap();
2835
2836        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2837        target_image
2838            .tensor()
2839            .map()
2840            .unwrap()
2841            .as_mut_slice()
2842            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2843
2844        compare_images(&dst, &target_image, 0.98, function!());
2845    }
2846
2847    #[test]
2848    fn test_yuyv_to_rgb_cpu() {
2849        let file = include_bytes!("../../../testdata/camera720p.yuyv").to_vec();
2850        let src = TensorImage::new(1280, 720, YUYV, None).unwrap();
2851        src.tensor()
2852            .map()
2853            .unwrap()
2854            .as_mut_slice()
2855            .copy_from_slice(&file);
2856
2857        let mut dst = TensorImage::new(1280, 720, RGB, None).unwrap();
2858        let mut cpu_converter = CPUProcessor::new();
2859
2860        cpu_converter
2861            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2862            .unwrap();
2863
2864        let target_image = TensorImage::new(1280, 720, RGB, None).unwrap();
2865        target_image
2866            .tensor()
2867            .map()
2868            .unwrap()
2869            .as_mut_slice()
2870            .as_chunks_mut::<3>()
2871            .0
2872            .iter_mut()
2873            .zip(
2874                include_bytes!("../../../testdata/camera720p.rgba")
2875                    .as_chunks::<4>()
2876                    .0,
2877            )
2878            .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
2879
2880        compare_images(&dst, &target_image, 0.98, function!());
2881    }
2882
2883    #[test]
2884    #[cfg(target_os = "linux")]
2885    fn test_yuyv_to_rgba_g2d() {
2886        if !is_g2d_available() {
2887            eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
2888            return;
2889        }
2890        if !is_dma_available() {
2891            eprintln!(
2892                "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2893            );
2894            return;
2895        }
2896
2897        let src = load_bytes_to_tensor(
2898            1280,
2899            720,
2900            YUYV,
2901            None,
2902            include_bytes!("../../../testdata/camera720p.yuyv"),
2903        )
2904        .unwrap();
2905
2906        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma)).unwrap();
2907        let mut g2d_converter = G2DProcessor::new().unwrap();
2908
2909        g2d_converter
2910            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2911            .unwrap();
2912
2913        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2914        target_image
2915            .tensor()
2916            .map()
2917            .unwrap()
2918            .as_mut_slice()
2919            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2920
2921        compare_images(&dst, &target_image, 0.98, function!());
2922    }
2923
2924    #[test]
2925    #[cfg(target_os = "linux")]
2926    #[cfg(feature = "opengl")]
2927    fn test_yuyv_to_rgba_opengl() {
2928        if !is_opengl_available() {
2929            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2930            return;
2931        }
2932        if !is_dma_available() {
2933            eprintln!(
2934                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
2935                function!()
2936            );
2937            return;
2938        }
2939
2940        let src = load_bytes_to_tensor(
2941            1280,
2942            720,
2943            YUYV,
2944            Some(TensorMemory::Dma),
2945            include_bytes!("../../../testdata/camera720p.yuyv"),
2946        )
2947        .unwrap();
2948
2949        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma)).unwrap();
2950        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2951
2952        gl_converter
2953            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2954            .unwrap();
2955
2956        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2957        target_image
2958            .tensor()
2959            .map()
2960            .unwrap()
2961            .as_mut_slice()
2962            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2963
2964        compare_images(&dst, &target_image, 0.98, function!());
2965    }
2966
2967    #[test]
2968    #[cfg(target_os = "linux")]
2969    fn test_yuyv_to_rgb_g2d() {
2970        if !is_g2d_available() {
2971            eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
2972            return;
2973        }
2974        if !is_dma_available() {
2975            eprintln!(
2976                "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2977            );
2978            return;
2979        }
2980
2981        let src = load_bytes_to_tensor(
2982            1280,
2983            720,
2984            YUYV,
2985            None,
2986            include_bytes!("../../../testdata/camera720p.yuyv"),
2987        )
2988        .unwrap();
2989
2990        let mut g2d_dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma)).unwrap();
2991        let mut g2d_converter = G2DProcessor::new().unwrap();
2992
2993        g2d_converter
2994            .convert(
2995                &src,
2996                &mut g2d_dst,
2997                Rotation::None,
2998                Flip::None,
2999                Crop::no_crop(),
3000            )
3001            .unwrap();
3002
3003        let mut cpu_dst = TensorImage::new(1280, 720, RGB, None).unwrap();
3004        let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3005
3006        cpu_converter
3007            .convert(
3008                &src,
3009                &mut cpu_dst,
3010                Rotation::None,
3011                Flip::None,
3012                Crop::no_crop(),
3013            )
3014            .unwrap();
3015
3016        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3017    }
3018
3019    #[test]
3020    #[cfg(target_os = "linux")]
3021    fn test_yuyv_to_yuyv_resize_g2d() {
3022        if !is_g2d_available() {
3023            eprintln!(
3024                "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
3025            );
3026            return;
3027        }
3028        if !is_dma_available() {
3029            eprintln!(
3030                "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3031            );
3032            return;
3033        }
3034
3035        let src = load_bytes_to_tensor(
3036            1280,
3037            720,
3038            YUYV,
3039            None,
3040            include_bytes!("../../../testdata/camera720p.yuyv"),
3041        )
3042        .unwrap();
3043
3044        let mut g2d_dst = TensorImage::new(600, 400, YUYV, Some(TensorMemory::Dma)).unwrap();
3045        let mut g2d_converter = G2DProcessor::new().unwrap();
3046
3047        g2d_converter
3048            .convert(
3049                &src,
3050                &mut g2d_dst,
3051                Rotation::None,
3052                Flip::None,
3053                Crop::no_crop(),
3054            )
3055            .unwrap();
3056
3057        let mut cpu_dst = TensorImage::new(600, 400, YUYV, None).unwrap();
3058        let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3059
3060        cpu_converter
3061            .convert(
3062                &src,
3063                &mut cpu_dst,
3064                Rotation::None,
3065                Flip::None,
3066                Crop::no_crop(),
3067            )
3068            .unwrap();
3069
3070        // TODO: compare YUYV and YUYV images without having to convert them to RGB
3071        compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.98, function!());
3072    }
3073
3074    #[test]
3075    fn test_yuyv_to_rgba_resize_cpu() {
3076        let src = load_bytes_to_tensor(
3077            1280,
3078            720,
3079            YUYV,
3080            None,
3081            include_bytes!("../../../testdata/camera720p.yuyv"),
3082        )
3083        .unwrap();
3084
3085        let (dst_width, dst_height) = (960, 540);
3086
3087        let mut dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
3088        let mut cpu_converter = CPUProcessor::new();
3089
3090        cpu_converter
3091            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3092            .unwrap();
3093
3094        let mut dst_target = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
3095        let src_target = load_bytes_to_tensor(
3096            1280,
3097            720,
3098            RGBA,
3099            None,
3100            include_bytes!("../../../testdata/camera720p.rgba"),
3101        )
3102        .unwrap();
3103        cpu_converter
3104            .convert(
3105                &src_target,
3106                &mut dst_target,
3107                Rotation::None,
3108                Flip::None,
3109                Crop::no_crop(),
3110            )
3111            .unwrap();
3112
3113        compare_images(&dst, &dst_target, 0.98, function!());
3114    }
3115
3116    #[test]
3117    #[cfg(target_os = "linux")]
3118    fn test_yuyv_to_rgba_crop_flip_g2d() {
3119        if !is_g2d_available() {
3120            eprintln!(
3121                "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
3122            );
3123            return;
3124        }
3125        if !is_dma_available() {
3126            eprintln!(
3127                "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3128            );
3129            return;
3130        }
3131
3132        let src = load_bytes_to_tensor(
3133            1280,
3134            720,
3135            YUYV,
3136            Some(TensorMemory::Dma),
3137            include_bytes!("../../../testdata/camera720p.yuyv"),
3138        )
3139        .unwrap();
3140
3141        let (dst_width, dst_height) = (640, 640);
3142
3143        let mut dst_g2d =
3144            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3145        let mut g2d_converter = G2DProcessor::new().unwrap();
3146
3147        g2d_converter
3148            .convert(
3149                &src,
3150                &mut dst_g2d,
3151                Rotation::None,
3152                Flip::Horizontal,
3153                Crop {
3154                    src_rect: Some(Rect {
3155                        left: 20,
3156                        top: 15,
3157                        width: 400,
3158                        height: 300,
3159                    }),
3160                    dst_rect: None,
3161                    dst_color: None,
3162                },
3163            )
3164            .unwrap();
3165
3166        let mut dst_cpu =
3167            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3168        let mut cpu_converter = CPUProcessor::new();
3169
3170        cpu_converter
3171            .convert(
3172                &src,
3173                &mut dst_cpu,
3174                Rotation::None,
3175                Flip::Horizontal,
3176                Crop {
3177                    src_rect: Some(Rect {
3178                        left: 20,
3179                        top: 15,
3180                        width: 400,
3181                        height: 300,
3182                    }),
3183                    dst_rect: None,
3184                    dst_color: None,
3185                },
3186            )
3187            .unwrap();
3188        compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
3189    }
3190
3191    #[test]
3192    #[cfg(target_os = "linux")]
3193    #[cfg(feature = "opengl")]
3194    fn test_yuyv_to_rgba_crop_flip_opengl() {
3195        if !is_opengl_available() {
3196            eprintln!("SKIPPED: {} - OpenGL not available", function!());
3197            return;
3198        }
3199
3200        if !is_dma_available() {
3201            eprintln!(
3202                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3203                function!()
3204            );
3205            return;
3206        }
3207
3208        let src = load_bytes_to_tensor(
3209            1280,
3210            720,
3211            YUYV,
3212            Some(TensorMemory::Dma),
3213            include_bytes!("../../../testdata/camera720p.yuyv"),
3214        )
3215        .unwrap();
3216
3217        let (dst_width, dst_height) = (640, 640);
3218
3219        let mut dst_gl =
3220            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3221        let mut gl_converter = GLProcessorThreaded::new().unwrap();
3222
3223        gl_converter
3224            .convert(
3225                &src,
3226                &mut dst_gl,
3227                Rotation::None,
3228                Flip::Horizontal,
3229                Crop {
3230                    src_rect: Some(Rect {
3231                        left: 20,
3232                        top: 15,
3233                        width: 400,
3234                        height: 300,
3235                    }),
3236                    dst_rect: None,
3237                    dst_color: None,
3238                },
3239            )
3240            .unwrap();
3241
3242        let mut dst_cpu =
3243            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3244        let mut cpu_converter = CPUProcessor::new();
3245
3246        cpu_converter
3247            .convert(
3248                &src,
3249                &mut dst_cpu,
3250                Rotation::None,
3251                Flip::Horizontal,
3252                Crop {
3253                    src_rect: Some(Rect {
3254                        left: 20,
3255                        top: 15,
3256                        width: 400,
3257                        height: 300,
3258                    }),
3259                    dst_rect: None,
3260                    dst_color: None,
3261                },
3262            )
3263            .unwrap();
3264        compare_images(&dst_gl, &dst_cpu, 0.98, function!());
3265    }
3266
3267    #[test]
3268    fn test_nv12_to_rgba_cpu() {
3269        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3270        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3271        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3272
3273        let mut dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
3274        let mut cpu_converter = CPUProcessor::new();
3275
3276        cpu_converter
3277            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3278            .unwrap();
3279
3280        let target_image = TensorImage::load_jpeg(
3281            include_bytes!("../../../testdata/zidane.jpg"),
3282            Some(RGBA),
3283            None,
3284        )
3285        .unwrap();
3286
3287        compare_images(&dst, &target_image, 0.98, function!());
3288    }
3289
3290    #[test]
3291    fn test_nv12_to_rgb_cpu() {
3292        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3293        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3294        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3295
3296        let mut dst = TensorImage::new(1280, 720, RGB, None).unwrap();
3297        let mut cpu_converter = CPUProcessor::new();
3298
3299        cpu_converter
3300            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3301            .unwrap();
3302
3303        let target_image = TensorImage::load_jpeg(
3304            include_bytes!("../../../testdata/zidane.jpg"),
3305            Some(RGB),
3306            None,
3307        )
3308        .unwrap();
3309
3310        compare_images(&dst, &target_image, 0.98, function!());
3311    }
3312
3313    #[test]
3314    fn test_nv12_to_grey_cpu() {
3315        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3316        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3317        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3318
3319        let mut dst = TensorImage::new(1280, 720, GREY, None).unwrap();
3320        let mut cpu_converter = CPUProcessor::new();
3321
3322        cpu_converter
3323            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3324            .unwrap();
3325
3326        let target_image = TensorImage::load_jpeg(
3327            include_bytes!("../../../testdata/zidane.jpg"),
3328            Some(GREY),
3329            None,
3330        )
3331        .unwrap();
3332
3333        compare_images(&dst, &target_image, 0.98, function!());
3334    }
3335
3336    #[test]
3337    fn test_nv12_to_yuyv_cpu() {
3338        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3339        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3340        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3341
3342        let mut dst = TensorImage::new(1280, 720, YUYV, None).unwrap();
3343        let mut cpu_converter = CPUProcessor::new();
3344
3345        cpu_converter
3346            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3347            .unwrap();
3348
3349        let target_image = TensorImage::load_jpeg(
3350            include_bytes!("../../../testdata/zidane.jpg"),
3351            Some(RGB),
3352            None,
3353        )
3354        .unwrap();
3355
3356        compare_images_convert_to_rgb(&dst, &target_image, 0.98, function!());
3357    }
3358
3359    #[test]
3360    fn test_cpu_resize_planar_rgb() {
3361        let src = TensorImage::new(4, 4, RGBA, None).unwrap();
3362        #[rustfmt::skip]
3363        let src_image = [
3364                    255, 0, 0, 255,     0, 255, 0, 255,     0, 0, 255, 255,     255, 255, 0, 255,
3365                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3366                    0, 0, 255, 0,       0, 255, 255, 255,   255, 255, 0, 0,     0, 0, 0, 255,
3367                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3368        ];
3369        src.tensor()
3370            .map()
3371            .unwrap()
3372            .as_mut_slice()
3373            .copy_from_slice(&src_image);
3374
3375        let mut cpu_dst = TensorImage::new(5, 5, PLANAR_RGB, None).unwrap();
3376        let mut cpu_converter = CPUProcessor::new();
3377
3378        cpu_converter
3379            .convert(
3380                &src,
3381                &mut cpu_dst,
3382                Rotation::None,
3383                Flip::None,
3384                Crop::new()
3385                    .with_dst_rect(Some(Rect {
3386                        left: 1,
3387                        top: 1,
3388                        width: 4,
3389                        height: 4,
3390                    }))
3391                    .with_dst_color(Some([114, 114, 114, 255])),
3392            )
3393            .unwrap();
3394
3395        #[rustfmt::skip]
3396        let expected_dst = [
3397            114, 114, 114, 114, 114,    114, 255, 0, 0, 255,    114, 255, 0, 255, 255,      114, 0, 0, 255, 0,        114, 255, 0, 255, 255,
3398            114, 114, 114, 114, 114,    114, 0, 255, 0, 255,    114, 0, 0, 0, 0,            114, 0, 255, 255, 0,      114, 0, 0, 0, 0,
3399            114, 114, 114, 114, 114,    114, 0, 0, 255, 0,      114, 0, 0, 255, 255,        114, 255, 255, 0, 0,      114, 0, 0, 255, 255,
3400        ];
3401
3402        assert_eq!(cpu_dst.tensor().map().unwrap().as_slice(), &expected_dst);
3403    }
3404
3405    #[test]
3406    fn test_cpu_resize_planar_rgba() {
3407        let src = TensorImage::new(4, 4, RGBA, None).unwrap();
3408        #[rustfmt::skip]
3409        let src_image = [
3410                    255, 0, 0, 255,     0, 255, 0, 255,     0, 0, 255, 255,     255, 255, 0, 255,
3411                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3412                    0, 0, 255, 0,       0, 255, 255, 255,   255, 255, 0, 0,     0, 0, 0, 255,
3413                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3414        ];
3415        src.tensor()
3416            .map()
3417            .unwrap()
3418            .as_mut_slice()
3419            .copy_from_slice(&src_image);
3420
3421        let mut cpu_dst = TensorImage::new(5, 5, PLANAR_RGBA, None).unwrap();
3422        let mut cpu_converter = CPUProcessor::new();
3423
3424        cpu_converter
3425            .convert(
3426                &src,
3427                &mut cpu_dst,
3428                Rotation::None,
3429                Flip::None,
3430                Crop::new()
3431                    .with_dst_rect(Some(Rect {
3432                        left: 1,
3433                        top: 1,
3434                        width: 4,
3435                        height: 4,
3436                    }))
3437                    .with_dst_color(Some([114, 114, 114, 255])),
3438            )
3439            .unwrap();
3440
3441        #[rustfmt::skip]
3442        let expected_dst = [
3443            114, 114, 114, 114, 114,    114, 255, 0, 0, 255,        114, 255, 0, 255, 255,      114, 0, 0, 255, 0,        114, 255, 0, 255, 255,
3444            114, 114, 114, 114, 114,    114, 0, 255, 0, 255,        114, 0, 0, 0, 0,            114, 0, 255, 255, 0,      114, 0, 0, 0, 0,
3445            114, 114, 114, 114, 114,    114, 0, 0, 255, 0,          114, 0, 0, 255, 255,        114, 255, 255, 0, 0,      114, 0, 0, 255, 255,
3446            255, 255, 255, 255, 255,    255, 255, 255, 255, 255,    255, 0, 255, 0, 255,        255, 0, 255, 0, 255,      255, 0, 255, 0, 255,
3447        ];
3448
3449        assert_eq!(cpu_dst.tensor().map().unwrap().as_slice(), &expected_dst);
3450    }
3451
3452    #[test]
3453    #[cfg(target_os = "linux")]
3454    #[cfg(feature = "opengl")]
3455    fn test_opengl_resize_planar_rgb() {
3456        if !is_opengl_available() {
3457            eprintln!("SKIPPED: {} - OpenGL not available", function!());
3458            return;
3459        }
3460
3461        if !is_dma_available() {
3462            eprintln!(
3463                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3464                function!()
3465            );
3466            return;
3467        }
3468
3469        let dst_width = 640;
3470        let dst_height = 640;
3471        let file = include_bytes!("../../../testdata/test_image.jpg").to_vec();
3472        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
3473
3474        let mut cpu_dst = TensorImage::new(dst_width, dst_height, PLANAR_RGB, None).unwrap();
3475        let mut cpu_converter = CPUProcessor::new();
3476        cpu_converter
3477            .convert(
3478                &src,
3479                &mut cpu_dst,
3480                Rotation::None,
3481                Flip::None,
3482                Crop::no_crop(),
3483            )
3484            .unwrap();
3485        cpu_converter
3486            .convert(
3487                &src,
3488                &mut cpu_dst,
3489                Rotation::None,
3490                Flip::None,
3491                Crop::new()
3492                    .with_dst_rect(Some(Rect {
3493                        left: 102,
3494                        top: 102,
3495                        width: 440,
3496                        height: 440,
3497                    }))
3498                    .with_dst_color(Some([114, 114, 114, 114])),
3499            )
3500            .unwrap();
3501
3502        let mut gl_dst = TensorImage::new(dst_width, dst_height, PLANAR_RGB, None).unwrap();
3503        let mut gl_converter = GLProcessorThreaded::new().unwrap();
3504
3505        gl_converter
3506            .convert(
3507                &src,
3508                &mut gl_dst,
3509                Rotation::None,
3510                Flip::None,
3511                Crop::new()
3512                    .with_dst_rect(Some(Rect {
3513                        left: 102,
3514                        top: 102,
3515                        width: 440,
3516                        height: 440,
3517                    }))
3518                    .with_dst_color(Some([114, 114, 114, 114])),
3519            )
3520            .unwrap();
3521        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3522    }
3523
3524    #[test]
3525    fn test_cpu_resize_nv16() {
3526        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
3527        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
3528
3529        let mut cpu_nv16_dst = TensorImage::new(640, 640, NV16, None).unwrap();
3530        let mut cpu_rgb_dst = TensorImage::new(640, 640, RGB, None).unwrap();
3531        let mut cpu_converter = CPUProcessor::new();
3532
3533        cpu_converter
3534            .convert(
3535                &src,
3536                &mut cpu_nv16_dst,
3537                Rotation::None,
3538                Flip::None,
3539                // Crop::no_crop(),
3540                Crop::new()
3541                    .with_dst_rect(Some(Rect {
3542                        left: 20,
3543                        top: 140,
3544                        width: 600,
3545                        height: 360,
3546                    }))
3547                    .with_dst_color(Some([255, 128, 0, 255])),
3548            )
3549            .unwrap();
3550
3551        cpu_converter
3552            .convert(
3553                &src,
3554                &mut cpu_rgb_dst,
3555                Rotation::None,
3556                Flip::None,
3557                Crop::new()
3558                    .with_dst_rect(Some(Rect {
3559                        left: 20,
3560                        top: 140,
3561                        width: 600,
3562                        height: 360,
3563                    }))
3564                    .with_dst_color(Some([255, 128, 0, 255])),
3565            )
3566            .unwrap();
3567        compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
3568    }
3569
3570    fn load_bytes_to_tensor(
3571        width: usize,
3572        height: usize,
3573        fourcc: FourCharCode,
3574        memory: Option<TensorMemory>,
3575        bytes: &[u8],
3576    ) -> Result<TensorImage, Error> {
3577        let src = TensorImage::new(width, height, fourcc, memory)?;
3578        src.tensor().map()?.as_mut_slice().copy_from_slice(bytes);
3579        Ok(src)
3580    }
3581
3582    fn compare_images(img1: &TensorImage, img2: &TensorImage, threshold: f64, name: &str) {
3583        assert_eq!(img1.height(), img2.height(), "Heights differ");
3584        assert_eq!(img1.width(), img2.width(), "Widths differ");
3585        assert_eq!(img1.fourcc(), img2.fourcc(), "FourCC differ");
3586        assert!(
3587            matches!(img1.fourcc(), RGB | RGBA | GREY | PLANAR_RGB),
3588            "FourCC must be RGB or RGBA for comparison"
3589        );
3590
3591        let image1 = match img1.fourcc() {
3592            RGB => image::RgbImage::from_vec(
3593                img1.width() as u32,
3594                img1.height() as u32,
3595                img1.tensor().map().unwrap().to_vec(),
3596            )
3597            .unwrap(),
3598            RGBA => image::RgbaImage::from_vec(
3599                img1.width() as u32,
3600                img1.height() as u32,
3601                img1.tensor().map().unwrap().to_vec(),
3602            )
3603            .unwrap()
3604            .convert(),
3605            GREY => image::GrayImage::from_vec(
3606                img1.width() as u32,
3607                img1.height() as u32,
3608                img1.tensor().map().unwrap().to_vec(),
3609            )
3610            .unwrap()
3611            .convert(),
3612            PLANAR_RGB => image::GrayImage::from_vec(
3613                img1.width() as u32,
3614                (img1.height() * 3) as u32,
3615                img1.tensor().map().unwrap().to_vec(),
3616            )
3617            .unwrap()
3618            .convert(),
3619            _ => return,
3620        };
3621
3622        let image2 = match img2.fourcc() {
3623            RGB => image::RgbImage::from_vec(
3624                img2.width() as u32,
3625                img2.height() as u32,
3626                img2.tensor().map().unwrap().to_vec(),
3627            )
3628            .unwrap(),
3629            RGBA => image::RgbaImage::from_vec(
3630                img2.width() as u32,
3631                img2.height() as u32,
3632                img2.tensor().map().unwrap().to_vec(),
3633            )
3634            .unwrap()
3635            .convert(),
3636            GREY => image::GrayImage::from_vec(
3637                img2.width() as u32,
3638                img2.height() as u32,
3639                img2.tensor().map().unwrap().to_vec(),
3640            )
3641            .unwrap()
3642            .convert(),
3643            PLANAR_RGB => image::GrayImage::from_vec(
3644                img2.width() as u32,
3645                (img2.height() * 3) as u32,
3646                img2.tensor().map().unwrap().to_vec(),
3647            )
3648            .unwrap()
3649            .convert(),
3650            _ => return,
3651        };
3652
3653        let similarity = image_compare::rgb_similarity_structure(
3654            &image_compare::Algorithm::RootMeanSquared,
3655            &image1,
3656            &image2,
3657        )
3658        .expect("Image Comparison failed");
3659        if similarity.score < threshold {
3660            // image1.save(format!("{name}_1.png"));
3661            // image2.save(format!("{name}_2.png"));
3662            similarity
3663                .image
3664                .to_color_map()
3665                .save(format!("{name}.png"))
3666                .unwrap();
3667            panic!(
3668                "{name}: converted image and target image have similarity score too low: {} < {}",
3669                similarity.score, threshold
3670            )
3671        }
3672    }
3673
3674    fn compare_images_convert_to_rgb(
3675        img1: &TensorImage,
3676        img2: &TensorImage,
3677        threshold: f64,
3678        name: &str,
3679    ) {
3680        assert_eq!(img1.height(), img2.height(), "Heights differ");
3681        assert_eq!(img1.width(), img2.width(), "Widths differ");
3682
3683        let mut img_rgb1 =
3684            TensorImage::new(img1.width(), img1.height(), RGB, Some(TensorMemory::Mem)).unwrap();
3685        let mut img_rgb2 =
3686            TensorImage::new(img1.width(), img1.height(), RGB, Some(TensorMemory::Mem)).unwrap();
3687        CPUProcessor::convert_format(img1, &mut img_rgb1).unwrap();
3688        CPUProcessor::convert_format(img2, &mut img_rgb2).unwrap();
3689
3690        let image1 = image::RgbImage::from_vec(
3691            img_rgb1.width() as u32,
3692            img_rgb1.height() as u32,
3693            img_rgb1.tensor().map().unwrap().to_vec(),
3694        )
3695        .unwrap();
3696
3697        let image2 = image::RgbImage::from_vec(
3698            img_rgb2.width() as u32,
3699            img_rgb2.height() as u32,
3700            img_rgb2.tensor().map().unwrap().to_vec(),
3701        )
3702        .unwrap();
3703
3704        let similarity = image_compare::rgb_similarity_structure(
3705            &image_compare::Algorithm::RootMeanSquared,
3706            &image1,
3707            &image2,
3708        )
3709        .expect("Image Comparison failed");
3710        if similarity.score < threshold {
3711            // image1.save(format!("{name}_1.png"));
3712            // image2.save(format!("{name}_2.png"));
3713            similarity
3714                .image
3715                .to_color_map()
3716                .save(format!("{name}.png"))
3717                .unwrap();
3718            panic!(
3719                "{name}: converted image and target image have similarity score too low: {} < {}",
3720                similarity.score, threshold
3721            )
3722        }
3723    }
3724
3725    // =========================================================================
3726    // NV12 Format Tests
3727    // =========================================================================
3728
3729    #[test]
3730    fn test_nv12_tensor_image_creation() {
3731        let width = 640;
3732        let height = 480;
3733        let img = TensorImage::new(width, height, NV12, None).unwrap();
3734
3735        assert_eq!(img.width(), width);
3736        assert_eq!(img.height(), height);
3737        assert_eq!(img.fourcc(), NV12);
3738        // NV12 uses shape [H*3/2, W] to store Y plane + UV plane
3739        assert_eq!(img.tensor().shape(), &[height * 3 / 2, width]);
3740    }
3741
3742    #[test]
3743    fn test_nv12_channels() {
3744        let img = TensorImage::new(640, 480, NV12, None).unwrap();
3745        // NV12 reports 2 channels (Y + interleaved UV)
3746        assert_eq!(img.channels(), 2);
3747    }
3748
3749    // =========================================================================
3750    // TensorImageRef Tests
3751    // =========================================================================
3752
3753    #[test]
3754    fn test_tensor_image_ref_from_planar_tensor() {
3755        // Create a planar RGB tensor [3, 480, 640]
3756        let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
3757
3758        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3759
3760        assert_eq!(img_ref.width(), 640);
3761        assert_eq!(img_ref.height(), 480);
3762        assert_eq!(img_ref.channels(), 3);
3763        assert_eq!(img_ref.fourcc(), PLANAR_RGB);
3764        assert!(img_ref.is_planar());
3765    }
3766
3767    #[test]
3768    fn test_tensor_image_ref_from_interleaved_tensor() {
3769        // Create an interleaved RGBA tensor [480, 640, 4]
3770        let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
3771
3772        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, RGBA).unwrap();
3773
3774        assert_eq!(img_ref.width(), 640);
3775        assert_eq!(img_ref.height(), 480);
3776        assert_eq!(img_ref.channels(), 4);
3777        assert_eq!(img_ref.fourcc(), RGBA);
3778        assert!(!img_ref.is_planar());
3779    }
3780
3781    #[test]
3782    fn test_tensor_image_ref_invalid_shape() {
3783        // 2D tensor should fail
3784        let mut tensor = Tensor::<u8>::new(&[480, 640], None, None).unwrap();
3785        let result = TensorImageRef::from_borrowed_tensor(&mut tensor, RGB);
3786        assert!(matches!(result, Err(Error::InvalidShape(_))));
3787    }
3788
3789    #[test]
3790    fn test_tensor_image_ref_wrong_channels() {
3791        // RGBA expects 4 channels but tensor has 3
3792        let mut tensor = Tensor::<u8>::new(&[480, 640, 3], None, None).unwrap();
3793        let result = TensorImageRef::from_borrowed_tensor(&mut tensor, RGBA);
3794        assert!(matches!(result, Err(Error::InvalidShape(_))));
3795    }
3796
3797    #[test]
3798    fn test_tensor_image_dst_trait_tensor_image() {
3799        let img = TensorImage::new(640, 480, RGB, None).unwrap();
3800
3801        // Test TensorImageDst trait implementation
3802        fn check_dst<T: TensorImageDst>(dst: &T) {
3803            assert_eq!(dst.width(), 640);
3804            assert_eq!(dst.height(), 480);
3805            assert_eq!(dst.channels(), 3);
3806            assert!(!dst.is_planar());
3807        }
3808
3809        check_dst(&img);
3810    }
3811
3812    #[test]
3813    fn test_tensor_image_dst_trait_tensor_image_ref() {
3814        let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
3815        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3816
3817        fn check_dst<T: TensorImageDst>(dst: &T) {
3818            assert_eq!(dst.width(), 640);
3819            assert_eq!(dst.height(), 480);
3820            assert_eq!(dst.channels(), 3);
3821            assert!(dst.is_planar());
3822        }
3823
3824        check_dst(&img_ref);
3825    }
3826}