1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
63
64use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
65use edgefirst_tensor::{
66 DType, PixelFormat, PixelLayout, Tensor, TensorDyn, TensorMemory, TensorTrait as _,
67};
68use enum_dispatch::enum_dispatch;
69use std::{fmt::Display, time::Instant};
70use zune_jpeg::{
71 zune_core::{colorspace::ColorSpace, options::DecoderOptions},
72 JpegDecoder,
73};
74use zune_png::PngDecoder;
75
76pub use cpu::CPUProcessor;
77pub use error::{Error, Result};
78#[cfg(target_os = "linux")]
79pub use g2d::G2DProcessor;
80#[cfg(target_os = "linux")]
81#[cfg(feature = "opengl")]
82pub use opengl_headless::GLProcessorThreaded;
83#[cfg(target_os = "linux")]
84#[cfg(feature = "opengl")]
85pub use opengl_headless::Int8InterpolationMode;
86#[cfg(target_os = "linux")]
87#[cfg(feature = "opengl")]
88pub use opengl_headless::{probe_egl_displays, EglDisplayInfo, EglDisplayKind};
89
90mod cpu;
91mod error;
92mod g2d;
93#[path = "gl/mod.rs"]
94mod opengl_headless;
95
96fn rotate_flip_to_dyn(
101 src: &Tensor<u8>,
102 src_fmt: PixelFormat,
103 rotation: Rotation,
104 flip: Flip,
105 memory: Option<TensorMemory>,
106) -> Result<TensorDyn, Error> {
107 let src_w = src.width().unwrap();
108 let src_h = src.height().unwrap();
109 let channels = src_fmt.channels();
110
111 let (dst_w, dst_h) = match rotation {
112 Rotation::None | Rotation::Rotate180 => (src_w, src_h),
113 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (src_h, src_w),
114 };
115
116 let dst = Tensor::<u8>::image(dst_w, dst_h, src_fmt, memory)?;
117 let src_map = src.map()?;
118 let mut dst_map = dst.map()?;
119
120 CPUProcessor::flip_rotate_ndarray_pf(
121 &src_map,
122 &mut dst_map,
123 dst_w,
124 dst_h,
125 channels,
126 rotation,
127 flip,
128 )?;
129 drop(dst_map);
130 drop(src_map);
131
132 Ok(TensorDyn::from(dst))
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Rotation {
137 None = 0,
138 Clockwise90 = 1,
139 Rotate180 = 2,
140 CounterClockwise90 = 3,
141}
142impl Rotation {
143 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
156 match angle.rem_euclid(360) {
157 0 => Rotation::None,
158 90 => Rotation::Clockwise90,
159 180 => Rotation::Rotate180,
160 270 => Rotation::CounterClockwise90,
161 _ => panic!("rotation angle is not a multiple of 90"),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum Flip {
168 None = 0,
169 Vertical = 1,
170 Horizontal = 2,
171}
172
173#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
175pub enum ColorMode {
176 #[default]
181 Class,
182 Instance,
187 Track,
190}
191
192impl ColorMode {
193 #[inline]
195 pub fn index(self, idx: usize, label: usize) -> usize {
196 match self {
197 ColorMode::Class => label,
198 ColorMode::Instance | ColorMode::Track => idx,
199 }
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
215pub struct MaskOverlay<'a> {
216 pub background: Option<&'a TensorDyn>,
217 pub opacity: f32,
218 pub letterbox: Option<[f32; 4]>,
228 pub color_mode: ColorMode,
229}
230
231impl Default for MaskOverlay<'_> {
232 fn default() -> Self {
233 Self {
234 background: None,
235 opacity: 1.0,
236 letterbox: None,
237 color_mode: ColorMode::Class,
238 }
239 }
240}
241
242impl<'a> MaskOverlay<'a> {
243 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
248 self.background = Some(bg);
249 self
250 }
251
252 pub fn with_opacity(mut self, opacity: f32) -> Self {
253 self.opacity = opacity.clamp(0.0, 1.0);
254 self
255 }
256
257 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
258 self.color_mode = mode;
259 self
260 }
261
262 pub fn with_letterbox_crop(mut self, crop: &Crop, model_w: usize, model_h: usize) -> Self {
272 if let Some(r) = crop.dst_rect {
273 self.letterbox = Some([
274 r.left as f32 / model_w as f32,
275 r.top as f32 / model_h as f32,
276 (r.left + r.width) as f32 / model_w as f32,
277 (r.top + r.height) as f32 / model_h as f32,
278 ]);
279 }
280 self
281 }
282
283 fn apply_background(&self, dst: &mut TensorDyn) -> Result<MaskOverlay<'static>> {
286 use edgefirst_tensor::TensorMapTrait;
287 if let Some(bg) = self.background {
288 if bg.shape() != dst.shape() {
289 return Err(Error::InvalidShape(
290 "background shape does not match dst".into(),
291 ));
292 }
293 if bg.format() != dst.format() {
294 return Err(Error::InvalidShape(
295 "background pixel format does not match dst".into(),
296 ));
297 }
298 let bg_u8 = bg.as_u8().ok_or(Error::NotAnImage)?;
299 let dst_u8 = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
300 let bg_map = bg_u8.map()?;
301 let mut dst_map = dst_u8.map()?;
302 let bg_slice = bg_map.as_slice();
303 let dst_slice = dst_map.as_mut_slice();
304 if bg_slice.len() != dst_slice.len() {
305 return Err(Error::InvalidShape(
306 "background buffer size does not match dst".into(),
307 ));
308 }
309 dst_slice.copy_from_slice(bg_slice);
310 }
311 Ok(MaskOverlay {
312 background: None,
313 opacity: self.opacity.clamp(0.0, 1.0),
314 letterbox: self.letterbox,
315 color_mode: self.color_mode,
316 })
317 }
318}
319
320#[inline]
329fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
330 let b = bbox.bbox.to_canonical();
331 let [lx0, ly0, lx1, ly1] = lb;
332 let inv_w = if lx1 > lx0 { 1.0 / (lx1 - lx0) } else { 1.0 };
333 let inv_h = if ly1 > ly0 { 1.0 / (ly1 - ly0) } else { 1.0 };
334 DetectBox {
335 bbox: edgefirst_decoder::BoundingBox {
336 xmin: ((b.xmin - lx0) * inv_w).clamp(0.0, 1.0),
337 ymin: ((b.ymin - ly0) * inv_h).clamp(0.0, 1.0),
338 xmax: ((b.xmax - lx0) * inv_w).clamp(0.0, 1.0),
339 ymax: ((b.ymax - ly0) * inv_h).clamp(0.0, 1.0),
340 },
341 ..bbox
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub struct Crop {
347 pub src_rect: Option<Rect>,
348 pub dst_rect: Option<Rect>,
349 pub dst_color: Option<[u8; 4]>,
350}
351
352impl Default for Crop {
353 fn default() -> Self {
354 Crop::new()
355 }
356}
357impl Crop {
358 pub fn new() -> Self {
360 Crop {
361 src_rect: None,
362 dst_rect: None,
363 dst_color: None,
364 }
365 }
366
367 pub fn with_src_rect(mut self, src_rect: Option<Rect>) -> Self {
369 self.src_rect = src_rect;
370 self
371 }
372
373 pub fn with_dst_rect(mut self, dst_rect: Option<Rect>) -> Self {
375 self.dst_rect = dst_rect;
376 self
377 }
378
379 pub fn with_dst_color(mut self, dst_color: Option<[u8; 4]>) -> Self {
381 self.dst_color = dst_color;
382 self
383 }
384
385 pub fn no_crop() -> Self {
387 Crop::new()
388 }
389
390 pub(crate) fn check_crop_dims(
392 &self,
393 src_w: usize,
394 src_h: usize,
395 dst_w: usize,
396 dst_h: usize,
397 ) -> Result<(), Error> {
398 let src_ok = self
399 .src_rect
400 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
401 let dst_ok = self
402 .dst_rect
403 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
404 match (src_ok, dst_ok) {
405 (true, true) => Ok(()),
406 (true, false) => Err(Error::CropInvalid(format!(
407 "Dest crop invalid: {:?}",
408 self.dst_rect
409 ))),
410 (false, true) => Err(Error::CropInvalid(format!(
411 "Src crop invalid: {:?}",
412 self.src_rect
413 ))),
414 (false, false) => Err(Error::CropInvalid(format!(
415 "Dest and Src crop invalid: {:?} {:?}",
416 self.dst_rect, self.src_rect
417 ))),
418 }
419 }
420
421 pub fn check_crop_dyn(
423 &self,
424 src: &edgefirst_tensor::TensorDyn,
425 dst: &edgefirst_tensor::TensorDyn,
426 ) -> Result<(), Error> {
427 self.check_crop_dims(
428 src.width().unwrap_or(0),
429 src.height().unwrap_or(0),
430 dst.width().unwrap_or(0),
431 dst.height().unwrap_or(0),
432 )
433 }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct Rect {
438 pub left: usize,
439 pub top: usize,
440 pub width: usize,
441 pub height: usize,
442}
443
444impl Rect {
445 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
447 Self {
448 left,
449 top,
450 width,
451 height,
452 }
453 }
454
455 pub fn check_rect_dyn(&self, image: &TensorDyn) -> bool {
457 let w = image.width().unwrap_or(0);
458 let h = image.height().unwrap_or(0);
459 self.left + self.width <= w && self.top + self.height <= h
460 }
461}
462
463#[enum_dispatch(ImageProcessor)]
464pub trait ImageProcessorTrait {
465 fn convert(
481 &mut self,
482 src: &TensorDyn,
483 dst: &mut TensorDyn,
484 rotation: Rotation,
485 flip: Flip,
486 crop: Crop,
487 ) -> Result<()>;
488
489 fn draw_decoded_masks(
509 &mut self,
510 dst: &mut TensorDyn,
511 detect: &[DetectBox],
512 segmentation: &[Segmentation],
513 overlay: MaskOverlay<'_>,
514 ) -> Result<()>;
515
516 fn draw_proto_masks(
533 &mut self,
534 dst: &mut TensorDyn,
535 detect: &[DetectBox],
536 proto_data: &ProtoData,
537 overlay: MaskOverlay<'_>,
538 ) -> Result<()>;
539
540 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
543}
544
545#[derive(Debug, Clone, Default)]
551pub struct ImageProcessorConfig {
552 #[cfg(target_os = "linux")]
560 #[cfg(feature = "opengl")]
561 pub egl_display: Option<EglDisplayKind>,
562
563 pub backend: ComputeBackend,
575}
576
577#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
584pub enum ComputeBackend {
585 #[default]
587 Auto,
588 Cpu,
590 G2d,
592 OpenGl,
594}
595
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
602pub(crate) enum ForcedBackend {
603 Cpu,
604 G2d,
605 OpenGl,
606}
607
608#[derive(Debug)]
611pub struct ImageProcessor {
612 pub cpu: Option<CPUProcessor>,
615
616 #[cfg(target_os = "linux")]
617 pub g2d: Option<G2DProcessor>,
621 #[cfg(target_os = "linux")]
622 #[cfg(feature = "opengl")]
623 pub opengl: Option<GLProcessorThreaded>,
627
628 pub(crate) forced_backend: Option<ForcedBackend>,
630}
631
632unsafe impl Send for ImageProcessor {}
633unsafe impl Sync for ImageProcessor {}
634
635impl ImageProcessor {
636 pub fn new() -> Result<Self> {
654 Self::with_config(ImageProcessorConfig::default())
655 }
656
657 #[allow(unused_variables)]
666 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
667 match config.backend {
671 ComputeBackend::Cpu => {
672 log::info!("ComputeBackend::Cpu — CPU only");
673 return Ok(Self {
674 cpu: Some(CPUProcessor::new()),
675 #[cfg(target_os = "linux")]
676 g2d: None,
677 #[cfg(target_os = "linux")]
678 #[cfg(feature = "opengl")]
679 opengl: None,
680 forced_backend: None,
681 });
682 }
683 ComputeBackend::G2d => {
684 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
685 #[cfg(target_os = "linux")]
686 {
687 let g2d = match G2DProcessor::new() {
688 Ok(g) => Some(g),
689 Err(e) => {
690 log::warn!("G2D requested but failed to initialize: {e:?}");
691 None
692 }
693 };
694 return Ok(Self {
695 cpu: Some(CPUProcessor::new()),
696 g2d,
697 #[cfg(feature = "opengl")]
698 opengl: None,
699 forced_backend: None,
700 });
701 }
702 #[cfg(not(target_os = "linux"))]
703 {
704 log::warn!("G2D requested but not available on this platform, using CPU");
705 return Ok(Self {
706 cpu: Some(CPUProcessor::new()),
707 forced_backend: None,
708 });
709 }
710 }
711 ComputeBackend::OpenGl => {
712 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
713 #[cfg(target_os = "linux")]
714 {
715 #[cfg(feature = "opengl")]
716 let opengl = match GLProcessorThreaded::new(config.egl_display) {
717 Ok(gl) => Some(gl),
718 Err(e) => {
719 log::warn!("OpenGL requested but failed to initialize: {e:?}");
720 None
721 }
722 };
723 return Ok(Self {
724 cpu: Some(CPUProcessor::new()),
725 g2d: None,
726 #[cfg(feature = "opengl")]
727 opengl,
728 forced_backend: None,
729 });
730 }
731 #[cfg(not(target_os = "linux"))]
732 {
733 log::warn!("OpenGL requested but not available on this platform, using CPU");
734 return Ok(Self {
735 cpu: Some(CPUProcessor::new()),
736 forced_backend: None,
737 });
738 }
739 }
740 ComputeBackend::Auto => { }
741 }
742
743 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
748 let val_lower = val.to_lowercase();
749 let forced = match val_lower.as_str() {
750 "cpu" => ForcedBackend::Cpu,
751 "g2d" => ForcedBackend::G2d,
752 "opengl" => ForcedBackend::OpenGl,
753 other => {
754 return Err(Error::ForcedBackendUnavailable(format!(
755 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
756 )));
757 }
758 };
759
760 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
761
762 return match forced {
763 ForcedBackend::Cpu => Ok(Self {
764 cpu: Some(CPUProcessor::new()),
765 #[cfg(target_os = "linux")]
766 g2d: None,
767 #[cfg(target_os = "linux")]
768 #[cfg(feature = "opengl")]
769 opengl: None,
770 forced_backend: Some(ForcedBackend::Cpu),
771 }),
772 ForcedBackend::G2d => {
773 #[cfg(target_os = "linux")]
774 {
775 let g2d = G2DProcessor::new().map_err(|e| {
776 Error::ForcedBackendUnavailable(format!(
777 "g2d forced but failed to initialize: {e:?}"
778 ))
779 })?;
780 Ok(Self {
781 cpu: None,
782 g2d: Some(g2d),
783 #[cfg(feature = "opengl")]
784 opengl: None,
785 forced_backend: Some(ForcedBackend::G2d),
786 })
787 }
788 #[cfg(not(target_os = "linux"))]
789 {
790 Err(Error::ForcedBackendUnavailable(
791 "g2d backend is only available on Linux".into(),
792 ))
793 }
794 }
795 ForcedBackend::OpenGl => {
796 #[cfg(target_os = "linux")]
797 #[cfg(feature = "opengl")]
798 {
799 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
800 Error::ForcedBackendUnavailable(format!(
801 "opengl forced but failed to initialize: {e:?}"
802 ))
803 })?;
804 Ok(Self {
805 cpu: None,
806 g2d: None,
807 opengl: Some(opengl),
808 forced_backend: Some(ForcedBackend::OpenGl),
809 })
810 }
811 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
812 {
813 Err(Error::ForcedBackendUnavailable(
814 "opengl backend requires Linux with the 'opengl' feature enabled"
815 .into(),
816 ))
817 }
818 }
819 };
820 }
821
822 #[cfg(target_os = "linux")]
824 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
825 .map(|x| x != "0" && x.to_lowercase() != "false")
826 .unwrap_or(false)
827 {
828 log::debug!("EDGEFIRST_DISABLE_G2D is set");
829 None
830 } else {
831 match G2DProcessor::new() {
832 Ok(g2d_converter) => Some(g2d_converter),
833 Err(err) => {
834 log::warn!("Failed to initialize G2D converter: {err:?}");
835 None
836 }
837 }
838 };
839
840 #[cfg(target_os = "linux")]
841 #[cfg(feature = "opengl")]
842 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
843 .map(|x| x != "0" && x.to_lowercase() != "false")
844 .unwrap_or(false)
845 {
846 log::debug!("EDGEFIRST_DISABLE_GL is set");
847 None
848 } else {
849 match GLProcessorThreaded::new(config.egl_display) {
850 Ok(gl_converter) => Some(gl_converter),
851 Err(err) => {
852 log::warn!("Failed to initialize GL converter: {err:?}");
853 None
854 }
855 }
856 };
857
858 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
859 .map(|x| x != "0" && x.to_lowercase() != "false")
860 .unwrap_or(false)
861 {
862 log::debug!("EDGEFIRST_DISABLE_CPU is set");
863 None
864 } else {
865 Some(CPUProcessor::new())
866 };
867 Ok(Self {
868 cpu,
869 #[cfg(target_os = "linux")]
870 g2d,
871 #[cfg(target_os = "linux")]
872 #[cfg(feature = "opengl")]
873 opengl,
874 forced_backend: None,
875 })
876 }
877
878 #[cfg(target_os = "linux")]
881 #[cfg(feature = "opengl")]
882 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
883 if let Some(ref mut gl) = self.opengl {
884 gl.set_int8_interpolation_mode(mode)?;
885 }
886 Ok(())
887 }
888
889 pub fn create_image(
922 &self,
923 width: usize,
924 height: usize,
925 format: PixelFormat,
926 dtype: DType,
927 memory: Option<TensorMemory>,
928 ) -> Result<TensorDyn> {
929 if let Some(mem) = memory {
931 return Ok(TensorDyn::image(width, height, format, dtype, Some(mem))?);
932 }
933
934 #[cfg(target_os = "linux")]
937 {
938 #[cfg(feature = "opengl")]
939 let gl_uses_pbo = self
940 .opengl
941 .as_ref()
942 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
943 #[cfg(not(feature = "opengl"))]
944 let gl_uses_pbo = false;
945
946 if !gl_uses_pbo {
947 if let Ok(img) = TensorDyn::image(
948 width,
949 height,
950 format,
951 dtype,
952 Some(edgefirst_tensor::TensorMemory::Dma),
953 ) {
954 return Ok(img);
955 }
956 }
957 }
958
959 #[cfg(target_os = "linux")]
963 #[cfg(feature = "opengl")]
964 if dtype.size() == 1 {
965 if let Some(gl) = &self.opengl {
966 match gl.create_pbo_image(width, height, format) {
967 Ok(t) => {
968 if dtype == DType::I8 {
969 debug_assert!(
977 t.chroma().is_none(),
978 "PBO i8 transmute requires chroma == None"
979 );
980 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
981 return Ok(TensorDyn::from(t_i8));
982 }
983 return Ok(TensorDyn::from(t));
984 }
985 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
986 }
987 }
988 }
989
990 Ok(TensorDyn::image(
992 width,
993 height,
994 format,
995 dtype,
996 Some(edgefirst_tensor::TensorMemory::Mem),
997 )?)
998 }
999
1000 #[cfg(target_os = "linux")]
1052 pub fn import_image(
1053 &self,
1054 image: edgefirst_tensor::PlaneDescriptor,
1055 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
1056 width: usize,
1057 height: usize,
1058 format: PixelFormat,
1059 dtype: DType,
1060 ) -> Result<TensorDyn> {
1061 use edgefirst_tensor::{Tensor, TensorMemory};
1062
1063 let image_stride = image.stride();
1065 let image_offset = image.offset();
1066 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
1067 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
1068
1069 if let Some(chroma_pd) = chroma {
1070 if dtype != DType::U8 && dtype != DType::I8 {
1075 return Err(Error::NotSupported(format!(
1076 "multiplane import only supports U8/I8, got {dtype:?}"
1077 )));
1078 }
1079 if format.layout() != PixelLayout::SemiPlanar {
1080 return Err(Error::NotSupported(format!(
1081 "import_image with chroma requires a semi-planar format, got {format:?}"
1082 )));
1083 }
1084
1085 let chroma_h = match format {
1086 PixelFormat::Nv12 => {
1087 if !height.is_multiple_of(2) {
1088 return Err(Error::InvalidShape(format!(
1089 "NV12 requires even height, got {height}"
1090 )));
1091 }
1092 height / 2
1093 }
1094 PixelFormat::Nv16 => {
1097 return Err(Error::NotSupported(
1098 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
1099 ))
1100 }
1101 _ => {
1102 return Err(Error::NotSupported(format!(
1103 "unsupported semi-planar format: {format:?}"
1104 )))
1105 }
1106 };
1107
1108 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
1109 if luma.memory() != TensorMemory::Dma {
1110 return Err(Error::NotSupported(format!(
1111 "luma fd must be DMA-backed, got {:?}",
1112 luma.memory()
1113 )));
1114 }
1115
1116 let chroma_tensor =
1117 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
1118 if chroma_tensor.memory() != TensorMemory::Dma {
1119 return Err(Error::NotSupported(format!(
1120 "chroma fd must be DMA-backed, got {:?}",
1121 chroma_tensor.memory()
1122 )));
1123 }
1124
1125 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
1128
1129 if let Some(s) = image_stride {
1131 tensor.set_row_stride(s)?;
1132 }
1133 if let Some(o) = image_offset {
1134 tensor.set_plane_offset(o);
1135 }
1136
1137 if let Some(chroma_ref) = tensor.chroma_mut() {
1142 if let Some(s) = chroma_stride {
1143 if s < width {
1144 return Err(Error::InvalidShape(format!(
1145 "chroma stride {s} < minimum {width} for {format:?}"
1146 )));
1147 }
1148 chroma_ref.set_row_stride_unchecked(s);
1149 }
1150 if let Some(o) = chroma_offset {
1151 chroma_ref.set_plane_offset(o);
1152 }
1153 }
1154
1155 if dtype == DType::I8 {
1156 const {
1160 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
1161 assert!(
1162 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
1163 );
1164 }
1165 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
1166 return Ok(TensorDyn::from(tensor_i8));
1167 }
1168 Ok(TensorDyn::from(tensor))
1169 } else {
1170 let shape = match format.layout() {
1172 PixelLayout::Packed => vec![height, width, format.channels()],
1173 PixelLayout::Planar => vec![format.channels(), height, width],
1174 PixelLayout::SemiPlanar => {
1175 let total_h = match format {
1176 PixelFormat::Nv12 => {
1177 if !height.is_multiple_of(2) {
1178 return Err(Error::InvalidShape(format!(
1179 "NV12 requires even height, got {height}"
1180 )));
1181 }
1182 height * 3 / 2
1183 }
1184 PixelFormat::Nv16 => height * 2,
1185 _ => {
1186 return Err(Error::InvalidShape(format!(
1187 "unknown semi-planar height multiplier for {format:?}"
1188 )))
1189 }
1190 };
1191 vec![total_h, width]
1192 }
1193 _ => {
1194 return Err(Error::NotSupported(format!(
1195 "unsupported pixel layout for import_image: {:?}",
1196 format.layout()
1197 )));
1198 }
1199 };
1200 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
1201 if tensor.memory() != TensorMemory::Dma {
1202 return Err(Error::NotSupported(format!(
1203 "import_image requires DMA-backed fd, got {:?}",
1204 tensor.memory()
1205 )));
1206 }
1207 let mut tensor = tensor.with_format(format)?;
1208 if let Some(s) = image_stride {
1209 tensor.set_row_stride(s)?;
1210 }
1211 if let Some(o) = image_offset {
1212 tensor.set_plane_offset(o);
1213 }
1214 Ok(tensor)
1215 }
1216 }
1217
1218 pub fn draw_masks(
1226 &mut self,
1227 decoder: &edgefirst_decoder::Decoder,
1228 outputs: &[&TensorDyn],
1229 dst: &mut TensorDyn,
1230 overlay: MaskOverlay<'_>,
1231 ) -> Result<Vec<DetectBox>> {
1232 let mut output_boxes = Vec::with_capacity(100);
1233
1234 let proto_result = decoder
1236 .decode_proto(outputs, &mut output_boxes)
1237 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
1238
1239 if let Some(proto_data) = proto_result {
1240 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
1241 } else {
1242 let mut output_masks = Vec::with_capacity(100);
1244 decoder
1245 .decode(outputs, &mut output_boxes, &mut output_masks)
1246 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
1247 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
1248 }
1249 Ok(output_boxes)
1250 }
1251
1252 #[cfg(feature = "tracker")]
1260 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
1261 &mut self,
1262 decoder: &edgefirst_decoder::Decoder,
1263 tracker: &mut TR,
1264 timestamp: u64,
1265 outputs: &[&TensorDyn],
1266 dst: &mut TensorDyn,
1267 overlay: MaskOverlay<'_>,
1268 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
1269 let mut output_boxes = Vec::with_capacity(100);
1270 let mut output_tracks = Vec::new();
1271
1272 let proto_result = decoder
1273 .decode_proto_tracked(
1274 tracker,
1275 timestamp,
1276 outputs,
1277 &mut output_boxes,
1278 &mut output_tracks,
1279 )
1280 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
1281
1282 if let Some(proto_data) = proto_result {
1283 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
1284 } else {
1285 let mut output_masks = Vec::with_capacity(100);
1289 decoder
1290 .decode_tracked(
1291 tracker,
1292 timestamp,
1293 outputs,
1294 &mut output_boxes,
1295 &mut output_masks,
1296 &mut output_tracks,
1297 )
1298 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
1299 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
1300 }
1301 Ok((output_boxes, output_tracks))
1302 }
1303
1304 pub fn materialize_masks(
1328 &self,
1329 detect: &[DetectBox],
1330 proto_data: &ProtoData,
1331 letterbox: Option<[f32; 4]>,
1332 ) -> Result<Vec<Segmentation>> {
1333 let cpu = self.cpu.as_ref().ok_or(Error::NoConverter)?;
1334 cpu.materialize_segmentations(detect, proto_data, letterbox)
1335 }
1336}
1337
1338impl ImageProcessorTrait for ImageProcessor {
1339 fn convert(
1345 &mut self,
1346 src: &TensorDyn,
1347 dst: &mut TensorDyn,
1348 rotation: Rotation,
1349 flip: Flip,
1350 crop: Crop,
1351 ) -> Result<()> {
1352 let start = Instant::now();
1353 let src_fmt = src.format();
1354 let dst_fmt = dst.format();
1355 log::trace!(
1356 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
1357 rotation={rotation:?}, flip={flip:?}, backend={:?}",
1358 src.dtype(),
1359 src.memory(),
1360 dst.dtype(),
1361 dst.memory(),
1362 self.forced_backend,
1363 );
1364
1365 if let Some(forced) = self.forced_backend {
1367 return match forced {
1368 ForcedBackend::Cpu => {
1369 if let Some(cpu) = self.cpu.as_mut() {
1370 let r = cpu.convert(src, dst, rotation, flip, crop);
1371 log::trace!(
1372 "convert: forced=cpu result={} ({:?})",
1373 if r.is_ok() { "ok" } else { "err" },
1374 start.elapsed()
1375 );
1376 return r;
1377 }
1378 Err(Error::ForcedBackendUnavailable("cpu".into()))
1379 }
1380 ForcedBackend::G2d => {
1381 #[cfg(target_os = "linux")]
1382 if let Some(g2d) = self.g2d.as_mut() {
1383 let r = g2d.convert(src, dst, rotation, flip, crop);
1384 log::trace!(
1385 "convert: forced=g2d result={} ({:?})",
1386 if r.is_ok() { "ok" } else { "err" },
1387 start.elapsed()
1388 );
1389 return r;
1390 }
1391 Err(Error::ForcedBackendUnavailable("g2d".into()))
1392 }
1393 ForcedBackend::OpenGl => {
1394 #[cfg(target_os = "linux")]
1395 #[cfg(feature = "opengl")]
1396 if let Some(opengl) = self.opengl.as_mut() {
1397 let r = opengl.convert(src, dst, rotation, flip, crop);
1398 log::trace!(
1399 "convert: forced=opengl result={} ({:?})",
1400 if r.is_ok() { "ok" } else { "err" },
1401 start.elapsed()
1402 );
1403 return r;
1404 }
1405 Err(Error::ForcedBackendUnavailable("opengl".into()))
1406 }
1407 };
1408 }
1409
1410 #[cfg(target_os = "linux")]
1412 #[cfg(feature = "opengl")]
1413 if let Some(opengl) = self.opengl.as_mut() {
1414 match opengl.convert(src, dst, rotation, flip, crop) {
1415 Ok(_) => {
1416 log::trace!(
1417 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
1418 start.elapsed()
1419 );
1420 return Ok(());
1421 }
1422 Err(e) => {
1423 log::trace!("convert: auto opengl declined {src_fmt:?}→{dst_fmt:?}: {e}");
1424 }
1425 }
1426 }
1427
1428 #[cfg(target_os = "linux")]
1429 if let Some(g2d) = self.g2d.as_mut() {
1430 match g2d.convert(src, dst, rotation, flip, crop) {
1431 Ok(_) => {
1432 log::trace!(
1433 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
1434 start.elapsed()
1435 );
1436 return Ok(());
1437 }
1438 Err(e) => {
1439 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
1440 }
1441 }
1442 }
1443
1444 if let Some(cpu) = self.cpu.as_mut() {
1445 match cpu.convert(src, dst, rotation, flip, crop) {
1446 Ok(_) => {
1447 log::trace!(
1448 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
1449 start.elapsed()
1450 );
1451 return Ok(());
1452 }
1453 Err(e) => {
1454 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
1455 return Err(e);
1456 }
1457 }
1458 }
1459 Err(Error::NoConverter)
1460 }
1461
1462 fn draw_decoded_masks(
1463 &mut self,
1464 dst: &mut TensorDyn,
1465 detect: &[DetectBox],
1466 segmentation: &[Segmentation],
1467 overlay: MaskOverlay<'_>,
1468 ) -> Result<()> {
1469 let start = Instant::now();
1470
1471 if detect.is_empty() && segmentation.is_empty() {
1472 return Ok(());
1473 }
1474
1475 let lb_boxes: Vec<DetectBox>;
1478 let lb_segs: Vec<Segmentation>;
1479 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
1480 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
1481 lb_segs = if segmentation.len() == lb_boxes.len() {
1484 segmentation
1485 .iter()
1486 .zip(lb_boxes.iter())
1487 .map(|(s, d)| Segmentation {
1488 xmin: d.bbox.xmin,
1489 ymin: d.bbox.ymin,
1490 xmax: d.bbox.xmax,
1491 ymax: d.bbox.ymax,
1492 segmentation: s.segmentation.clone(),
1493 })
1494 .collect()
1495 } else {
1496 segmentation.to_vec()
1497 };
1498 (lb_boxes.as_slice(), lb_segs.as_slice())
1499 } else {
1500 (detect, segmentation)
1501 };
1502
1503 if let Some(forced) = self.forced_backend {
1505 return match forced {
1506 ForcedBackend::Cpu => {
1507 let overlay = overlay.apply_background(dst)?;
1509 if let Some(cpu) = self.cpu.as_mut() {
1510 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
1511 }
1512 Err(Error::ForcedBackendUnavailable("cpu".into()))
1513 }
1514 ForcedBackend::G2d => Err(Error::NotSupported(
1515 "g2d does not support draw_decoded_masks".into(),
1516 )),
1517 ForcedBackend::OpenGl => {
1518 #[cfg(target_os = "linux")]
1520 #[cfg(feature = "opengl")]
1521 if let Some(opengl) = self.opengl.as_mut() {
1522 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
1523 }
1524 Err(Error::ForcedBackendUnavailable("opengl".into()))
1525 }
1526 };
1527 }
1528
1529 #[cfg(target_os = "linux")]
1533 #[cfg(feature = "opengl")]
1534 if let Some(opengl) = self.opengl.as_mut() {
1535 log::trace!(
1536 "draw_decoded_masks started with opengl in {:?}",
1537 start.elapsed()
1538 );
1539 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
1540 Ok(_) => {
1541 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
1542 return Ok(());
1543 }
1544 Err(e) => {
1545 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
1546 }
1547 }
1548 }
1549
1550 let overlay = overlay.apply_background(dst)?;
1552 log::trace!(
1553 "draw_decoded_masks started with cpu in {:?}",
1554 start.elapsed()
1555 );
1556 if let Some(cpu) = self.cpu.as_mut() {
1557 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
1558 Ok(_) => {
1559 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
1560 return Ok(());
1561 }
1562 Err(e) => {
1563 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
1564 return Err(e);
1565 }
1566 }
1567 }
1568 Err(Error::NoConverter)
1569 }
1570
1571 fn draw_proto_masks(
1572 &mut self,
1573 dst: &mut TensorDyn,
1574 detect: &[DetectBox],
1575 proto_data: &ProtoData,
1576 overlay: MaskOverlay<'_>,
1577 ) -> Result<()> {
1578 let start = Instant::now();
1579
1580 if detect.is_empty() {
1581 return Ok(());
1582 }
1583
1584 let lb_boxes: Vec<DetectBox>;
1590 let render_detect = if let Some(lb) = overlay.letterbox {
1591 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
1592 lb_boxes.as_slice()
1593 } else {
1594 detect
1595 };
1596
1597 if let Some(forced) = self.forced_backend {
1599 return match forced {
1600 ForcedBackend::Cpu => {
1601 let overlay = overlay.apply_background(dst)?;
1602 if let Some(cpu) = self.cpu.as_mut() {
1603 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
1604 }
1605 Err(Error::ForcedBackendUnavailable("cpu".into()))
1606 }
1607 ForcedBackend::G2d => Err(Error::NotSupported(
1608 "g2d does not support draw_proto_masks".into(),
1609 )),
1610 ForcedBackend::OpenGl => {
1611 #[cfg(target_os = "linux")]
1612 #[cfg(feature = "opengl")]
1613 if let Some(opengl) = self.opengl.as_mut() {
1614 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
1615 }
1616 Err(Error::ForcedBackendUnavailable("opengl".into()))
1617 }
1618 };
1619 }
1620
1621 #[cfg(target_os = "linux")]
1628 #[cfg(feature = "opengl")]
1629 if let Some(opengl) = self.opengl.as_mut() {
1630 let Some(cpu) = self.cpu.as_ref() else {
1631 return Err(Error::Internal(
1632 "draw_proto_masks requires CPU backend for hybrid path".into(),
1633 ));
1634 };
1635 log::trace!(
1636 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
1637 start.elapsed()
1638 );
1639 let segmentation =
1640 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?;
1641 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
1642 Ok(_) => {
1643 log::trace!(
1644 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
1645 start.elapsed()
1646 );
1647 return Ok(());
1648 }
1649 Err(e) => {
1650 log::trace!("draw_proto_masks hybrid path failed, falling back to cpu: {e:?}");
1651 }
1652 }
1653 }
1654
1655 let overlay = overlay.apply_background(dst)?;
1657 let Some(cpu) = self.cpu.as_mut() else {
1658 return Err(Error::Internal(
1659 "draw_proto_masks requires CPU backend for fallback path".into(),
1660 ));
1661 };
1662 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
1663 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
1664 }
1665
1666 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
1667 let start = Instant::now();
1668
1669 if let Some(forced) = self.forced_backend {
1671 return match forced {
1672 ForcedBackend::Cpu => {
1673 if let Some(cpu) = self.cpu.as_mut() {
1674 return cpu.set_class_colors(colors);
1675 }
1676 Err(Error::ForcedBackendUnavailable("cpu".into()))
1677 }
1678 ForcedBackend::G2d => Err(Error::NotSupported(
1679 "g2d does not support set_class_colors".into(),
1680 )),
1681 ForcedBackend::OpenGl => {
1682 #[cfg(target_os = "linux")]
1683 #[cfg(feature = "opengl")]
1684 if let Some(opengl) = self.opengl.as_mut() {
1685 return opengl.set_class_colors(colors);
1686 }
1687 Err(Error::ForcedBackendUnavailable("opengl".into()))
1688 }
1689 };
1690 }
1691
1692 #[cfg(target_os = "linux")]
1695 #[cfg(feature = "opengl")]
1696 if let Some(opengl) = self.opengl.as_mut() {
1697 log::trace!("image started with opengl in {:?}", start.elapsed());
1698 match opengl.set_class_colors(colors) {
1699 Ok(_) => {
1700 log::trace!("colors set with opengl in {:?}", start.elapsed());
1701 return Ok(());
1702 }
1703 Err(e) => {
1704 log::trace!("colors didn't set with opengl: {e:?}")
1705 }
1706 }
1707 }
1708 log::trace!("image started with cpu in {:?}", start.elapsed());
1709 if let Some(cpu) = self.cpu.as_mut() {
1710 match cpu.set_class_colors(colors) {
1711 Ok(_) => {
1712 log::trace!("colors set with cpu in {:?}", start.elapsed());
1713 return Ok(());
1714 }
1715 Err(e) => {
1716 log::trace!("colors didn't set with cpu: {e:?}");
1717 return Err(e);
1718 }
1719 }
1720 }
1721 Err(Error::NoConverter)
1722 }
1723}
1724
1725fn read_exif_orientation(exif_bytes: &[u8]) -> (Rotation, Flip) {
1731 let exifreader = exif::Reader::new();
1732 let Ok(exif_) = exifreader.read_raw(exif_bytes.to_vec()) else {
1733 return (Rotation::None, Flip::None);
1734 };
1735 let Some(orientation) = exif_.get_field(exif::Tag::Orientation, exif::In::PRIMARY) else {
1736 return (Rotation::None, Flip::None);
1737 };
1738 match orientation.value.get_uint(0) {
1739 Some(1) => (Rotation::None, Flip::None),
1740 Some(2) => (Rotation::None, Flip::Horizontal),
1741 Some(3) => (Rotation::Rotate180, Flip::None),
1742 Some(4) => (Rotation::Rotate180, Flip::Horizontal),
1743 Some(5) => (Rotation::Clockwise90, Flip::Horizontal),
1744 Some(6) => (Rotation::Clockwise90, Flip::None),
1745 Some(7) => (Rotation::CounterClockwise90, Flip::Horizontal),
1746 Some(8) => (Rotation::CounterClockwise90, Flip::None),
1747 Some(v) => {
1748 log::warn!("broken orientation EXIF value: {v}");
1749 (Rotation::None, Flip::None)
1750 }
1751 None => (Rotation::None, Flip::None),
1752 }
1753}
1754
1755fn pixelfmt_to_colorspace(fmt: PixelFormat) -> Option<ColorSpace> {
1758 match fmt {
1759 PixelFormat::Rgb => Some(ColorSpace::RGB),
1760 PixelFormat::Rgba => Some(ColorSpace::RGBA),
1761 PixelFormat::Grey => Some(ColorSpace::Luma),
1762 _ => None,
1763 }
1764}
1765
1766fn colorspace_to_pixelfmt(cs: ColorSpace) -> Option<PixelFormat> {
1768 match cs {
1769 ColorSpace::RGB => Some(PixelFormat::Rgb),
1770 ColorSpace::RGBA => Some(PixelFormat::Rgba),
1771 ColorSpace::Luma => Some(PixelFormat::Grey),
1772 _ => None,
1773 }
1774}
1775
1776fn load_jpeg(
1778 image: &[u8],
1779 format: Option<PixelFormat>,
1780 memory: Option<TensorMemory>,
1781) -> Result<TensorDyn> {
1782 let colour = match format {
1783 Some(f) => pixelfmt_to_colorspace(f)
1784 .ok_or_else(|| Error::NotSupported(format!("Unsupported image format {f:?}")))?,
1785 None => ColorSpace::RGB,
1786 };
1787 let options = DecoderOptions::default().jpeg_set_out_colorspace(colour);
1788 let mut decoder = JpegDecoder::new_with_options(image, options);
1789 decoder.decode_headers()?;
1790
1791 let image_info = decoder.info().ok_or(Error::Internal(
1792 "JPEG did not return decoded image info".to_string(),
1793 ))?;
1794
1795 let converted_cs = decoder
1796 .get_output_colorspace()
1797 .ok_or(Error::Internal("No output colorspace".to_string()))?;
1798
1799 let converted_fmt = colorspace_to_pixelfmt(converted_cs).ok_or(Error::NotSupported(
1800 "Unsupported JPEG decoder output".to_string(),
1801 ))?;
1802
1803 let dest_fmt = format.unwrap_or(converted_fmt);
1804
1805 let (rotation, flip) = decoder
1806 .exif()
1807 .map(|x| read_exif_orientation(x))
1808 .unwrap_or((Rotation::None, Flip::None));
1809
1810 let w = image_info.width as usize;
1811 let h = image_info.height as usize;
1812
1813 if (rotation, flip) == (Rotation::None, Flip::None) {
1814 let mut img = Tensor::<u8>::image(w, h, dest_fmt, memory)?;
1815
1816 if converted_fmt != dest_fmt {
1817 let tmp = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
1818 decoder.decode_into(&mut tmp.map()?)?;
1819 CPUProcessor::convert_format_pf(&tmp, &mut img, converted_fmt, dest_fmt)?;
1820 return Ok(TensorDyn::from(img));
1821 }
1822 decoder.decode_into(&mut img.map()?)?;
1823 return Ok(TensorDyn::from(img));
1824 }
1825
1826 let mut tmp = Tensor::<u8>::image(w, h, dest_fmt, Some(TensorMemory::Mem))?;
1827
1828 if converted_fmt != dest_fmt {
1829 let tmp2 = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
1830 decoder.decode_into(&mut tmp2.map()?)?;
1831 CPUProcessor::convert_format_pf(&tmp2, &mut tmp, converted_fmt, dest_fmt)?;
1832 } else {
1833 decoder.decode_into(&mut tmp.map()?)?;
1834 }
1835
1836 rotate_flip_to_dyn(&tmp, dest_fmt, rotation, flip, memory)
1837}
1838
1839fn load_png(
1841 image: &[u8],
1842 format: Option<PixelFormat>,
1843 memory: Option<TensorMemory>,
1844) -> Result<TensorDyn> {
1845 let fmt = format.unwrap_or(PixelFormat::Rgb);
1846 let alpha = match fmt {
1847 PixelFormat::Rgb => false,
1848 PixelFormat::Rgba => true,
1849 _ => {
1850 return Err(Error::NotImplemented(
1851 "Unsupported image format".to_string(),
1852 ));
1853 }
1854 };
1855
1856 let options = DecoderOptions::default()
1857 .png_set_add_alpha_channel(alpha)
1858 .png_set_decode_animated(false);
1859 let mut decoder = PngDecoder::new_with_options(image, options);
1860 decoder.decode_headers()?;
1861 let image_info = decoder.get_info().ok_or(Error::Internal(
1862 "PNG did not return decoded image info".to_string(),
1863 ))?;
1864
1865 let (rotation, flip) = image_info
1866 .exif
1867 .as_ref()
1868 .map(|x| read_exif_orientation(x))
1869 .unwrap_or((Rotation::None, Flip::None));
1870
1871 if (rotation, flip) == (Rotation::None, Flip::None) {
1872 let img = Tensor::<u8>::image(image_info.width, image_info.height, fmt, memory)?;
1873 decoder.decode_into(&mut img.map()?)?;
1874 return Ok(TensorDyn::from(img));
1875 }
1876
1877 let tmp = Tensor::<u8>::image(
1878 image_info.width,
1879 image_info.height,
1880 fmt,
1881 Some(TensorMemory::Mem),
1882 )?;
1883 decoder.decode_into(&mut tmp.map()?)?;
1884
1885 rotate_flip_to_dyn(&tmp, fmt, rotation, flip, memory)
1886}
1887
1888pub fn load_image(
1907 image: &[u8],
1908 format: Option<PixelFormat>,
1909 memory: Option<TensorMemory>,
1910) -> Result<TensorDyn> {
1911 if let Ok(i) = load_jpeg(image, format, memory) {
1912 return Ok(i);
1913 }
1914 if let Ok(i) = load_png(image, format, memory) {
1915 return Ok(i);
1916 }
1917 Err(Error::NotSupported(
1918 "Could not decode as jpeg or png".to_string(),
1919 ))
1920}
1921
1922pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
1926 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
1927 "save_jpeg requires u8 tensor".to_string(),
1928 ))?;
1929 let fmt = t.format().ok_or(Error::NotAnImage)?;
1930 if fmt.layout() != PixelLayout::Packed {
1931 return Err(Error::NotImplemented(
1932 "Saving planar images is not supported".to_string(),
1933 ));
1934 }
1935
1936 let colour = match fmt {
1937 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
1938 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
1939 _ => {
1940 return Err(Error::NotImplemented(
1941 "Unsupported image format for saving".to_string(),
1942 ));
1943 }
1944 };
1945
1946 let w = t.width().ok_or(Error::NotAnImage)?;
1947 let h = t.height().ok_or(Error::NotAnImage)?;
1948 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
1949 let tensor_map = t.map()?;
1950
1951 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
1952
1953 Ok(())
1954}
1955
1956pub(crate) struct FunctionTimer<T: Display> {
1957 name: T,
1958 start: std::time::Instant,
1959}
1960
1961impl<T: Display> FunctionTimer<T> {
1962 pub fn new(name: T) -> Self {
1963 Self {
1964 name,
1965 start: std::time::Instant::now(),
1966 }
1967 }
1968}
1969
1970impl<T: Display> Drop for FunctionTimer<T> {
1971 fn drop(&mut self) {
1972 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
1973 }
1974}
1975
1976const DEFAULT_COLORS: [[f32; 4]; 20] = [
1977 [0., 1., 0., 0.7],
1978 [1., 0.5568628, 0., 0.7],
1979 [0.25882353, 0.15294118, 0.13333333, 0.7],
1980 [0.8, 0.7647059, 0.78039216, 0.7],
1981 [0.3137255, 0.3137255, 0.3137255, 0.7],
1982 [0.1411765, 0.3098039, 0.1215686, 0.7],
1983 [1., 0.95686275, 0.5137255, 0.7],
1984 [0.3529412, 0.32156863, 0., 0.7],
1985 [0.4235294, 0.6235294, 0.6509804, 0.7],
1986 [0.5098039, 0.5098039, 0.7294118, 0.7],
1987 [0.00784314, 0.18823529, 0.29411765, 0.7],
1988 [0.0, 0.2706, 1.0, 0.7],
1989 [0.0, 0.0, 0.0, 0.7],
1990 [0.0, 0.5, 0.0, 0.7],
1991 [1.0, 0.0, 0.0, 0.7],
1992 [0.0, 0.0, 1.0, 0.7],
1993 [1.0, 0.5, 0.5, 0.7],
1994 [0.1333, 0.5451, 0.1333, 0.7],
1995 [0.1176, 0.4118, 0.8235, 0.7],
1996 [1., 1., 1., 0.7],
1997];
1998
1999const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
2000 let mut result = [[0; M]; N];
2001 let mut i = 0;
2002 while i < N {
2003 let mut j = 0;
2004 while j < M {
2005 result[i][j] = (a[i][j] * 255.0).round() as u8;
2006 j += 1;
2007 }
2008 i += 1;
2009 }
2010 result
2011}
2012
2013const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
2014
2015#[cfg(test)]
2016#[cfg_attr(coverage_nightly, coverage(off))]
2017mod image_tests {
2018 use super::*;
2019 use crate::{CPUProcessor, Rotation};
2020 #[cfg(target_os = "linux")]
2021 use edgefirst_tensor::is_dma_available;
2022 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
2023 use image::buffer::ConvertBuffer;
2024
2025 fn convert_img(
2031 proc: &mut dyn ImageProcessorTrait,
2032 src: TensorDyn,
2033 dst: TensorDyn,
2034 rotation: Rotation,
2035 flip: Flip,
2036 crop: Crop,
2037 ) -> (Result<()>, TensorDyn, TensorDyn) {
2038 let src_fourcc = src.format().unwrap();
2039 let dst_fourcc = dst.format().unwrap();
2040 let src_dyn = src;
2041 let mut dst_dyn = dst;
2042 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
2043 let src_back = {
2044 let mut __t = src_dyn.into_u8().unwrap();
2045 __t.set_format(src_fourcc).unwrap();
2046 TensorDyn::from(__t)
2047 };
2048 let dst_back = {
2049 let mut __t = dst_dyn.into_u8().unwrap();
2050 __t.set_format(dst_fourcc).unwrap();
2051 TensorDyn::from(__t)
2052 };
2053 (result, src_back, dst_back)
2054 }
2055
2056 #[ctor::ctor]
2057 fn init() {
2058 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
2059 }
2060
2061 macro_rules! function {
2062 () => {{
2063 fn f() {}
2064 fn type_name_of<T>(_: T) -> &'static str {
2065 std::any::type_name::<T>()
2066 }
2067 let name = type_name_of(f);
2068
2069 match &name[..name.len() - 3].rfind(':') {
2071 Some(pos) => &name[pos + 1..name.len() - 3],
2072 None => &name[..name.len() - 3],
2073 }
2074 }};
2075 }
2076
2077 #[test]
2078 fn test_invalid_crop() {
2079 let src = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
2080 let dst = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
2081
2082 let crop = Crop::new()
2083 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
2084 .with_dst_rect(Some(Rect::new(0, 0, 150, 150)));
2085
2086 let result = crop.check_crop_dyn(&src, &dst);
2087 assert!(matches!(
2088 result,
2089 Err(Error::CropInvalid(e)) if e.starts_with("Dest and Src crop invalid")
2090 ));
2091
2092 let crop = crop.with_src_rect(Some(Rect::new(0, 0, 10, 10)));
2093 let result = crop.check_crop_dyn(&src, &dst);
2094 assert!(matches!(
2095 result,
2096 Err(Error::CropInvalid(e)) if e.starts_with("Dest crop invalid")
2097 ));
2098
2099 let crop = crop
2100 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
2101 .with_dst_rect(Some(Rect::new(0, 0, 50, 50)));
2102 let result = crop.check_crop_dyn(&src, &dst);
2103 assert!(matches!(
2104 result,
2105 Err(Error::CropInvalid(e)) if e.starts_with("Src crop invalid")
2106 ));
2107
2108 let crop = crop.with_src_rect(Some(Rect::new(50, 50, 50, 50)));
2109
2110 let result = crop.check_crop_dyn(&src, &dst);
2111 assert!(result.is_ok());
2112 }
2113
2114 #[test]
2115 fn test_invalid_tensor_format() -> Result<(), Error> {
2116 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
2118 let result = tensor.set_format(PixelFormat::Rgb);
2119 assert!(result.is_err(), "4D tensor should reject set_format");
2120
2121 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
2123 let result = tensor.set_format(PixelFormat::Rgb);
2124 assert!(result.is_err(), "4-channel tensor should reject RGB format");
2125
2126 Ok(())
2127 }
2128
2129 #[test]
2130 fn test_invalid_image_file() -> Result<(), Error> {
2131 let result = crate::load_image(&[123; 5000], None, None);
2132 assert!(matches!(
2133 result,
2134 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
2135
2136 Ok(())
2137 }
2138
2139 #[test]
2140 fn test_invalid_jpeg_format() -> Result<(), Error> {
2141 let result = crate::load_image(&[123; 5000], Some(PixelFormat::Yuyv), None);
2142 assert!(matches!(
2143 result,
2144 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
2145
2146 Ok(())
2147 }
2148
2149 #[test]
2150 fn test_load_resize_save() {
2151 let file = include_bytes!(concat!(
2152 env!("CARGO_MANIFEST_DIR"),
2153 "/../../testdata/zidane.jpg"
2154 ));
2155 let img = crate::load_image(file, Some(PixelFormat::Rgba), None).unwrap();
2156 assert_eq!(img.width(), Some(1280));
2157 assert_eq!(img.height(), Some(720));
2158
2159 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None).unwrap();
2160 let mut converter = CPUProcessor::new();
2161 let (result, _img, dst) = convert_img(
2162 &mut converter,
2163 img,
2164 dst,
2165 Rotation::None,
2166 Flip::None,
2167 Crop::no_crop(),
2168 );
2169 result.unwrap();
2170 assert_eq!(dst.width(), Some(640));
2171 assert_eq!(dst.height(), Some(360));
2172
2173 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
2174
2175 let file = std::fs::read("zidane_resized.jpg").unwrap();
2176 let img = crate::load_image(&file, None, None).unwrap();
2177 assert_eq!(img.width(), Some(640));
2178 assert_eq!(img.height(), Some(360));
2179 assert_eq!(img.format().unwrap(), PixelFormat::Rgb);
2180 }
2181
2182 #[test]
2183 fn test_from_tensor_planar() -> Result<(), Error> {
2184 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
2185 tensor.map()?.copy_from_slice(include_bytes!(concat!(
2186 env!("CARGO_MANIFEST_DIR"),
2187 "/../../testdata/camera720p.8bps"
2188 )));
2189 let planar = {
2190 tensor
2191 .set_format(PixelFormat::PlanarRgb)
2192 .map_err(|e| crate::Error::Internal(e.to_string()))?;
2193 TensorDyn::from(tensor)
2194 };
2195
2196 let rbga = load_bytes_to_tensor(
2197 1280,
2198 720,
2199 PixelFormat::Rgba,
2200 None,
2201 include_bytes!(concat!(
2202 env!("CARGO_MANIFEST_DIR"),
2203 "/../../testdata/camera720p.rgba"
2204 )),
2205 )?;
2206 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
2207
2208 Ok(())
2209 }
2210
2211 #[test]
2212 fn test_from_tensor_invalid_format() {
2213 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
2216 }
2217
2218 #[test]
2219 #[should_panic(expected = "Failed to save planar RGB image")]
2220 fn test_save_planar() {
2221 let planar_img = load_bytes_to_tensor(
2222 1280,
2223 720,
2224 PixelFormat::PlanarRgb,
2225 None,
2226 include_bytes!(concat!(
2227 env!("CARGO_MANIFEST_DIR"),
2228 "/../../testdata/camera720p.8bps"
2229 )),
2230 )
2231 .unwrap();
2232
2233 let save_path = "/tmp/planar_rgb.jpg";
2234 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
2235 }
2236
2237 #[test]
2238 #[should_panic(expected = "Failed to save YUYV image")]
2239 fn test_save_yuyv() {
2240 let planar_img = load_bytes_to_tensor(
2241 1280,
2242 720,
2243 PixelFormat::Yuyv,
2244 None,
2245 include_bytes!(concat!(
2246 env!("CARGO_MANIFEST_DIR"),
2247 "/../../testdata/camera720p.yuyv"
2248 )),
2249 )
2250 .unwrap();
2251
2252 let save_path = "/tmp/yuyv.jpg";
2253 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
2254 }
2255
2256 #[test]
2257 fn test_rotation_angle() {
2258 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
2259 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
2260 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
2261 assert_eq!(
2262 Rotation::from_degrees_clockwise(270),
2263 Rotation::CounterClockwise90
2264 );
2265 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
2266 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
2267 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
2268 assert_eq!(
2269 Rotation::from_degrees_clockwise(630),
2270 Rotation::CounterClockwise90
2271 );
2272 }
2273
2274 #[test]
2275 #[should_panic(expected = "rotation angle is not a multiple of 90")]
2276 fn test_rotation_angle_panic() {
2277 Rotation::from_degrees_clockwise(361);
2278 }
2279
2280 #[test]
2281 fn test_disable_env_var() -> Result<(), Error> {
2282 let saved_force = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
2286 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
2287
2288 #[cfg(target_os = "linux")]
2289 {
2290 let original = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
2291 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
2292 let converter = ImageProcessor::new()?;
2293 match original {
2294 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
2295 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
2296 }
2297 assert!(converter.g2d.is_none());
2298 }
2299
2300 #[cfg(target_os = "linux")]
2301 #[cfg(feature = "opengl")]
2302 {
2303 let original = std::env::var("EDGEFIRST_DISABLE_GL").ok();
2304 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
2305 let converter = ImageProcessor::new()?;
2306 match original {
2307 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
2308 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
2309 }
2310 assert!(converter.opengl.is_none());
2311 }
2312
2313 let original = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
2314 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
2315 let converter = ImageProcessor::new()?;
2316 match original {
2317 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
2318 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
2319 }
2320 assert!(converter.cpu.is_none());
2321
2322 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
2323 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
2324 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
2325 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
2326 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
2327 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
2328 let mut converter = ImageProcessor::new()?;
2329
2330 let src = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
2331 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None)?;
2332 let (result, _src, _dst) = convert_img(
2333 &mut converter,
2334 src,
2335 dst,
2336 Rotation::None,
2337 Flip::None,
2338 Crop::no_crop(),
2339 );
2340 assert!(matches!(result, Err(Error::NoConverter)));
2341
2342 match original_cpu {
2343 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
2344 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
2345 }
2346 match original_gl {
2347 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
2348 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
2349 }
2350 match original_g2d {
2351 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
2352 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
2353 }
2354 match saved_force {
2355 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
2356 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
2357 }
2358
2359 Ok(())
2360 }
2361
2362 #[test]
2363 fn test_unsupported_conversion() {
2364 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
2365 let dst = TensorDyn::image(640, 360, PixelFormat::Nv12, DType::U8, None).unwrap();
2366 let mut converter = ImageProcessor::new().unwrap();
2367 let (result, _src, _dst) = convert_img(
2368 &mut converter,
2369 src,
2370 dst,
2371 Rotation::None,
2372 Flip::None,
2373 Crop::no_crop(),
2374 );
2375 log::debug!("result: {:?}", result);
2376 assert!(matches!(
2377 result,
2378 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
2379 ));
2380 }
2381
2382 #[test]
2383 fn test_load_grey() {
2384 let grey_img = crate::load_image(
2385 include_bytes!(concat!(
2386 env!("CARGO_MANIFEST_DIR"),
2387 "/../../testdata/grey.jpg"
2388 )),
2389 Some(PixelFormat::Rgba),
2390 None,
2391 )
2392 .unwrap();
2393
2394 let grey_but_rgb_img = crate::load_image(
2395 include_bytes!(concat!(
2396 env!("CARGO_MANIFEST_DIR"),
2397 "/../../testdata/grey-rgb.jpg"
2398 )),
2399 Some(PixelFormat::Rgba),
2400 None,
2401 )
2402 .unwrap();
2403
2404 compare_images(&grey_img, &grey_but_rgb_img, 0.99, function!());
2405 }
2406
2407 #[test]
2408 fn test_new_nv12() {
2409 let nv12 = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
2410 assert_eq!(nv12.height(), Some(720));
2411 assert_eq!(nv12.width(), Some(1280));
2412 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
2413 assert_eq!(nv12.format().unwrap().channels(), 1);
2415 assert!(nv12.format().is_some_and(
2416 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
2417 ))
2418 }
2419
2420 #[test]
2421 #[cfg(target_os = "linux")]
2422 fn test_new_image_converter() {
2423 let dst_width = 640;
2424 let dst_height = 360;
2425 let file = include_bytes!(concat!(
2426 env!("CARGO_MANIFEST_DIR"),
2427 "/../../testdata/zidane.jpg"
2428 ))
2429 .to_vec();
2430 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2431
2432 let mut converter = ImageProcessor::new().unwrap();
2433 let converter_dst = converter
2434 .create_image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
2435 .unwrap();
2436 let (result, src, converter_dst) = convert_img(
2437 &mut converter,
2438 src,
2439 converter_dst,
2440 Rotation::None,
2441 Flip::None,
2442 Crop::no_crop(),
2443 );
2444 result.unwrap();
2445
2446 let cpu_dst =
2447 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2448 let mut cpu_converter = CPUProcessor::new();
2449 let (result, _src, cpu_dst) = convert_img(
2450 &mut cpu_converter,
2451 src,
2452 cpu_dst,
2453 Rotation::None,
2454 Flip::None,
2455 Crop::no_crop(),
2456 );
2457 result.unwrap();
2458
2459 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
2460 }
2461
2462 #[test]
2463 #[cfg(target_os = "linux")]
2464 fn test_create_image_dtype_i8() {
2465 let mut converter = ImageProcessor::new().unwrap();
2466
2467 let dst = converter
2469 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2470 .unwrap();
2471 assert_eq!(dst.dtype(), DType::I8);
2472 assert!(dst.width() == Some(320));
2473 assert!(dst.height() == Some(240));
2474 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
2475
2476 let dst_u8 = converter
2478 .create_image(320, 240, PixelFormat::Rgb, DType::U8, None)
2479 .unwrap();
2480 assert_eq!(dst_u8.dtype(), DType::U8);
2481
2482 let file = include_bytes!(concat!(
2484 env!("CARGO_MANIFEST_DIR"),
2485 "/../../testdata/zidane.jpg"
2486 ))
2487 .to_vec();
2488 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2489 let mut dst_i8 = converter
2490 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2491 .unwrap();
2492 converter
2493 .convert(
2494 &src,
2495 &mut dst_i8,
2496 Rotation::None,
2497 Flip::None,
2498 Crop::no_crop(),
2499 )
2500 .unwrap();
2501 }
2502
2503 #[test]
2504 #[ignore] fn test_crop_skip() {
2508 let file = include_bytes!(concat!(
2509 env!("CARGO_MANIFEST_DIR"),
2510 "/../../testdata/zidane.jpg"
2511 ))
2512 .to_vec();
2513 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2514
2515 let mut converter = ImageProcessor::new().unwrap();
2516 let converter_dst = converter
2517 .create_image(1280, 720, PixelFormat::Rgba, DType::U8, None)
2518 .unwrap();
2519 let crop = Crop::new()
2520 .with_src_rect(Some(Rect::new(0, 0, 640, 640)))
2521 .with_dst_rect(Some(Rect::new(0, 0, 640, 640)));
2522 let (result, src, converter_dst) = convert_img(
2523 &mut converter,
2524 src,
2525 converter_dst,
2526 Rotation::None,
2527 Flip::None,
2528 crop,
2529 );
2530 result.unwrap();
2531
2532 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
2533 let mut cpu_converter = CPUProcessor::new();
2534 let (result, _src, cpu_dst) = convert_img(
2535 &mut cpu_converter,
2536 src,
2537 cpu_dst,
2538 Rotation::None,
2539 Flip::None,
2540 crop,
2541 );
2542 result.unwrap();
2543
2544 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
2545 }
2546
2547 #[test]
2548 fn test_invalid_pixel_format() {
2549 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
2552 }
2553
2554 #[cfg(target_os = "linux")]
2556 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2557
2558 #[cfg(target_os = "linux")]
2559 fn is_g2d_available() -> bool {
2560 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
2561 }
2562
2563 #[cfg(target_os = "linux")]
2564 #[cfg(feature = "opengl")]
2565 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2566
2567 #[cfg(target_os = "linux")]
2568 #[cfg(feature = "opengl")]
2569 fn is_opengl_available() -> bool {
2571 #[cfg(all(target_os = "linux", feature = "opengl"))]
2572 {
2573 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
2574 }
2575
2576 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
2577 {
2578 false
2579 }
2580 }
2581
2582 #[test]
2583 fn test_load_jpeg_with_exif() {
2584 let file = include_bytes!(concat!(
2585 env!("CARGO_MANIFEST_DIR"),
2586 "/../../testdata/zidane_rotated_exif.jpg"
2587 ))
2588 .to_vec();
2589 let loaded = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2590
2591 assert_eq!(loaded.height(), Some(1280));
2592 assert_eq!(loaded.width(), Some(720));
2593
2594 let file = include_bytes!(concat!(
2595 env!("CARGO_MANIFEST_DIR"),
2596 "/../../testdata/zidane.jpg"
2597 ))
2598 .to_vec();
2599 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2600
2601 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
2602
2603 let cpu_dst =
2604 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2605 let mut cpu_converter = CPUProcessor::new();
2606
2607 let (result, _cpu_src, cpu_dst) = convert_img(
2608 &mut cpu_converter,
2609 cpu_src,
2610 cpu_dst,
2611 Rotation::Clockwise90,
2612 Flip::None,
2613 Crop::no_crop(),
2614 );
2615 result.unwrap();
2616
2617 compare_images(&loaded, &cpu_dst, 0.98, function!());
2618 }
2619
2620 #[test]
2621 fn test_load_png_with_exif() {
2622 let file = include_bytes!(concat!(
2623 env!("CARGO_MANIFEST_DIR"),
2624 "/../../testdata/zidane_rotated_exif_180.png"
2625 ))
2626 .to_vec();
2627 let loaded = crate::load_png(&file, Some(PixelFormat::Rgba), None).unwrap();
2628
2629 assert_eq!(loaded.height(), Some(720));
2630 assert_eq!(loaded.width(), Some(1280));
2631
2632 let file = include_bytes!(concat!(
2633 env!("CARGO_MANIFEST_DIR"),
2634 "/../../testdata/zidane.jpg"
2635 ))
2636 .to_vec();
2637 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2638
2639 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
2640 let mut cpu_converter = CPUProcessor::new();
2641
2642 let (result, _cpu_src, cpu_dst) = convert_img(
2643 &mut cpu_converter,
2644 cpu_src,
2645 cpu_dst,
2646 Rotation::Rotate180,
2647 Flip::None,
2648 Crop::no_crop(),
2649 );
2650 result.unwrap();
2651
2652 compare_images(&loaded, &cpu_dst, 0.98, function!());
2653 }
2654
2655 #[test]
2656 #[cfg(target_os = "linux")]
2657 fn test_g2d_resize() {
2658 if !is_g2d_available() {
2659 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
2660 return;
2661 }
2662 if !is_dma_available() {
2663 eprintln!(
2664 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2665 );
2666 return;
2667 }
2668
2669 let dst_width = 640;
2670 let dst_height = 360;
2671 let file = include_bytes!(concat!(
2672 env!("CARGO_MANIFEST_DIR"),
2673 "/../../testdata/zidane.jpg"
2674 ))
2675 .to_vec();
2676 let src =
2677 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
2678
2679 let g2d_dst = TensorDyn::image(
2680 dst_width,
2681 dst_height,
2682 PixelFormat::Rgba,
2683 DType::U8,
2684 Some(TensorMemory::Dma),
2685 )
2686 .unwrap();
2687 let mut g2d_converter = G2DProcessor::new().unwrap();
2688 let (result, src, g2d_dst) = convert_img(
2689 &mut g2d_converter,
2690 src,
2691 g2d_dst,
2692 Rotation::None,
2693 Flip::None,
2694 Crop::no_crop(),
2695 );
2696 result.unwrap();
2697
2698 let cpu_dst =
2699 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2700 let mut cpu_converter = CPUProcessor::new();
2701 let (result, _src, cpu_dst) = convert_img(
2702 &mut cpu_converter,
2703 src,
2704 cpu_dst,
2705 Rotation::None,
2706 Flip::None,
2707 Crop::no_crop(),
2708 );
2709 result.unwrap();
2710
2711 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2712 }
2713
2714 #[test]
2715 #[cfg(target_os = "linux")]
2716 #[cfg(feature = "opengl")]
2717 fn test_opengl_resize() {
2718 if !is_opengl_available() {
2719 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2720 return;
2721 }
2722
2723 let dst_width = 640;
2724 let dst_height = 360;
2725 let file = include_bytes!(concat!(
2726 env!("CARGO_MANIFEST_DIR"),
2727 "/../../testdata/zidane.jpg"
2728 ))
2729 .to_vec();
2730 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2731
2732 let cpu_dst =
2733 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2734 let mut cpu_converter = CPUProcessor::new();
2735 let (result, src, cpu_dst) = convert_img(
2736 &mut cpu_converter,
2737 src,
2738 cpu_dst,
2739 Rotation::None,
2740 Flip::None,
2741 Crop::no_crop(),
2742 );
2743 result.unwrap();
2744
2745 let mut src = src;
2746 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2747
2748 for _ in 0..5 {
2749 let gl_dst =
2750 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
2751 .unwrap();
2752 let (result, src_back, gl_dst) = convert_img(
2753 &mut gl_converter,
2754 src,
2755 gl_dst,
2756 Rotation::None,
2757 Flip::None,
2758 Crop::no_crop(),
2759 );
2760 result.unwrap();
2761 src = src_back;
2762
2763 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2764 }
2765 }
2766
2767 #[test]
2768 #[cfg(target_os = "linux")]
2769 #[cfg(feature = "opengl")]
2770 fn test_opengl_10_threads() {
2771 if !is_opengl_available() {
2772 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2773 return;
2774 }
2775
2776 let handles: Vec<_> = (0..10)
2777 .map(|i| {
2778 std::thread::Builder::new()
2779 .name(format!("Thread {i}"))
2780 .spawn(test_opengl_resize)
2781 .unwrap()
2782 })
2783 .collect();
2784 handles.into_iter().for_each(|h| {
2785 if let Err(e) = h.join() {
2786 std::panic::resume_unwind(e)
2787 }
2788 });
2789 }
2790
2791 #[test]
2792 #[cfg(target_os = "linux")]
2793 #[cfg(feature = "opengl")]
2794 fn test_opengl_grey() {
2795 if !is_opengl_available() {
2796 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2797 return;
2798 }
2799
2800 let img = crate::load_image(
2801 include_bytes!(concat!(
2802 env!("CARGO_MANIFEST_DIR"),
2803 "/../../testdata/grey.jpg"
2804 )),
2805 Some(PixelFormat::Grey),
2806 None,
2807 )
2808 .unwrap();
2809
2810 let gl_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
2811 let cpu_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
2812
2813 let mut converter = CPUProcessor::new();
2814
2815 let (result, img, cpu_dst) = convert_img(
2816 &mut converter,
2817 img,
2818 cpu_dst,
2819 Rotation::None,
2820 Flip::None,
2821 Crop::no_crop(),
2822 );
2823 result.unwrap();
2824
2825 let mut gl = GLProcessorThreaded::new(None).unwrap();
2826 let (result, _img, gl_dst) = convert_img(
2827 &mut gl,
2828 img,
2829 gl_dst,
2830 Rotation::None,
2831 Flip::None,
2832 Crop::no_crop(),
2833 );
2834 result.unwrap();
2835
2836 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2837 }
2838
2839 #[test]
2840 #[cfg(target_os = "linux")]
2841 fn test_g2d_src_crop() {
2842 if !is_g2d_available() {
2843 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
2844 return;
2845 }
2846 if !is_dma_available() {
2847 eprintln!(
2848 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2849 );
2850 return;
2851 }
2852
2853 let dst_width = 640;
2854 let dst_height = 640;
2855 let file = include_bytes!(concat!(
2856 env!("CARGO_MANIFEST_DIR"),
2857 "/../../testdata/zidane.jpg"
2858 ))
2859 .to_vec();
2860 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2861
2862 let cpu_dst =
2863 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2864 let mut cpu_converter = CPUProcessor::new();
2865 let crop = Crop {
2866 src_rect: Some(Rect {
2867 left: 0,
2868 top: 0,
2869 width: 640,
2870 height: 360,
2871 }),
2872 dst_rect: None,
2873 dst_color: None,
2874 };
2875 let (result, src, cpu_dst) = convert_img(
2876 &mut cpu_converter,
2877 src,
2878 cpu_dst,
2879 Rotation::None,
2880 Flip::None,
2881 crop,
2882 );
2883 result.unwrap();
2884
2885 let g2d_dst =
2886 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2887 let mut g2d_converter = G2DProcessor::new().unwrap();
2888 let (result, _src, g2d_dst) = convert_img(
2889 &mut g2d_converter,
2890 src,
2891 g2d_dst,
2892 Rotation::None,
2893 Flip::None,
2894 crop,
2895 );
2896 result.unwrap();
2897
2898 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2899 }
2900
2901 #[test]
2902 #[cfg(target_os = "linux")]
2903 fn test_g2d_dst_crop() {
2904 if !is_g2d_available() {
2905 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
2906 return;
2907 }
2908 if !is_dma_available() {
2909 eprintln!(
2910 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2911 );
2912 return;
2913 }
2914
2915 let dst_width = 640;
2916 let dst_height = 640;
2917 let file = include_bytes!(concat!(
2918 env!("CARGO_MANIFEST_DIR"),
2919 "/../../testdata/zidane.jpg"
2920 ))
2921 .to_vec();
2922 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2923
2924 let cpu_dst =
2925 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2926 let mut cpu_converter = CPUProcessor::new();
2927 let crop = Crop {
2928 src_rect: None,
2929 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2930 dst_color: None,
2931 };
2932 let (result, src, cpu_dst) = convert_img(
2933 &mut cpu_converter,
2934 src,
2935 cpu_dst,
2936 Rotation::None,
2937 Flip::None,
2938 crop,
2939 );
2940 result.unwrap();
2941
2942 let g2d_dst =
2943 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2944 let mut g2d_converter = G2DProcessor::new().unwrap();
2945 let (result, _src, g2d_dst) = convert_img(
2946 &mut g2d_converter,
2947 src,
2948 g2d_dst,
2949 Rotation::None,
2950 Flip::None,
2951 crop,
2952 );
2953 result.unwrap();
2954
2955 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2956 }
2957
2958 #[test]
2959 #[cfg(target_os = "linux")]
2960 fn test_g2d_all_rgba() {
2961 if !is_g2d_available() {
2962 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
2963 return;
2964 }
2965 if !is_dma_available() {
2966 eprintln!(
2967 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2968 );
2969 return;
2970 }
2971
2972 let dst_width = 640;
2973 let dst_height = 640;
2974 let file = include_bytes!(concat!(
2975 env!("CARGO_MANIFEST_DIR"),
2976 "/../../testdata/zidane.jpg"
2977 ))
2978 .to_vec();
2979 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2980 let src_dyn = src;
2981
2982 let mut cpu_dst =
2983 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2984 let mut cpu_converter = CPUProcessor::new();
2985 let mut g2d_dst =
2986 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2987 let mut g2d_converter = G2DProcessor::new().unwrap();
2988
2989 let crop = Crop {
2990 src_rect: Some(Rect::new(50, 120, 1024, 576)),
2991 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2992 dst_color: None,
2993 };
2994
2995 for rot in [
2996 Rotation::None,
2997 Rotation::Clockwise90,
2998 Rotation::Rotate180,
2999 Rotation::CounterClockwise90,
3000 ] {
3001 cpu_dst
3002 .as_u8()
3003 .unwrap()
3004 .map()
3005 .unwrap()
3006 .as_mut_slice()
3007 .fill(114);
3008 g2d_dst
3009 .as_u8()
3010 .unwrap()
3011 .map()
3012 .unwrap()
3013 .as_mut_slice()
3014 .fill(114);
3015 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
3016 let mut cpu_dst_dyn = cpu_dst;
3017 cpu_converter
3018 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
3019 .unwrap();
3020 cpu_dst = {
3021 let mut __t = cpu_dst_dyn.into_u8().unwrap();
3022 __t.set_format(PixelFormat::Rgba).unwrap();
3023 TensorDyn::from(__t)
3024 };
3025
3026 let mut g2d_dst_dyn = g2d_dst;
3027 g2d_converter
3028 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
3029 .unwrap();
3030 g2d_dst = {
3031 let mut __t = g2d_dst_dyn.into_u8().unwrap();
3032 __t.set_format(PixelFormat::Rgba).unwrap();
3033 TensorDyn::from(__t)
3034 };
3035
3036 compare_images(
3037 &g2d_dst,
3038 &cpu_dst,
3039 0.98,
3040 &format!("{} {:?} {:?}", function!(), rot, flip),
3041 );
3042 }
3043 }
3044 }
3045
3046 #[test]
3047 #[cfg(target_os = "linux")]
3048 #[cfg(feature = "opengl")]
3049 fn test_opengl_src_crop() {
3050 if !is_opengl_available() {
3051 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3052 return;
3053 }
3054
3055 let dst_width = 640;
3056 let dst_height = 360;
3057 let file = include_bytes!(concat!(
3058 env!("CARGO_MANIFEST_DIR"),
3059 "/../../testdata/zidane.jpg"
3060 ))
3061 .to_vec();
3062 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3063 let crop = Crop {
3064 src_rect: Some(Rect {
3065 left: 320,
3066 top: 180,
3067 width: 1280 - 320,
3068 height: 720 - 180,
3069 }),
3070 dst_rect: None,
3071 dst_color: None,
3072 };
3073
3074 let cpu_dst =
3075 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3076 let mut cpu_converter = CPUProcessor::new();
3077 let (result, src, cpu_dst) = convert_img(
3078 &mut cpu_converter,
3079 src,
3080 cpu_dst,
3081 Rotation::None,
3082 Flip::None,
3083 crop,
3084 );
3085 result.unwrap();
3086
3087 let gl_dst =
3088 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3089 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3090 let (result, _src, gl_dst) = convert_img(
3091 &mut gl_converter,
3092 src,
3093 gl_dst,
3094 Rotation::None,
3095 Flip::None,
3096 crop,
3097 );
3098 result.unwrap();
3099
3100 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3101 }
3102
3103 #[test]
3104 #[cfg(target_os = "linux")]
3105 #[cfg(feature = "opengl")]
3106 fn test_opengl_dst_crop() {
3107 if !is_opengl_available() {
3108 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3109 return;
3110 }
3111
3112 let dst_width = 640;
3113 let dst_height = 640;
3114 let file = include_bytes!(concat!(
3115 env!("CARGO_MANIFEST_DIR"),
3116 "/../../testdata/zidane.jpg"
3117 ))
3118 .to_vec();
3119 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3120
3121 let cpu_dst =
3122 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3123 let mut cpu_converter = CPUProcessor::new();
3124 let crop = Crop {
3125 src_rect: None,
3126 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3127 dst_color: None,
3128 };
3129 let (result, src, cpu_dst) = convert_img(
3130 &mut cpu_converter,
3131 src,
3132 cpu_dst,
3133 Rotation::None,
3134 Flip::None,
3135 crop,
3136 );
3137 result.unwrap();
3138
3139 let gl_dst =
3140 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3141 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3142 let (result, _src, gl_dst) = convert_img(
3143 &mut gl_converter,
3144 src,
3145 gl_dst,
3146 Rotation::None,
3147 Flip::None,
3148 crop,
3149 );
3150 result.unwrap();
3151
3152 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3153 }
3154
3155 #[test]
3156 #[cfg(target_os = "linux")]
3157 #[cfg(feature = "opengl")]
3158 fn test_opengl_all_rgba() {
3159 if !is_opengl_available() {
3160 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3161 return;
3162 }
3163
3164 let dst_width = 640;
3165 let dst_height = 640;
3166 let file = include_bytes!(concat!(
3167 env!("CARGO_MANIFEST_DIR"),
3168 "/../../testdata/zidane.jpg"
3169 ))
3170 .to_vec();
3171
3172 let mut cpu_converter = CPUProcessor::new();
3173
3174 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3175
3176 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
3177 if is_dma_available() {
3178 mem.push(Some(TensorMemory::Dma));
3179 }
3180 let crop = Crop {
3181 src_rect: Some(Rect::new(50, 120, 1024, 576)),
3182 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3183 dst_color: None,
3184 };
3185 for m in mem {
3186 let src = crate::load_image(&file, Some(PixelFormat::Rgba), m).unwrap();
3187 let src_dyn = src;
3188
3189 for rot in [
3190 Rotation::None,
3191 Rotation::Clockwise90,
3192 Rotation::Rotate180,
3193 Rotation::CounterClockwise90,
3194 ] {
3195 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
3196 let cpu_dst =
3197 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
3198 .unwrap();
3199 let gl_dst =
3200 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
3201 .unwrap();
3202 cpu_dst
3203 .as_u8()
3204 .unwrap()
3205 .map()
3206 .unwrap()
3207 .as_mut_slice()
3208 .fill(114);
3209 gl_dst
3210 .as_u8()
3211 .unwrap()
3212 .map()
3213 .unwrap()
3214 .as_mut_slice()
3215 .fill(114);
3216
3217 let mut cpu_dst_dyn = cpu_dst;
3218 cpu_converter
3219 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
3220 .unwrap();
3221 let cpu_dst = {
3222 let mut __t = cpu_dst_dyn.into_u8().unwrap();
3223 __t.set_format(PixelFormat::Rgba).unwrap();
3224 TensorDyn::from(__t)
3225 };
3226
3227 let mut gl_dst_dyn = gl_dst;
3228 gl_converter
3229 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
3230 .map_err(|e| {
3231 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
3232 e
3233 })
3234 .unwrap();
3235 let gl_dst = {
3236 let mut __t = gl_dst_dyn.into_u8().unwrap();
3237 __t.set_format(PixelFormat::Rgba).unwrap();
3238 TensorDyn::from(__t)
3239 };
3240
3241 compare_images(
3242 &gl_dst,
3243 &cpu_dst,
3244 0.98,
3245 &format!("{} {:?} {:?}", function!(), rot, flip),
3246 );
3247 }
3248 }
3249 }
3250 }
3251
3252 #[test]
3253 #[cfg(target_os = "linux")]
3254 fn test_cpu_rotate() {
3255 for rot in [
3256 Rotation::Clockwise90,
3257 Rotation::Rotate180,
3258 Rotation::CounterClockwise90,
3259 ] {
3260 test_cpu_rotate_(rot);
3261 }
3262 }
3263
3264 #[cfg(target_os = "linux")]
3265 fn test_cpu_rotate_(rot: Rotation) {
3266 let file = include_bytes!(concat!(
3270 env!("CARGO_MANIFEST_DIR"),
3271 "/../../testdata/zidane.jpg"
3272 ))
3273 .to_vec();
3274
3275 let unchanged_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3276 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3277
3278 let (dst_width, dst_height) = match rot {
3279 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
3280 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
3281 (src.height().unwrap(), src.width().unwrap())
3282 }
3283 };
3284
3285 let cpu_dst =
3286 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3287 let mut cpu_converter = CPUProcessor::new();
3288
3289 let (result, src, cpu_dst) = convert_img(
3292 &mut cpu_converter,
3293 src,
3294 cpu_dst,
3295 rot,
3296 Flip::None,
3297 Crop::no_crop(),
3298 );
3299 result.unwrap();
3300
3301 let (result, cpu_dst, src) = convert_img(
3302 &mut cpu_converter,
3303 cpu_dst,
3304 src,
3305 rot,
3306 Flip::None,
3307 Crop::no_crop(),
3308 );
3309 result.unwrap();
3310
3311 let (result, src, cpu_dst) = convert_img(
3312 &mut cpu_converter,
3313 src,
3314 cpu_dst,
3315 rot,
3316 Flip::None,
3317 Crop::no_crop(),
3318 );
3319 result.unwrap();
3320
3321 let (result, _cpu_dst, src) = convert_img(
3322 &mut cpu_converter,
3323 cpu_dst,
3324 src,
3325 rot,
3326 Flip::None,
3327 Crop::no_crop(),
3328 );
3329 result.unwrap();
3330
3331 compare_images(&src, &unchanged_src, 0.98, function!());
3332 }
3333
3334 #[test]
3335 #[cfg(target_os = "linux")]
3336 #[cfg(feature = "opengl")]
3337 fn test_opengl_rotate() {
3338 if !is_opengl_available() {
3339 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3340 return;
3341 }
3342
3343 let size = (1280, 720);
3344 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
3345
3346 if is_dma_available() {
3347 mem.push(Some(TensorMemory::Dma));
3348 }
3349 for m in mem {
3350 for rot in [
3351 Rotation::Clockwise90,
3352 Rotation::Rotate180,
3353 Rotation::CounterClockwise90,
3354 ] {
3355 test_opengl_rotate_(size, rot, m);
3356 }
3357 }
3358 }
3359
3360 #[cfg(target_os = "linux")]
3361 #[cfg(feature = "opengl")]
3362 fn test_opengl_rotate_(
3363 size: (usize, usize),
3364 rot: Rotation,
3365 tensor_memory: Option<TensorMemory>,
3366 ) {
3367 let (dst_width, dst_height) = match rot {
3368 Rotation::None | Rotation::Rotate180 => size,
3369 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
3370 };
3371
3372 let file = include_bytes!(concat!(
3373 env!("CARGO_MANIFEST_DIR"),
3374 "/../../testdata/zidane.jpg"
3375 ))
3376 .to_vec();
3377 let src = crate::load_image(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
3378
3379 let cpu_dst =
3380 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3381 let mut cpu_converter = CPUProcessor::new();
3382
3383 let (result, mut src, cpu_dst) = convert_img(
3384 &mut cpu_converter,
3385 src,
3386 cpu_dst,
3387 rot,
3388 Flip::None,
3389 Crop::no_crop(),
3390 );
3391 result.unwrap();
3392
3393 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3394
3395 for _ in 0..5 {
3396 let gl_dst = TensorDyn::image(
3397 dst_width,
3398 dst_height,
3399 PixelFormat::Rgba,
3400 DType::U8,
3401 tensor_memory,
3402 )
3403 .unwrap();
3404 let (result, src_back, gl_dst) = convert_img(
3405 &mut gl_converter,
3406 src,
3407 gl_dst,
3408 rot,
3409 Flip::None,
3410 Crop::no_crop(),
3411 );
3412 result.unwrap();
3413 src = src_back;
3414 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3415 }
3416 }
3417
3418 #[test]
3419 #[cfg(target_os = "linux")]
3420 fn test_g2d_rotate() {
3421 if !is_g2d_available() {
3422 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
3423 return;
3424 }
3425 if !is_dma_available() {
3426 eprintln!(
3427 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3428 );
3429 return;
3430 }
3431
3432 let size = (1280, 720);
3433 for rot in [
3434 Rotation::Clockwise90,
3435 Rotation::Rotate180,
3436 Rotation::CounterClockwise90,
3437 ] {
3438 test_g2d_rotate_(size, rot);
3439 }
3440 }
3441
3442 #[cfg(target_os = "linux")]
3443 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
3444 let (dst_width, dst_height) = match rot {
3445 Rotation::None | Rotation::Rotate180 => size,
3446 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
3447 };
3448
3449 let file = include_bytes!(concat!(
3450 env!("CARGO_MANIFEST_DIR"),
3451 "/../../testdata/zidane.jpg"
3452 ))
3453 .to_vec();
3454 let src =
3455 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
3456
3457 let cpu_dst =
3458 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3459 let mut cpu_converter = CPUProcessor::new();
3460
3461 let (result, src, cpu_dst) = convert_img(
3462 &mut cpu_converter,
3463 src,
3464 cpu_dst,
3465 rot,
3466 Flip::None,
3467 Crop::no_crop(),
3468 );
3469 result.unwrap();
3470
3471 let g2d_dst = TensorDyn::image(
3472 dst_width,
3473 dst_height,
3474 PixelFormat::Rgba,
3475 DType::U8,
3476 Some(TensorMemory::Dma),
3477 )
3478 .unwrap();
3479 let mut g2d_converter = G2DProcessor::new().unwrap();
3480
3481 let (result, _src, g2d_dst) = convert_img(
3482 &mut g2d_converter,
3483 src,
3484 g2d_dst,
3485 rot,
3486 Flip::None,
3487 Crop::no_crop(),
3488 );
3489 result.unwrap();
3490
3491 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3492 }
3493
3494 #[test]
3495 fn test_rgba_to_yuyv_resize_cpu() {
3496 let src = load_bytes_to_tensor(
3497 1280,
3498 720,
3499 PixelFormat::Rgba,
3500 None,
3501 include_bytes!(concat!(
3502 env!("CARGO_MANIFEST_DIR"),
3503 "/../../testdata/camera720p.rgba"
3504 )),
3505 )
3506 .unwrap();
3507
3508 let (dst_width, dst_height) = (640, 360);
3509
3510 let dst =
3511 TensorDyn::image(dst_width, dst_height, PixelFormat::Yuyv, DType::U8, None).unwrap();
3512
3513 let dst_through_yuyv =
3514 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3515 let dst_direct =
3516 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3517
3518 let mut cpu_converter = CPUProcessor::new();
3519
3520 let (result, src, dst) = convert_img(
3521 &mut cpu_converter,
3522 src,
3523 dst,
3524 Rotation::None,
3525 Flip::None,
3526 Crop::no_crop(),
3527 );
3528 result.unwrap();
3529
3530 let (result, _dst, dst_through_yuyv) = convert_img(
3531 &mut cpu_converter,
3532 dst,
3533 dst_through_yuyv,
3534 Rotation::None,
3535 Flip::None,
3536 Crop::no_crop(),
3537 );
3538 result.unwrap();
3539
3540 let (result, _src, dst_direct) = convert_img(
3541 &mut cpu_converter,
3542 src,
3543 dst_direct,
3544 Rotation::None,
3545 Flip::None,
3546 Crop::no_crop(),
3547 );
3548 result.unwrap();
3549
3550 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
3551 }
3552
3553 #[test]
3554 #[cfg(target_os = "linux")]
3555 #[cfg(feature = "opengl")]
3556 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
3557 fn test_rgba_to_yuyv_resize_opengl() {
3558 if !is_opengl_available() {
3559 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3560 return;
3561 }
3562
3563 if !is_dma_available() {
3564 eprintln!(
3565 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3566 function!()
3567 );
3568 return;
3569 }
3570
3571 let src = load_bytes_to_tensor(
3572 1280,
3573 720,
3574 PixelFormat::Rgba,
3575 None,
3576 include_bytes!(concat!(
3577 env!("CARGO_MANIFEST_DIR"),
3578 "/../../testdata/camera720p.rgba"
3579 )),
3580 )
3581 .unwrap();
3582
3583 let (dst_width, dst_height) = (640, 360);
3584
3585 let dst = TensorDyn::image(
3586 dst_width,
3587 dst_height,
3588 PixelFormat::Yuyv,
3589 DType::U8,
3590 Some(TensorMemory::Dma),
3591 )
3592 .unwrap();
3593
3594 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3595
3596 let (result, src, dst) = convert_img(
3597 &mut gl_converter,
3598 src,
3599 dst,
3600 Rotation::None,
3601 Flip::None,
3602 Crop::new()
3603 .with_dst_rect(Some(Rect::new(100, 100, 100, 100)))
3604 .with_dst_color(Some([255, 255, 255, 255])),
3605 );
3606 result.unwrap();
3607
3608 std::fs::write(
3609 "rgba_to_yuyv_opengl.yuyv",
3610 dst.as_u8().unwrap().map().unwrap().as_slice(),
3611 )
3612 .unwrap();
3613 let cpu_dst = TensorDyn::image(
3614 dst_width,
3615 dst_height,
3616 PixelFormat::Yuyv,
3617 DType::U8,
3618 Some(TensorMemory::Dma),
3619 )
3620 .unwrap();
3621 let (result, _src, cpu_dst) = convert_img(
3622 &mut CPUProcessor::new(),
3623 src,
3624 cpu_dst,
3625 Rotation::None,
3626 Flip::None,
3627 Crop::no_crop(),
3628 );
3629 result.unwrap();
3630
3631 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
3632 }
3633
3634 #[test]
3635 #[cfg(target_os = "linux")]
3636 fn test_rgba_to_yuyv_resize_g2d() {
3637 if !is_g2d_available() {
3638 eprintln!(
3639 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
3640 );
3641 return;
3642 }
3643 if !is_dma_available() {
3644 eprintln!(
3645 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3646 );
3647 return;
3648 }
3649
3650 let src = load_bytes_to_tensor(
3651 1280,
3652 720,
3653 PixelFormat::Rgba,
3654 Some(TensorMemory::Dma),
3655 include_bytes!(concat!(
3656 env!("CARGO_MANIFEST_DIR"),
3657 "/../../testdata/camera720p.rgba"
3658 )),
3659 )
3660 .unwrap();
3661
3662 let (dst_width, dst_height) = (1280, 720);
3663
3664 let cpu_dst = TensorDyn::image(
3665 dst_width,
3666 dst_height,
3667 PixelFormat::Yuyv,
3668 DType::U8,
3669 Some(TensorMemory::Dma),
3670 )
3671 .unwrap();
3672
3673 let g2d_dst = TensorDyn::image(
3674 dst_width,
3675 dst_height,
3676 PixelFormat::Yuyv,
3677 DType::U8,
3678 Some(TensorMemory::Dma),
3679 )
3680 .unwrap();
3681
3682 let mut g2d_converter = G2DProcessor::new().unwrap();
3683 let crop = Crop {
3684 src_rect: None,
3685 dst_rect: Some(Rect::new(100, 100, 2, 2)),
3686 dst_color: None,
3687 };
3688
3689 g2d_dst
3690 .as_u8()
3691 .unwrap()
3692 .map()
3693 .unwrap()
3694 .as_mut_slice()
3695 .fill(128);
3696 let (result, src, g2d_dst) = convert_img(
3697 &mut g2d_converter,
3698 src,
3699 g2d_dst,
3700 Rotation::None,
3701 Flip::None,
3702 crop,
3703 );
3704 result.unwrap();
3705
3706 let cpu_dst_img = cpu_dst;
3707 cpu_dst_img
3708 .as_u8()
3709 .unwrap()
3710 .map()
3711 .unwrap()
3712 .as_mut_slice()
3713 .fill(128);
3714 let (result, _src, cpu_dst) = convert_img(
3715 &mut CPUProcessor::new(),
3716 src,
3717 cpu_dst_img,
3718 Rotation::None,
3719 Flip::None,
3720 crop,
3721 );
3722 result.unwrap();
3723
3724 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
3725 }
3726
3727 #[test]
3728 fn test_yuyv_to_rgba_cpu() {
3729 let file = include_bytes!(concat!(
3730 env!("CARGO_MANIFEST_DIR"),
3731 "/../../testdata/camera720p.yuyv"
3732 ))
3733 .to_vec();
3734 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
3735 src.as_u8()
3736 .unwrap()
3737 .map()
3738 .unwrap()
3739 .as_mut_slice()
3740 .copy_from_slice(&file);
3741
3742 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3743 let mut cpu_converter = CPUProcessor::new();
3744
3745 let (result, _src, dst) = convert_img(
3746 &mut cpu_converter,
3747 src,
3748 dst,
3749 Rotation::None,
3750 Flip::None,
3751 Crop::no_crop(),
3752 );
3753 result.unwrap();
3754
3755 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3756 target_image
3757 .as_u8()
3758 .unwrap()
3759 .map()
3760 .unwrap()
3761 .as_mut_slice()
3762 .copy_from_slice(include_bytes!(concat!(
3763 env!("CARGO_MANIFEST_DIR"),
3764 "/../../testdata/camera720p.rgba"
3765 )));
3766
3767 compare_images(&dst, &target_image, 0.98, function!());
3768 }
3769
3770 #[test]
3771 fn test_yuyv_to_rgb_cpu() {
3772 let file = include_bytes!(concat!(
3773 env!("CARGO_MANIFEST_DIR"),
3774 "/../../testdata/camera720p.yuyv"
3775 ))
3776 .to_vec();
3777 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
3778 src.as_u8()
3779 .unwrap()
3780 .map()
3781 .unwrap()
3782 .as_mut_slice()
3783 .copy_from_slice(&file);
3784
3785 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3786 let mut cpu_converter = CPUProcessor::new();
3787
3788 let (result, _src, dst) = convert_img(
3789 &mut cpu_converter,
3790 src,
3791 dst,
3792 Rotation::None,
3793 Flip::None,
3794 Crop::no_crop(),
3795 );
3796 result.unwrap();
3797
3798 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3799 target_image
3800 .as_u8()
3801 .unwrap()
3802 .map()
3803 .unwrap()
3804 .as_mut_slice()
3805 .as_chunks_mut::<3>()
3806 .0
3807 .iter_mut()
3808 .zip(
3809 include_bytes!(concat!(
3810 env!("CARGO_MANIFEST_DIR"),
3811 "/../../testdata/camera720p.rgba"
3812 ))
3813 .as_chunks::<4>()
3814 .0,
3815 )
3816 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
3817
3818 compare_images(&dst, &target_image, 0.98, function!());
3819 }
3820
3821 #[test]
3822 #[cfg(target_os = "linux")]
3823 fn test_yuyv_to_rgba_g2d() {
3824 if !is_g2d_available() {
3825 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
3826 return;
3827 }
3828 if !is_dma_available() {
3829 eprintln!(
3830 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3831 );
3832 return;
3833 }
3834
3835 let src = load_bytes_to_tensor(
3836 1280,
3837 720,
3838 PixelFormat::Yuyv,
3839 None,
3840 include_bytes!(concat!(
3841 env!("CARGO_MANIFEST_DIR"),
3842 "/../../testdata/camera720p.yuyv"
3843 )),
3844 )
3845 .unwrap();
3846
3847 let dst = TensorDyn::image(
3848 1280,
3849 720,
3850 PixelFormat::Rgba,
3851 DType::U8,
3852 Some(TensorMemory::Dma),
3853 )
3854 .unwrap();
3855 let mut g2d_converter = G2DProcessor::new().unwrap();
3856
3857 let (result, _src, dst) = convert_img(
3858 &mut g2d_converter,
3859 src,
3860 dst,
3861 Rotation::None,
3862 Flip::None,
3863 Crop::no_crop(),
3864 );
3865 result.unwrap();
3866
3867 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3868 target_image
3869 .as_u8()
3870 .unwrap()
3871 .map()
3872 .unwrap()
3873 .as_mut_slice()
3874 .copy_from_slice(include_bytes!(concat!(
3875 env!("CARGO_MANIFEST_DIR"),
3876 "/../../testdata/camera720p.rgba"
3877 )));
3878
3879 compare_images(&dst, &target_image, 0.98, function!());
3880 }
3881
3882 #[test]
3883 #[cfg(target_os = "linux")]
3884 #[cfg(feature = "opengl")]
3885 fn test_yuyv_to_rgba_opengl() {
3886 if !is_opengl_available() {
3887 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3888 return;
3889 }
3890 if !is_dma_available() {
3891 eprintln!(
3892 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3893 function!()
3894 );
3895 return;
3896 }
3897
3898 let src = load_bytes_to_tensor(
3899 1280,
3900 720,
3901 PixelFormat::Yuyv,
3902 Some(TensorMemory::Dma),
3903 include_bytes!(concat!(
3904 env!("CARGO_MANIFEST_DIR"),
3905 "/../../testdata/camera720p.yuyv"
3906 )),
3907 )
3908 .unwrap();
3909
3910 let dst = TensorDyn::image(
3911 1280,
3912 720,
3913 PixelFormat::Rgba,
3914 DType::U8,
3915 Some(TensorMemory::Dma),
3916 )
3917 .unwrap();
3918 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3919
3920 let (result, _src, dst) = convert_img(
3921 &mut gl_converter,
3922 src,
3923 dst,
3924 Rotation::None,
3925 Flip::None,
3926 Crop::no_crop(),
3927 );
3928 result.unwrap();
3929
3930 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3931 target_image
3932 .as_u8()
3933 .unwrap()
3934 .map()
3935 .unwrap()
3936 .as_mut_slice()
3937 .copy_from_slice(include_bytes!(concat!(
3938 env!("CARGO_MANIFEST_DIR"),
3939 "/../../testdata/camera720p.rgba"
3940 )));
3941
3942 compare_images(&dst, &target_image, 0.98, function!());
3943 }
3944
3945 #[test]
3946 #[cfg(target_os = "linux")]
3947 fn test_yuyv_to_rgb_g2d() {
3948 if !is_g2d_available() {
3949 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
3950 return;
3951 }
3952 if !is_dma_available() {
3953 eprintln!(
3954 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3955 );
3956 return;
3957 }
3958
3959 let src = load_bytes_to_tensor(
3960 1280,
3961 720,
3962 PixelFormat::Yuyv,
3963 None,
3964 include_bytes!(concat!(
3965 env!("CARGO_MANIFEST_DIR"),
3966 "/../../testdata/camera720p.yuyv"
3967 )),
3968 )
3969 .unwrap();
3970
3971 let g2d_dst = TensorDyn::image(
3972 1280,
3973 720,
3974 PixelFormat::Rgb,
3975 DType::U8,
3976 Some(TensorMemory::Dma),
3977 )
3978 .unwrap();
3979 let mut g2d_converter = G2DProcessor::new().unwrap();
3980
3981 let (result, src, g2d_dst) = convert_img(
3982 &mut g2d_converter,
3983 src,
3984 g2d_dst,
3985 Rotation::None,
3986 Flip::None,
3987 Crop::no_crop(),
3988 );
3989 result.unwrap();
3990
3991 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3992 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3993
3994 let (result, _src, cpu_dst) = convert_img(
3995 &mut cpu_converter,
3996 src,
3997 cpu_dst,
3998 Rotation::None,
3999 Flip::None,
4000 Crop::no_crop(),
4001 );
4002 result.unwrap();
4003
4004 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4005 }
4006
4007 #[test]
4008 #[cfg(target_os = "linux")]
4009 fn test_yuyv_to_yuyv_resize_g2d() {
4010 if !is_g2d_available() {
4011 eprintln!(
4012 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
4013 );
4014 return;
4015 }
4016 if !is_dma_available() {
4017 eprintln!(
4018 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4019 );
4020 return;
4021 }
4022
4023 let src = load_bytes_to_tensor(
4024 1280,
4025 720,
4026 PixelFormat::Yuyv,
4027 None,
4028 include_bytes!(concat!(
4029 env!("CARGO_MANIFEST_DIR"),
4030 "/../../testdata/camera720p.yuyv"
4031 )),
4032 )
4033 .unwrap();
4034
4035 let g2d_dst = TensorDyn::image(
4036 600,
4037 400,
4038 PixelFormat::Yuyv,
4039 DType::U8,
4040 Some(TensorMemory::Dma),
4041 )
4042 .unwrap();
4043 let mut g2d_converter = G2DProcessor::new().unwrap();
4044
4045 let (result, src, g2d_dst) = convert_img(
4046 &mut g2d_converter,
4047 src,
4048 g2d_dst,
4049 Rotation::None,
4050 Flip::None,
4051 Crop::no_crop(),
4052 );
4053 result.unwrap();
4054
4055 let cpu_dst = TensorDyn::image(600, 400, PixelFormat::Yuyv, DType::U8, None).unwrap();
4056 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4057
4058 let (result, _src, cpu_dst) = convert_img(
4059 &mut cpu_converter,
4060 src,
4061 cpu_dst,
4062 Rotation::None,
4063 Flip::None,
4064 Crop::no_crop(),
4065 );
4066 result.unwrap();
4067
4068 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.98, function!());
4070 }
4071
4072 #[test]
4073 fn test_yuyv_to_rgba_resize_cpu() {
4074 let src = load_bytes_to_tensor(
4075 1280,
4076 720,
4077 PixelFormat::Yuyv,
4078 None,
4079 include_bytes!(concat!(
4080 env!("CARGO_MANIFEST_DIR"),
4081 "/../../testdata/camera720p.yuyv"
4082 )),
4083 )
4084 .unwrap();
4085
4086 let (dst_width, dst_height) = (960, 540);
4087
4088 let dst =
4089 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
4090 let mut cpu_converter = CPUProcessor::new();
4091
4092 let (result, _src, dst) = convert_img(
4093 &mut cpu_converter,
4094 src,
4095 dst,
4096 Rotation::None,
4097 Flip::None,
4098 Crop::no_crop(),
4099 );
4100 result.unwrap();
4101
4102 let dst_target =
4103 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
4104 let src_target = load_bytes_to_tensor(
4105 1280,
4106 720,
4107 PixelFormat::Rgba,
4108 None,
4109 include_bytes!(concat!(
4110 env!("CARGO_MANIFEST_DIR"),
4111 "/../../testdata/camera720p.rgba"
4112 )),
4113 )
4114 .unwrap();
4115 let (result, _src_target, dst_target) = convert_img(
4116 &mut cpu_converter,
4117 src_target,
4118 dst_target,
4119 Rotation::None,
4120 Flip::None,
4121 Crop::no_crop(),
4122 );
4123 result.unwrap();
4124
4125 compare_images(&dst, &dst_target, 0.98, function!());
4126 }
4127
4128 #[test]
4129 #[cfg(target_os = "linux")]
4130 fn test_yuyv_to_rgba_crop_flip_g2d() {
4131 if !is_g2d_available() {
4132 eprintln!(
4133 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
4134 );
4135 return;
4136 }
4137 if !is_dma_available() {
4138 eprintln!(
4139 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4140 );
4141 return;
4142 }
4143
4144 let src = load_bytes_to_tensor(
4145 1280,
4146 720,
4147 PixelFormat::Yuyv,
4148 Some(TensorMemory::Dma),
4149 include_bytes!(concat!(
4150 env!("CARGO_MANIFEST_DIR"),
4151 "/../../testdata/camera720p.yuyv"
4152 )),
4153 )
4154 .unwrap();
4155
4156 let (dst_width, dst_height) = (640, 640);
4157
4158 let dst_g2d = TensorDyn::image(
4159 dst_width,
4160 dst_height,
4161 PixelFormat::Rgba,
4162 DType::U8,
4163 Some(TensorMemory::Dma),
4164 )
4165 .unwrap();
4166 let mut g2d_converter = G2DProcessor::new().unwrap();
4167 let crop = Crop {
4168 src_rect: Some(Rect {
4169 left: 20,
4170 top: 15,
4171 width: 400,
4172 height: 300,
4173 }),
4174 dst_rect: None,
4175 dst_color: None,
4176 };
4177
4178 let (result, src, dst_g2d) = convert_img(
4179 &mut g2d_converter,
4180 src,
4181 dst_g2d,
4182 Rotation::None,
4183 Flip::Horizontal,
4184 crop,
4185 );
4186 result.unwrap();
4187
4188 let dst_cpu = TensorDyn::image(
4189 dst_width,
4190 dst_height,
4191 PixelFormat::Rgba,
4192 DType::U8,
4193 Some(TensorMemory::Dma),
4194 )
4195 .unwrap();
4196 let mut cpu_converter = CPUProcessor::new();
4197
4198 let (result, _src, dst_cpu) = convert_img(
4199 &mut cpu_converter,
4200 src,
4201 dst_cpu,
4202 Rotation::None,
4203 Flip::Horizontal,
4204 crop,
4205 );
4206 result.unwrap();
4207 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
4208 }
4209
4210 #[test]
4211 #[cfg(target_os = "linux")]
4212 #[cfg(feature = "opengl")]
4213 fn test_yuyv_to_rgba_crop_flip_opengl() {
4214 if !is_opengl_available() {
4215 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4216 return;
4217 }
4218
4219 if !is_dma_available() {
4220 eprintln!(
4221 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4222 function!()
4223 );
4224 return;
4225 }
4226
4227 let src = load_bytes_to_tensor(
4228 1280,
4229 720,
4230 PixelFormat::Yuyv,
4231 Some(TensorMemory::Dma),
4232 include_bytes!(concat!(
4233 env!("CARGO_MANIFEST_DIR"),
4234 "/../../testdata/camera720p.yuyv"
4235 )),
4236 )
4237 .unwrap();
4238
4239 let (dst_width, dst_height) = (640, 640);
4240
4241 let dst_gl = TensorDyn::image(
4242 dst_width,
4243 dst_height,
4244 PixelFormat::Rgba,
4245 DType::U8,
4246 Some(TensorMemory::Dma),
4247 )
4248 .unwrap();
4249 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4250 let crop = Crop {
4251 src_rect: Some(Rect {
4252 left: 20,
4253 top: 15,
4254 width: 400,
4255 height: 300,
4256 }),
4257 dst_rect: None,
4258 dst_color: None,
4259 };
4260
4261 let (result, src, dst_gl) = convert_img(
4262 &mut gl_converter,
4263 src,
4264 dst_gl,
4265 Rotation::None,
4266 Flip::Horizontal,
4267 crop,
4268 );
4269 result.unwrap();
4270
4271 let dst_cpu = TensorDyn::image(
4272 dst_width,
4273 dst_height,
4274 PixelFormat::Rgba,
4275 DType::U8,
4276 Some(TensorMemory::Dma),
4277 )
4278 .unwrap();
4279 let mut cpu_converter = CPUProcessor::new();
4280
4281 let (result, _src, dst_cpu) = convert_img(
4282 &mut cpu_converter,
4283 src,
4284 dst_cpu,
4285 Rotation::None,
4286 Flip::Horizontal,
4287 crop,
4288 );
4289 result.unwrap();
4290 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
4291 }
4292
4293 #[test]
4294 fn test_vyuy_to_rgba_cpu() {
4295 let file = include_bytes!(concat!(
4296 env!("CARGO_MANIFEST_DIR"),
4297 "/../../testdata/camera720p.vyuy"
4298 ))
4299 .to_vec();
4300 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
4301 src.as_u8()
4302 .unwrap()
4303 .map()
4304 .unwrap()
4305 .as_mut_slice()
4306 .copy_from_slice(&file);
4307
4308 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4309 let mut cpu_converter = CPUProcessor::new();
4310
4311 let (result, _src, dst) = convert_img(
4312 &mut cpu_converter,
4313 src,
4314 dst,
4315 Rotation::None,
4316 Flip::None,
4317 Crop::no_crop(),
4318 );
4319 result.unwrap();
4320
4321 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4322 target_image
4323 .as_u8()
4324 .unwrap()
4325 .map()
4326 .unwrap()
4327 .as_mut_slice()
4328 .copy_from_slice(include_bytes!(concat!(
4329 env!("CARGO_MANIFEST_DIR"),
4330 "/../../testdata/camera720p.rgba"
4331 )));
4332
4333 compare_images(&dst, &target_image, 0.98, function!());
4334 }
4335
4336 #[test]
4337 fn test_vyuy_to_rgb_cpu() {
4338 let file = include_bytes!(concat!(
4339 env!("CARGO_MANIFEST_DIR"),
4340 "/../../testdata/camera720p.vyuy"
4341 ))
4342 .to_vec();
4343 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
4344 src.as_u8()
4345 .unwrap()
4346 .map()
4347 .unwrap()
4348 .as_mut_slice()
4349 .copy_from_slice(&file);
4350
4351 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4352 let mut cpu_converter = CPUProcessor::new();
4353
4354 let (result, _src, dst) = convert_img(
4355 &mut cpu_converter,
4356 src,
4357 dst,
4358 Rotation::None,
4359 Flip::None,
4360 Crop::no_crop(),
4361 );
4362 result.unwrap();
4363
4364 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4365 target_image
4366 .as_u8()
4367 .unwrap()
4368 .map()
4369 .unwrap()
4370 .as_mut_slice()
4371 .as_chunks_mut::<3>()
4372 .0
4373 .iter_mut()
4374 .zip(
4375 include_bytes!(concat!(
4376 env!("CARGO_MANIFEST_DIR"),
4377 "/../../testdata/camera720p.rgba"
4378 ))
4379 .as_chunks::<4>()
4380 .0,
4381 )
4382 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
4383
4384 compare_images(&dst, &target_image, 0.98, function!());
4385 }
4386
4387 #[test]
4388 #[cfg(target_os = "linux")]
4389 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
4390 fn test_vyuy_to_rgba_g2d() {
4391 if !is_g2d_available() {
4392 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
4393 return;
4394 }
4395 if !is_dma_available() {
4396 eprintln!(
4397 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4398 );
4399 return;
4400 }
4401
4402 let src = load_bytes_to_tensor(
4403 1280,
4404 720,
4405 PixelFormat::Vyuy,
4406 None,
4407 include_bytes!(concat!(
4408 env!("CARGO_MANIFEST_DIR"),
4409 "/../../testdata/camera720p.vyuy"
4410 )),
4411 )
4412 .unwrap();
4413
4414 let dst = TensorDyn::image(
4415 1280,
4416 720,
4417 PixelFormat::Rgba,
4418 DType::U8,
4419 Some(TensorMemory::Dma),
4420 )
4421 .unwrap();
4422 let mut g2d_converter = G2DProcessor::new().unwrap();
4423
4424 let (result, _src, dst) = convert_img(
4425 &mut g2d_converter,
4426 src,
4427 dst,
4428 Rotation::None,
4429 Flip::None,
4430 Crop::no_crop(),
4431 );
4432 match result {
4433 Err(Error::G2D(_)) => {
4434 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
4435 return;
4436 }
4437 r => r.unwrap(),
4438 }
4439
4440 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4441 target_image
4442 .as_u8()
4443 .unwrap()
4444 .map()
4445 .unwrap()
4446 .as_mut_slice()
4447 .copy_from_slice(include_bytes!(concat!(
4448 env!("CARGO_MANIFEST_DIR"),
4449 "/../../testdata/camera720p.rgba"
4450 )));
4451
4452 compare_images(&dst, &target_image, 0.98, function!());
4453 }
4454
4455 #[test]
4456 #[cfg(target_os = "linux")]
4457 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
4458 fn test_vyuy_to_rgb_g2d() {
4459 if !is_g2d_available() {
4460 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
4461 return;
4462 }
4463 if !is_dma_available() {
4464 eprintln!(
4465 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4466 );
4467 return;
4468 }
4469
4470 let src = load_bytes_to_tensor(
4471 1280,
4472 720,
4473 PixelFormat::Vyuy,
4474 None,
4475 include_bytes!(concat!(
4476 env!("CARGO_MANIFEST_DIR"),
4477 "/../../testdata/camera720p.vyuy"
4478 )),
4479 )
4480 .unwrap();
4481
4482 let g2d_dst = TensorDyn::image(
4483 1280,
4484 720,
4485 PixelFormat::Rgb,
4486 DType::U8,
4487 Some(TensorMemory::Dma),
4488 )
4489 .unwrap();
4490 let mut g2d_converter = G2DProcessor::new().unwrap();
4491
4492 let (result, src, g2d_dst) = convert_img(
4493 &mut g2d_converter,
4494 src,
4495 g2d_dst,
4496 Rotation::None,
4497 Flip::None,
4498 Crop::no_crop(),
4499 );
4500 match result {
4501 Err(Error::G2D(_)) => {
4502 eprintln!(
4503 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
4504 );
4505 return;
4506 }
4507 r => r.unwrap(),
4508 }
4509
4510 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4511 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4512
4513 let (result, _src, cpu_dst) = convert_img(
4514 &mut cpu_converter,
4515 src,
4516 cpu_dst,
4517 Rotation::None,
4518 Flip::None,
4519 Crop::no_crop(),
4520 );
4521 result.unwrap();
4522
4523 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4524 }
4525
4526 #[test]
4527 #[cfg(target_os = "linux")]
4528 #[cfg(feature = "opengl")]
4529 fn test_vyuy_to_rgba_opengl() {
4530 if !is_opengl_available() {
4531 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4532 return;
4533 }
4534 if !is_dma_available() {
4535 eprintln!(
4536 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4537 function!()
4538 );
4539 return;
4540 }
4541
4542 let src = load_bytes_to_tensor(
4543 1280,
4544 720,
4545 PixelFormat::Vyuy,
4546 Some(TensorMemory::Dma),
4547 include_bytes!(concat!(
4548 env!("CARGO_MANIFEST_DIR"),
4549 "/../../testdata/camera720p.vyuy"
4550 )),
4551 )
4552 .unwrap();
4553
4554 let dst = TensorDyn::image(
4555 1280,
4556 720,
4557 PixelFormat::Rgba,
4558 DType::U8,
4559 Some(TensorMemory::Dma),
4560 )
4561 .unwrap();
4562 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4563
4564 let (result, _src, dst) = convert_img(
4565 &mut gl_converter,
4566 src,
4567 dst,
4568 Rotation::None,
4569 Flip::None,
4570 Crop::no_crop(),
4571 );
4572 match result {
4573 Err(Error::NotSupported(_)) => {
4574 eprintln!(
4575 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
4576 function!()
4577 );
4578 return;
4579 }
4580 r => r.unwrap(),
4581 }
4582
4583 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4584 target_image
4585 .as_u8()
4586 .unwrap()
4587 .map()
4588 .unwrap()
4589 .as_mut_slice()
4590 .copy_from_slice(include_bytes!(concat!(
4591 env!("CARGO_MANIFEST_DIR"),
4592 "/../../testdata/camera720p.rgba"
4593 )));
4594
4595 compare_images(&dst, &target_image, 0.98, function!());
4596 }
4597
4598 #[test]
4599 fn test_nv12_to_rgba_cpu() {
4600 let file = include_bytes!(concat!(
4601 env!("CARGO_MANIFEST_DIR"),
4602 "/../../testdata/zidane.nv12"
4603 ))
4604 .to_vec();
4605 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4606 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4607 .copy_from_slice(&file);
4608
4609 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4610 let mut cpu_converter = CPUProcessor::new();
4611
4612 let (result, _src, dst) = convert_img(
4613 &mut cpu_converter,
4614 src,
4615 dst,
4616 Rotation::None,
4617 Flip::None,
4618 Crop::no_crop(),
4619 );
4620 result.unwrap();
4621
4622 let target_image = crate::load_image(
4623 include_bytes!(concat!(
4624 env!("CARGO_MANIFEST_DIR"),
4625 "/../../testdata/zidane.jpg"
4626 )),
4627 Some(PixelFormat::Rgba),
4628 None,
4629 )
4630 .unwrap();
4631
4632 compare_images(&dst, &target_image, 0.98, function!());
4633 }
4634
4635 #[test]
4636 fn test_nv12_to_rgb_cpu() {
4637 let file = include_bytes!(concat!(
4638 env!("CARGO_MANIFEST_DIR"),
4639 "/../../testdata/zidane.nv12"
4640 ))
4641 .to_vec();
4642 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4643 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4644 .copy_from_slice(&file);
4645
4646 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4647 let mut cpu_converter = CPUProcessor::new();
4648
4649 let (result, _src, dst) = convert_img(
4650 &mut cpu_converter,
4651 src,
4652 dst,
4653 Rotation::None,
4654 Flip::None,
4655 Crop::no_crop(),
4656 );
4657 result.unwrap();
4658
4659 let target_image = crate::load_image(
4660 include_bytes!(concat!(
4661 env!("CARGO_MANIFEST_DIR"),
4662 "/../../testdata/zidane.jpg"
4663 )),
4664 Some(PixelFormat::Rgb),
4665 None,
4666 )
4667 .unwrap();
4668
4669 compare_images(&dst, &target_image, 0.98, function!());
4670 }
4671
4672 #[test]
4673 fn test_nv12_to_grey_cpu() {
4674 let file = include_bytes!(concat!(
4675 env!("CARGO_MANIFEST_DIR"),
4676 "/../../testdata/zidane.nv12"
4677 ))
4678 .to_vec();
4679 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4680 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4681 .copy_from_slice(&file);
4682
4683 let dst = TensorDyn::image(1280, 720, PixelFormat::Grey, DType::U8, None).unwrap();
4684 let mut cpu_converter = CPUProcessor::new();
4685
4686 let (result, _src, dst) = convert_img(
4687 &mut cpu_converter,
4688 src,
4689 dst,
4690 Rotation::None,
4691 Flip::None,
4692 Crop::no_crop(),
4693 );
4694 result.unwrap();
4695
4696 let target_image = crate::load_image(
4697 include_bytes!(concat!(
4698 env!("CARGO_MANIFEST_DIR"),
4699 "/../../testdata/zidane.jpg"
4700 )),
4701 Some(PixelFormat::Grey),
4702 None,
4703 )
4704 .unwrap();
4705
4706 compare_images(&dst, &target_image, 0.98, function!());
4707 }
4708
4709 #[test]
4710 fn test_nv12_to_yuyv_cpu() {
4711 let file = include_bytes!(concat!(
4712 env!("CARGO_MANIFEST_DIR"),
4713 "/../../testdata/zidane.nv12"
4714 ))
4715 .to_vec();
4716 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4717 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4718 .copy_from_slice(&file);
4719
4720 let dst = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
4721 let mut cpu_converter = CPUProcessor::new();
4722
4723 let (result, _src, dst) = convert_img(
4724 &mut cpu_converter,
4725 src,
4726 dst,
4727 Rotation::None,
4728 Flip::None,
4729 Crop::no_crop(),
4730 );
4731 result.unwrap();
4732
4733 let target_image = crate::load_image(
4734 include_bytes!(concat!(
4735 env!("CARGO_MANIFEST_DIR"),
4736 "/../../testdata/zidane.jpg"
4737 )),
4738 Some(PixelFormat::Rgb),
4739 None,
4740 )
4741 .unwrap();
4742
4743 compare_images_convert_to_rgb(&dst, &target_image, 0.98, function!());
4744 }
4745
4746 #[test]
4747 fn test_cpu_resize_planar_rgb() {
4748 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
4749 #[rustfmt::skip]
4750 let src_image = [
4751 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
4752 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4753 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
4754 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4755 ];
4756 src.as_u8()
4757 .unwrap()
4758 .map()
4759 .unwrap()
4760 .as_mut_slice()
4761 .copy_from_slice(&src_image);
4762
4763 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
4764 let mut cpu_converter = CPUProcessor::new();
4765
4766 let (result, _src, cpu_dst) = convert_img(
4767 &mut cpu_converter,
4768 src,
4769 cpu_dst,
4770 Rotation::None,
4771 Flip::None,
4772 Crop::new()
4773 .with_dst_rect(Some(Rect {
4774 left: 1,
4775 top: 1,
4776 width: 4,
4777 height: 4,
4778 }))
4779 .with_dst_color(Some([114, 114, 114, 255])),
4780 );
4781 result.unwrap();
4782
4783 #[rustfmt::skip]
4784 let expected_dst = [
4785 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,
4786 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,
4787 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,
4788 ];
4789
4790 assert_eq!(
4791 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
4792 &expected_dst
4793 );
4794 }
4795
4796 #[test]
4797 fn test_cpu_resize_planar_rgba() {
4798 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
4799 #[rustfmt::skip]
4800 let src_image = [
4801 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
4802 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4803 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
4804 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4805 ];
4806 src.as_u8()
4807 .unwrap()
4808 .map()
4809 .unwrap()
4810 .as_mut_slice()
4811 .copy_from_slice(&src_image);
4812
4813 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgba, DType::U8, None).unwrap();
4814 let mut cpu_converter = CPUProcessor::new();
4815
4816 let (result, _src, cpu_dst) = convert_img(
4817 &mut cpu_converter,
4818 src,
4819 cpu_dst,
4820 Rotation::None,
4821 Flip::None,
4822 Crop::new()
4823 .with_dst_rect(Some(Rect {
4824 left: 1,
4825 top: 1,
4826 width: 4,
4827 height: 4,
4828 }))
4829 .with_dst_color(Some([114, 114, 114, 255])),
4830 );
4831 result.unwrap();
4832
4833 #[rustfmt::skip]
4834 let expected_dst = [
4835 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,
4836 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,
4837 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,
4838 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,
4839 ];
4840
4841 assert_eq!(
4842 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
4843 &expected_dst
4844 );
4845 }
4846
4847 #[test]
4848 #[cfg(target_os = "linux")]
4849 #[cfg(feature = "opengl")]
4850 fn test_opengl_resize_planar_rgb() {
4851 if !is_opengl_available() {
4852 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4853 return;
4854 }
4855
4856 if !is_dma_available() {
4857 eprintln!(
4858 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4859 function!()
4860 );
4861 return;
4862 }
4863
4864 let dst_width = 640;
4865 let dst_height = 640;
4866 let file = include_bytes!(concat!(
4867 env!("CARGO_MANIFEST_DIR"),
4868 "/../../testdata/test_image.jpg"
4869 ))
4870 .to_vec();
4871 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
4872
4873 let cpu_dst = TensorDyn::image(
4874 dst_width,
4875 dst_height,
4876 PixelFormat::PlanarRgb,
4877 DType::U8,
4878 None,
4879 )
4880 .unwrap();
4881 let mut cpu_converter = CPUProcessor::new();
4882 let (result, src, cpu_dst) = convert_img(
4883 &mut cpu_converter,
4884 src,
4885 cpu_dst,
4886 Rotation::None,
4887 Flip::None,
4888 Crop::no_crop(),
4889 );
4890 result.unwrap();
4891 let crop_letterbox = Crop::new()
4892 .with_dst_rect(Some(Rect {
4893 left: 102,
4894 top: 102,
4895 width: 440,
4896 height: 440,
4897 }))
4898 .with_dst_color(Some([114, 114, 114, 114]));
4899 let (result, src, cpu_dst) = convert_img(
4900 &mut cpu_converter,
4901 src,
4902 cpu_dst,
4903 Rotation::None,
4904 Flip::None,
4905 crop_letterbox,
4906 );
4907 result.unwrap();
4908
4909 let gl_dst = TensorDyn::image(
4910 dst_width,
4911 dst_height,
4912 PixelFormat::PlanarRgb,
4913 DType::U8,
4914 None,
4915 )
4916 .unwrap();
4917 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4918
4919 let (result, _src, gl_dst) = convert_img(
4920 &mut gl_converter,
4921 src,
4922 gl_dst,
4923 Rotation::None,
4924 Flip::None,
4925 crop_letterbox,
4926 );
4927 result.unwrap();
4928 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
4929 }
4930
4931 #[test]
4932 fn test_cpu_resize_nv16() {
4933 let file = include_bytes!(concat!(
4934 env!("CARGO_MANIFEST_DIR"),
4935 "/../../testdata/zidane.jpg"
4936 ))
4937 .to_vec();
4938 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
4939
4940 let cpu_nv16_dst = TensorDyn::image(640, 640, PixelFormat::Nv16, DType::U8, None).unwrap();
4941 let cpu_rgb_dst = TensorDyn::image(640, 640, PixelFormat::Rgb, DType::U8, None).unwrap();
4942 let mut cpu_converter = CPUProcessor::new();
4943 let crop = Crop::new()
4944 .with_dst_rect(Some(Rect {
4945 left: 20,
4946 top: 140,
4947 width: 600,
4948 height: 360,
4949 }))
4950 .with_dst_color(Some([255, 128, 0, 255]));
4951
4952 let (result, src, cpu_nv16_dst) = convert_img(
4953 &mut cpu_converter,
4954 src,
4955 cpu_nv16_dst,
4956 Rotation::None,
4957 Flip::None,
4958 crop,
4959 );
4960 result.unwrap();
4961
4962 let (result, _src, cpu_rgb_dst) = convert_img(
4963 &mut cpu_converter,
4964 src,
4965 cpu_rgb_dst,
4966 Rotation::None,
4967 Flip::None,
4968 crop,
4969 );
4970 result.unwrap();
4971 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
4972 }
4973
4974 fn load_bytes_to_tensor(
4975 width: usize,
4976 height: usize,
4977 format: PixelFormat,
4978 memory: Option<TensorMemory>,
4979 bytes: &[u8],
4980 ) -> Result<TensorDyn, Error> {
4981 let src = TensorDyn::image(width, height, format, DType::U8, memory)?;
4982 src.as_u8()
4983 .unwrap()
4984 .map()?
4985 .as_mut_slice()
4986 .copy_from_slice(bytes);
4987 Ok(src)
4988 }
4989
4990 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
4991 assert_eq!(img1.height(), img2.height(), "Heights differ");
4992 assert_eq!(img1.width(), img2.width(), "Widths differ");
4993 assert_eq!(
4994 img1.format().unwrap(),
4995 img2.format().unwrap(),
4996 "PixelFormat differ"
4997 );
4998 assert!(
4999 matches!(
5000 img1.format().unwrap(),
5001 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
5002 ),
5003 "format must be Rgb or Rgba for comparison"
5004 );
5005
5006 let image1 = match img1.format().unwrap() {
5007 PixelFormat::Rgb => image::RgbImage::from_vec(
5008 img1.width().unwrap() as u32,
5009 img1.height().unwrap() as u32,
5010 img1.as_u8().unwrap().map().unwrap().to_vec(),
5011 )
5012 .unwrap(),
5013 PixelFormat::Rgba => image::RgbaImage::from_vec(
5014 img1.width().unwrap() as u32,
5015 img1.height().unwrap() as u32,
5016 img1.as_u8().unwrap().map().unwrap().to_vec(),
5017 )
5018 .unwrap()
5019 .convert(),
5020 PixelFormat::Grey => image::GrayImage::from_vec(
5021 img1.width().unwrap() as u32,
5022 img1.height().unwrap() as u32,
5023 img1.as_u8().unwrap().map().unwrap().to_vec(),
5024 )
5025 .unwrap()
5026 .convert(),
5027 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
5028 img1.width().unwrap() as u32,
5029 (img1.height().unwrap() * 3) as u32,
5030 img1.as_u8().unwrap().map().unwrap().to_vec(),
5031 )
5032 .unwrap()
5033 .convert(),
5034 _ => return,
5035 };
5036
5037 let image2 = match img2.format().unwrap() {
5038 PixelFormat::Rgb => image::RgbImage::from_vec(
5039 img2.width().unwrap() as u32,
5040 img2.height().unwrap() as u32,
5041 img2.as_u8().unwrap().map().unwrap().to_vec(),
5042 )
5043 .unwrap(),
5044 PixelFormat::Rgba => image::RgbaImage::from_vec(
5045 img2.width().unwrap() as u32,
5046 img2.height().unwrap() as u32,
5047 img2.as_u8().unwrap().map().unwrap().to_vec(),
5048 )
5049 .unwrap()
5050 .convert(),
5051 PixelFormat::Grey => image::GrayImage::from_vec(
5052 img2.width().unwrap() as u32,
5053 img2.height().unwrap() as u32,
5054 img2.as_u8().unwrap().map().unwrap().to_vec(),
5055 )
5056 .unwrap()
5057 .convert(),
5058 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
5059 img2.width().unwrap() as u32,
5060 (img2.height().unwrap() * 3) as u32,
5061 img2.as_u8().unwrap().map().unwrap().to_vec(),
5062 )
5063 .unwrap()
5064 .convert(),
5065 _ => return,
5066 };
5067
5068 let similarity = image_compare::rgb_similarity_structure(
5069 &image_compare::Algorithm::RootMeanSquared,
5070 &image1,
5071 &image2,
5072 )
5073 .expect("Image Comparison failed");
5074 if similarity.score < threshold {
5075 similarity
5078 .image
5079 .to_color_map()
5080 .save(format!("{name}.png"))
5081 .unwrap();
5082 panic!(
5083 "{name}: converted image and target image have similarity score too low: {} < {}",
5084 similarity.score, threshold
5085 )
5086 }
5087 }
5088
5089 fn compare_images_convert_to_rgb(
5090 img1: &TensorDyn,
5091 img2: &TensorDyn,
5092 threshold: f64,
5093 name: &str,
5094 ) {
5095 assert_eq!(img1.height(), img2.height(), "Heights differ");
5096 assert_eq!(img1.width(), img2.width(), "Widths differ");
5097
5098 let mut img_rgb1 = TensorDyn::image(
5099 img1.width().unwrap(),
5100 img1.height().unwrap(),
5101 PixelFormat::Rgb,
5102 DType::U8,
5103 Some(TensorMemory::Mem),
5104 )
5105 .unwrap();
5106 let mut img_rgb2 = TensorDyn::image(
5107 img1.width().unwrap(),
5108 img1.height().unwrap(),
5109 PixelFormat::Rgb,
5110 DType::U8,
5111 Some(TensorMemory::Mem),
5112 )
5113 .unwrap();
5114 let mut __cv = CPUProcessor::default();
5115 let r1 = __cv.convert(
5116 img1,
5117 &mut img_rgb1,
5118 crate::Rotation::None,
5119 crate::Flip::None,
5120 crate::Crop::default(),
5121 );
5122 let r2 = __cv.convert(
5123 img2,
5124 &mut img_rgb2,
5125 crate::Rotation::None,
5126 crate::Flip::None,
5127 crate::Crop::default(),
5128 );
5129 if r1.is_err() || r2.is_err() {
5130 let w = img1.width().unwrap() as u32;
5132 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
5133 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
5134 let h1 = (data1.len() as u32) / w;
5135 let h2 = (data2.len() as u32) / w;
5136 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
5137 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
5138 let similarity = image_compare::gray_similarity_structure(
5139 &image_compare::Algorithm::RootMeanSquared,
5140 &g1,
5141 &g2,
5142 )
5143 .expect("Image Comparison failed");
5144 if similarity.score < threshold {
5145 panic!(
5146 "{name}: converted image and target image have similarity score too low: {} < {}",
5147 similarity.score, threshold
5148 )
5149 }
5150 return;
5151 }
5152
5153 let image1 = image::RgbImage::from_vec(
5154 img_rgb1.width().unwrap() as u32,
5155 img_rgb1.height().unwrap() as u32,
5156 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
5157 )
5158 .unwrap();
5159
5160 let image2 = image::RgbImage::from_vec(
5161 img_rgb2.width().unwrap() as u32,
5162 img_rgb2.height().unwrap() as u32,
5163 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
5164 )
5165 .unwrap();
5166
5167 let similarity = image_compare::rgb_similarity_structure(
5168 &image_compare::Algorithm::RootMeanSquared,
5169 &image1,
5170 &image2,
5171 )
5172 .expect("Image Comparison failed");
5173 if similarity.score < threshold {
5174 similarity
5177 .image
5178 .to_color_map()
5179 .save(format!("{name}.png"))
5180 .unwrap();
5181 panic!(
5182 "{name}: converted image and target image have similarity score too low: {} < {}",
5183 similarity.score, threshold
5184 )
5185 }
5186 }
5187
5188 #[test]
5193 fn test_nv12_image_creation() {
5194 let width = 640;
5195 let height = 480;
5196 let img = TensorDyn::image(width, height, PixelFormat::Nv12, DType::U8, None).unwrap();
5197
5198 assert_eq!(img.width(), Some(width));
5199 assert_eq!(img.height(), Some(height));
5200 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
5201 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
5203 }
5204
5205 #[test]
5206 fn test_nv12_channels() {
5207 let img = TensorDyn::image(640, 480, PixelFormat::Nv12, DType::U8, None).unwrap();
5208 assert_eq!(img.format().unwrap().channels(), 1);
5210 }
5211
5212 #[test]
5217 fn test_tensor_set_format_planar() {
5218 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
5219 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
5220 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
5221 assert_eq!(tensor.width(), Some(640));
5222 assert_eq!(tensor.height(), Some(480));
5223 }
5224
5225 #[test]
5226 fn test_tensor_set_format_interleaved() {
5227 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
5228 tensor.set_format(PixelFormat::Rgba).unwrap();
5229 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
5230 assert_eq!(tensor.width(), Some(640));
5231 assert_eq!(tensor.height(), Some(480));
5232 }
5233
5234 #[test]
5235 fn test_tensordyn_image_rgb() {
5236 let img = TensorDyn::image(640, 480, PixelFormat::Rgb, DType::U8, None).unwrap();
5237 assert_eq!(img.width(), Some(640));
5238 assert_eq!(img.height(), Some(480));
5239 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5240 }
5241
5242 #[test]
5243 fn test_tensordyn_image_planar_rgb() {
5244 let img = TensorDyn::image(640, 480, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
5245 assert_eq!(img.width(), Some(640));
5246 assert_eq!(img.height(), Some(480));
5247 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5248 }
5249
5250 #[test]
5251 fn test_rgb_int8_format() {
5252 let img = TensorDyn::image(
5254 1280,
5255 720,
5256 PixelFormat::Rgb,
5257 DType::I8,
5258 Some(TensorMemory::Mem),
5259 )
5260 .unwrap();
5261 assert_eq!(img.width(), Some(1280));
5262 assert_eq!(img.height(), Some(720));
5263 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5264 assert_eq!(img.dtype(), DType::I8);
5265 }
5266
5267 #[test]
5268 fn test_planar_rgb_int8_format() {
5269 let img = TensorDyn::image(
5270 1280,
5271 720,
5272 PixelFormat::PlanarRgb,
5273 DType::I8,
5274 Some(TensorMemory::Mem),
5275 )
5276 .unwrap();
5277 assert_eq!(img.width(), Some(1280));
5278 assert_eq!(img.height(), Some(720));
5279 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5280 assert_eq!(img.dtype(), DType::I8);
5281 }
5282
5283 #[test]
5284 fn test_rgb_from_tensor() {
5285 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
5286 tensor.set_format(PixelFormat::Rgb).unwrap();
5287 let img = TensorDyn::from(tensor);
5288 assert_eq!(img.width(), Some(1280));
5289 assert_eq!(img.height(), Some(720));
5290 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5291 }
5292
5293 #[test]
5294 fn test_planar_rgb_from_tensor() {
5295 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
5296 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
5297 let img = TensorDyn::from(tensor);
5298 assert_eq!(img.width(), Some(1280));
5299 assert_eq!(img.height(), Some(720));
5300 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5301 }
5302
5303 #[test]
5304 fn test_dtype_determines_int8() {
5305 let u8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::U8, None).unwrap();
5307 let i8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::I8, None).unwrap();
5308 assert_eq!(u8_img.dtype(), DType::U8);
5309 assert_eq!(i8_img.dtype(), DType::I8);
5310 }
5311
5312 #[test]
5313 fn test_pixel_layout_packed_vs_planar() {
5314 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
5316 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
5317 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
5318 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
5319 }
5320
5321 #[cfg(target_os = "linux")]
5326 #[cfg(feature = "opengl")]
5327 #[test]
5328 fn test_convert_pbo_to_pbo() {
5329 let mut converter = ImageProcessor::new().unwrap();
5330
5331 let is_pbo = converter
5333 .opengl
5334 .as_ref()
5335 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
5336 if !is_pbo {
5337 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
5338 return;
5339 }
5340
5341 let src_w = 640;
5342 let src_h = 480;
5343 let dst_w = 320;
5344 let dst_h = 240;
5345
5346 let pbo_src = converter
5348 .create_image(src_w, src_h, PixelFormat::Rgba, DType::U8, None)
5349 .unwrap();
5350 assert_eq!(
5351 pbo_src.as_u8().unwrap().memory(),
5352 TensorMemory::Pbo,
5353 "create_image should produce a PBO tensor"
5354 );
5355
5356 let file = include_bytes!(concat!(
5358 env!("CARGO_MANIFEST_DIR"),
5359 "/../../testdata/zidane.jpg"
5360 ))
5361 .to_vec();
5362 let jpeg_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
5363
5364 let mem_src = TensorDyn::image(
5366 src_w,
5367 src_h,
5368 PixelFormat::Rgba,
5369 DType::U8,
5370 Some(TensorMemory::Mem),
5371 )
5372 .unwrap();
5373 let (result, _jpeg_src, mem_src) = convert_img(
5374 &mut CPUProcessor::new(),
5375 jpeg_src,
5376 mem_src,
5377 Rotation::None,
5378 Flip::None,
5379 Crop::no_crop(),
5380 );
5381 result.unwrap();
5382
5383 {
5385 let src_data = mem_src.as_u8().unwrap().map().unwrap();
5386 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
5387 pbo_map.copy_from_slice(&src_data);
5388 }
5389
5390 let pbo_dst = converter
5392 .create_image(dst_w, dst_h, PixelFormat::Rgba, DType::U8, None)
5393 .unwrap();
5394 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
5395
5396 let mut pbo_dst = pbo_dst;
5398 let result = converter.convert(
5399 &pbo_src,
5400 &mut pbo_dst,
5401 Rotation::None,
5402 Flip::None,
5403 Crop::no_crop(),
5404 );
5405 result.unwrap();
5406
5407 let cpu_dst = TensorDyn::image(
5409 dst_w,
5410 dst_h,
5411 PixelFormat::Rgba,
5412 DType::U8,
5413 Some(TensorMemory::Mem),
5414 )
5415 .unwrap();
5416 let (result, _mem_src, cpu_dst) = convert_img(
5417 &mut CPUProcessor::new(),
5418 mem_src,
5419 cpu_dst,
5420 Rotation::None,
5421 Flip::None,
5422 Crop::no_crop(),
5423 );
5424 result.unwrap();
5425
5426 let pbo_dst_img = {
5427 let mut __t = pbo_dst.into_u8().unwrap();
5428 __t.set_format(PixelFormat::Rgba).unwrap();
5429 TensorDyn::from(__t)
5430 };
5431 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
5432 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
5433 }
5434
5435 #[test]
5436 fn test_image_bgra() {
5437 let img = TensorDyn::image(
5438 640,
5439 480,
5440 PixelFormat::Bgra,
5441 DType::U8,
5442 Some(edgefirst_tensor::TensorMemory::Mem),
5443 )
5444 .unwrap();
5445 assert_eq!(img.width(), Some(640));
5446 assert_eq!(img.height(), Some(480));
5447 assert_eq!(img.format().unwrap().channels(), 4);
5448 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
5449 }
5450
5451 #[test]
5456 fn test_force_backend_cpu() {
5457 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5458 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5459 let result = ImageProcessor::new();
5460 match original {
5461 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5462 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5463 }
5464 let converter = result.unwrap();
5465 assert!(converter.cpu.is_some());
5466 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
5467 }
5468
5469 #[test]
5470 fn test_force_backend_invalid() {
5471 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5472 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
5473 let result = ImageProcessor::new();
5474 match original {
5475 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5476 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5477 }
5478 assert!(
5479 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
5480 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
5481 );
5482 }
5483
5484 #[test]
5485 fn test_force_backend_unset() {
5486 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5487 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
5488 let result = ImageProcessor::new();
5489 match original {
5490 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5491 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5492 }
5493 let converter = result.unwrap();
5494 assert!(converter.forced_backend.is_none());
5495 }
5496
5497 #[test]
5502 fn test_draw_proto_masks_no_cpu_returns_error() {
5503 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
5505 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
5506 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
5507 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
5508 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
5509 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
5510
5511 let result = ImageProcessor::new();
5512
5513 match original_cpu {
5514 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
5515 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
5516 }
5517 match original_gl {
5518 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
5519 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
5520 }
5521 match original_g2d {
5522 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
5523 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
5524 }
5525
5526 let mut converter = result.unwrap();
5527 assert!(converter.cpu.is_none(), "CPU should be disabled");
5528
5529 let dst = TensorDyn::image(
5530 640,
5531 480,
5532 PixelFormat::Rgba,
5533 DType::U8,
5534 Some(TensorMemory::Mem),
5535 )
5536 .unwrap();
5537 let mut dst_dyn = dst;
5538 let det = [DetectBox {
5539 bbox: edgefirst_decoder::BoundingBox {
5540 xmin: 0.1,
5541 ymin: 0.1,
5542 xmax: 0.5,
5543 ymax: 0.5,
5544 },
5545 score: 0.9,
5546 label: 0,
5547 }];
5548 let proto_data = ProtoData {
5549 mask_coefficients: vec![vec![0.5; 4]],
5550 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
5551 };
5552 let result =
5553 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
5554 assert!(
5555 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
5556 "draw_proto_masks without CPU should return Internal error: {result:?}"
5557 );
5558 }
5559
5560 #[test]
5561 fn test_draw_proto_masks_cpu_fallback_works() {
5562 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5564 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5565 let result = ImageProcessor::new();
5566 match original {
5567 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5568 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5569 }
5570
5571 let mut converter = result.unwrap();
5572 assert!(converter.cpu.is_some());
5573
5574 let dst = TensorDyn::image(
5575 64,
5576 64,
5577 PixelFormat::Rgba,
5578 DType::U8,
5579 Some(TensorMemory::Mem),
5580 )
5581 .unwrap();
5582 let mut dst_dyn = dst;
5583 let det = [DetectBox {
5584 bbox: edgefirst_decoder::BoundingBox {
5585 xmin: 0.1,
5586 ymin: 0.1,
5587 xmax: 0.5,
5588 ymax: 0.5,
5589 },
5590 score: 0.9,
5591 label: 0,
5592 }];
5593 let proto_data = ProtoData {
5594 mask_coefficients: vec![vec![0.5; 4]],
5595 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
5596 };
5597 let result =
5598 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
5599 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
5600 }
5601
5602 #[test]
5603 fn test_set_format_then_cpu_convert() {
5604 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5606 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5607 let mut processor = ImageProcessor::new().unwrap();
5608 match original {
5609 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5610 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5611 }
5612
5613 let image = include_bytes!(concat!(
5615 env!("CARGO_MANIFEST_DIR"),
5616 "/../../testdata/zidane.jpg"
5617 ));
5618 let src = load_image(image, Some(PixelFormat::Rgba), None).unwrap();
5619
5620 let mut dst =
5622 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
5623 dst.set_format(PixelFormat::Rgb).unwrap();
5624
5625 processor
5627 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
5628 .unwrap();
5629
5630 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
5632 assert_eq!(dst.width(), Some(640));
5633 assert_eq!(dst.height(), Some(640));
5634 }
5635
5636 #[test]
5642 fn test_multiple_image_processors_same_thread() {
5643 let mut processors: Vec<ImageProcessor> = (0..4)
5644 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
5645 .collect();
5646
5647 for proc in &mut processors {
5648 let src = proc
5649 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
5650 .expect("create src failed");
5651 let mut dst = proc
5652 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
5653 .expect("create dst failed");
5654 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
5655 .expect("convert failed");
5656 assert_eq!(dst.width(), Some(64));
5657 assert_eq!(dst.height(), Some(64));
5658 }
5659 }
5660
5661 #[test]
5668 fn test_multiple_image_processors_separate_threads() {
5669 use std::sync::mpsc;
5670 use std::time::Duration;
5671
5672 const TIMEOUT: Duration = Duration::from_secs(60);
5673
5674 let (tx, rx) = mpsc::channel::<()>();
5675
5676 std::thread::spawn(move || {
5677 let handles: Vec<_> = (0..4)
5678 .map(|i| {
5679 std::thread::spawn(move || {
5680 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
5681 panic!("ImageProcessor::new() failed on thread {i}: {e}")
5682 });
5683 let src = proc
5684 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
5685 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
5686 let mut dst = proc
5687 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
5688 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
5689 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
5690 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
5691 assert_eq!(dst.width(), Some(64));
5692 assert_eq!(dst.height(), Some(64));
5693 })
5694 })
5695 .collect();
5696
5697 for (i, h) in handles.into_iter().enumerate() {
5698 h.join()
5699 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
5700 }
5701
5702 let _ = tx.send(());
5703 });
5704
5705 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
5706 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
5707 });
5708 }
5709
5710 #[test]
5717 fn test_image_processors_concurrent_operations() {
5718 use std::sync::{mpsc, Arc, Barrier};
5719 use std::time::Duration;
5720
5721 const N: usize = 4;
5722 const ROUNDS: usize = 10;
5723 const TIMEOUT: Duration = Duration::from_secs(60);
5724
5725 let (tx, rx) = mpsc::channel::<()>();
5726
5727 std::thread::spawn(move || {
5728 let barrier = Arc::new(Barrier::new(N));
5729
5730 let handles: Vec<_> = (0..N)
5731 .map(|i| {
5732 let barrier = Arc::clone(&barrier);
5733 std::thread::spawn(move || {
5734 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
5735 panic!("ImageProcessor::new() failed on thread {i}: {e}")
5736 });
5737
5738 barrier.wait();
5740
5741 for round in 0..ROUNDS {
5743 let src = proc
5744 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
5745 .unwrap_or_else(|e| {
5746 panic!("create src failed on thread {i} round {round}: {e}")
5747 });
5748 let mut dst = proc
5749 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
5750 .unwrap_or_else(|e| {
5751 panic!("create dst failed on thread {i} round {round}: {e}")
5752 });
5753 proc.convert(
5754 &src,
5755 &mut dst,
5756 Rotation::None,
5757 Flip::None,
5758 Crop::default(),
5759 )
5760 .unwrap_or_else(|e| {
5761 panic!("convert failed on thread {i} round {round}: {e}")
5762 });
5763 assert_eq!(dst.width(), Some(64));
5764 assert_eq!(dst.height(), Some(64));
5765 }
5766 })
5767 })
5768 .collect();
5769
5770 for (i, h) in handles.into_iter().enumerate() {
5771 h.join()
5772 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
5773 }
5774
5775 let _ = tx.send(());
5776 });
5777
5778 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
5779 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
5780 });
5781 }
5782}