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