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)]
896pub struct Crop {
897    pub src_rect: Option<Rect>,
898    pub dst_rect: Option<Rect>,
899    pub dst_color: Option<[u8; 4]>,
900}
901
902impl Default for Crop {
903    fn default() -> Self {
904        Crop::new()
905    }
906}
907impl Crop {
908    // Creates a new Crop with default values (no cropping).
909    pub fn new() -> Self {
910        Crop {
911            src_rect: None,
912            dst_rect: None,
913            dst_color: None,
914        }
915    }
916
917    // Sets the source rectangle for cropping.
918    pub fn with_src_rect(mut self, src_rect: Option<Rect>) -> Self {
919        self.src_rect = src_rect;
920        self
921    }
922
923    // Sets the destination rectangle for cropping.
924    pub fn with_dst_rect(mut self, dst_rect: Option<Rect>) -> Self {
925        self.dst_rect = dst_rect;
926        self
927    }
928
929    // Sets the destination color for areas outside the cropped region.
930    pub fn with_dst_color(mut self, dst_color: Option<[u8; 4]>) -> Self {
931        self.dst_color = dst_color;
932        self
933    }
934
935    // Creates a new Crop with no cropping.
936    pub fn no_crop() -> Self {
937        Crop::new()
938    }
939
940    // Checks if the crop rectangles are valid for the given source and
941    // destination images.
942    pub fn check_crop(&self, src: &TensorImage, dst: &TensorImage) -> Result<(), Error> {
943        let src = self.src_rect.is_none_or(|x| x.check_rect(src));
944        let dst = self.dst_rect.is_none_or(|x| x.check_rect(dst));
945        match (src, dst) {
946            (true, true) => Ok(()),
947            (true, false) => Err(Error::CropInvalid(format!(
948                "Dest crop invalid: {:?}",
949                self.dst_rect
950            ))),
951            (false, true) => Err(Error::CropInvalid(format!(
952                "Src crop invalid: {:?}",
953                self.src_rect
954            ))),
955            (false, false) => Err(Error::CropInvalid(format!(
956                "Dest and Src crop invalid: {:?} {:?}",
957                self.dst_rect, self.src_rect
958            ))),
959        }
960    }
961
962    // Checks if the crop rectangles are valid for the given source and
963    // destination images (using TensorImageRef for destination).
964    pub fn check_crop_ref(&self, src: &TensorImage, dst: &TensorImageRef<'_>) -> Result<(), Error> {
965        let src = self.src_rect.is_none_or(|x| x.check_rect(src));
966        let dst = self.dst_rect.is_none_or(|x| x.check_rect_dst(dst));
967        match (src, dst) {
968            (true, true) => Ok(()),
969            (true, false) => Err(Error::CropInvalid(format!(
970                "Dest crop invalid: {:?}",
971                self.dst_rect
972            ))),
973            (false, true) => Err(Error::CropInvalid(format!(
974                "Src crop invalid: {:?}",
975                self.src_rect
976            ))),
977            (false, false) => Err(Error::CropInvalid(format!(
978                "Dest and Src crop invalid: {:?} {:?}",
979                self.dst_rect, self.src_rect
980            ))),
981        }
982    }
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986pub struct Rect {
987    pub left: usize,
988    pub top: usize,
989    pub width: usize,
990    pub height: usize,
991}
992
993impl Rect {
994    // Creates a new Rect with the specified left, top, width, and height.
995    pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
996        Self {
997            left,
998            top,
999            width,
1000            height,
1001        }
1002    }
1003
1004    // Checks if the rectangle is valid for the given image.
1005    pub fn check_rect(&self, image: &TensorImage) -> bool {
1006        self.left + self.width <= image.width() && self.top + self.height <= image.height()
1007    }
1008
1009    // Checks if the rectangle is valid for the given destination image.
1010    pub fn check_rect_dst<D: TensorImageDst>(&self, image: &D) -> bool {
1011        self.left + self.width <= image.width() && self.top + self.height <= image.height()
1012    }
1013}
1014
1015#[enum_dispatch(ImageProcessor)]
1016pub trait ImageProcessorTrait {
1017    /// Converts the source image to the destination image format and size. The
1018    /// image is cropped first, then flipped, then rotated
1019    ///
1020    /// # Arguments
1021    ///
1022    /// * `dst` - The destination image to be converted to.
1023    /// * `src` - The source image to convert from.
1024    /// * `rotation` - The rotation to apply to the destination image.
1025    /// * `flip` - Flips the image
1026    /// * `crop` - An optional rectangle specifying the area to crop from the
1027    ///   source image
1028    ///
1029    /// # Returns
1030    ///
1031    /// A `Result` indicating success or failure of the conversion.
1032    fn convert(
1033        &mut self,
1034        src: &TensorImage,
1035        dst: &mut TensorImage,
1036        rotation: Rotation,
1037        flip: Flip,
1038        crop: Crop,
1039    ) -> Result<()>;
1040
1041    /// Converts the source image to a borrowed destination tensor for zero-copy
1042    /// preprocessing.
1043    ///
1044    /// This variant accepts a `TensorImageRef` as the destination, enabling
1045    /// direct writes into external buffers (e.g., model input tensors) without
1046    /// intermediate copies.
1047    ///
1048    /// # Arguments
1049    ///
1050    /// * `src` - The source image to convert from.
1051    /// * `dst` - A borrowed tensor image wrapping the destination buffer.
1052    /// * `rotation` - The rotation to apply to the destination image.
1053    /// * `flip` - Flips the image
1054    /// * `crop` - An optional rectangle specifying the area to crop from the
1055    ///   source image
1056    ///
1057    /// # Returns
1058    ///
1059    /// A `Result` indicating success or failure of the conversion.
1060    fn convert_ref(
1061        &mut self,
1062        src: &TensorImage,
1063        dst: &mut TensorImageRef<'_>,
1064        rotation: Rotation,
1065        flip: Flip,
1066        crop: Crop,
1067    ) -> Result<()>;
1068
1069    #[cfg(feature = "decoder")]
1070    fn render_to_image(
1071        &mut self,
1072        dst: &mut TensorImage,
1073        detect: &[DetectBox],
1074        segmentation: &[Segmentation],
1075    ) -> Result<()>;
1076
1077    #[cfg(feature = "decoder")]
1078    /// Sets the colors used for rendering segmentation masks. Up to 17 colors
1079    /// can be set.
1080    fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
1081}
1082
1083/// Image converter that uses available hardware acceleration or CPU as a
1084/// fallback.
1085#[derive(Debug)]
1086pub struct ImageProcessor {
1087    /// CPU-based image converter as a fallback. This is only None if the
1088    /// EDGEFIRST_DISABLE_CPU environment variable is set.
1089    pub cpu: Option<CPUProcessor>,
1090
1091    #[cfg(target_os = "linux")]
1092    /// G2D-based image converter for Linux systems. This is only available if
1093    /// the EDGEFIRST_DISABLE_G2D environment variable is not set and libg2d.so
1094    /// is available.
1095    pub g2d: Option<G2DProcessor>,
1096    #[cfg(target_os = "linux")]
1097    #[cfg(feature = "opengl")]
1098    /// OpenGL-based image converter for Linux systems. This is only available
1099    /// if the EDGEFIRST_DISABLE_GL environment variable is not set and OpenGL
1100    /// ES is available.
1101    pub opengl: Option<GLProcessorThreaded>,
1102}
1103
1104unsafe impl Send for ImageProcessor {}
1105unsafe impl Sync for ImageProcessor {}
1106
1107impl ImageProcessor {
1108    /// Creates a new `ImageProcessor` instance, initializing available
1109    /// hardware converters based on the system capabilities and environment
1110    /// variables.
1111    ///
1112    /// # Examples
1113    /// ```rust
1114    /// # use edgefirst_image::{ImageProcessor, TensorImage, RGBA, RGB, Rotation, Flip, Crop, ImageProcessorTrait};
1115    /// # fn main() -> Result<(), edgefirst_image::Error> {
1116    /// let image = include_bytes!("../../../testdata/zidane.jpg");
1117    /// let img = TensorImage::load(image, Some(RGBA), None)?;
1118    /// let mut converter = ImageProcessor::new()?;
1119    /// let mut dst = TensorImage::new(640, 480, RGB, None)?;
1120    /// converter.convert(&img, &mut dst, Rotation::None, Flip::None, Crop::default())?;
1121    /// # Ok(())
1122    /// # }
1123    pub fn new() -> Result<Self> {
1124        #[cfg(target_os = "linux")]
1125        let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
1126            .map(|x| x != "0" && x.to_lowercase() != "false")
1127            .unwrap_or(false)
1128        {
1129            log::debug!("EDGEFIRST_DISABLE_G2D is set");
1130            None
1131        } else {
1132            match G2DProcessor::new() {
1133                Ok(g2d_converter) => Some(g2d_converter),
1134                Err(err) => {
1135                    log::warn!("Failed to initialize G2D converter: {err:?}");
1136                    None
1137                }
1138            }
1139        };
1140
1141        #[cfg(target_os = "linux")]
1142        #[cfg(feature = "opengl")]
1143        let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1144            .map(|x| x != "0" && x.to_lowercase() != "false")
1145            .unwrap_or(false)
1146        {
1147            log::debug!("EDGEFIRST_DISABLE_GL is set");
1148            None
1149        } else {
1150            match GLProcessorThreaded::new() {
1151                Ok(gl_converter) => Some(gl_converter),
1152                Err(err) => {
1153                    log::warn!("Failed to initialize GL converter: {err:?}");
1154                    None
1155                }
1156            }
1157        };
1158
1159        let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1160            .map(|x| x != "0" && x.to_lowercase() != "false")
1161            .unwrap_or(false)
1162        {
1163            log::debug!("EDGEFIRST_DISABLE_CPU is set");
1164            None
1165        } else {
1166            Some(CPUProcessor::new())
1167        };
1168        Ok(Self {
1169            cpu,
1170            #[cfg(target_os = "linux")]
1171            g2d,
1172            #[cfg(target_os = "linux")]
1173            #[cfg(feature = "opengl")]
1174            opengl,
1175        })
1176    }
1177}
1178
1179impl ImageProcessorTrait for ImageProcessor {
1180    /// Converts the source image to the destination image format and size. The
1181    /// image is cropped first, then flipped, then rotated
1182    ///
1183    /// Prefer hardware accelerators when available, falling back to CPU if
1184    /// necessary.
1185    fn convert(
1186        &mut self,
1187        src: &TensorImage,
1188        dst: &mut TensorImage,
1189        rotation: Rotation,
1190        flip: Flip,
1191        crop: Crop,
1192    ) -> Result<()> {
1193        let start = Instant::now();
1194
1195        #[cfg(target_os = "linux")]
1196        if let Some(g2d) = self.g2d.as_mut() {
1197            log::trace!("image started with g2d in {:?}", start.elapsed());
1198            match g2d.convert(src, dst, rotation, flip, crop) {
1199                Ok(_) => {
1200                    log::trace!("image converted with g2d in {:?}", start.elapsed());
1201                    return Ok(());
1202                }
1203                Err(e) => {
1204                    log::trace!("image didn't convert with g2d: {e:?}")
1205                }
1206            }
1207        }
1208
1209        // if the image is just a copy without an resizing, the send it to the CPU and
1210        // skip OpenGL
1211        let src_shape = match crop.src_rect {
1212            Some(s) => (s.width, s.height),
1213            None => (src.width(), src.height()),
1214        };
1215        let dst_shape = match crop.dst_rect {
1216            Some(d) => (d.width, d.height),
1217            None => (dst.width(), dst.height()),
1218        };
1219
1220        // TODO: Check if still use CPU when rotation or flip is enabled
1221        if src_shape == dst_shape && flip == Flip::None && rotation == Rotation::None {
1222            if let Some(cpu) = self.cpu.as_mut() {
1223                match cpu.convert(src, dst, rotation, flip, crop) {
1224                    Ok(_) => {
1225                        log::trace!("image converted with cpu in {:?}", start.elapsed());
1226                        return Ok(());
1227                    }
1228                    Err(e) => {
1229                        log::trace!("image didn't convert with cpu: {e:?}");
1230                        return Err(e);
1231                    }
1232                }
1233            }
1234        }
1235
1236        #[cfg(target_os = "linux")]
1237        #[cfg(feature = "opengl")]
1238        if let Some(opengl) = self.opengl.as_mut() {
1239            log::trace!("image started with opengl in {:?}", start.elapsed());
1240            match opengl.convert(src, dst, rotation, flip, crop) {
1241                Ok(_) => {
1242                    log::trace!("image converted with opengl in {:?}", start.elapsed());
1243                    return Ok(());
1244                }
1245                Err(e) => {
1246                    log::trace!("image didn't convert with opengl: {e:?}")
1247                }
1248            }
1249        }
1250        log::trace!("image started with cpu in {:?}", start.elapsed());
1251        if let Some(cpu) = self.cpu.as_mut() {
1252            match cpu.convert(src, dst, rotation, flip, crop) {
1253                Ok(_) => {
1254                    log::trace!("image converted with cpu in {:?}", start.elapsed());
1255                    return Ok(());
1256                }
1257                Err(e) => {
1258                    log::trace!("image didn't convert with cpu: {e:?}");
1259                    return Err(e);
1260                }
1261            }
1262        }
1263        Err(Error::NoConverter)
1264    }
1265
1266    fn convert_ref(
1267        &mut self,
1268        src: &TensorImage,
1269        dst: &mut TensorImageRef<'_>,
1270        rotation: Rotation,
1271        flip: Flip,
1272        crop: Crop,
1273    ) -> Result<()> {
1274        let start = Instant::now();
1275
1276        // For TensorImageRef, we prefer CPU since hardware accelerators typically
1277        // don't support PLANAR_RGB output which is the common model input format.
1278        // The CPU path uses the generic conversion functions that work with any
1279        // TensorImageDst implementation.
1280        if let Some(cpu) = self.cpu.as_mut() {
1281            match cpu.convert_ref(src, dst, rotation, flip, crop) {
1282                Ok(_) => {
1283                    log::trace!("image converted with cpu (ref) in {:?}", start.elapsed());
1284                    return Ok(());
1285                }
1286                Err(e) => {
1287                    log::trace!("image didn't convert with cpu (ref): {e:?}");
1288                    return Err(e);
1289                }
1290            }
1291        }
1292
1293        Err(Error::NoConverter)
1294    }
1295
1296    #[cfg(feature = "decoder")]
1297    fn render_to_image(
1298        &mut self,
1299        dst: &mut TensorImage,
1300        detect: &[DetectBox],
1301        segmentation: &[Segmentation],
1302    ) -> Result<()> {
1303        let start = Instant::now();
1304
1305        if detect.is_empty() && segmentation.is_empty() {
1306            return Ok(());
1307        }
1308
1309        // skip G2D as it doesn't support rendering to image
1310
1311        #[cfg(target_os = "linux")]
1312        #[cfg(feature = "opengl")]
1313        if let Some(opengl) = self.opengl.as_mut() {
1314            log::trace!("image started with opengl in {:?}", start.elapsed());
1315            match opengl.render_to_image(dst, detect, segmentation) {
1316                Ok(_) => {
1317                    log::trace!("image rendered with opengl in {:?}", start.elapsed());
1318                    return Ok(());
1319                }
1320                Err(e) => {
1321                    log::trace!("image didn't render with opengl: {e:?}")
1322                }
1323            }
1324        }
1325        log::trace!("image started with cpu in {:?}", start.elapsed());
1326        if let Some(cpu) = self.cpu.as_mut() {
1327            match cpu.render_to_image(dst, detect, segmentation) {
1328                Ok(_) => {
1329                    log::trace!("image render with cpu in {:?}", start.elapsed());
1330                    return Ok(());
1331                }
1332                Err(e) => {
1333                    log::trace!("image didn't render with cpu: {e:?}");
1334                    return Err(e);
1335                }
1336            }
1337        }
1338        Err(Error::NoConverter)
1339    }
1340
1341    #[cfg(feature = "decoder")]
1342    fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
1343        let start = Instant::now();
1344
1345        // skip G2D as it doesn't support rendering to image
1346
1347        #[cfg(target_os = "linux")]
1348        #[cfg(feature = "opengl")]
1349        if let Some(opengl) = self.opengl.as_mut() {
1350            log::trace!("image started with opengl in {:?}", start.elapsed());
1351            match opengl.set_class_colors(colors) {
1352                Ok(_) => {
1353                    log::trace!("colors set with opengl in {:?}", start.elapsed());
1354                    return Ok(());
1355                }
1356                Err(e) => {
1357                    log::trace!("colors didn't set with opengl: {e:?}")
1358                }
1359            }
1360        }
1361        log::trace!("image started with cpu in {:?}", start.elapsed());
1362        if let Some(cpu) = self.cpu.as_mut() {
1363            match cpu.set_class_colors(colors) {
1364                Ok(_) => {
1365                    log::trace!("colors set with cpu in {:?}", start.elapsed());
1366                    return Ok(());
1367                }
1368                Err(e) => {
1369                    log::trace!("colors didn't set with cpu: {e:?}");
1370                    return Err(e);
1371                }
1372            }
1373        }
1374        Err(Error::NoConverter)
1375    }
1376}
1377
1378fn fourcc_channels(fourcc: FourCharCode) -> Result<usize> {
1379    match fourcc {
1380        RGBA => Ok(4), // RGBA has 4 channels (R, G, B, A)
1381        RGB => Ok(3),  // RGB has 3 channels (R, G, B)
1382        YUYV => Ok(2), // YUYV has 2 channels (Y and UV)
1383        GREY => Ok(1), // Y800 has 1 channel (Y)
1384        NV12 => Ok(2), // NV12 has 2 channel. 2nd channel is half empty
1385        NV16 => Ok(2), // NV16 has 2 channel. 2nd channel is full size
1386        PLANAR_RGB => Ok(3),
1387        PLANAR_RGBA => Ok(4),
1388        _ => Err(Error::NotSupported(format!(
1389            "Unsupported fourcc: {}",
1390            fourcc.to_string()
1391        ))),
1392    }
1393}
1394
1395fn fourcc_planar(fourcc: FourCharCode) -> Result<bool> {
1396    match fourcc {
1397        RGBA => Ok(false),       // RGBA has 4 channels (R, G, B, A)
1398        RGB => Ok(false),        // RGB has 3 channels (R, G, B)
1399        YUYV => Ok(false),       // YUYV has 2 channels (Y and UV)
1400        GREY => Ok(false),       // Y800 has 1 channel (Y)
1401        NV12 => Ok(true),        // Planar YUV
1402        NV16 => Ok(true),        // Planar YUV
1403        PLANAR_RGB => Ok(true),  // Planar RGB
1404        PLANAR_RGBA => Ok(true), // Planar RGBA
1405        _ => Err(Error::NotSupported(format!(
1406            "Unsupported fourcc: {}",
1407            fourcc.to_string()
1408        ))),
1409    }
1410}
1411
1412pub(crate) struct FunctionTimer<T: Display> {
1413    name: T,
1414    start: std::time::Instant,
1415}
1416
1417impl<T: Display> FunctionTimer<T> {
1418    pub fn new(name: T) -> Self {
1419        Self {
1420            name,
1421            start: std::time::Instant::now(),
1422        }
1423    }
1424}
1425
1426impl<T: Display> Drop for FunctionTimer<T> {
1427    fn drop(&mut self) {
1428        log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
1429    }
1430}
1431
1432#[cfg(feature = "decoder")]
1433const DEFAULT_COLORS: [[f32; 4]; 20] = [
1434    [0., 1., 0., 0.7],
1435    [1., 0.5568628, 0., 0.7],
1436    [0.25882353, 0.15294118, 0.13333333, 0.7],
1437    [0.8, 0.7647059, 0.78039216, 0.7],
1438    [0.3137255, 0.3137255, 0.3137255, 0.7],
1439    [0.1411765, 0.3098039, 0.1215686, 0.7],
1440    [1., 0.95686275, 0.5137255, 0.7],
1441    [0.3529412, 0.32156863, 0., 0.7],
1442    [0.4235294, 0.6235294, 0.6509804, 0.7],
1443    [0.5098039, 0.5098039, 0.7294118, 0.7],
1444    [0.00784314, 0.18823529, 0.29411765, 0.7],
1445    [0.0, 0.2706, 1.0, 0.7],
1446    [0.0, 0.0, 0.0, 0.7],
1447    [0.0, 0.5, 0.0, 0.7],
1448    [1.0, 0.0, 0.0, 0.7],
1449    [0.0, 0.0, 1.0, 0.7],
1450    [1.0, 0.5, 0.5, 0.7],
1451    [0.1333, 0.5451, 0.1333, 0.7],
1452    [0.1176, 0.4118, 0.8235, 0.7],
1453    [1., 1., 1., 0.7],
1454];
1455
1456#[cfg(feature = "decoder")]
1457const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
1458    let mut result = [[0; M]; N];
1459    let mut i = 0;
1460    while i < N {
1461        let mut j = 0;
1462        while j < M {
1463            result[i][j] = (a[i][j] * 255.0).round() as u8;
1464            j += 1;
1465        }
1466        i += 1;
1467    }
1468    result
1469}
1470
1471#[cfg(feature = "decoder")]
1472const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
1473
1474#[cfg(test)]
1475#[cfg_attr(coverage_nightly, coverage(off))]
1476mod image_tests {
1477    use super::*;
1478    use crate::{CPUProcessor, Rotation};
1479    #[cfg(target_os = "linux")]
1480    use edgefirst_tensor::is_dma_available;
1481    use edgefirst_tensor::{TensorMapTrait, TensorMemory};
1482    use image::buffer::ConvertBuffer;
1483
1484    #[ctor::ctor]
1485    fn init() {
1486        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
1487    }
1488
1489    macro_rules! function {
1490        () => {{
1491            fn f() {}
1492            fn type_name_of<T>(_: T) -> &'static str {
1493                std::any::type_name::<T>()
1494            }
1495            let name = type_name_of(f);
1496
1497            // Find and cut the rest of the path
1498            match &name[..name.len() - 3].rfind(':') {
1499                Some(pos) => &name[pos + 1..name.len() - 3],
1500                None => &name[..name.len() - 3],
1501            }
1502        }};
1503    }
1504
1505    #[test]
1506    fn test_invalid_crop() {
1507        let src = TensorImage::new(100, 100, RGB, None).unwrap();
1508        let dst = TensorImage::new(100, 100, RGB, None).unwrap();
1509
1510        let crop = Crop::new()
1511            .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1512            .with_dst_rect(Some(Rect::new(0, 0, 150, 150)));
1513
1514        let result = crop.check_crop(&src, &dst);
1515        assert!(matches!(
1516            result,
1517            Err(Error::CropInvalid(e)) if e.starts_with("Dest and Src crop invalid")
1518        ));
1519
1520        let crop = crop.with_src_rect(Some(Rect::new(0, 0, 10, 10)));
1521        let result = crop.check_crop(&src, &dst);
1522        assert!(matches!(
1523            result,
1524            Err(Error::CropInvalid(e)) if e.starts_with("Dest crop invalid")
1525        ));
1526
1527        let crop = crop
1528            .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1529            .with_dst_rect(Some(Rect::new(0, 0, 50, 50)));
1530        let result = crop.check_crop(&src, &dst);
1531        assert!(matches!(
1532            result,
1533            Err(Error::CropInvalid(e)) if e.starts_with("Src crop invalid")
1534        ));
1535
1536        let crop = crop.with_src_rect(Some(Rect::new(50, 50, 50, 50)));
1537
1538        let result = crop.check_crop(&src, &dst);
1539        assert!(result.is_ok());
1540    }
1541
1542    #[test]
1543    fn test_invalid_tensor() -> Result<(), Error> {
1544        let tensor = Tensor::new(&[720, 1280, 4, 1], None, None)?;
1545        let result = TensorImage::from_tensor(tensor, RGB);
1546        assert!(matches!(
1547            result,
1548            Err(Error::InvalidShape(e)) if e.starts_with("Tensor shape must have 3 dimensions, got")
1549        ));
1550
1551        let tensor = Tensor::new(&[720, 1280, 4], None, None)?;
1552        let result = TensorImage::from_tensor(tensor, RGB);
1553        assert!(matches!(
1554            result,
1555            Err(Error::InvalidShape(e)) if e.starts_with("Invalid tensor shape")
1556        ));
1557
1558        Ok(())
1559    }
1560
1561    #[test]
1562    fn test_invalid_image_file() -> Result<(), Error> {
1563        let result = TensorImage::load(&[123; 5000], None, 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_invalid_jpeg_fourcc() -> Result<(), Error> {
1573        let result = TensorImage::load(&[123; 5000], Some(YUYV), None);
1574        assert!(matches!(
1575            result,
1576            Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
1577
1578        Ok(())
1579    }
1580
1581    #[test]
1582    fn test_load_resize_save() {
1583        let file = include_bytes!("../../../testdata/zidane.jpg");
1584        let img = TensorImage::load_jpeg(file, Some(RGBA), None).unwrap();
1585        assert_eq!(img.width(), 1280);
1586        assert_eq!(img.height(), 720);
1587
1588        let mut dst = TensorImage::new(640, 360, RGBA, None).unwrap();
1589        let mut converter = CPUProcessor::new();
1590        converter
1591            .convert(&img, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
1592            .unwrap();
1593        assert_eq!(dst.width(), 640);
1594        assert_eq!(dst.height(), 360);
1595
1596        dst.save_jpeg("zidane_resized.jpg", 80).unwrap();
1597
1598        let file = std::fs::read("zidane_resized.jpg").unwrap();
1599        let img = TensorImage::load_jpeg(&file, None, None).unwrap();
1600        assert_eq!(img.width(), 640);
1601        assert_eq!(img.height(), 360);
1602        assert_eq!(img.fourcc(), RGB);
1603    }
1604
1605    #[test]
1606    fn test_from_tensor_planar() -> Result<(), Error> {
1607        let tensor = Tensor::new(&[3, 720, 1280], None, None)?;
1608        tensor
1609            .map()?
1610            .copy_from_slice(include_bytes!("../../../testdata/camera720p.8bps"));
1611        let planar = TensorImage::from_tensor(tensor, PLANAR_RGB)?;
1612
1613        let rbga = load_bytes_to_tensor(
1614            1280,
1615            720,
1616            RGBA,
1617            None,
1618            include_bytes!("../../../testdata/camera720p.rgba"),
1619        )?;
1620        compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
1621
1622        Ok(())
1623    }
1624
1625    #[test]
1626    fn test_from_tensor_invalid_fourcc() {
1627        let tensor = Tensor::new(&[3, 720, 1280], None, None).unwrap();
1628        let result = TensorImage::from_tensor(tensor, four_char_code!("TEST"));
1629        matches!(result, Err(Error::NotSupported(e)) if e.starts_with("Unsupported fourcc : TEST"));
1630    }
1631
1632    #[test]
1633    #[should_panic(expected = "Failed to save planar RGB image")]
1634    fn test_save_planar() {
1635        let planar_img = load_bytes_to_tensor(
1636            1280,
1637            720,
1638            PLANAR_RGB,
1639            None,
1640            include_bytes!("../../../testdata/camera720p.8bps"),
1641        )
1642        .unwrap();
1643
1644        let save_path = "/tmp/planar_rgb.jpg";
1645        planar_img
1646            .save_jpeg(save_path, 90)
1647            .expect("Failed to save planar RGB image");
1648    }
1649
1650    #[test]
1651    #[should_panic(expected = "Failed to save YUYV image")]
1652    fn test_save_yuyv() {
1653        let planar_img = load_bytes_to_tensor(
1654            1280,
1655            720,
1656            YUYV,
1657            None,
1658            include_bytes!("../../../testdata/camera720p.yuyv"),
1659        )
1660        .unwrap();
1661
1662        let save_path = "/tmp/yuyv.jpg";
1663        planar_img
1664            .save_jpeg(save_path, 90)
1665            .expect("Failed to save YUYV image");
1666    }
1667
1668    #[test]
1669    fn test_rotation_angle() {
1670        assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
1671        assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
1672        assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
1673        assert_eq!(
1674            Rotation::from_degrees_clockwise(270),
1675            Rotation::CounterClockwise90
1676        );
1677        assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
1678        assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
1679        assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
1680        assert_eq!(
1681            Rotation::from_degrees_clockwise(630),
1682            Rotation::CounterClockwise90
1683        );
1684    }
1685
1686    #[test]
1687    #[should_panic(expected = "rotation angle is not a multiple of 90")]
1688    fn test_rotation_angle_panic() {
1689        Rotation::from_degrees_clockwise(361);
1690    }
1691
1692    #[test]
1693    fn test_disable_env_var() -> Result<(), Error> {
1694        #[cfg(target_os = "linux")]
1695        {
1696            let original = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1697            unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1698            let converter = ImageProcessor::new()?;
1699            match original {
1700                Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1701                None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1702            }
1703            assert!(converter.g2d.is_none());
1704        }
1705
1706        #[cfg(target_os = "linux")]
1707        #[cfg(feature = "opengl")]
1708        {
1709            let original = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1710            unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1711            let converter = ImageProcessor::new()?;
1712            match original {
1713                Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1714                None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1715            }
1716            assert!(converter.opengl.is_none());
1717        }
1718
1719        let original = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1720        unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1721        let converter = ImageProcessor::new()?;
1722        match original {
1723            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1724            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1725        }
1726        assert!(converter.cpu.is_none());
1727
1728        let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1729        unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1730        let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1731        unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1732        let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1733        unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1734        let mut converter = ImageProcessor::new()?;
1735
1736        let src = TensorImage::new(1280, 720, RGBA, None)?;
1737        let mut dst = TensorImage::new(640, 360, RGBA, None)?;
1738        let result = converter.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop());
1739        assert!(matches!(result, Err(Error::NoConverter)));
1740
1741        match original_cpu {
1742            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1743            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1744        }
1745        match original_gl {
1746            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1747            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1748        }
1749        match original_g2d {
1750            Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1751            None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1752        }
1753
1754        Ok(())
1755    }
1756
1757    #[test]
1758    fn test_unsupported_conversion() {
1759        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
1760        let mut dst = TensorImage::new(640, 360, NV12, None).unwrap();
1761        let mut converter = ImageProcessor::new().unwrap();
1762        let result = converter.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop());
1763        log::debug!("result: {:?}", result);
1764        assert!(matches!(
1765            result,
1766            Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
1767        ));
1768    }
1769
1770    #[test]
1771    fn test_load_grey() {
1772        let grey_img = TensorImage::load_jpeg(
1773            include_bytes!("../../../testdata/grey.jpg"),
1774            Some(RGBA),
1775            None,
1776        )
1777        .unwrap();
1778
1779        let grey_but_rgb_img = TensorImage::load_jpeg(
1780            include_bytes!("../../../testdata/grey-rgb.jpg"),
1781            Some(RGBA),
1782            None,
1783        )
1784        .unwrap();
1785
1786        compare_images(&grey_img, &grey_but_rgb_img, 0.99, function!());
1787    }
1788
1789    #[test]
1790    fn test_new_nv12() {
1791        let nv12 = TensorImage::new(1280, 720, NV12, None).unwrap();
1792        assert_eq!(nv12.height(), 720);
1793        assert_eq!(nv12.width(), 1280);
1794        assert_eq!(nv12.fourcc(), NV12);
1795        assert_eq!(nv12.channels(), 2);
1796        assert!(nv12.is_planar())
1797    }
1798
1799    #[test]
1800    #[cfg(target_os = "linux")]
1801    fn test_new_image_converter() {
1802        let dst_width = 640;
1803        let dst_height = 360;
1804        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1805        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1806
1807        let mut converter_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1808        let mut converter = ImageProcessor::new().unwrap();
1809        converter
1810            .convert(
1811                &src,
1812                &mut converter_dst,
1813                Rotation::None,
1814                Flip::None,
1815                Crop::no_crop(),
1816            )
1817            .unwrap();
1818
1819        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1820        let mut cpu_converter = CPUProcessor::new();
1821        cpu_converter
1822            .convert(
1823                &src,
1824                &mut cpu_dst,
1825                Rotation::None,
1826                Flip::None,
1827                Crop::no_crop(),
1828            )
1829            .unwrap();
1830
1831        compare_images(&converter_dst, &cpu_dst, 0.98, function!());
1832    }
1833
1834    #[test]
1835    fn test_crop_skip() {
1836        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1837        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1838
1839        let mut converter_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1840        let mut converter = ImageProcessor::new().unwrap();
1841        let crop = Crop::new()
1842            .with_src_rect(Some(Rect::new(0, 0, 640, 640)))
1843            .with_dst_rect(Some(Rect::new(0, 0, 640, 640)));
1844        converter
1845            .convert(&src, &mut converter_dst, Rotation::None, Flip::None, crop)
1846            .unwrap();
1847
1848        let mut cpu_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1849        let mut cpu_converter = CPUProcessor::new();
1850        cpu_converter
1851            .convert(&src, &mut cpu_dst, Rotation::None, Flip::None, crop)
1852            .unwrap();
1853
1854        compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
1855    }
1856
1857    #[test]
1858    fn test_invalid_fourcc() {
1859        let result = TensorImage::new(1280, 720, four_char_code!("TEST"), None);
1860        assert!(matches!(
1861            result,
1862            Err(Error::NotSupported(e)) if e == "Unsupported fourcc: TEST"
1863        ));
1864    }
1865
1866    // Helper function to check if G2D library is available (Linux/i.MX8 only)
1867    #[cfg(target_os = "linux")]
1868    static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1869
1870    #[cfg(target_os = "linux")]
1871    fn is_g2d_available() -> bool {
1872        *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
1873    }
1874
1875    #[cfg(target_os = "linux")]
1876    #[cfg(feature = "opengl")]
1877    static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1878
1879    #[cfg(target_os = "linux")]
1880    #[cfg(feature = "opengl")]
1881    // Helper function to check if OpenGL is available
1882    fn is_opengl_available() -> bool {
1883        #[cfg(all(target_os = "linux", feature = "opengl"))]
1884        {
1885            *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new().is_ok())
1886        }
1887
1888        #[cfg(not(all(target_os = "linux", feature = "opengl")))]
1889        {
1890            false
1891        }
1892    }
1893
1894    #[test]
1895    fn test_load_jpeg_with_exif() {
1896        let file = include_bytes!("../../../testdata/zidane_rotated_exif.jpg").to_vec();
1897        let loaded = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1898
1899        assert_eq!(loaded.height(), 1280);
1900        assert_eq!(loaded.width(), 720);
1901
1902        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1903        let cpu_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1904
1905        let (dst_width, dst_height) = (cpu_src.height(), cpu_src.width());
1906
1907        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1908        let mut cpu_converter = CPUProcessor::new();
1909
1910        cpu_converter
1911            .convert(
1912                &cpu_src,
1913                &mut cpu_dst,
1914                Rotation::Clockwise90,
1915                Flip::None,
1916                Crop::no_crop(),
1917            )
1918            .unwrap();
1919
1920        compare_images(&loaded, &cpu_dst, 0.98, function!());
1921    }
1922
1923    #[test]
1924    fn test_load_png_with_exif() {
1925        let file = include_bytes!("../../../testdata/zidane_rotated_exif_180.png").to_vec();
1926        let loaded = TensorImage::load_png(&file, Some(RGBA), None).unwrap();
1927
1928        assert_eq!(loaded.height(), 720);
1929        assert_eq!(loaded.width(), 1280);
1930
1931        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1932        let cpu_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
1933
1934        let mut cpu_dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
1935        let mut cpu_converter = CPUProcessor::new();
1936
1937        cpu_converter
1938            .convert(
1939                &cpu_src,
1940                &mut cpu_dst,
1941                Rotation::Rotate180,
1942                Flip::None,
1943                Crop::no_crop(),
1944            )
1945            .unwrap();
1946
1947        compare_images(&loaded, &cpu_dst, 0.98, function!());
1948    }
1949
1950    #[test]
1951    #[cfg(target_os = "linux")]
1952    fn test_g2d_resize() {
1953        if !is_g2d_available() {
1954            eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
1955            return;
1956        }
1957        if !is_dma_available() {
1958            eprintln!(
1959                "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
1960            );
1961            return;
1962        }
1963
1964        let dst_width = 640;
1965        let dst_height = 360;
1966        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
1967        let src = TensorImage::load_jpeg(&file, Some(RGBA), Some(TensorMemory::Dma)).unwrap();
1968
1969        let mut g2d_dst =
1970            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
1971        let mut g2d_converter = G2DProcessor::new().unwrap();
1972        g2d_converter
1973            .convert(
1974                &src,
1975                &mut g2d_dst,
1976                Rotation::None,
1977                Flip::None,
1978                Crop::no_crop(),
1979            )
1980            .unwrap();
1981
1982        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
1983        let mut cpu_converter = CPUProcessor::new();
1984        cpu_converter
1985            .convert(
1986                &src,
1987                &mut cpu_dst,
1988                Rotation::None,
1989                Flip::None,
1990                Crop::no_crop(),
1991            )
1992            .unwrap();
1993
1994        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
1995    }
1996
1997    #[test]
1998    #[cfg(target_os = "linux")]
1999    #[cfg(feature = "opengl")]
2000    fn test_opengl_resize() {
2001        if !is_opengl_available() {
2002            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2003            return;
2004        }
2005
2006        let dst_width = 640;
2007        let dst_height = 360;
2008        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2009        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2010
2011        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2012        let mut cpu_converter = CPUProcessor::new();
2013        cpu_converter
2014            .convert(
2015                &src,
2016                &mut cpu_dst,
2017                Rotation::None,
2018                Flip::None,
2019                Crop::no_crop(),
2020            )
2021            .unwrap();
2022        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2023        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2024
2025        for _ in 0..5 {
2026            gl_converter
2027                .convert(
2028                    &src,
2029                    &mut gl_dst,
2030                    Rotation::None,
2031                    Flip::None,
2032                    Crop::no_crop(),
2033                )
2034                .unwrap();
2035
2036            compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2037        }
2038
2039        drop(gl_dst);
2040    }
2041
2042    #[test]
2043    #[cfg(target_os = "linux")]
2044    #[cfg(feature = "opengl")]
2045    fn test_opengl_10_threads() {
2046        if !is_opengl_available() {
2047            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2048            return;
2049        }
2050
2051        let handles: Vec<_> = (0..10)
2052            .map(|i| {
2053                std::thread::Builder::new()
2054                    .name(format!("Thread {i}"))
2055                    .spawn(test_opengl_resize)
2056                    .unwrap()
2057            })
2058            .collect();
2059        handles.into_iter().for_each(|h| {
2060            if let Err(e) = h.join() {
2061                std::panic::resume_unwind(e)
2062            }
2063        });
2064    }
2065
2066    #[test]
2067    #[cfg(target_os = "linux")]
2068    #[cfg(feature = "opengl")]
2069    fn test_opengl_grey() {
2070        if !is_opengl_available() {
2071            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2072            return;
2073        }
2074
2075        let img = TensorImage::load_jpeg(
2076            include_bytes!("../../../testdata/grey.jpg"),
2077            Some(GREY),
2078            None,
2079        )
2080        .unwrap();
2081
2082        let mut gl_dst = TensorImage::new(640, 640, GREY, None).unwrap();
2083        let mut cpu_dst = TensorImage::new(640, 640, GREY, None).unwrap();
2084
2085        let mut converter = CPUProcessor::new();
2086
2087        converter
2088            .convert(
2089                &img,
2090                &mut cpu_dst,
2091                Rotation::None,
2092                Flip::None,
2093                Crop::no_crop(),
2094            )
2095            .unwrap();
2096
2097        let mut gl = GLProcessorThreaded::new().unwrap();
2098        gl.convert(
2099            &img,
2100            &mut gl_dst,
2101            Rotation::None,
2102            Flip::None,
2103            Crop::no_crop(),
2104        )
2105        .unwrap();
2106
2107        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2108    }
2109
2110    #[test]
2111    #[cfg(target_os = "linux")]
2112    fn test_g2d_src_crop() {
2113        if !is_g2d_available() {
2114            eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
2115            return;
2116        }
2117        if !is_dma_available() {
2118            eprintln!(
2119                "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2120            );
2121            return;
2122        }
2123
2124        let dst_width = 640;
2125        let dst_height = 640;
2126        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2127        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2128
2129        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2130        let mut cpu_converter = CPUProcessor::new();
2131        cpu_converter
2132            .convert(
2133                &src,
2134                &mut cpu_dst,
2135                Rotation::None,
2136                Flip::None,
2137                Crop {
2138                    src_rect: Some(Rect {
2139                        left: 0,
2140                        top: 0,
2141                        width: 640,
2142                        height: 360,
2143                    }),
2144                    dst_rect: None,
2145                    dst_color: None,
2146                },
2147            )
2148            .unwrap();
2149
2150        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2151        let mut g2d_converter = G2DProcessor::new().unwrap();
2152        g2d_converter
2153            .convert(
2154                &src,
2155                &mut g2d_dst,
2156                Rotation::None,
2157                Flip::None,
2158                Crop {
2159                    src_rect: Some(Rect {
2160                        left: 0,
2161                        top: 0,
2162                        width: 640,
2163                        height: 360,
2164                    }),
2165                    dst_rect: None,
2166                    dst_color: None,
2167                },
2168            )
2169            .unwrap();
2170
2171        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2172    }
2173
2174    #[test]
2175    #[cfg(target_os = "linux")]
2176    fn test_g2d_dst_crop() {
2177        if !is_g2d_available() {
2178            eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
2179            return;
2180        }
2181        if !is_dma_available() {
2182            eprintln!(
2183                "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2184            );
2185            return;
2186        }
2187
2188        let dst_width = 640;
2189        let dst_height = 640;
2190        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2191        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2192
2193        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2194        let mut cpu_converter = CPUProcessor::new();
2195        cpu_converter
2196            .convert(
2197                &src,
2198                &mut cpu_dst,
2199                Rotation::None,
2200                Flip::None,
2201                Crop {
2202                    src_rect: None,
2203                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2204                    dst_color: None,
2205                },
2206            )
2207            .unwrap();
2208
2209        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2210        let mut g2d_converter = G2DProcessor::new().unwrap();
2211        g2d_converter
2212            .convert(
2213                &src,
2214                &mut g2d_dst,
2215                Rotation::None,
2216                Flip::None,
2217                Crop {
2218                    src_rect: None,
2219                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2220                    dst_color: None,
2221                },
2222            )
2223            .unwrap();
2224
2225        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2226    }
2227
2228    #[test]
2229    #[cfg(target_os = "linux")]
2230    fn test_g2d_all_rgba() {
2231        if !is_g2d_available() {
2232            eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
2233            return;
2234        }
2235        if !is_dma_available() {
2236            eprintln!(
2237                "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2238            );
2239            return;
2240        }
2241
2242        let dst_width = 640;
2243        let dst_height = 640;
2244        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2245        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2246
2247        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2248        let mut cpu_converter = CPUProcessor::new();
2249        let mut g2d_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2250        let mut g2d_converter = G2DProcessor::new().unwrap();
2251
2252        for rot in [
2253            Rotation::None,
2254            Rotation::Clockwise90,
2255            Rotation::Rotate180,
2256            Rotation::CounterClockwise90,
2257        ] {
2258            cpu_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2259            g2d_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2260            for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2261                cpu_converter
2262                    .convert(
2263                        &src,
2264                        &mut cpu_dst,
2265                        Rotation::None,
2266                        Flip::None,
2267                        Crop {
2268                            src_rect: Some(Rect::new(50, 120, 1024, 576)),
2269                            dst_rect: Some(Rect::new(100, 100, 512, 288)),
2270                            dst_color: None,
2271                        },
2272                    )
2273                    .unwrap();
2274
2275                g2d_converter
2276                    .convert(
2277                        &src,
2278                        &mut g2d_dst,
2279                        Rotation::None,
2280                        Flip::None,
2281                        Crop {
2282                            src_rect: Some(Rect::new(50, 120, 1024, 576)),
2283                            dst_rect: Some(Rect::new(100, 100, 512, 288)),
2284                            dst_color: None,
2285                        },
2286                    )
2287                    .unwrap();
2288
2289                compare_images(
2290                    &g2d_dst,
2291                    &cpu_dst,
2292                    0.98,
2293                    &format!("{} {:?} {:?}", function!(), rot, flip),
2294                );
2295            }
2296        }
2297    }
2298
2299    #[test]
2300    #[cfg(target_os = "linux")]
2301    #[cfg(feature = "opengl")]
2302    fn test_opengl_src_crop() {
2303        if !is_opengl_available() {
2304            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2305            return;
2306        }
2307
2308        let dst_width = 640;
2309        let dst_height = 360;
2310        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2311        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2312
2313        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2314        let mut cpu_converter = CPUProcessor::new();
2315        cpu_converter
2316            .convert(
2317                &src,
2318                &mut cpu_dst,
2319                Rotation::None,
2320                Flip::None,
2321                Crop {
2322                    src_rect: Some(Rect {
2323                        left: 320,
2324                        top: 180,
2325                        width: 1280 - 320,
2326                        height: 720 - 180,
2327                    }),
2328                    dst_rect: None,
2329                    dst_color: None,
2330                },
2331            )
2332            .unwrap();
2333
2334        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2335        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2336
2337        gl_converter
2338            .convert(
2339                &src,
2340                &mut gl_dst,
2341                Rotation::None,
2342                Flip::None,
2343                Crop {
2344                    src_rect: Some(Rect {
2345                        left: 320,
2346                        top: 180,
2347                        width: 1280 - 320,
2348                        height: 720 - 180,
2349                    }),
2350                    dst_rect: None,
2351                    dst_color: None,
2352                },
2353            )
2354            .unwrap();
2355
2356        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2357    }
2358
2359    #[test]
2360    #[cfg(target_os = "linux")]
2361    #[cfg(feature = "opengl")]
2362    fn test_opengl_dst_crop() {
2363        if !is_opengl_available() {
2364            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2365            return;
2366        }
2367
2368        let dst_width = 640;
2369        let dst_height = 640;
2370        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2371        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2372
2373        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2374        let mut cpu_converter = CPUProcessor::new();
2375        cpu_converter
2376            .convert(
2377                &src,
2378                &mut cpu_dst,
2379                Rotation::None,
2380                Flip::None,
2381                Crop {
2382                    src_rect: None,
2383                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2384                    dst_color: None,
2385                },
2386            )
2387            .unwrap();
2388
2389        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2390        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2391        gl_converter
2392            .convert(
2393                &src,
2394                &mut gl_dst,
2395                Rotation::None,
2396                Flip::None,
2397                Crop {
2398                    src_rect: None,
2399                    dst_rect: Some(Rect::new(100, 100, 512, 288)),
2400                    dst_color: None,
2401                },
2402            )
2403            .unwrap();
2404
2405        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2406    }
2407
2408    #[test]
2409    #[cfg(target_os = "linux")]
2410    #[cfg(feature = "opengl")]
2411    fn test_opengl_all_rgba() {
2412        if !is_opengl_available() {
2413            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2414            return;
2415        }
2416
2417        let dst_width = 640;
2418        let dst_height = 640;
2419        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2420
2421        let mut cpu_converter = CPUProcessor::new();
2422
2423        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2424
2425        let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
2426        if is_dma_available() {
2427            mem.push(Some(TensorMemory::Dma));
2428        }
2429        for m in mem {
2430            let src = TensorImage::load_jpeg(&file, Some(RGBA), m).unwrap();
2431
2432            for rot in [
2433                Rotation::None,
2434                Rotation::Clockwise90,
2435                Rotation::Rotate180,
2436                Rotation::CounterClockwise90,
2437            ] {
2438                for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2439                    let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, m).unwrap();
2440                    let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, m).unwrap();
2441                    cpu_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2442                    gl_dst.tensor.map().unwrap().as_mut_slice().fill(114);
2443                    cpu_converter
2444                        .convert(
2445                            &src,
2446                            &mut cpu_dst,
2447                            Rotation::None,
2448                            Flip::None,
2449                            Crop {
2450                                src_rect: Some(Rect::new(50, 120, 1024, 576)),
2451                                dst_rect: Some(Rect::new(100, 100, 512, 288)),
2452                                dst_color: None,
2453                            },
2454                        )
2455                        .unwrap();
2456
2457                    gl_converter
2458                        .convert(
2459                            &src,
2460                            &mut gl_dst,
2461                            Rotation::None,
2462                            Flip::None,
2463                            Crop {
2464                                src_rect: Some(Rect::new(50, 120, 1024, 576)),
2465                                dst_rect: Some(Rect::new(100, 100, 512, 288)),
2466                                dst_color: None,
2467                            },
2468                        )
2469                        .map_err(|e| {
2470                            log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
2471                            e
2472                        })
2473                        .unwrap();
2474
2475                    compare_images(
2476                        &gl_dst,
2477                        &cpu_dst,
2478                        0.98,
2479                        &format!("{} {:?} {:?}", function!(), rot, flip),
2480                    );
2481                }
2482            }
2483        }
2484    }
2485
2486    #[test]
2487    #[cfg(target_os = "linux")]
2488    fn test_cpu_rotate() {
2489        for rot in [
2490            Rotation::Clockwise90,
2491            Rotation::Rotate180,
2492            Rotation::CounterClockwise90,
2493        ] {
2494            test_cpu_rotate_(rot);
2495        }
2496    }
2497
2498    #[cfg(target_os = "linux")]
2499    fn test_cpu_rotate_(rot: Rotation) {
2500        // This test rotates the image 4 times and checks that the image was returned to
2501        // be the same Currently doesn't check if rotations actually rotated in
2502        // right direction
2503        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2504
2505        let unchanged_src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2506        let mut src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
2507
2508        let (dst_width, dst_height) = match rot {
2509            Rotation::None | Rotation::Rotate180 => (src.width(), src.height()),
2510            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (src.height(), src.width()),
2511        };
2512
2513        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2514        let mut cpu_converter = CPUProcessor::new();
2515
2516        // After rotating 4 times, the image should be the same as the original
2517
2518        cpu_converter
2519            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2520            .unwrap();
2521
2522        cpu_converter
2523            .convert(&cpu_dst, &mut src, rot, Flip::None, Crop::no_crop())
2524            .unwrap();
2525
2526        cpu_converter
2527            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2528            .unwrap();
2529
2530        cpu_converter
2531            .convert(&cpu_dst, &mut src, rot, Flip::None, Crop::no_crop())
2532            .unwrap();
2533
2534        compare_images(&src, &unchanged_src, 0.98, function!());
2535    }
2536
2537    #[test]
2538    #[cfg(target_os = "linux")]
2539    #[cfg(feature = "opengl")]
2540    fn test_opengl_rotate() {
2541        if !is_opengl_available() {
2542            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2543            return;
2544        }
2545
2546        let size = (1280, 720);
2547        let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
2548
2549        if is_dma_available() {
2550            mem.push(Some(TensorMemory::Dma));
2551        }
2552        for m in mem {
2553            for rot in [
2554                Rotation::Clockwise90,
2555                Rotation::Rotate180,
2556                Rotation::CounterClockwise90,
2557            ] {
2558                test_opengl_rotate_(size, rot, m);
2559            }
2560        }
2561    }
2562
2563    #[cfg(target_os = "linux")]
2564    #[cfg(feature = "opengl")]
2565    fn test_opengl_rotate_(
2566        size: (usize, usize),
2567        rot: Rotation,
2568        tensor_memory: Option<TensorMemory>,
2569    ) {
2570        let (dst_width, dst_height) = match rot {
2571            Rotation::None | Rotation::Rotate180 => size,
2572            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2573        };
2574
2575        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2576        let src = TensorImage::load_jpeg(&file, Some(RGBA), tensor_memory).unwrap();
2577
2578        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2579        let mut cpu_converter = CPUProcessor::new();
2580
2581        cpu_converter
2582            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2583            .unwrap();
2584
2585        let mut gl_dst = TensorImage::new(dst_width, dst_height, RGBA, tensor_memory).unwrap();
2586        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2587
2588        for _ in 0..5 {
2589            gl_converter
2590                .convert(&src, &mut gl_dst, rot, Flip::None, Crop::no_crop())
2591                .unwrap();
2592            compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2593        }
2594    }
2595
2596    #[test]
2597    #[cfg(target_os = "linux")]
2598    fn test_g2d_rotate() {
2599        if !is_g2d_available() {
2600            eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
2601            return;
2602        }
2603        if !is_dma_available() {
2604            eprintln!(
2605                "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2606            );
2607            return;
2608        }
2609
2610        let size = (1280, 720);
2611        for rot in [
2612            Rotation::Clockwise90,
2613            Rotation::Rotate180,
2614            Rotation::CounterClockwise90,
2615        ] {
2616            test_g2d_rotate_(size, rot);
2617        }
2618    }
2619
2620    #[cfg(target_os = "linux")]
2621    fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
2622        let (dst_width, dst_height) = match rot {
2623            Rotation::None | Rotation::Rotate180 => size,
2624            Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2625        };
2626
2627        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
2628        let src = TensorImage::load_jpeg(&file, Some(RGBA), Some(TensorMemory::Dma)).unwrap();
2629
2630        let mut cpu_dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2631        let mut cpu_converter = CPUProcessor::new();
2632
2633        cpu_converter
2634            .convert(&src, &mut cpu_dst, rot, Flip::None, Crop::no_crop())
2635            .unwrap();
2636
2637        let mut g2d_dst =
2638            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
2639        let mut g2d_converter = G2DProcessor::new().unwrap();
2640
2641        g2d_converter
2642            .convert(&src, &mut g2d_dst, rot, Flip::None, Crop::no_crop())
2643            .unwrap();
2644
2645        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2646    }
2647
2648    #[test]
2649    fn test_rgba_to_yuyv_resize_cpu() {
2650        let src = load_bytes_to_tensor(
2651            1280,
2652            720,
2653            RGBA,
2654            None,
2655            include_bytes!("../../../testdata/camera720p.rgba"),
2656        )
2657        .unwrap();
2658
2659        let (dst_width, dst_height) = (640, 360);
2660
2661        let mut dst = TensorImage::new(dst_width, dst_height, YUYV, None).unwrap();
2662
2663        let mut dst_through_yuyv = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2664        let mut dst_direct = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
2665
2666        let mut cpu_converter = CPUProcessor::new();
2667
2668        cpu_converter
2669            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2670            .unwrap();
2671
2672        cpu_converter
2673            .convert(
2674                &dst,
2675                &mut dst_through_yuyv,
2676                Rotation::None,
2677                Flip::None,
2678                Crop::no_crop(),
2679            )
2680            .unwrap();
2681
2682        cpu_converter
2683            .convert(
2684                &src,
2685                &mut dst_direct,
2686                Rotation::None,
2687                Flip::None,
2688                Crop::no_crop(),
2689            )
2690            .unwrap();
2691
2692        compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
2693    }
2694
2695    #[test]
2696    #[cfg(target_os = "linux")]
2697    #[cfg(feature = "opengl")]
2698    #[ignore = "opengl doesn't support rendering to YUYV texture"]
2699    fn test_rgba_to_yuyv_resize_opengl() {
2700        if !is_opengl_available() {
2701            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2702            return;
2703        }
2704
2705        if !is_dma_available() {
2706            eprintln!(
2707                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
2708                function!()
2709            );
2710            return;
2711        }
2712
2713        let src = load_bytes_to_tensor(
2714            1280,
2715            720,
2716            RGBA,
2717            None,
2718            include_bytes!("../../../testdata/camera720p.rgba"),
2719        )
2720        .unwrap();
2721
2722        let (dst_width, dst_height) = (640, 360);
2723
2724        let mut dst =
2725            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2726
2727        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2728
2729        gl_converter
2730            .convert(
2731                &src,
2732                &mut dst,
2733                Rotation::None,
2734                Flip::None,
2735                Crop::new()
2736                    .with_dst_rect(Some(Rect::new(100, 100, 100, 100)))
2737                    .with_dst_color(Some([255, 255, 255, 255])),
2738            )
2739            .unwrap();
2740
2741        std::fs::write(
2742            "rgba_to_yuyv_opengl.yuyv",
2743            dst.tensor().map().unwrap().as_slice(),
2744        )
2745        .unwrap();
2746        let mut cpu_dst =
2747            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2748        CPUProcessor::new()
2749            .convert(
2750                &src,
2751                &mut cpu_dst,
2752                Rotation::None,
2753                Flip::None,
2754                Crop::no_crop(),
2755            )
2756            .unwrap();
2757
2758        compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
2759    }
2760
2761    #[test]
2762    #[cfg(target_os = "linux")]
2763    fn test_rgba_to_yuyv_resize_g2d() {
2764        if !is_g2d_available() {
2765            eprintln!(
2766                "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
2767            );
2768            return;
2769        }
2770        if !is_dma_available() {
2771            eprintln!(
2772                "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2773            );
2774            return;
2775        }
2776
2777        let src = load_bytes_to_tensor(
2778            1280,
2779            720,
2780            RGBA,
2781            Some(TensorMemory::Dma),
2782            include_bytes!("../../../testdata/camera720p.rgba"),
2783        )
2784        .unwrap();
2785
2786        let (dst_width, dst_height) = (1280, 720);
2787
2788        let mut cpu_dst =
2789            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2790
2791        let mut g2d_dst =
2792            TensorImage::new(dst_width, dst_height, YUYV, Some(TensorMemory::Dma)).unwrap();
2793
2794        let mut g2d_converter = G2DProcessor::new().unwrap();
2795
2796        g2d_dst.tensor.map().unwrap().as_mut_slice().fill(128);
2797        g2d_converter
2798            .convert(
2799                &src,
2800                &mut g2d_dst,
2801                Rotation::None,
2802                Flip::None,
2803                Crop {
2804                    src_rect: None,
2805                    dst_rect: Some(Rect::new(100, 100, 2, 2)),
2806                    dst_color: None,
2807                },
2808            )
2809            .unwrap();
2810
2811        cpu_dst.tensor.map().unwrap().as_mut_slice().fill(128);
2812        CPUProcessor::new()
2813            .convert(
2814                &src,
2815                &mut cpu_dst,
2816                Rotation::None,
2817                Flip::None,
2818                Crop {
2819                    src_rect: None,
2820                    dst_rect: Some(Rect::new(100, 100, 2, 2)),
2821                    dst_color: None,
2822                },
2823            )
2824            .unwrap();
2825
2826        compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
2827    }
2828
2829    #[test]
2830    fn test_yuyv_to_rgba_cpu() {
2831        let file = include_bytes!("../../../testdata/camera720p.yuyv").to_vec();
2832        let src = TensorImage::new(1280, 720, YUYV, None).unwrap();
2833        src.tensor()
2834            .map()
2835            .unwrap()
2836            .as_mut_slice()
2837            .copy_from_slice(&file);
2838
2839        let mut dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
2840        let mut cpu_converter = CPUProcessor::new();
2841
2842        cpu_converter
2843            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2844            .unwrap();
2845
2846        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2847        target_image
2848            .tensor()
2849            .map()
2850            .unwrap()
2851            .as_mut_slice()
2852            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2853
2854        compare_images(&dst, &target_image, 0.98, function!());
2855    }
2856
2857    #[test]
2858    fn test_yuyv_to_rgb_cpu() {
2859        let file = include_bytes!("../../../testdata/camera720p.yuyv").to_vec();
2860        let src = TensorImage::new(1280, 720, YUYV, None).unwrap();
2861        src.tensor()
2862            .map()
2863            .unwrap()
2864            .as_mut_slice()
2865            .copy_from_slice(&file);
2866
2867        let mut dst = TensorImage::new(1280, 720, RGB, None).unwrap();
2868        let mut cpu_converter = CPUProcessor::new();
2869
2870        cpu_converter
2871            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2872            .unwrap();
2873
2874        let target_image = TensorImage::new(1280, 720, RGB, None).unwrap();
2875        target_image
2876            .tensor()
2877            .map()
2878            .unwrap()
2879            .as_mut_slice()
2880            .as_chunks_mut::<3>()
2881            .0
2882            .iter_mut()
2883            .zip(
2884                include_bytes!("../../../testdata/camera720p.rgba")
2885                    .as_chunks::<4>()
2886                    .0,
2887            )
2888            .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
2889
2890        compare_images(&dst, &target_image, 0.98, function!());
2891    }
2892
2893    #[test]
2894    #[cfg(target_os = "linux")]
2895    fn test_yuyv_to_rgba_g2d() {
2896        if !is_g2d_available() {
2897            eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
2898            return;
2899        }
2900        if !is_dma_available() {
2901            eprintln!(
2902                "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2903            );
2904            return;
2905        }
2906
2907        let src = load_bytes_to_tensor(
2908            1280,
2909            720,
2910            YUYV,
2911            None,
2912            include_bytes!("../../../testdata/camera720p.yuyv"),
2913        )
2914        .unwrap();
2915
2916        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma)).unwrap();
2917        let mut g2d_converter = G2DProcessor::new().unwrap();
2918
2919        g2d_converter
2920            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2921            .unwrap();
2922
2923        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2924        target_image
2925            .tensor()
2926            .map()
2927            .unwrap()
2928            .as_mut_slice()
2929            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2930
2931        compare_images(&dst, &target_image, 0.98, function!());
2932    }
2933
2934    #[test]
2935    #[cfg(target_os = "linux")]
2936    #[cfg(feature = "opengl")]
2937    fn test_yuyv_to_rgba_opengl() {
2938        if !is_opengl_available() {
2939            eprintln!("SKIPPED: {} - OpenGL not available", function!());
2940            return;
2941        }
2942        if !is_dma_available() {
2943            eprintln!(
2944                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
2945                function!()
2946            );
2947            return;
2948        }
2949
2950        let src = load_bytes_to_tensor(
2951            1280,
2952            720,
2953            YUYV,
2954            Some(TensorMemory::Dma),
2955            include_bytes!("../../../testdata/camera720p.yuyv"),
2956        )
2957        .unwrap();
2958
2959        let mut dst = TensorImage::new(1280, 720, RGBA, Some(TensorMemory::Dma)).unwrap();
2960        let mut gl_converter = GLProcessorThreaded::new().unwrap();
2961
2962        gl_converter
2963            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
2964            .unwrap();
2965
2966        let target_image = TensorImage::new(1280, 720, RGBA, None).unwrap();
2967        target_image
2968            .tensor()
2969            .map()
2970            .unwrap()
2971            .as_mut_slice()
2972            .copy_from_slice(include_bytes!("../../../testdata/camera720p.rgba"));
2973
2974        compare_images(&dst, &target_image, 0.98, function!());
2975    }
2976
2977    #[test]
2978    #[cfg(target_os = "linux")]
2979    fn test_yuyv_to_rgb_g2d() {
2980        if !is_g2d_available() {
2981            eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
2982            return;
2983        }
2984        if !is_dma_available() {
2985            eprintln!(
2986                "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2987            );
2988            return;
2989        }
2990
2991        let src = load_bytes_to_tensor(
2992            1280,
2993            720,
2994            YUYV,
2995            None,
2996            include_bytes!("../../../testdata/camera720p.yuyv"),
2997        )
2998        .unwrap();
2999
3000        let mut g2d_dst = TensorImage::new(1280, 720, RGB, Some(TensorMemory::Dma)).unwrap();
3001        let mut g2d_converter = G2DProcessor::new().unwrap();
3002
3003        g2d_converter
3004            .convert(
3005                &src,
3006                &mut g2d_dst,
3007                Rotation::None,
3008                Flip::None,
3009                Crop::no_crop(),
3010            )
3011            .unwrap();
3012
3013        let mut cpu_dst = TensorImage::new(1280, 720, RGB, None).unwrap();
3014        let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3015
3016        cpu_converter
3017            .convert(
3018                &src,
3019                &mut cpu_dst,
3020                Rotation::None,
3021                Flip::None,
3022                Crop::no_crop(),
3023            )
3024            .unwrap();
3025
3026        compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3027    }
3028
3029    #[test]
3030    #[cfg(target_os = "linux")]
3031    fn test_yuyv_to_yuyv_resize_g2d() {
3032        if !is_g2d_available() {
3033            eprintln!(
3034                "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
3035            );
3036            return;
3037        }
3038        if !is_dma_available() {
3039            eprintln!(
3040                "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3041            );
3042            return;
3043        }
3044
3045        let src = load_bytes_to_tensor(
3046            1280,
3047            720,
3048            YUYV,
3049            None,
3050            include_bytes!("../../../testdata/camera720p.yuyv"),
3051        )
3052        .unwrap();
3053
3054        let mut g2d_dst = TensorImage::new(600, 400, YUYV, Some(TensorMemory::Dma)).unwrap();
3055        let mut g2d_converter = G2DProcessor::new().unwrap();
3056
3057        g2d_converter
3058            .convert(
3059                &src,
3060                &mut g2d_dst,
3061                Rotation::None,
3062                Flip::None,
3063                Crop::no_crop(),
3064            )
3065            .unwrap();
3066
3067        let mut cpu_dst = TensorImage::new(600, 400, YUYV, None).unwrap();
3068        let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3069
3070        cpu_converter
3071            .convert(
3072                &src,
3073                &mut cpu_dst,
3074                Rotation::None,
3075                Flip::None,
3076                Crop::no_crop(),
3077            )
3078            .unwrap();
3079
3080        // TODO: compare YUYV and YUYV images without having to convert them to RGB
3081        compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.98, function!());
3082    }
3083
3084    #[test]
3085    fn test_yuyv_to_rgba_resize_cpu() {
3086        let src = load_bytes_to_tensor(
3087            1280,
3088            720,
3089            YUYV,
3090            None,
3091            include_bytes!("../../../testdata/camera720p.yuyv"),
3092        )
3093        .unwrap();
3094
3095        let (dst_width, dst_height) = (960, 540);
3096
3097        let mut dst = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
3098        let mut cpu_converter = CPUProcessor::new();
3099
3100        cpu_converter
3101            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3102            .unwrap();
3103
3104        let mut dst_target = TensorImage::new(dst_width, dst_height, RGBA, None).unwrap();
3105        let src_target = load_bytes_to_tensor(
3106            1280,
3107            720,
3108            RGBA,
3109            None,
3110            include_bytes!("../../../testdata/camera720p.rgba"),
3111        )
3112        .unwrap();
3113        cpu_converter
3114            .convert(
3115                &src_target,
3116                &mut dst_target,
3117                Rotation::None,
3118                Flip::None,
3119                Crop::no_crop(),
3120            )
3121            .unwrap();
3122
3123        compare_images(&dst, &dst_target, 0.98, function!());
3124    }
3125
3126    #[test]
3127    #[cfg(target_os = "linux")]
3128    fn test_yuyv_to_rgba_crop_flip_g2d() {
3129        if !is_g2d_available() {
3130            eprintln!(
3131                "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
3132            );
3133            return;
3134        }
3135        if !is_dma_available() {
3136            eprintln!(
3137                "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3138            );
3139            return;
3140        }
3141
3142        let src = load_bytes_to_tensor(
3143            1280,
3144            720,
3145            YUYV,
3146            Some(TensorMemory::Dma),
3147            include_bytes!("../../../testdata/camera720p.yuyv"),
3148        )
3149        .unwrap();
3150
3151        let (dst_width, dst_height) = (640, 640);
3152
3153        let mut dst_g2d =
3154            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3155        let mut g2d_converter = G2DProcessor::new().unwrap();
3156
3157        g2d_converter
3158            .convert(
3159                &src,
3160                &mut dst_g2d,
3161                Rotation::None,
3162                Flip::Horizontal,
3163                Crop {
3164                    src_rect: Some(Rect {
3165                        left: 20,
3166                        top: 15,
3167                        width: 400,
3168                        height: 300,
3169                    }),
3170                    dst_rect: None,
3171                    dst_color: None,
3172                },
3173            )
3174            .unwrap();
3175
3176        let mut dst_cpu =
3177            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3178        let mut cpu_converter = CPUProcessor::new();
3179
3180        cpu_converter
3181            .convert(
3182                &src,
3183                &mut dst_cpu,
3184                Rotation::None,
3185                Flip::Horizontal,
3186                Crop {
3187                    src_rect: Some(Rect {
3188                        left: 20,
3189                        top: 15,
3190                        width: 400,
3191                        height: 300,
3192                    }),
3193                    dst_rect: None,
3194                    dst_color: None,
3195                },
3196            )
3197            .unwrap();
3198        compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
3199    }
3200
3201    #[test]
3202    #[cfg(target_os = "linux")]
3203    #[cfg(feature = "opengl")]
3204    fn test_yuyv_to_rgba_crop_flip_opengl() {
3205        if !is_opengl_available() {
3206            eprintln!("SKIPPED: {} - OpenGL not available", function!());
3207            return;
3208        }
3209
3210        if !is_dma_available() {
3211            eprintln!(
3212                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3213                function!()
3214            );
3215            return;
3216        }
3217
3218        let src = load_bytes_to_tensor(
3219            1280,
3220            720,
3221            YUYV,
3222            Some(TensorMemory::Dma),
3223            include_bytes!("../../../testdata/camera720p.yuyv"),
3224        )
3225        .unwrap();
3226
3227        let (dst_width, dst_height) = (640, 640);
3228
3229        let mut dst_gl =
3230            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3231        let mut gl_converter = GLProcessorThreaded::new().unwrap();
3232
3233        gl_converter
3234            .convert(
3235                &src,
3236                &mut dst_gl,
3237                Rotation::None,
3238                Flip::Horizontal,
3239                Crop {
3240                    src_rect: Some(Rect {
3241                        left: 20,
3242                        top: 15,
3243                        width: 400,
3244                        height: 300,
3245                    }),
3246                    dst_rect: None,
3247                    dst_color: None,
3248                },
3249            )
3250            .unwrap();
3251
3252        let mut dst_cpu =
3253            TensorImage::new(dst_width, dst_height, RGBA, Some(TensorMemory::Dma)).unwrap();
3254        let mut cpu_converter = CPUProcessor::new();
3255
3256        cpu_converter
3257            .convert(
3258                &src,
3259                &mut dst_cpu,
3260                Rotation::None,
3261                Flip::Horizontal,
3262                Crop {
3263                    src_rect: Some(Rect {
3264                        left: 20,
3265                        top: 15,
3266                        width: 400,
3267                        height: 300,
3268                    }),
3269                    dst_rect: None,
3270                    dst_color: None,
3271                },
3272            )
3273            .unwrap();
3274        compare_images(&dst_gl, &dst_cpu, 0.98, function!());
3275    }
3276
3277    #[test]
3278    fn test_nv12_to_rgba_cpu() {
3279        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3280        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3281        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3282
3283        let mut dst = TensorImage::new(1280, 720, RGBA, None).unwrap();
3284        let mut cpu_converter = CPUProcessor::new();
3285
3286        cpu_converter
3287            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3288            .unwrap();
3289
3290        let target_image = TensorImage::load_jpeg(
3291            include_bytes!("../../../testdata/zidane.jpg"),
3292            Some(RGBA),
3293            None,
3294        )
3295        .unwrap();
3296
3297        compare_images(&dst, &target_image, 0.98, function!());
3298    }
3299
3300    #[test]
3301    fn test_nv12_to_rgb_cpu() {
3302        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3303        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3304        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3305
3306        let mut dst = TensorImage::new(1280, 720, RGB, None).unwrap();
3307        let mut cpu_converter = CPUProcessor::new();
3308
3309        cpu_converter
3310            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3311            .unwrap();
3312
3313        let target_image = TensorImage::load_jpeg(
3314            include_bytes!("../../../testdata/zidane.jpg"),
3315            Some(RGB),
3316            None,
3317        )
3318        .unwrap();
3319
3320        compare_images(&dst, &target_image, 0.98, function!());
3321    }
3322
3323    #[test]
3324    fn test_nv12_to_grey_cpu() {
3325        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3326        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3327        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3328
3329        let mut dst = TensorImage::new(1280, 720, GREY, None).unwrap();
3330        let mut cpu_converter = CPUProcessor::new();
3331
3332        cpu_converter
3333            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3334            .unwrap();
3335
3336        let target_image = TensorImage::load_jpeg(
3337            include_bytes!("../../../testdata/zidane.jpg"),
3338            Some(GREY),
3339            None,
3340        )
3341        .unwrap();
3342
3343        compare_images(&dst, &target_image, 0.98, function!());
3344    }
3345
3346    #[test]
3347    fn test_nv12_to_yuyv_cpu() {
3348        let file = include_bytes!("../../../testdata/zidane.nv12").to_vec();
3349        let src = TensorImage::new(1280, 720, NV12, None).unwrap();
3350        src.tensor().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)].copy_from_slice(&file);
3351
3352        let mut dst = TensorImage::new(1280, 720, YUYV, None).unwrap();
3353        let mut cpu_converter = CPUProcessor::new();
3354
3355        cpu_converter
3356            .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::no_crop())
3357            .unwrap();
3358
3359        let target_image = TensorImage::load_jpeg(
3360            include_bytes!("../../../testdata/zidane.jpg"),
3361            Some(RGB),
3362            None,
3363        )
3364        .unwrap();
3365
3366        compare_images_convert_to_rgb(&dst, &target_image, 0.98, function!());
3367    }
3368
3369    #[test]
3370    fn test_cpu_resize_planar_rgb() {
3371        let src = TensorImage::new(4, 4, RGBA, None).unwrap();
3372        #[rustfmt::skip]
3373        let src_image = [
3374                    255, 0, 0, 255,     0, 255, 0, 255,     0, 0, 255, 255,     255, 255, 0, 255,
3375                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3376                    0, 0, 255, 0,       0, 255, 255, 255,   255, 255, 0, 0,     0, 0, 0, 255,
3377                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3378        ];
3379        src.tensor()
3380            .map()
3381            .unwrap()
3382            .as_mut_slice()
3383            .copy_from_slice(&src_image);
3384
3385        let mut cpu_dst = TensorImage::new(5, 5, PLANAR_RGB, None).unwrap();
3386        let mut cpu_converter = CPUProcessor::new();
3387
3388        cpu_converter
3389            .convert(
3390                &src,
3391                &mut cpu_dst,
3392                Rotation::None,
3393                Flip::None,
3394                Crop::new()
3395                    .with_dst_rect(Some(Rect {
3396                        left: 1,
3397                        top: 1,
3398                        width: 4,
3399                        height: 4,
3400                    }))
3401                    .with_dst_color(Some([114, 114, 114, 255])),
3402            )
3403            .unwrap();
3404
3405        #[rustfmt::skip]
3406        let expected_dst = [
3407            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,
3408            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,
3409            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,
3410        ];
3411
3412        assert_eq!(cpu_dst.tensor().map().unwrap().as_slice(), &expected_dst);
3413    }
3414
3415    #[test]
3416    fn test_cpu_resize_planar_rgba() {
3417        let src = TensorImage::new(4, 4, RGBA, None).unwrap();
3418        #[rustfmt::skip]
3419        let src_image = [
3420                    255, 0, 0, 255,     0, 255, 0, 255,     0, 0, 255, 255,     255, 255, 0, 255,
3421                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3422                    0, 0, 255, 0,       0, 255, 255, 255,   255, 255, 0, 0,     0, 0, 0, 255,
3423                    255, 0, 0, 0,       0, 0, 0, 255,       255,  0, 255, 0,    255, 0, 255, 255,
3424        ];
3425        src.tensor()
3426            .map()
3427            .unwrap()
3428            .as_mut_slice()
3429            .copy_from_slice(&src_image);
3430
3431        let mut cpu_dst = TensorImage::new(5, 5, PLANAR_RGBA, None).unwrap();
3432        let mut cpu_converter = CPUProcessor::new();
3433
3434        cpu_converter
3435            .convert(
3436                &src,
3437                &mut cpu_dst,
3438                Rotation::None,
3439                Flip::None,
3440                Crop::new()
3441                    .with_dst_rect(Some(Rect {
3442                        left: 1,
3443                        top: 1,
3444                        width: 4,
3445                        height: 4,
3446                    }))
3447                    .with_dst_color(Some([114, 114, 114, 255])),
3448            )
3449            .unwrap();
3450
3451        #[rustfmt::skip]
3452        let expected_dst = [
3453            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,
3454            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,
3455            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,
3456            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,
3457        ];
3458
3459        assert_eq!(cpu_dst.tensor().map().unwrap().as_slice(), &expected_dst);
3460    }
3461
3462    #[test]
3463    #[cfg(target_os = "linux")]
3464    #[cfg(feature = "opengl")]
3465    fn test_opengl_resize_planar_rgb() {
3466        if !is_opengl_available() {
3467            eprintln!("SKIPPED: {} - OpenGL not available", function!());
3468            return;
3469        }
3470
3471        if !is_dma_available() {
3472            eprintln!(
3473                "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3474                function!()
3475            );
3476            return;
3477        }
3478
3479        let dst_width = 640;
3480        let dst_height = 640;
3481        let file = include_bytes!("../../../testdata/test_image.jpg").to_vec();
3482        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
3483
3484        let mut cpu_dst = TensorImage::new(dst_width, dst_height, PLANAR_RGB, None).unwrap();
3485        let mut cpu_converter = CPUProcessor::new();
3486        cpu_converter
3487            .convert(
3488                &src,
3489                &mut cpu_dst,
3490                Rotation::None,
3491                Flip::None,
3492                Crop::no_crop(),
3493            )
3494            .unwrap();
3495        cpu_converter
3496            .convert(
3497                &src,
3498                &mut cpu_dst,
3499                Rotation::None,
3500                Flip::None,
3501                Crop::new()
3502                    .with_dst_rect(Some(Rect {
3503                        left: 102,
3504                        top: 102,
3505                        width: 440,
3506                        height: 440,
3507                    }))
3508                    .with_dst_color(Some([114, 114, 114, 114])),
3509            )
3510            .unwrap();
3511
3512        let mut gl_dst = TensorImage::new(dst_width, dst_height, PLANAR_RGB, None).unwrap();
3513        let mut gl_converter = GLProcessorThreaded::new().unwrap();
3514
3515        gl_converter
3516            .convert(
3517                &src,
3518                &mut gl_dst,
3519                Rotation::None,
3520                Flip::None,
3521                Crop::new()
3522                    .with_dst_rect(Some(Rect {
3523                        left: 102,
3524                        top: 102,
3525                        width: 440,
3526                        height: 440,
3527                    }))
3528                    .with_dst_color(Some([114, 114, 114, 114])),
3529            )
3530            .unwrap();
3531        compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3532    }
3533
3534    #[test]
3535    fn test_cpu_resize_nv16() {
3536        let file = include_bytes!("../../../testdata/zidane.jpg").to_vec();
3537        let src = TensorImage::load_jpeg(&file, Some(RGBA), None).unwrap();
3538
3539        let mut cpu_nv16_dst = TensorImage::new(640, 640, NV16, None).unwrap();
3540        let mut cpu_rgb_dst = TensorImage::new(640, 640, RGB, None).unwrap();
3541        let mut cpu_converter = CPUProcessor::new();
3542
3543        cpu_converter
3544            .convert(
3545                &src,
3546                &mut cpu_nv16_dst,
3547                Rotation::None,
3548                Flip::None,
3549                // Crop::no_crop(),
3550                Crop::new()
3551                    .with_dst_rect(Some(Rect {
3552                        left: 20,
3553                        top: 140,
3554                        width: 600,
3555                        height: 360,
3556                    }))
3557                    .with_dst_color(Some([255, 128, 0, 255])),
3558            )
3559            .unwrap();
3560
3561        cpu_converter
3562            .convert(
3563                &src,
3564                &mut cpu_rgb_dst,
3565                Rotation::None,
3566                Flip::None,
3567                Crop::new()
3568                    .with_dst_rect(Some(Rect {
3569                        left: 20,
3570                        top: 140,
3571                        width: 600,
3572                        height: 360,
3573                    }))
3574                    .with_dst_color(Some([255, 128, 0, 255])),
3575            )
3576            .unwrap();
3577        compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
3578    }
3579
3580    fn load_bytes_to_tensor(
3581        width: usize,
3582        height: usize,
3583        fourcc: FourCharCode,
3584        memory: Option<TensorMemory>,
3585        bytes: &[u8],
3586    ) -> Result<TensorImage, Error> {
3587        let src = TensorImage::new(width, height, fourcc, memory)?;
3588        src.tensor().map()?.as_mut_slice().copy_from_slice(bytes);
3589        Ok(src)
3590    }
3591
3592    fn compare_images(img1: &TensorImage, img2: &TensorImage, threshold: f64, name: &str) {
3593        assert_eq!(img1.height(), img2.height(), "Heights differ");
3594        assert_eq!(img1.width(), img2.width(), "Widths differ");
3595        assert_eq!(img1.fourcc(), img2.fourcc(), "FourCC differ");
3596        assert!(
3597            matches!(img1.fourcc(), RGB | RGBA | GREY | PLANAR_RGB),
3598            "FourCC must be RGB or RGBA for comparison"
3599        );
3600
3601        let image1 = match img1.fourcc() {
3602            RGB => image::RgbImage::from_vec(
3603                img1.width() as u32,
3604                img1.height() as u32,
3605                img1.tensor().map().unwrap().to_vec(),
3606            )
3607            .unwrap(),
3608            RGBA => image::RgbaImage::from_vec(
3609                img1.width() as u32,
3610                img1.height() as u32,
3611                img1.tensor().map().unwrap().to_vec(),
3612            )
3613            .unwrap()
3614            .convert(),
3615            GREY => image::GrayImage::from_vec(
3616                img1.width() as u32,
3617                img1.height() as u32,
3618                img1.tensor().map().unwrap().to_vec(),
3619            )
3620            .unwrap()
3621            .convert(),
3622            PLANAR_RGB => image::GrayImage::from_vec(
3623                img1.width() as u32,
3624                (img1.height() * 3) as u32,
3625                img1.tensor().map().unwrap().to_vec(),
3626            )
3627            .unwrap()
3628            .convert(),
3629            _ => return,
3630        };
3631
3632        let image2 = match img2.fourcc() {
3633            RGB => image::RgbImage::from_vec(
3634                img2.width() as u32,
3635                img2.height() as u32,
3636                img2.tensor().map().unwrap().to_vec(),
3637            )
3638            .unwrap(),
3639            RGBA => image::RgbaImage::from_vec(
3640                img2.width() as u32,
3641                img2.height() as u32,
3642                img2.tensor().map().unwrap().to_vec(),
3643            )
3644            .unwrap()
3645            .convert(),
3646            GREY => image::GrayImage::from_vec(
3647                img2.width() as u32,
3648                img2.height() as u32,
3649                img2.tensor().map().unwrap().to_vec(),
3650            )
3651            .unwrap()
3652            .convert(),
3653            PLANAR_RGB => image::GrayImage::from_vec(
3654                img2.width() as u32,
3655                (img2.height() * 3) as u32,
3656                img2.tensor().map().unwrap().to_vec(),
3657            )
3658            .unwrap()
3659            .convert(),
3660            _ => return,
3661        };
3662
3663        let similarity = image_compare::rgb_similarity_structure(
3664            &image_compare::Algorithm::RootMeanSquared,
3665            &image1,
3666            &image2,
3667        )
3668        .expect("Image Comparison failed");
3669        if similarity.score < threshold {
3670            // image1.save(format!("{name}_1.png"));
3671            // image2.save(format!("{name}_2.png"));
3672            similarity
3673                .image
3674                .to_color_map()
3675                .save(format!("{name}.png"))
3676                .unwrap();
3677            panic!(
3678                "{name}: converted image and target image have similarity score too low: {} < {}",
3679                similarity.score, threshold
3680            )
3681        }
3682    }
3683
3684    fn compare_images_convert_to_rgb(
3685        img1: &TensorImage,
3686        img2: &TensorImage,
3687        threshold: f64,
3688        name: &str,
3689    ) {
3690        assert_eq!(img1.height(), img2.height(), "Heights differ");
3691        assert_eq!(img1.width(), img2.width(), "Widths differ");
3692
3693        let mut img_rgb1 =
3694            TensorImage::new(img1.width(), img1.height(), RGB, Some(TensorMemory::Mem)).unwrap();
3695        let mut img_rgb2 =
3696            TensorImage::new(img1.width(), img1.height(), RGB, Some(TensorMemory::Mem)).unwrap();
3697        CPUProcessor::convert_format(img1, &mut img_rgb1).unwrap();
3698        CPUProcessor::convert_format(img2, &mut img_rgb2).unwrap();
3699
3700        let image1 = image::RgbImage::from_vec(
3701            img_rgb1.width() as u32,
3702            img_rgb1.height() as u32,
3703            img_rgb1.tensor().map().unwrap().to_vec(),
3704        )
3705        .unwrap();
3706
3707        let image2 = image::RgbImage::from_vec(
3708            img_rgb2.width() as u32,
3709            img_rgb2.height() as u32,
3710            img_rgb2.tensor().map().unwrap().to_vec(),
3711        )
3712        .unwrap();
3713
3714        let similarity = image_compare::rgb_similarity_structure(
3715            &image_compare::Algorithm::RootMeanSquared,
3716            &image1,
3717            &image2,
3718        )
3719        .expect("Image Comparison failed");
3720        if similarity.score < threshold {
3721            // image1.save(format!("{name}_1.png"));
3722            // image2.save(format!("{name}_2.png"));
3723            similarity
3724                .image
3725                .to_color_map()
3726                .save(format!("{name}.png"))
3727                .unwrap();
3728            panic!(
3729                "{name}: converted image and target image have similarity score too low: {} < {}",
3730                similarity.score, threshold
3731            )
3732        }
3733    }
3734
3735    // =========================================================================
3736    // NV12 Format Tests
3737    // =========================================================================
3738
3739    #[test]
3740    fn test_nv12_tensor_image_creation() {
3741        let width = 640;
3742        let height = 480;
3743        let img = TensorImage::new(width, height, NV12, None).unwrap();
3744
3745        assert_eq!(img.width(), width);
3746        assert_eq!(img.height(), height);
3747        assert_eq!(img.fourcc(), NV12);
3748        // NV12 uses shape [H*3/2, W] to store Y plane + UV plane
3749        assert_eq!(img.tensor().shape(), &[height * 3 / 2, width]);
3750    }
3751
3752    #[test]
3753    fn test_nv12_channels() {
3754        let img = TensorImage::new(640, 480, NV12, None).unwrap();
3755        // NV12 reports 2 channels (Y + interleaved UV)
3756        assert_eq!(img.channels(), 2);
3757    }
3758
3759    // =========================================================================
3760    // TensorImageRef Tests
3761    // =========================================================================
3762
3763    #[test]
3764    fn test_tensor_image_ref_from_planar_tensor() {
3765        // Create a planar RGB tensor [3, 480, 640]
3766        let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
3767
3768        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3769
3770        assert_eq!(img_ref.width(), 640);
3771        assert_eq!(img_ref.height(), 480);
3772        assert_eq!(img_ref.channels(), 3);
3773        assert_eq!(img_ref.fourcc(), PLANAR_RGB);
3774        assert!(img_ref.is_planar());
3775    }
3776
3777    #[test]
3778    fn test_tensor_image_ref_from_interleaved_tensor() {
3779        // Create an interleaved RGBA tensor [480, 640, 4]
3780        let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
3781
3782        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, RGBA).unwrap();
3783
3784        assert_eq!(img_ref.width(), 640);
3785        assert_eq!(img_ref.height(), 480);
3786        assert_eq!(img_ref.channels(), 4);
3787        assert_eq!(img_ref.fourcc(), RGBA);
3788        assert!(!img_ref.is_planar());
3789    }
3790
3791    #[test]
3792    fn test_tensor_image_ref_invalid_shape() {
3793        // 2D tensor should fail
3794        let mut tensor = Tensor::<u8>::new(&[480, 640], None, None).unwrap();
3795        let result = TensorImageRef::from_borrowed_tensor(&mut tensor, RGB);
3796        assert!(matches!(result, Err(Error::InvalidShape(_))));
3797    }
3798
3799    #[test]
3800    fn test_tensor_image_ref_wrong_channels() {
3801        // RGBA expects 4 channels but tensor has 3
3802        let mut tensor = Tensor::<u8>::new(&[480, 640, 3], None, None).unwrap();
3803        let result = TensorImageRef::from_borrowed_tensor(&mut tensor, RGBA);
3804        assert!(matches!(result, Err(Error::InvalidShape(_))));
3805    }
3806
3807    #[test]
3808    fn test_tensor_image_dst_trait_tensor_image() {
3809        let img = TensorImage::new(640, 480, RGB, None).unwrap();
3810
3811        // Test TensorImageDst trait implementation
3812        fn check_dst<T: TensorImageDst>(dst: &T) {
3813            assert_eq!(dst.width(), 640);
3814            assert_eq!(dst.height(), 480);
3815            assert_eq!(dst.channels(), 3);
3816            assert!(!dst.is_planar());
3817        }
3818
3819        check_dst(&img);
3820    }
3821
3822    #[test]
3823    fn test_tensor_image_dst_trait_tensor_image_ref() {
3824        let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
3825        let img_ref = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3826
3827        fn check_dst<T: TensorImageDst>(dst: &T) {
3828            assert_eq!(dst.width(), 640);
3829            assert_eq!(dst.height(), 480);
3830            assert_eq!(dst.channels(), 3);
3831            assert!(dst.is_planar());
3832        }
3833
3834        check_dst(&img_ref);
3835    }
3836}