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