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