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