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
90#[derive(Debug, Clone)]
95pub(crate) struct MaskResult {
96 pub(crate) x: usize,
98 pub(crate) y: usize,
100 pub(crate) w: usize,
102 pub(crate) h: usize,
104 pub(crate) pixels: Vec<u8>,
106}
107
108#[must_use]
114#[derive(Debug, Clone, Copy)]
115pub struct MaskRegion {
116 pub atlas_y_offset: usize,
118 pub padded_x: usize,
120 pub padded_y: usize,
122 pub padded_w: usize,
124 pub padded_h: usize,
126 pub bbox_x: usize,
128 pub bbox_y: usize,
130 pub bbox_w: usize,
132 pub bbox_h: usize,
134}
135
136mod cpu;
137mod error;
138mod g2d;
139#[path = "gl/mod.rs"]
140mod opengl_headless;
141
142fn rotate_flip_to_dyn(
147 src: &Tensor<u8>,
148 src_fmt: PixelFormat,
149 rotation: Rotation,
150 flip: Flip,
151 memory: Option<TensorMemory>,
152) -> Result<TensorDyn, Error> {
153 let src_w = src.width().unwrap();
154 let src_h = src.height().unwrap();
155 let channels = src_fmt.channels();
156
157 let (dst_w, dst_h) = match rotation {
158 Rotation::None | Rotation::Rotate180 => (src_w, src_h),
159 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (src_h, src_w),
160 };
161
162 let dst = Tensor::<u8>::image(dst_w, dst_h, src_fmt, memory)?;
163 let src_map = src.map()?;
164 let mut dst_map = dst.map()?;
165
166 CPUProcessor::flip_rotate_ndarray_pf(
167 &src_map,
168 &mut dst_map,
169 dst_w,
170 dst_h,
171 channels,
172 rotation,
173 flip,
174 )?;
175 drop(dst_map);
176 drop(src_map);
177
178 Ok(TensorDyn::from(dst))
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum Rotation {
183 None = 0,
184 Clockwise90 = 1,
185 Rotate180 = 2,
186 CounterClockwise90 = 3,
187}
188impl Rotation {
189 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
202 match angle.rem_euclid(360) {
203 0 => Rotation::None,
204 90 => Rotation::Clockwise90,
205 180 => Rotation::Rotate180,
206 270 => Rotation::CounterClockwise90,
207 _ => panic!("rotation angle is not a multiple of 90"),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Flip {
214 None = 0,
215 Vertical = 1,
216 Horizontal = 2,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct Crop {
221 pub src_rect: Option<Rect>,
222 pub dst_rect: Option<Rect>,
223 pub dst_color: Option<[u8; 4]>,
224}
225
226impl Default for Crop {
227 fn default() -> Self {
228 Crop::new()
229 }
230}
231impl Crop {
232 pub fn new() -> Self {
234 Crop {
235 src_rect: None,
236 dst_rect: None,
237 dst_color: None,
238 }
239 }
240
241 pub fn with_src_rect(mut self, src_rect: Option<Rect>) -> Self {
243 self.src_rect = src_rect;
244 self
245 }
246
247 pub fn with_dst_rect(mut self, dst_rect: Option<Rect>) -> Self {
249 self.dst_rect = dst_rect;
250 self
251 }
252
253 pub fn with_dst_color(mut self, dst_color: Option<[u8; 4]>) -> Self {
255 self.dst_color = dst_color;
256 self
257 }
258
259 pub fn no_crop() -> Self {
261 Crop::new()
262 }
263
264 pub(crate) fn check_crop_dims(
266 &self,
267 src_w: usize,
268 src_h: usize,
269 dst_w: usize,
270 dst_h: usize,
271 ) -> Result<(), Error> {
272 let src_ok = self
273 .src_rect
274 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
275 let dst_ok = self
276 .dst_rect
277 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
278 match (src_ok, dst_ok) {
279 (true, true) => Ok(()),
280 (true, false) => Err(Error::CropInvalid(format!(
281 "Dest crop invalid: {:?}",
282 self.dst_rect
283 ))),
284 (false, true) => Err(Error::CropInvalid(format!(
285 "Src crop invalid: {:?}",
286 self.src_rect
287 ))),
288 (false, false) => Err(Error::CropInvalid(format!(
289 "Dest and Src crop invalid: {:?} {:?}",
290 self.dst_rect, self.src_rect
291 ))),
292 }
293 }
294
295 pub fn check_crop_dyn(
297 &self,
298 src: &edgefirst_tensor::TensorDyn,
299 dst: &edgefirst_tensor::TensorDyn,
300 ) -> Result<(), Error> {
301 self.check_crop_dims(
302 src.width().unwrap_or(0),
303 src.height().unwrap_or(0),
304 dst.width().unwrap_or(0),
305 dst.height().unwrap_or(0),
306 )
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub struct Rect {
312 pub left: usize,
313 pub top: usize,
314 pub width: usize,
315 pub height: usize,
316}
317
318impl Rect {
319 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
321 Self {
322 left,
323 top,
324 width,
325 height,
326 }
327 }
328
329 pub fn check_rect_dyn(&self, image: &TensorDyn) -> bool {
331 let w = image.width().unwrap_or(0);
332 let h = image.height().unwrap_or(0);
333 self.left + self.width <= w && self.top + self.height <= h
334 }
335}
336
337#[enum_dispatch(ImageProcessor)]
338pub trait ImageProcessorTrait {
339 fn convert(
355 &mut self,
356 src: &TensorDyn,
357 dst: &mut TensorDyn,
358 rotation: Rotation,
359 flip: Flip,
360 crop: Crop,
361 ) -> Result<()>;
362
363 fn draw_masks(
379 &mut self,
380 dst: &mut TensorDyn,
381 detect: &[DetectBox],
382 segmentation: &[Segmentation],
383 ) -> Result<()>;
384
385 fn draw_masks_proto(
400 &mut self,
401 dst: &mut TensorDyn,
402 detect: &[DetectBox],
403 proto_data: &ProtoData,
404 ) -> Result<()>;
405
406 fn decode_masks_atlas(
421 &mut self,
422 detect: &[DetectBox],
423 proto_data: ProtoData,
424 output_width: usize,
425 output_height: usize,
426 ) -> Result<(Vec<u8>, Vec<MaskRegion>)>;
427
428 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
431}
432
433#[derive(Debug, Clone, Default)]
439pub struct ImageProcessorConfig {
440 #[cfg(target_os = "linux")]
448 #[cfg(feature = "opengl")]
449 pub egl_display: Option<EglDisplayKind>,
450
451 pub backend: ComputeBackend,
463}
464
465#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
472pub enum ComputeBackend {
473 #[default]
475 Auto,
476 Cpu,
478 G2d,
480 OpenGl,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
490pub(crate) enum ForcedBackend {
491 Cpu,
492 G2d,
493 OpenGl,
494}
495
496#[derive(Debug)]
499pub struct ImageProcessor {
500 pub cpu: Option<CPUProcessor>,
503
504 #[cfg(target_os = "linux")]
505 pub g2d: Option<G2DProcessor>,
509 #[cfg(target_os = "linux")]
510 #[cfg(feature = "opengl")]
511 pub opengl: Option<GLProcessorThreaded>,
515
516 pub(crate) forced_backend: Option<ForcedBackend>,
518}
519
520unsafe impl Send for ImageProcessor {}
521unsafe impl Sync for ImageProcessor {}
522
523impl ImageProcessor {
524 pub fn new() -> Result<Self> {
542 Self::with_config(ImageProcessorConfig::default())
543 }
544
545 #[allow(unused_variables)]
554 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
555 match config.backend {
559 ComputeBackend::Cpu => {
560 log::info!("ComputeBackend::Cpu — CPU only");
561 return Ok(Self {
562 cpu: Some(CPUProcessor::new()),
563 #[cfg(target_os = "linux")]
564 g2d: None,
565 #[cfg(target_os = "linux")]
566 #[cfg(feature = "opengl")]
567 opengl: None,
568 forced_backend: None,
569 });
570 }
571 ComputeBackend::G2d => {
572 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
573 #[cfg(target_os = "linux")]
574 {
575 let g2d = match G2DProcessor::new() {
576 Ok(g) => Some(g),
577 Err(e) => {
578 log::warn!("G2D requested but failed to initialize: {e:?}");
579 None
580 }
581 };
582 return Ok(Self {
583 cpu: Some(CPUProcessor::new()),
584 g2d,
585 #[cfg(feature = "opengl")]
586 opengl: None,
587 forced_backend: None,
588 });
589 }
590 #[cfg(not(target_os = "linux"))]
591 {
592 log::warn!("G2D requested but not available on this platform, using CPU");
593 return Ok(Self {
594 cpu: Some(CPUProcessor::new()),
595 forced_backend: None,
596 });
597 }
598 }
599 ComputeBackend::OpenGl => {
600 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
601 #[cfg(target_os = "linux")]
602 {
603 #[cfg(feature = "opengl")]
604 let opengl = match GLProcessorThreaded::new(config.egl_display) {
605 Ok(gl) => Some(gl),
606 Err(e) => {
607 log::warn!("OpenGL requested but failed to initialize: {e:?}");
608 None
609 }
610 };
611 return Ok(Self {
612 cpu: Some(CPUProcessor::new()),
613 g2d: None,
614 #[cfg(feature = "opengl")]
615 opengl,
616 forced_backend: None,
617 });
618 }
619 #[cfg(not(target_os = "linux"))]
620 {
621 log::warn!("OpenGL requested but not available on this platform, using CPU");
622 return Ok(Self {
623 cpu: Some(CPUProcessor::new()),
624 forced_backend: None,
625 });
626 }
627 }
628 ComputeBackend::Auto => { }
629 }
630
631 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
636 let val_lower = val.to_lowercase();
637 let forced = match val_lower.as_str() {
638 "cpu" => ForcedBackend::Cpu,
639 "g2d" => ForcedBackend::G2d,
640 "opengl" => ForcedBackend::OpenGl,
641 other => {
642 return Err(Error::ForcedBackendUnavailable(format!(
643 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
644 )));
645 }
646 };
647
648 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
649
650 return match forced {
651 ForcedBackend::Cpu => Ok(Self {
652 cpu: Some(CPUProcessor::new()),
653 #[cfg(target_os = "linux")]
654 g2d: None,
655 #[cfg(target_os = "linux")]
656 #[cfg(feature = "opengl")]
657 opengl: None,
658 forced_backend: Some(ForcedBackend::Cpu),
659 }),
660 ForcedBackend::G2d => {
661 #[cfg(target_os = "linux")]
662 {
663 let g2d = G2DProcessor::new().map_err(|e| {
664 Error::ForcedBackendUnavailable(format!(
665 "g2d forced but failed to initialize: {e:?}"
666 ))
667 })?;
668 Ok(Self {
669 cpu: None,
670 g2d: Some(g2d),
671 #[cfg(feature = "opengl")]
672 opengl: None,
673 forced_backend: Some(ForcedBackend::G2d),
674 })
675 }
676 #[cfg(not(target_os = "linux"))]
677 {
678 Err(Error::ForcedBackendUnavailable(
679 "g2d backend is only available on Linux".into(),
680 ))
681 }
682 }
683 ForcedBackend::OpenGl => {
684 #[cfg(target_os = "linux")]
685 #[cfg(feature = "opengl")]
686 {
687 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
688 Error::ForcedBackendUnavailable(format!(
689 "opengl forced but failed to initialize: {e:?}"
690 ))
691 })?;
692 Ok(Self {
693 cpu: None,
694 g2d: None,
695 opengl: Some(opengl),
696 forced_backend: Some(ForcedBackend::OpenGl),
697 })
698 }
699 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
700 {
701 Err(Error::ForcedBackendUnavailable(
702 "opengl backend requires Linux with the 'opengl' feature enabled"
703 .into(),
704 ))
705 }
706 }
707 };
708 }
709
710 #[cfg(target_os = "linux")]
712 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
713 .map(|x| x != "0" && x.to_lowercase() != "false")
714 .unwrap_or(false)
715 {
716 log::debug!("EDGEFIRST_DISABLE_G2D is set");
717 None
718 } else {
719 match G2DProcessor::new() {
720 Ok(g2d_converter) => Some(g2d_converter),
721 Err(err) => {
722 log::warn!("Failed to initialize G2D converter: {err:?}");
723 None
724 }
725 }
726 };
727
728 #[cfg(target_os = "linux")]
729 #[cfg(feature = "opengl")]
730 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
731 .map(|x| x != "0" && x.to_lowercase() != "false")
732 .unwrap_or(false)
733 {
734 log::debug!("EDGEFIRST_DISABLE_GL is set");
735 None
736 } else {
737 match GLProcessorThreaded::new(config.egl_display) {
738 Ok(gl_converter) => Some(gl_converter),
739 Err(err) => {
740 log::warn!("Failed to initialize GL converter: {err:?}");
741 None
742 }
743 }
744 };
745
746 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
747 .map(|x| x != "0" && x.to_lowercase() != "false")
748 .unwrap_or(false)
749 {
750 log::debug!("EDGEFIRST_DISABLE_CPU is set");
751 None
752 } else {
753 Some(CPUProcessor::new())
754 };
755 Ok(Self {
756 cpu,
757 #[cfg(target_os = "linux")]
758 g2d,
759 #[cfg(target_os = "linux")]
760 #[cfg(feature = "opengl")]
761 opengl,
762 forced_backend: None,
763 })
764 }
765
766 #[cfg(target_os = "linux")]
769 #[cfg(feature = "opengl")]
770 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
771 if let Some(ref mut gl) = self.opengl {
772 gl.set_int8_interpolation_mode(mode)?;
773 }
774 Ok(())
775 }
776
777 pub fn create_image(
810 &self,
811 width: usize,
812 height: usize,
813 format: PixelFormat,
814 dtype: DType,
815 memory: Option<TensorMemory>,
816 ) -> Result<TensorDyn> {
817 if let Some(mem) = memory {
819 return Ok(TensorDyn::image(width, height, format, dtype, Some(mem))?);
820 }
821
822 #[cfg(target_os = "linux")]
825 {
826 #[cfg(feature = "opengl")]
827 let gl_uses_pbo = self
828 .opengl
829 .as_ref()
830 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
831 #[cfg(not(feature = "opengl"))]
832 let gl_uses_pbo = false;
833
834 if !gl_uses_pbo {
835 if let Ok(img) = TensorDyn::image(
836 width,
837 height,
838 format,
839 dtype,
840 Some(edgefirst_tensor::TensorMemory::Dma),
841 ) {
842 return Ok(img);
843 }
844 }
845 }
846
847 #[cfg(target_os = "linux")]
851 #[cfg(feature = "opengl")]
852 if dtype.size() == 1 {
853 if let Some(gl) = &self.opengl {
854 match gl.create_pbo_image(width, height, format) {
855 Ok(t) => {
856 if dtype == DType::I8 {
857 debug_assert!(
865 t.chroma().is_none(),
866 "PBO i8 transmute requires chroma == None"
867 );
868 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
869 return Ok(TensorDyn::from(t_i8));
870 }
871 return Ok(TensorDyn::from(t));
872 }
873 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
874 }
875 }
876 }
877
878 Ok(TensorDyn::image(
880 width,
881 height,
882 format,
883 dtype,
884 Some(edgefirst_tensor::TensorMemory::Mem),
885 )?)
886 }
887}
888
889impl ImageProcessorTrait for ImageProcessor {
890 fn convert(
896 &mut self,
897 src: &TensorDyn,
898 dst: &mut TensorDyn,
899 rotation: Rotation,
900 flip: Flip,
901 crop: Crop,
902 ) -> Result<()> {
903 let start = Instant::now();
904 let src_fmt = src.format();
905 let dst_fmt = dst.format();
906 log::trace!(
907 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
908 rotation={rotation:?}, flip={flip:?}, backend={:?}",
909 src.dtype(),
910 src.memory(),
911 dst.dtype(),
912 dst.memory(),
913 self.forced_backend,
914 );
915
916 if let Some(forced) = self.forced_backend {
918 return match forced {
919 ForcedBackend::Cpu => {
920 if let Some(cpu) = self.cpu.as_mut() {
921 let r = cpu.convert(src, dst, rotation, flip, crop);
922 log::trace!(
923 "convert: forced=cpu result={} ({:?})",
924 if r.is_ok() { "ok" } else { "err" },
925 start.elapsed()
926 );
927 return r;
928 }
929 Err(Error::ForcedBackendUnavailable("cpu".into()))
930 }
931 ForcedBackend::G2d => {
932 #[cfg(target_os = "linux")]
933 if let Some(g2d) = self.g2d.as_mut() {
934 let r = g2d.convert(src, dst, rotation, flip, crop);
935 log::trace!(
936 "convert: forced=g2d result={} ({:?})",
937 if r.is_ok() { "ok" } else { "err" },
938 start.elapsed()
939 );
940 return r;
941 }
942 Err(Error::ForcedBackendUnavailable("g2d".into()))
943 }
944 ForcedBackend::OpenGl => {
945 #[cfg(target_os = "linux")]
946 #[cfg(feature = "opengl")]
947 if let Some(opengl) = self.opengl.as_mut() {
948 let r = opengl.convert(src, dst, rotation, flip, crop);
949 log::trace!(
950 "convert: forced=opengl result={} ({:?})",
951 if r.is_ok() { "ok" } else { "err" },
952 start.elapsed()
953 );
954 return r;
955 }
956 Err(Error::ForcedBackendUnavailable("opengl".into()))
957 }
958 };
959 }
960
961 #[cfg(target_os = "linux")]
963 #[cfg(feature = "opengl")]
964 if let Some(opengl) = self.opengl.as_mut() {
965 match opengl.convert(src, dst, rotation, flip, crop) {
966 Ok(_) => {
967 log::trace!(
968 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
969 start.elapsed()
970 );
971 return Ok(());
972 }
973 Err(e) => {
974 log::trace!("convert: auto opengl declined {src_fmt:?}→{dst_fmt:?}: {e}");
975 }
976 }
977 }
978
979 #[cfg(target_os = "linux")]
980 if let Some(g2d) = self.g2d.as_mut() {
981 match g2d.convert(src, dst, rotation, flip, crop) {
982 Ok(_) => {
983 log::trace!(
984 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
985 start.elapsed()
986 );
987 return Ok(());
988 }
989 Err(e) => {
990 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
991 }
992 }
993 }
994
995 if let Some(cpu) = self.cpu.as_mut() {
996 match cpu.convert(src, dst, rotation, flip, crop) {
997 Ok(_) => {
998 log::trace!(
999 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
1000 start.elapsed()
1001 );
1002 return Ok(());
1003 }
1004 Err(e) => {
1005 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
1006 return Err(e);
1007 }
1008 }
1009 }
1010 Err(Error::NoConverter)
1011 }
1012
1013 fn draw_masks(
1014 &mut self,
1015 dst: &mut TensorDyn,
1016 detect: &[DetectBox],
1017 segmentation: &[Segmentation],
1018 ) -> Result<()> {
1019 let start = Instant::now();
1020
1021 if detect.is_empty() && segmentation.is_empty() {
1022 return Ok(());
1023 }
1024
1025 if let Some(forced) = self.forced_backend {
1027 return match forced {
1028 ForcedBackend::Cpu => {
1029 if let Some(cpu) = self.cpu.as_mut() {
1030 return cpu.draw_masks(dst, detect, segmentation);
1031 }
1032 Err(Error::ForcedBackendUnavailable("cpu".into()))
1033 }
1034 ForcedBackend::G2d => Err(Error::NotSupported(
1035 "g2d does not support draw_masks".into(),
1036 )),
1037 ForcedBackend::OpenGl => {
1038 #[cfg(target_os = "linux")]
1039 #[cfg(feature = "opengl")]
1040 if let Some(opengl) = self.opengl.as_mut() {
1041 return opengl.draw_masks(dst, detect, segmentation);
1042 }
1043 Err(Error::ForcedBackendUnavailable("opengl".into()))
1044 }
1045 };
1046 }
1047
1048 #[cfg(target_os = "linux")]
1051 #[cfg(feature = "opengl")]
1052 if let Some(opengl) = self.opengl.as_mut() {
1053 log::trace!("draw_masks started with opengl in {:?}", start.elapsed());
1054 match opengl.draw_masks(dst, detect, segmentation) {
1055 Ok(_) => {
1056 log::trace!("draw_masks with opengl in {:?}", start.elapsed());
1057 return Ok(());
1058 }
1059 Err(e) => {
1060 log::trace!("draw_masks didn't work with opengl: {e:?}")
1061 }
1062 }
1063 }
1064 log::trace!("draw_masks started with cpu in {:?}", start.elapsed());
1065 if let Some(cpu) = self.cpu.as_mut() {
1066 match cpu.draw_masks(dst, detect, segmentation) {
1067 Ok(_) => {
1068 log::trace!("draw_masks with cpu in {:?}", start.elapsed());
1069 return Ok(());
1070 }
1071 Err(e) => {
1072 log::trace!("draw_masks didn't work with cpu: {e:?}");
1073 return Err(e);
1074 }
1075 }
1076 }
1077 Err(Error::NoConverter)
1078 }
1079
1080 fn draw_masks_proto(
1081 &mut self,
1082 dst: &mut TensorDyn,
1083 detect: &[DetectBox],
1084 proto_data: &ProtoData,
1085 ) -> Result<()> {
1086 let start = Instant::now();
1087
1088 if detect.is_empty() {
1089 return Ok(());
1090 }
1091
1092 if let Some(forced) = self.forced_backend {
1094 return match forced {
1095 ForcedBackend::Cpu => {
1096 if let Some(cpu) = self.cpu.as_mut() {
1097 return cpu.draw_masks_proto(dst, detect, proto_data);
1098 }
1099 Err(Error::ForcedBackendUnavailable("cpu".into()))
1100 }
1101 ForcedBackend::G2d => Err(Error::NotSupported(
1102 "g2d does not support draw_masks_proto".into(),
1103 )),
1104 ForcedBackend::OpenGl => {
1105 #[cfg(target_os = "linux")]
1106 #[cfg(feature = "opengl")]
1107 if let Some(opengl) = self.opengl.as_mut() {
1108 return opengl.draw_masks_proto(dst, detect, proto_data);
1109 }
1110 Err(Error::ForcedBackendUnavailable("opengl".into()))
1111 }
1112 };
1113 }
1114
1115 #[cfg(target_os = "linux")]
1121 #[cfg(feature = "opengl")]
1122 if let Some(opengl) = self.opengl.as_mut() {
1123 let Some(cpu) = self.cpu.as_ref() else {
1124 return Err(Error::Internal(
1125 "draw_masks_proto requires CPU backend for hybrid path".into(),
1126 ));
1127 };
1128 log::trace!(
1129 "draw_masks_proto started with hybrid (cpu+opengl) in {:?}",
1130 start.elapsed()
1131 );
1132 let segmentation = cpu.materialize_segmentations(detect, proto_data)?;
1133 match opengl.draw_masks(dst, detect, &segmentation) {
1134 Ok(_) => {
1135 log::trace!(
1136 "draw_masks_proto with hybrid (cpu+opengl) in {:?}",
1137 start.elapsed()
1138 );
1139 return Ok(());
1140 }
1141 Err(e) => {
1142 log::trace!("draw_masks_proto hybrid path failed, falling back to cpu: {e:?}");
1143 }
1144 }
1145 }
1146
1147 let Some(cpu) = self.cpu.as_mut() else {
1149 return Err(Error::Internal(
1150 "draw_masks_proto requires CPU backend for fallback path".into(),
1151 ));
1152 };
1153 log::trace!("draw_masks_proto started with cpu in {:?}", start.elapsed());
1154 cpu.draw_masks_proto(dst, detect, proto_data)
1155 }
1156
1157 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
1158 let start = Instant::now();
1159
1160 if let Some(forced) = self.forced_backend {
1162 return match forced {
1163 ForcedBackend::Cpu => {
1164 if let Some(cpu) = self.cpu.as_mut() {
1165 return cpu.set_class_colors(colors);
1166 }
1167 Err(Error::ForcedBackendUnavailable("cpu".into()))
1168 }
1169 ForcedBackend::G2d => Err(Error::NotSupported(
1170 "g2d does not support set_class_colors".into(),
1171 )),
1172 ForcedBackend::OpenGl => {
1173 #[cfg(target_os = "linux")]
1174 #[cfg(feature = "opengl")]
1175 if let Some(opengl) = self.opengl.as_mut() {
1176 return opengl.set_class_colors(colors);
1177 }
1178 Err(Error::ForcedBackendUnavailable("opengl".into()))
1179 }
1180 };
1181 }
1182
1183 #[cfg(target_os = "linux")]
1186 #[cfg(feature = "opengl")]
1187 if let Some(opengl) = self.opengl.as_mut() {
1188 log::trace!("image started with opengl in {:?}", start.elapsed());
1189 match opengl.set_class_colors(colors) {
1190 Ok(_) => {
1191 log::trace!("colors set with opengl in {:?}", start.elapsed());
1192 return Ok(());
1193 }
1194 Err(e) => {
1195 log::trace!("colors didn't set with opengl: {e:?}")
1196 }
1197 }
1198 }
1199 log::trace!("image started with cpu in {:?}", start.elapsed());
1200 if let Some(cpu) = self.cpu.as_mut() {
1201 match cpu.set_class_colors(colors) {
1202 Ok(_) => {
1203 log::trace!("colors set with cpu in {:?}", start.elapsed());
1204 return Ok(());
1205 }
1206 Err(e) => {
1207 log::trace!("colors didn't set with cpu: {e:?}");
1208 return Err(e);
1209 }
1210 }
1211 }
1212 Err(Error::NoConverter)
1213 }
1214
1215 fn decode_masks_atlas(
1216 &mut self,
1217 detect: &[DetectBox],
1218 proto_data: ProtoData,
1219 output_width: usize,
1220 output_height: usize,
1221 ) -> Result<(Vec<u8>, Vec<MaskRegion>)> {
1222 if detect.is_empty() {
1223 return Ok((Vec::new(), Vec::new()));
1224 }
1225
1226 if let Some(forced) = self.forced_backend {
1228 return match forced {
1229 ForcedBackend::Cpu => {
1230 if let Some(cpu) = self.cpu.as_mut() {
1231 return cpu.decode_masks_atlas(
1232 detect,
1233 proto_data,
1234 output_width,
1235 output_height,
1236 );
1237 }
1238 Err(Error::ForcedBackendUnavailable("cpu".into()))
1239 }
1240 ForcedBackend::G2d => Err(Error::NotSupported(
1241 "g2d does not support decode_masks_atlas".into(),
1242 )),
1243 ForcedBackend::OpenGl => {
1244 #[cfg(target_os = "linux")]
1245 #[cfg(feature = "opengl")]
1246 if let Some(opengl) = self.opengl.as_mut() {
1247 return opengl.decode_masks_atlas(
1248 detect,
1249 proto_data,
1250 output_width,
1251 output_height,
1252 );
1253 }
1254 Err(Error::ForcedBackendUnavailable("opengl".into()))
1255 }
1256 };
1257 }
1258
1259 #[cfg(target_os = "linux")]
1260 #[cfg(feature = "opengl")]
1261 {
1262 let has_opengl = self.opengl.is_some();
1263 if has_opengl {
1264 let opengl = self.opengl.as_mut().unwrap();
1265 match opengl.decode_masks_atlas(detect, proto_data, output_width, output_height) {
1266 Ok(r) => return Ok(r),
1267 Err(e) => {
1268 log::trace!("decode_masks_atlas didn't work with opengl: {e:?}");
1269 return Err(e);
1270 }
1271 }
1272 }
1273 }
1274 if let Some(cpu) = self.cpu.as_mut() {
1276 return cpu.decode_masks_atlas(detect, proto_data, output_width, output_height);
1277 }
1278 Err(Error::NoConverter)
1279 }
1280}
1281
1282fn read_exif_orientation(exif_bytes: &[u8]) -> (Rotation, Flip) {
1288 let exifreader = exif::Reader::new();
1289 let Ok(exif_) = exifreader.read_raw(exif_bytes.to_vec()) else {
1290 return (Rotation::None, Flip::None);
1291 };
1292 let Some(orientation) = exif_.get_field(exif::Tag::Orientation, exif::In::PRIMARY) else {
1293 return (Rotation::None, Flip::None);
1294 };
1295 match orientation.value.get_uint(0) {
1296 Some(1) => (Rotation::None, Flip::None),
1297 Some(2) => (Rotation::None, Flip::Horizontal),
1298 Some(3) => (Rotation::Rotate180, Flip::None),
1299 Some(4) => (Rotation::Rotate180, Flip::Horizontal),
1300 Some(5) => (Rotation::Clockwise90, Flip::Horizontal),
1301 Some(6) => (Rotation::Clockwise90, Flip::None),
1302 Some(7) => (Rotation::CounterClockwise90, Flip::Horizontal),
1303 Some(8) => (Rotation::CounterClockwise90, Flip::None),
1304 Some(v) => {
1305 log::warn!("broken orientation EXIF value: {v}");
1306 (Rotation::None, Flip::None)
1307 }
1308 None => (Rotation::None, Flip::None),
1309 }
1310}
1311
1312fn pixelfmt_to_colorspace(fmt: PixelFormat) -> Option<ColorSpace> {
1315 match fmt {
1316 PixelFormat::Rgb => Some(ColorSpace::RGB),
1317 PixelFormat::Rgba => Some(ColorSpace::RGBA),
1318 PixelFormat::Grey => Some(ColorSpace::Luma),
1319 _ => None,
1320 }
1321}
1322
1323fn colorspace_to_pixelfmt(cs: ColorSpace) -> Option<PixelFormat> {
1325 match cs {
1326 ColorSpace::RGB => Some(PixelFormat::Rgb),
1327 ColorSpace::RGBA => Some(PixelFormat::Rgba),
1328 ColorSpace::Luma => Some(PixelFormat::Grey),
1329 _ => None,
1330 }
1331}
1332
1333fn load_jpeg(
1335 image: &[u8],
1336 format: Option<PixelFormat>,
1337 memory: Option<TensorMemory>,
1338) -> Result<TensorDyn> {
1339 let colour = match format {
1340 Some(f) => pixelfmt_to_colorspace(f)
1341 .ok_or_else(|| Error::NotSupported(format!("Unsupported image format {f:?}")))?,
1342 None => ColorSpace::RGB,
1343 };
1344 let options = DecoderOptions::default().jpeg_set_out_colorspace(colour);
1345 let mut decoder = JpegDecoder::new_with_options(image, options);
1346 decoder.decode_headers()?;
1347
1348 let image_info = decoder.info().ok_or(Error::Internal(
1349 "JPEG did not return decoded image info".to_string(),
1350 ))?;
1351
1352 let converted_cs = decoder
1353 .get_output_colorspace()
1354 .ok_or(Error::Internal("No output colorspace".to_string()))?;
1355
1356 let converted_fmt = colorspace_to_pixelfmt(converted_cs).ok_or(Error::NotSupported(
1357 "Unsupported JPEG decoder output".to_string(),
1358 ))?;
1359
1360 let dest_fmt = format.unwrap_or(converted_fmt);
1361
1362 let (rotation, flip) = decoder
1363 .exif()
1364 .map(|x| read_exif_orientation(x))
1365 .unwrap_or((Rotation::None, Flip::None));
1366
1367 let w = image_info.width as usize;
1368 let h = image_info.height as usize;
1369
1370 if (rotation, flip) == (Rotation::None, Flip::None) {
1371 let mut img = Tensor::<u8>::image(w, h, dest_fmt, memory)?;
1372
1373 if converted_fmt != dest_fmt {
1374 let tmp = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
1375 decoder.decode_into(&mut tmp.map()?)?;
1376 CPUProcessor::convert_format_pf(&tmp, &mut img, converted_fmt, dest_fmt)?;
1377 return Ok(TensorDyn::from(img));
1378 }
1379 decoder.decode_into(&mut img.map()?)?;
1380 return Ok(TensorDyn::from(img));
1381 }
1382
1383 let mut tmp = Tensor::<u8>::image(w, h, dest_fmt, Some(TensorMemory::Mem))?;
1384
1385 if converted_fmt != dest_fmt {
1386 let tmp2 = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
1387 decoder.decode_into(&mut tmp2.map()?)?;
1388 CPUProcessor::convert_format_pf(&tmp2, &mut tmp, converted_fmt, dest_fmt)?;
1389 } else {
1390 decoder.decode_into(&mut tmp.map()?)?;
1391 }
1392
1393 rotate_flip_to_dyn(&tmp, dest_fmt, rotation, flip, memory)
1394}
1395
1396fn load_png(
1398 image: &[u8],
1399 format: Option<PixelFormat>,
1400 memory: Option<TensorMemory>,
1401) -> Result<TensorDyn> {
1402 let fmt = format.unwrap_or(PixelFormat::Rgb);
1403 let alpha = match fmt {
1404 PixelFormat::Rgb => false,
1405 PixelFormat::Rgba => true,
1406 _ => {
1407 return Err(Error::NotImplemented(
1408 "Unsupported image format".to_string(),
1409 ));
1410 }
1411 };
1412
1413 let options = DecoderOptions::default()
1414 .png_set_add_alpha_channel(alpha)
1415 .png_set_decode_animated(false);
1416 let mut decoder = PngDecoder::new_with_options(image, options);
1417 decoder.decode_headers()?;
1418 let image_info = decoder.get_info().ok_or(Error::Internal(
1419 "PNG did not return decoded image info".to_string(),
1420 ))?;
1421
1422 let (rotation, flip) = image_info
1423 .exif
1424 .as_ref()
1425 .map(|x| read_exif_orientation(x))
1426 .unwrap_or((Rotation::None, Flip::None));
1427
1428 if (rotation, flip) == (Rotation::None, Flip::None) {
1429 let img = Tensor::<u8>::image(image_info.width, image_info.height, fmt, memory)?;
1430 decoder.decode_into(&mut img.map()?)?;
1431 return Ok(TensorDyn::from(img));
1432 }
1433
1434 let tmp = Tensor::<u8>::image(
1435 image_info.width,
1436 image_info.height,
1437 fmt,
1438 Some(TensorMemory::Mem),
1439 )?;
1440 decoder.decode_into(&mut tmp.map()?)?;
1441
1442 rotate_flip_to_dyn(&tmp, fmt, rotation, flip, memory)
1443}
1444
1445pub fn load_image(
1464 image: &[u8],
1465 format: Option<PixelFormat>,
1466 memory: Option<TensorMemory>,
1467) -> Result<TensorDyn> {
1468 if let Ok(i) = load_jpeg(image, format, memory) {
1469 return Ok(i);
1470 }
1471 if let Ok(i) = load_png(image, format, memory) {
1472 return Ok(i);
1473 }
1474 Err(Error::NotSupported(
1475 "Could not decode as jpeg or png".to_string(),
1476 ))
1477}
1478
1479pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
1483 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
1484 "save_jpeg requires u8 tensor".to_string(),
1485 ))?;
1486 let fmt = t.format().ok_or(Error::NotAnImage)?;
1487 if fmt.layout() != PixelLayout::Packed {
1488 return Err(Error::NotImplemented(
1489 "Saving planar images is not supported".to_string(),
1490 ));
1491 }
1492
1493 let colour = match fmt {
1494 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
1495 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
1496 _ => {
1497 return Err(Error::NotImplemented(
1498 "Unsupported image format for saving".to_string(),
1499 ));
1500 }
1501 };
1502
1503 let w = t.width().ok_or(Error::NotAnImage)?;
1504 let h = t.height().ok_or(Error::NotAnImage)?;
1505 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
1506 let tensor_map = t.map()?;
1507
1508 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
1509
1510 Ok(())
1511}
1512
1513pub(crate) struct FunctionTimer<T: Display> {
1514 name: T,
1515 start: std::time::Instant,
1516}
1517
1518impl<T: Display> FunctionTimer<T> {
1519 pub fn new(name: T) -> Self {
1520 Self {
1521 name,
1522 start: std::time::Instant::now(),
1523 }
1524 }
1525}
1526
1527impl<T: Display> Drop for FunctionTimer<T> {
1528 fn drop(&mut self) {
1529 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
1530 }
1531}
1532
1533const DEFAULT_COLORS: [[f32; 4]; 20] = [
1534 [0., 1., 0., 0.7],
1535 [1., 0.5568628, 0., 0.7],
1536 [0.25882353, 0.15294118, 0.13333333, 0.7],
1537 [0.8, 0.7647059, 0.78039216, 0.7],
1538 [0.3137255, 0.3137255, 0.3137255, 0.7],
1539 [0.1411765, 0.3098039, 0.1215686, 0.7],
1540 [1., 0.95686275, 0.5137255, 0.7],
1541 [0.3529412, 0.32156863, 0., 0.7],
1542 [0.4235294, 0.6235294, 0.6509804, 0.7],
1543 [0.5098039, 0.5098039, 0.7294118, 0.7],
1544 [0.00784314, 0.18823529, 0.29411765, 0.7],
1545 [0.0, 0.2706, 1.0, 0.7],
1546 [0.0, 0.0, 0.0, 0.7],
1547 [0.0, 0.5, 0.0, 0.7],
1548 [1.0, 0.0, 0.0, 0.7],
1549 [0.0, 0.0, 1.0, 0.7],
1550 [1.0, 0.5, 0.5, 0.7],
1551 [0.1333, 0.5451, 0.1333, 0.7],
1552 [0.1176, 0.4118, 0.8235, 0.7],
1553 [1., 1., 1., 0.7],
1554];
1555
1556const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
1557 let mut result = [[0; M]; N];
1558 let mut i = 0;
1559 while i < N {
1560 let mut j = 0;
1561 while j < M {
1562 result[i][j] = (a[i][j] * 255.0).round() as u8;
1563 j += 1;
1564 }
1565 i += 1;
1566 }
1567 result
1568}
1569
1570const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
1571
1572#[cfg(test)]
1573#[cfg_attr(coverage_nightly, coverage(off))]
1574mod image_tests {
1575 use super::*;
1576 use crate::{CPUProcessor, Rotation};
1577 #[cfg(target_os = "linux")]
1578 use edgefirst_tensor::is_dma_available;
1579 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
1580 use image::buffer::ConvertBuffer;
1581
1582 fn convert_img(
1588 proc: &mut dyn ImageProcessorTrait,
1589 src: TensorDyn,
1590 dst: TensorDyn,
1591 rotation: Rotation,
1592 flip: Flip,
1593 crop: Crop,
1594 ) -> (Result<()>, TensorDyn, TensorDyn) {
1595 let src_fourcc = src.format().unwrap();
1596 let dst_fourcc = dst.format().unwrap();
1597 let src_dyn = src;
1598 let mut dst_dyn = dst;
1599 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
1600 let src_back = {
1601 let mut __t = src_dyn.into_u8().unwrap();
1602 __t.set_format(src_fourcc).unwrap();
1603 TensorDyn::from(__t)
1604 };
1605 let dst_back = {
1606 let mut __t = dst_dyn.into_u8().unwrap();
1607 __t.set_format(dst_fourcc).unwrap();
1608 TensorDyn::from(__t)
1609 };
1610 (result, src_back, dst_back)
1611 }
1612
1613 #[ctor::ctor]
1614 fn init() {
1615 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
1616 }
1617
1618 macro_rules! function {
1619 () => {{
1620 fn f() {}
1621 fn type_name_of<T>(_: T) -> &'static str {
1622 std::any::type_name::<T>()
1623 }
1624 let name = type_name_of(f);
1625
1626 match &name[..name.len() - 3].rfind(':') {
1628 Some(pos) => &name[pos + 1..name.len() - 3],
1629 None => &name[..name.len() - 3],
1630 }
1631 }};
1632 }
1633
1634 #[test]
1635 fn test_invalid_crop() {
1636 let src = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
1637 let dst = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
1638
1639 let crop = Crop::new()
1640 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1641 .with_dst_rect(Some(Rect::new(0, 0, 150, 150)));
1642
1643 let result = crop.check_crop_dyn(&src, &dst);
1644 assert!(matches!(
1645 result,
1646 Err(Error::CropInvalid(e)) if e.starts_with("Dest and Src crop invalid")
1647 ));
1648
1649 let crop = crop.with_src_rect(Some(Rect::new(0, 0, 10, 10)));
1650 let result = crop.check_crop_dyn(&src, &dst);
1651 assert!(matches!(
1652 result,
1653 Err(Error::CropInvalid(e)) if e.starts_with("Dest crop invalid")
1654 ));
1655
1656 let crop = crop
1657 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
1658 .with_dst_rect(Some(Rect::new(0, 0, 50, 50)));
1659 let result = crop.check_crop_dyn(&src, &dst);
1660 assert!(matches!(
1661 result,
1662 Err(Error::CropInvalid(e)) if e.starts_with("Src crop invalid")
1663 ));
1664
1665 let crop = crop.with_src_rect(Some(Rect::new(50, 50, 50, 50)));
1666
1667 let result = crop.check_crop_dyn(&src, &dst);
1668 assert!(result.is_ok());
1669 }
1670
1671 #[test]
1672 fn test_invalid_tensor_format() -> Result<(), Error> {
1673 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
1675 let result = tensor.set_format(PixelFormat::Rgb);
1676 assert!(result.is_err(), "4D tensor should reject set_format");
1677
1678 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
1680 let result = tensor.set_format(PixelFormat::Rgb);
1681 assert!(result.is_err(), "4-channel tensor should reject RGB format");
1682
1683 Ok(())
1684 }
1685
1686 #[test]
1687 fn test_invalid_image_file() -> Result<(), Error> {
1688 let result = crate::load_image(&[123; 5000], None, None);
1689 assert!(matches!(
1690 result,
1691 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
1692
1693 Ok(())
1694 }
1695
1696 #[test]
1697 fn test_invalid_jpeg_format() -> Result<(), Error> {
1698 let result = crate::load_image(&[123; 5000], Some(PixelFormat::Yuyv), None);
1699 assert!(matches!(
1700 result,
1701 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
1702
1703 Ok(())
1704 }
1705
1706 #[test]
1707 fn test_load_resize_save() {
1708 let file = include_bytes!(concat!(
1709 env!("CARGO_MANIFEST_DIR"),
1710 "/../../testdata/zidane.jpg"
1711 ));
1712 let img = crate::load_image(file, Some(PixelFormat::Rgba), None).unwrap();
1713 assert_eq!(img.width(), Some(1280));
1714 assert_eq!(img.height(), Some(720));
1715
1716 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None).unwrap();
1717 let mut converter = CPUProcessor::new();
1718 let (result, _img, dst) = convert_img(
1719 &mut converter,
1720 img,
1721 dst,
1722 Rotation::None,
1723 Flip::None,
1724 Crop::no_crop(),
1725 );
1726 result.unwrap();
1727 assert_eq!(dst.width(), Some(640));
1728 assert_eq!(dst.height(), Some(360));
1729
1730 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
1731
1732 let file = std::fs::read("zidane_resized.jpg").unwrap();
1733 let img = crate::load_image(&file, None, None).unwrap();
1734 assert_eq!(img.width(), Some(640));
1735 assert_eq!(img.height(), Some(360));
1736 assert_eq!(img.format().unwrap(), PixelFormat::Rgb);
1737 }
1738
1739 #[test]
1740 fn test_from_tensor_planar() -> Result<(), Error> {
1741 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
1742 tensor.map()?.copy_from_slice(include_bytes!(concat!(
1743 env!("CARGO_MANIFEST_DIR"),
1744 "/../../testdata/camera720p.8bps"
1745 )));
1746 let planar = {
1747 tensor
1748 .set_format(PixelFormat::PlanarRgb)
1749 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1750 TensorDyn::from(tensor)
1751 };
1752
1753 let rbga = load_bytes_to_tensor(
1754 1280,
1755 720,
1756 PixelFormat::Rgba,
1757 None,
1758 include_bytes!(concat!(
1759 env!("CARGO_MANIFEST_DIR"),
1760 "/../../testdata/camera720p.rgba"
1761 )),
1762 )?;
1763 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
1764
1765 Ok(())
1766 }
1767
1768 #[test]
1769 fn test_from_tensor_invalid_format() {
1770 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
1773 }
1774
1775 #[test]
1776 #[should_panic(expected = "Failed to save planar RGB image")]
1777 fn test_save_planar() {
1778 let planar_img = load_bytes_to_tensor(
1779 1280,
1780 720,
1781 PixelFormat::PlanarRgb,
1782 None,
1783 include_bytes!(concat!(
1784 env!("CARGO_MANIFEST_DIR"),
1785 "/../../testdata/camera720p.8bps"
1786 )),
1787 )
1788 .unwrap();
1789
1790 let save_path = "/tmp/planar_rgb.jpg";
1791 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
1792 }
1793
1794 #[test]
1795 #[should_panic(expected = "Failed to save YUYV image")]
1796 fn test_save_yuyv() {
1797 let planar_img = load_bytes_to_tensor(
1798 1280,
1799 720,
1800 PixelFormat::Yuyv,
1801 None,
1802 include_bytes!(concat!(
1803 env!("CARGO_MANIFEST_DIR"),
1804 "/../../testdata/camera720p.yuyv"
1805 )),
1806 )
1807 .unwrap();
1808
1809 let save_path = "/tmp/yuyv.jpg";
1810 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
1811 }
1812
1813 #[test]
1814 fn test_rotation_angle() {
1815 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
1816 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
1817 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
1818 assert_eq!(
1819 Rotation::from_degrees_clockwise(270),
1820 Rotation::CounterClockwise90
1821 );
1822 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
1823 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
1824 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
1825 assert_eq!(
1826 Rotation::from_degrees_clockwise(630),
1827 Rotation::CounterClockwise90
1828 );
1829 }
1830
1831 #[test]
1832 #[should_panic(expected = "rotation angle is not a multiple of 90")]
1833 fn test_rotation_angle_panic() {
1834 Rotation::from_degrees_clockwise(361);
1835 }
1836
1837 #[test]
1838 fn test_disable_env_var() -> Result<(), Error> {
1839 #[cfg(target_os = "linux")]
1840 {
1841 let original = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1842 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1843 let converter = ImageProcessor::new()?;
1844 match original {
1845 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1846 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1847 }
1848 assert!(converter.g2d.is_none());
1849 }
1850
1851 #[cfg(target_os = "linux")]
1852 #[cfg(feature = "opengl")]
1853 {
1854 let original = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1855 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1856 let converter = ImageProcessor::new()?;
1857 match original {
1858 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1859 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1860 }
1861 assert!(converter.opengl.is_none());
1862 }
1863
1864 let original = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1865 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1866 let converter = ImageProcessor::new()?;
1867 match original {
1868 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1869 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1870 }
1871 assert!(converter.cpu.is_none());
1872
1873 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
1874 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
1875 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
1876 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
1877 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
1878 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
1879 let mut converter = ImageProcessor::new()?;
1880
1881 let src = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1882 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None)?;
1883 let (result, _src, _dst) = convert_img(
1884 &mut converter,
1885 src,
1886 dst,
1887 Rotation::None,
1888 Flip::None,
1889 Crop::no_crop(),
1890 );
1891 assert!(matches!(result, Err(Error::NoConverter)));
1892
1893 match original_cpu {
1894 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
1895 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
1896 }
1897 match original_gl {
1898 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
1899 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
1900 }
1901 match original_g2d {
1902 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
1903 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
1904 }
1905
1906 Ok(())
1907 }
1908
1909 #[test]
1910 fn test_unsupported_conversion() {
1911 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
1912 let dst = TensorDyn::image(640, 360, PixelFormat::Nv12, DType::U8, None).unwrap();
1913 let mut converter = ImageProcessor::new().unwrap();
1914 let (result, _src, _dst) = convert_img(
1915 &mut converter,
1916 src,
1917 dst,
1918 Rotation::None,
1919 Flip::None,
1920 Crop::no_crop(),
1921 );
1922 log::debug!("result: {:?}", result);
1923 assert!(matches!(
1924 result,
1925 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
1926 ));
1927 }
1928
1929 #[test]
1930 fn test_load_grey() {
1931 let grey_img = crate::load_image(
1932 include_bytes!(concat!(
1933 env!("CARGO_MANIFEST_DIR"),
1934 "/../../testdata/grey.jpg"
1935 )),
1936 Some(PixelFormat::Rgba),
1937 None,
1938 )
1939 .unwrap();
1940
1941 let grey_but_rgb_img = crate::load_image(
1942 include_bytes!(concat!(
1943 env!("CARGO_MANIFEST_DIR"),
1944 "/../../testdata/grey-rgb.jpg"
1945 )),
1946 Some(PixelFormat::Rgba),
1947 None,
1948 )
1949 .unwrap();
1950
1951 compare_images(&grey_img, &grey_but_rgb_img, 0.99, function!());
1952 }
1953
1954 #[test]
1955 fn test_new_nv12() {
1956 let nv12 = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
1957 assert_eq!(nv12.height(), Some(720));
1958 assert_eq!(nv12.width(), Some(1280));
1959 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
1960 assert_eq!(nv12.format().unwrap().channels(), 1);
1962 assert!(nv12.format().is_some_and(
1963 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
1964 ))
1965 }
1966
1967 #[test]
1968 #[cfg(target_os = "linux")]
1969 fn test_new_image_converter() {
1970 let dst_width = 640;
1971 let dst_height = 360;
1972 let file = include_bytes!(concat!(
1973 env!("CARGO_MANIFEST_DIR"),
1974 "/../../testdata/zidane.jpg"
1975 ))
1976 .to_vec();
1977 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
1978
1979 let mut converter = ImageProcessor::new().unwrap();
1980 let converter_dst = converter
1981 .create_image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
1982 .unwrap();
1983 let (result, src, converter_dst) = convert_img(
1984 &mut converter,
1985 src,
1986 converter_dst,
1987 Rotation::None,
1988 Flip::None,
1989 Crop::no_crop(),
1990 );
1991 result.unwrap();
1992
1993 let cpu_dst =
1994 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
1995 let mut cpu_converter = CPUProcessor::new();
1996 let (result, _src, cpu_dst) = convert_img(
1997 &mut cpu_converter,
1998 src,
1999 cpu_dst,
2000 Rotation::None,
2001 Flip::None,
2002 Crop::no_crop(),
2003 );
2004 result.unwrap();
2005
2006 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
2007 }
2008
2009 #[test]
2010 #[cfg(target_os = "linux")]
2011 fn test_create_image_dtype_i8() {
2012 let mut converter = ImageProcessor::new().unwrap();
2013
2014 let dst = converter
2016 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2017 .unwrap();
2018 assert_eq!(dst.dtype(), DType::I8);
2019 assert!(dst.width() == Some(320));
2020 assert!(dst.height() == Some(240));
2021 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
2022
2023 let dst_u8 = converter
2025 .create_image(320, 240, PixelFormat::Rgb, DType::U8, None)
2026 .unwrap();
2027 assert_eq!(dst_u8.dtype(), DType::U8);
2028
2029 let file = include_bytes!(concat!(
2031 env!("CARGO_MANIFEST_DIR"),
2032 "/../../testdata/zidane.jpg"
2033 ))
2034 .to_vec();
2035 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2036 let mut dst_i8 = converter
2037 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2038 .unwrap();
2039 converter
2040 .convert(
2041 &src,
2042 &mut dst_i8,
2043 Rotation::None,
2044 Flip::None,
2045 Crop::no_crop(),
2046 )
2047 .unwrap();
2048 }
2049
2050 #[test]
2051 fn test_crop_skip() {
2052 let file = include_bytes!(concat!(
2053 env!("CARGO_MANIFEST_DIR"),
2054 "/../../testdata/zidane.jpg"
2055 ))
2056 .to_vec();
2057 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2058
2059 let mut converter = ImageProcessor::new().unwrap();
2060 let converter_dst = converter
2061 .create_image(1280, 720, PixelFormat::Rgba, DType::U8, None)
2062 .unwrap();
2063 let crop = Crop::new()
2064 .with_src_rect(Some(Rect::new(0, 0, 640, 640)))
2065 .with_dst_rect(Some(Rect::new(0, 0, 640, 640)));
2066 let (result, src, converter_dst) = convert_img(
2067 &mut converter,
2068 src,
2069 converter_dst,
2070 Rotation::None,
2071 Flip::None,
2072 crop,
2073 );
2074 result.unwrap();
2075
2076 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
2077 let mut cpu_converter = CPUProcessor::new();
2078 let (result, _src, cpu_dst) = convert_img(
2079 &mut cpu_converter,
2080 src,
2081 cpu_dst,
2082 Rotation::None,
2083 Flip::None,
2084 crop,
2085 );
2086 result.unwrap();
2087
2088 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
2089 }
2090
2091 #[test]
2092 fn test_invalid_pixel_format() {
2093 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
2096 }
2097
2098 #[cfg(target_os = "linux")]
2100 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2101
2102 #[cfg(target_os = "linux")]
2103 fn is_g2d_available() -> bool {
2104 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
2105 }
2106
2107 #[cfg(target_os = "linux")]
2108 #[cfg(feature = "opengl")]
2109 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2110
2111 #[cfg(target_os = "linux")]
2112 #[cfg(feature = "opengl")]
2113 fn is_opengl_available() -> bool {
2115 #[cfg(all(target_os = "linux", feature = "opengl"))]
2116 {
2117 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
2118 }
2119
2120 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
2121 {
2122 false
2123 }
2124 }
2125
2126 #[test]
2127 fn test_load_jpeg_with_exif() {
2128 let file = include_bytes!(concat!(
2129 env!("CARGO_MANIFEST_DIR"),
2130 "/../../testdata/zidane_rotated_exif.jpg"
2131 ))
2132 .to_vec();
2133 let loaded = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2134
2135 assert_eq!(loaded.height(), Some(1280));
2136 assert_eq!(loaded.width(), Some(720));
2137
2138 let file = include_bytes!(concat!(
2139 env!("CARGO_MANIFEST_DIR"),
2140 "/../../testdata/zidane.jpg"
2141 ))
2142 .to_vec();
2143 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2144
2145 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
2146
2147 let cpu_dst =
2148 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2149 let mut cpu_converter = CPUProcessor::new();
2150
2151 let (result, _cpu_src, cpu_dst) = convert_img(
2152 &mut cpu_converter,
2153 cpu_src,
2154 cpu_dst,
2155 Rotation::Clockwise90,
2156 Flip::None,
2157 Crop::no_crop(),
2158 );
2159 result.unwrap();
2160
2161 compare_images(&loaded, &cpu_dst, 0.98, function!());
2162 }
2163
2164 #[test]
2165 fn test_load_png_with_exif() {
2166 let file = include_bytes!(concat!(
2167 env!("CARGO_MANIFEST_DIR"),
2168 "/../../testdata/zidane_rotated_exif_180.png"
2169 ))
2170 .to_vec();
2171 let loaded = crate::load_png(&file, Some(PixelFormat::Rgba), None).unwrap();
2172
2173 assert_eq!(loaded.height(), Some(720));
2174 assert_eq!(loaded.width(), Some(1280));
2175
2176 let file = include_bytes!(concat!(
2177 env!("CARGO_MANIFEST_DIR"),
2178 "/../../testdata/zidane.jpg"
2179 ))
2180 .to_vec();
2181 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2182
2183 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
2184 let mut cpu_converter = CPUProcessor::new();
2185
2186 let (result, _cpu_src, cpu_dst) = convert_img(
2187 &mut cpu_converter,
2188 cpu_src,
2189 cpu_dst,
2190 Rotation::Rotate180,
2191 Flip::None,
2192 Crop::no_crop(),
2193 );
2194 result.unwrap();
2195
2196 compare_images(&loaded, &cpu_dst, 0.98, function!());
2197 }
2198
2199 #[test]
2200 #[cfg(target_os = "linux")]
2201 fn test_g2d_resize() {
2202 if !is_g2d_available() {
2203 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
2204 return;
2205 }
2206 if !is_dma_available() {
2207 eprintln!(
2208 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2209 );
2210 return;
2211 }
2212
2213 let dst_width = 640;
2214 let dst_height = 360;
2215 let file = include_bytes!(concat!(
2216 env!("CARGO_MANIFEST_DIR"),
2217 "/../../testdata/zidane.jpg"
2218 ))
2219 .to_vec();
2220 let src =
2221 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
2222
2223 let g2d_dst = TensorDyn::image(
2224 dst_width,
2225 dst_height,
2226 PixelFormat::Rgba,
2227 DType::U8,
2228 Some(TensorMemory::Dma),
2229 )
2230 .unwrap();
2231 let mut g2d_converter = G2DProcessor::new().unwrap();
2232 let (result, src, g2d_dst) = convert_img(
2233 &mut g2d_converter,
2234 src,
2235 g2d_dst,
2236 Rotation::None,
2237 Flip::None,
2238 Crop::no_crop(),
2239 );
2240 result.unwrap();
2241
2242 let cpu_dst =
2243 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2244 let mut cpu_converter = CPUProcessor::new();
2245 let (result, _src, cpu_dst) = convert_img(
2246 &mut cpu_converter,
2247 src,
2248 cpu_dst,
2249 Rotation::None,
2250 Flip::None,
2251 Crop::no_crop(),
2252 );
2253 result.unwrap();
2254
2255 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2256 }
2257
2258 #[test]
2259 #[cfg(target_os = "linux")]
2260 #[cfg(feature = "opengl")]
2261 fn test_opengl_resize() {
2262 if !is_opengl_available() {
2263 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2264 return;
2265 }
2266
2267 let dst_width = 640;
2268 let dst_height = 360;
2269 let file = include_bytes!(concat!(
2270 env!("CARGO_MANIFEST_DIR"),
2271 "/../../testdata/zidane.jpg"
2272 ))
2273 .to_vec();
2274 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2275
2276 let cpu_dst =
2277 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2278 let mut cpu_converter = CPUProcessor::new();
2279 let (result, src, cpu_dst) = convert_img(
2280 &mut cpu_converter,
2281 src,
2282 cpu_dst,
2283 Rotation::None,
2284 Flip::None,
2285 Crop::no_crop(),
2286 );
2287 result.unwrap();
2288
2289 let mut src = src;
2290 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2291
2292 for _ in 0..5 {
2293 let gl_dst =
2294 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
2295 .unwrap();
2296 let (result, src_back, gl_dst) = convert_img(
2297 &mut gl_converter,
2298 src,
2299 gl_dst,
2300 Rotation::None,
2301 Flip::None,
2302 Crop::no_crop(),
2303 );
2304 result.unwrap();
2305 src = src_back;
2306
2307 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2308 }
2309 }
2310
2311 #[test]
2312 #[ignore] #[cfg(target_os = "linux")]
2314 #[cfg(feature = "opengl")]
2315 fn test_opengl_10_threads() {
2316 if !is_opengl_available() {
2317 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2318 return;
2319 }
2320
2321 let handles: Vec<_> = (0..10)
2322 .map(|i| {
2323 std::thread::Builder::new()
2324 .name(format!("Thread {i}"))
2325 .spawn(test_opengl_resize)
2326 .unwrap()
2327 })
2328 .collect();
2329 handles.into_iter().for_each(|h| {
2330 if let Err(e) = h.join() {
2331 std::panic::resume_unwind(e)
2332 }
2333 });
2334 }
2335
2336 #[test]
2337 #[cfg(target_os = "linux")]
2338 #[cfg(feature = "opengl")]
2339 fn test_opengl_grey() {
2340 if !is_opengl_available() {
2341 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2342 return;
2343 }
2344
2345 let img = crate::load_image(
2346 include_bytes!(concat!(
2347 env!("CARGO_MANIFEST_DIR"),
2348 "/../../testdata/grey.jpg"
2349 )),
2350 Some(PixelFormat::Grey),
2351 None,
2352 )
2353 .unwrap();
2354
2355 let gl_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
2356 let cpu_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
2357
2358 let mut converter = CPUProcessor::new();
2359
2360 let (result, img, cpu_dst) = convert_img(
2361 &mut converter,
2362 img,
2363 cpu_dst,
2364 Rotation::None,
2365 Flip::None,
2366 Crop::no_crop(),
2367 );
2368 result.unwrap();
2369
2370 let mut gl = GLProcessorThreaded::new(None).unwrap();
2371 let (result, _img, gl_dst) = convert_img(
2372 &mut gl,
2373 img,
2374 gl_dst,
2375 Rotation::None,
2376 Flip::None,
2377 Crop::no_crop(),
2378 );
2379 result.unwrap();
2380
2381 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2382 }
2383
2384 #[test]
2385 #[cfg(target_os = "linux")]
2386 fn test_g2d_src_crop() {
2387 if !is_g2d_available() {
2388 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
2389 return;
2390 }
2391 if !is_dma_available() {
2392 eprintln!(
2393 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2394 );
2395 return;
2396 }
2397
2398 let dst_width = 640;
2399 let dst_height = 640;
2400 let file = include_bytes!(concat!(
2401 env!("CARGO_MANIFEST_DIR"),
2402 "/../../testdata/zidane.jpg"
2403 ))
2404 .to_vec();
2405 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2406
2407 let cpu_dst =
2408 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2409 let mut cpu_converter = CPUProcessor::new();
2410 let crop = Crop {
2411 src_rect: Some(Rect {
2412 left: 0,
2413 top: 0,
2414 width: 640,
2415 height: 360,
2416 }),
2417 dst_rect: None,
2418 dst_color: None,
2419 };
2420 let (result, src, cpu_dst) = convert_img(
2421 &mut cpu_converter,
2422 src,
2423 cpu_dst,
2424 Rotation::None,
2425 Flip::None,
2426 crop,
2427 );
2428 result.unwrap();
2429
2430 let g2d_dst =
2431 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2432 let mut g2d_converter = G2DProcessor::new().unwrap();
2433 let (result, _src, g2d_dst) = convert_img(
2434 &mut g2d_converter,
2435 src,
2436 g2d_dst,
2437 Rotation::None,
2438 Flip::None,
2439 crop,
2440 );
2441 result.unwrap();
2442
2443 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2444 }
2445
2446 #[test]
2447 #[cfg(target_os = "linux")]
2448 fn test_g2d_dst_crop() {
2449 if !is_g2d_available() {
2450 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
2451 return;
2452 }
2453 if !is_dma_available() {
2454 eprintln!(
2455 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2456 );
2457 return;
2458 }
2459
2460 let dst_width = 640;
2461 let dst_height = 640;
2462 let file = include_bytes!(concat!(
2463 env!("CARGO_MANIFEST_DIR"),
2464 "/../../testdata/zidane.jpg"
2465 ))
2466 .to_vec();
2467 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2468
2469 let cpu_dst =
2470 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2471 let mut cpu_converter = CPUProcessor::new();
2472 let crop = Crop {
2473 src_rect: None,
2474 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2475 dst_color: None,
2476 };
2477 let (result, src, cpu_dst) = convert_img(
2478 &mut cpu_converter,
2479 src,
2480 cpu_dst,
2481 Rotation::None,
2482 Flip::None,
2483 crop,
2484 );
2485 result.unwrap();
2486
2487 let g2d_dst =
2488 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2489 let mut g2d_converter = G2DProcessor::new().unwrap();
2490 let (result, _src, g2d_dst) = convert_img(
2491 &mut g2d_converter,
2492 src,
2493 g2d_dst,
2494 Rotation::None,
2495 Flip::None,
2496 crop,
2497 );
2498 result.unwrap();
2499
2500 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
2501 }
2502
2503 #[test]
2504 #[cfg(target_os = "linux")]
2505 fn test_g2d_all_rgba() {
2506 if !is_g2d_available() {
2507 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
2508 return;
2509 }
2510 if !is_dma_available() {
2511 eprintln!(
2512 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2513 );
2514 return;
2515 }
2516
2517 let dst_width = 640;
2518 let dst_height = 640;
2519 let file = include_bytes!(concat!(
2520 env!("CARGO_MANIFEST_DIR"),
2521 "/../../testdata/zidane.jpg"
2522 ))
2523 .to_vec();
2524 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2525 let src_dyn = src;
2526
2527 let mut cpu_dst =
2528 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2529 let mut cpu_converter = CPUProcessor::new();
2530 let mut g2d_dst =
2531 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2532 let mut g2d_converter = G2DProcessor::new().unwrap();
2533
2534 let crop = Crop {
2535 src_rect: Some(Rect::new(50, 120, 1024, 576)),
2536 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2537 dst_color: None,
2538 };
2539
2540 for rot in [
2541 Rotation::None,
2542 Rotation::Clockwise90,
2543 Rotation::Rotate180,
2544 Rotation::CounterClockwise90,
2545 ] {
2546 cpu_dst
2547 .as_u8()
2548 .unwrap()
2549 .map()
2550 .unwrap()
2551 .as_mut_slice()
2552 .fill(114);
2553 g2d_dst
2554 .as_u8()
2555 .unwrap()
2556 .map()
2557 .unwrap()
2558 .as_mut_slice()
2559 .fill(114);
2560 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2561 let mut cpu_dst_dyn = cpu_dst;
2562 cpu_converter
2563 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
2564 .unwrap();
2565 cpu_dst = {
2566 let mut __t = cpu_dst_dyn.into_u8().unwrap();
2567 __t.set_format(PixelFormat::Rgba).unwrap();
2568 TensorDyn::from(__t)
2569 };
2570
2571 let mut g2d_dst_dyn = g2d_dst;
2572 g2d_converter
2573 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
2574 .unwrap();
2575 g2d_dst = {
2576 let mut __t = g2d_dst_dyn.into_u8().unwrap();
2577 __t.set_format(PixelFormat::Rgba).unwrap();
2578 TensorDyn::from(__t)
2579 };
2580
2581 compare_images(
2582 &g2d_dst,
2583 &cpu_dst,
2584 0.98,
2585 &format!("{} {:?} {:?}", function!(), rot, flip),
2586 );
2587 }
2588 }
2589 }
2590
2591 #[test]
2592 #[cfg(target_os = "linux")]
2593 #[cfg(feature = "opengl")]
2594 fn test_opengl_src_crop() {
2595 if !is_opengl_available() {
2596 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2597 return;
2598 }
2599
2600 let dst_width = 640;
2601 let dst_height = 360;
2602 let file = include_bytes!(concat!(
2603 env!("CARGO_MANIFEST_DIR"),
2604 "/../../testdata/zidane.jpg"
2605 ))
2606 .to_vec();
2607 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2608 let crop = Crop {
2609 src_rect: Some(Rect {
2610 left: 320,
2611 top: 180,
2612 width: 1280 - 320,
2613 height: 720 - 180,
2614 }),
2615 dst_rect: None,
2616 dst_color: None,
2617 };
2618
2619 let cpu_dst =
2620 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2621 let mut cpu_converter = CPUProcessor::new();
2622 let (result, src, cpu_dst) = convert_img(
2623 &mut cpu_converter,
2624 src,
2625 cpu_dst,
2626 Rotation::None,
2627 Flip::None,
2628 crop,
2629 );
2630 result.unwrap();
2631
2632 let gl_dst =
2633 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2634 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2635 let (result, _src, gl_dst) = convert_img(
2636 &mut gl_converter,
2637 src,
2638 gl_dst,
2639 Rotation::None,
2640 Flip::None,
2641 crop,
2642 );
2643 result.unwrap();
2644
2645 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2646 }
2647
2648 #[test]
2649 #[cfg(target_os = "linux")]
2650 #[cfg(feature = "opengl")]
2651 fn test_opengl_dst_crop() {
2652 if !is_opengl_available() {
2653 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2654 return;
2655 }
2656
2657 let dst_width = 640;
2658 let dst_height = 640;
2659 let file = include_bytes!(concat!(
2660 env!("CARGO_MANIFEST_DIR"),
2661 "/../../testdata/zidane.jpg"
2662 ))
2663 .to_vec();
2664 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2665
2666 let cpu_dst =
2667 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2668 let mut cpu_converter = CPUProcessor::new();
2669 let crop = Crop {
2670 src_rect: None,
2671 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2672 dst_color: None,
2673 };
2674 let (result, src, cpu_dst) = convert_img(
2675 &mut cpu_converter,
2676 src,
2677 cpu_dst,
2678 Rotation::None,
2679 Flip::None,
2680 crop,
2681 );
2682 result.unwrap();
2683
2684 let gl_dst =
2685 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2686 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2687 let (result, _src, gl_dst) = convert_img(
2688 &mut gl_converter,
2689 src,
2690 gl_dst,
2691 Rotation::None,
2692 Flip::None,
2693 crop,
2694 );
2695 result.unwrap();
2696
2697 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2698 }
2699
2700 #[test]
2701 #[cfg(target_os = "linux")]
2702 #[cfg(feature = "opengl")]
2703 fn test_opengl_all_rgba() {
2704 if !is_opengl_available() {
2705 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2706 return;
2707 }
2708
2709 let dst_width = 640;
2710 let dst_height = 640;
2711 let file = include_bytes!(concat!(
2712 env!("CARGO_MANIFEST_DIR"),
2713 "/../../testdata/zidane.jpg"
2714 ))
2715 .to_vec();
2716
2717 let mut cpu_converter = CPUProcessor::new();
2718
2719 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2720
2721 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
2722 if is_dma_available() {
2723 mem.push(Some(TensorMemory::Dma));
2724 }
2725 let crop = Crop {
2726 src_rect: Some(Rect::new(50, 120, 1024, 576)),
2727 dst_rect: Some(Rect::new(100, 100, 512, 288)),
2728 dst_color: None,
2729 };
2730 for m in mem {
2731 let src = crate::load_image(&file, Some(PixelFormat::Rgba), m).unwrap();
2732 let src_dyn = src;
2733
2734 for rot in [
2735 Rotation::None,
2736 Rotation::Clockwise90,
2737 Rotation::Rotate180,
2738 Rotation::CounterClockwise90,
2739 ] {
2740 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
2741 let cpu_dst =
2742 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
2743 .unwrap();
2744 let gl_dst =
2745 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
2746 .unwrap();
2747 cpu_dst
2748 .as_u8()
2749 .unwrap()
2750 .map()
2751 .unwrap()
2752 .as_mut_slice()
2753 .fill(114);
2754 gl_dst
2755 .as_u8()
2756 .unwrap()
2757 .map()
2758 .unwrap()
2759 .as_mut_slice()
2760 .fill(114);
2761
2762 let mut cpu_dst_dyn = cpu_dst;
2763 cpu_converter
2764 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
2765 .unwrap();
2766 let cpu_dst = {
2767 let mut __t = cpu_dst_dyn.into_u8().unwrap();
2768 __t.set_format(PixelFormat::Rgba).unwrap();
2769 TensorDyn::from(__t)
2770 };
2771
2772 let mut gl_dst_dyn = gl_dst;
2773 gl_converter
2774 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
2775 .map_err(|e| {
2776 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
2777 e
2778 })
2779 .unwrap();
2780 let gl_dst = {
2781 let mut __t = gl_dst_dyn.into_u8().unwrap();
2782 __t.set_format(PixelFormat::Rgba).unwrap();
2783 TensorDyn::from(__t)
2784 };
2785
2786 compare_images(
2787 &gl_dst,
2788 &cpu_dst,
2789 0.98,
2790 &format!("{} {:?} {:?}", function!(), rot, flip),
2791 );
2792 }
2793 }
2794 }
2795 }
2796
2797 #[test]
2798 #[cfg(target_os = "linux")]
2799 fn test_cpu_rotate() {
2800 for rot in [
2801 Rotation::Clockwise90,
2802 Rotation::Rotate180,
2803 Rotation::CounterClockwise90,
2804 ] {
2805 test_cpu_rotate_(rot);
2806 }
2807 }
2808
2809 #[cfg(target_os = "linux")]
2810 fn test_cpu_rotate_(rot: Rotation) {
2811 let file = include_bytes!(concat!(
2815 env!("CARGO_MANIFEST_DIR"),
2816 "/../../testdata/zidane.jpg"
2817 ))
2818 .to_vec();
2819
2820 let unchanged_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2821 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2822
2823 let (dst_width, dst_height) = match rot {
2824 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
2825 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
2826 (src.height().unwrap(), src.width().unwrap())
2827 }
2828 };
2829
2830 let cpu_dst =
2831 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2832 let mut cpu_converter = CPUProcessor::new();
2833
2834 let (result, src, cpu_dst) = convert_img(
2837 &mut cpu_converter,
2838 src,
2839 cpu_dst,
2840 rot,
2841 Flip::None,
2842 Crop::no_crop(),
2843 );
2844 result.unwrap();
2845
2846 let (result, cpu_dst, src) = convert_img(
2847 &mut cpu_converter,
2848 cpu_dst,
2849 src,
2850 rot,
2851 Flip::None,
2852 Crop::no_crop(),
2853 );
2854 result.unwrap();
2855
2856 let (result, src, cpu_dst) = convert_img(
2857 &mut cpu_converter,
2858 src,
2859 cpu_dst,
2860 rot,
2861 Flip::None,
2862 Crop::no_crop(),
2863 );
2864 result.unwrap();
2865
2866 let (result, _cpu_dst, src) = convert_img(
2867 &mut cpu_converter,
2868 cpu_dst,
2869 src,
2870 rot,
2871 Flip::None,
2872 Crop::no_crop(),
2873 );
2874 result.unwrap();
2875
2876 compare_images(&src, &unchanged_src, 0.98, function!());
2877 }
2878
2879 #[test]
2880 #[cfg(target_os = "linux")]
2881 #[cfg(feature = "opengl")]
2882 fn test_opengl_rotate() {
2883 if !is_opengl_available() {
2884 eprintln!("SKIPPED: {} - OpenGL not available", function!());
2885 return;
2886 }
2887
2888 let size = (1280, 720);
2889 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
2890
2891 if is_dma_available() {
2892 mem.push(Some(TensorMemory::Dma));
2893 }
2894 for m in mem {
2895 for rot in [
2896 Rotation::Clockwise90,
2897 Rotation::Rotate180,
2898 Rotation::CounterClockwise90,
2899 ] {
2900 test_opengl_rotate_(size, rot, m);
2901 }
2902 }
2903 }
2904
2905 #[cfg(target_os = "linux")]
2906 #[cfg(feature = "opengl")]
2907 fn test_opengl_rotate_(
2908 size: (usize, usize),
2909 rot: Rotation,
2910 tensor_memory: Option<TensorMemory>,
2911 ) {
2912 let (dst_width, dst_height) = match rot {
2913 Rotation::None | Rotation::Rotate180 => size,
2914 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2915 };
2916
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), tensor_memory).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
2928 let (result, mut src, cpu_dst) = convert_img(
2929 &mut cpu_converter,
2930 src,
2931 cpu_dst,
2932 rot,
2933 Flip::None,
2934 Crop::no_crop(),
2935 );
2936 result.unwrap();
2937
2938 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
2939
2940 for _ in 0..5 {
2941 let gl_dst = TensorDyn::image(
2942 dst_width,
2943 dst_height,
2944 PixelFormat::Rgba,
2945 DType::U8,
2946 tensor_memory,
2947 )
2948 .unwrap();
2949 let (result, src_back, gl_dst) = convert_img(
2950 &mut gl_converter,
2951 src,
2952 gl_dst,
2953 rot,
2954 Flip::None,
2955 Crop::no_crop(),
2956 );
2957 result.unwrap();
2958 src = src_back;
2959 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
2960 }
2961 }
2962
2963 #[test]
2964 #[cfg(target_os = "linux")]
2965 fn test_g2d_rotate() {
2966 if !is_g2d_available() {
2967 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
2968 return;
2969 }
2970 if !is_dma_available() {
2971 eprintln!(
2972 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
2973 );
2974 return;
2975 }
2976
2977 let size = (1280, 720);
2978 for rot in [
2979 Rotation::Clockwise90,
2980 Rotation::Rotate180,
2981 Rotation::CounterClockwise90,
2982 ] {
2983 test_g2d_rotate_(size, rot);
2984 }
2985 }
2986
2987 #[cfg(target_os = "linux")]
2988 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
2989 let (dst_width, dst_height) = match rot {
2990 Rotation::None | Rotation::Rotate180 => size,
2991 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
2992 };
2993
2994 let file = include_bytes!(concat!(
2995 env!("CARGO_MANIFEST_DIR"),
2996 "/../../testdata/zidane.jpg"
2997 ))
2998 .to_vec();
2999 let src =
3000 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
3001
3002 let cpu_dst =
3003 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3004 let mut cpu_converter = CPUProcessor::new();
3005
3006 let (result, src, cpu_dst) = convert_img(
3007 &mut cpu_converter,
3008 src,
3009 cpu_dst,
3010 rot,
3011 Flip::None,
3012 Crop::no_crop(),
3013 );
3014 result.unwrap();
3015
3016 let g2d_dst = TensorDyn::image(
3017 dst_width,
3018 dst_height,
3019 PixelFormat::Rgba,
3020 DType::U8,
3021 Some(TensorMemory::Dma),
3022 )
3023 .unwrap();
3024 let mut g2d_converter = G2DProcessor::new().unwrap();
3025
3026 let (result, _src, g2d_dst) = convert_img(
3027 &mut g2d_converter,
3028 src,
3029 g2d_dst,
3030 rot,
3031 Flip::None,
3032 Crop::no_crop(),
3033 );
3034 result.unwrap();
3035
3036 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3037 }
3038
3039 #[test]
3040 fn test_rgba_to_yuyv_resize_cpu() {
3041 let src = load_bytes_to_tensor(
3042 1280,
3043 720,
3044 PixelFormat::Rgba,
3045 None,
3046 include_bytes!(concat!(
3047 env!("CARGO_MANIFEST_DIR"),
3048 "/../../testdata/camera720p.rgba"
3049 )),
3050 )
3051 .unwrap();
3052
3053 let (dst_width, dst_height) = (640, 360);
3054
3055 let dst =
3056 TensorDyn::image(dst_width, dst_height, PixelFormat::Yuyv, DType::U8, None).unwrap();
3057
3058 let dst_through_yuyv =
3059 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3060 let dst_direct =
3061 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3062
3063 let mut cpu_converter = CPUProcessor::new();
3064
3065 let (result, src, dst) = convert_img(
3066 &mut cpu_converter,
3067 src,
3068 dst,
3069 Rotation::None,
3070 Flip::None,
3071 Crop::no_crop(),
3072 );
3073 result.unwrap();
3074
3075 let (result, _dst, dst_through_yuyv) = convert_img(
3076 &mut cpu_converter,
3077 dst,
3078 dst_through_yuyv,
3079 Rotation::None,
3080 Flip::None,
3081 Crop::no_crop(),
3082 );
3083 result.unwrap();
3084
3085 let (result, _src, dst_direct) = convert_img(
3086 &mut cpu_converter,
3087 src,
3088 dst_direct,
3089 Rotation::None,
3090 Flip::None,
3091 Crop::no_crop(),
3092 );
3093 result.unwrap();
3094
3095 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
3096 }
3097
3098 #[test]
3099 #[cfg(target_os = "linux")]
3100 #[cfg(feature = "opengl")]
3101 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
3102 fn test_rgba_to_yuyv_resize_opengl() {
3103 if !is_opengl_available() {
3104 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3105 return;
3106 }
3107
3108 if !is_dma_available() {
3109 eprintln!(
3110 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3111 function!()
3112 );
3113 return;
3114 }
3115
3116 let src = load_bytes_to_tensor(
3117 1280,
3118 720,
3119 PixelFormat::Rgba,
3120 None,
3121 include_bytes!(concat!(
3122 env!("CARGO_MANIFEST_DIR"),
3123 "/../../testdata/camera720p.rgba"
3124 )),
3125 )
3126 .unwrap();
3127
3128 let (dst_width, dst_height) = (640, 360);
3129
3130 let dst = TensorDyn::image(
3131 dst_width,
3132 dst_height,
3133 PixelFormat::Yuyv,
3134 DType::U8,
3135 Some(TensorMemory::Dma),
3136 )
3137 .unwrap();
3138
3139 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3140
3141 let (result, src, dst) = convert_img(
3142 &mut gl_converter,
3143 src,
3144 dst,
3145 Rotation::None,
3146 Flip::None,
3147 Crop::new()
3148 .with_dst_rect(Some(Rect::new(100, 100, 100, 100)))
3149 .with_dst_color(Some([255, 255, 255, 255])),
3150 );
3151 result.unwrap();
3152
3153 std::fs::write(
3154 "rgba_to_yuyv_opengl.yuyv",
3155 dst.as_u8().unwrap().map().unwrap().as_slice(),
3156 )
3157 .unwrap();
3158 let cpu_dst = TensorDyn::image(
3159 dst_width,
3160 dst_height,
3161 PixelFormat::Yuyv,
3162 DType::U8,
3163 Some(TensorMemory::Dma),
3164 )
3165 .unwrap();
3166 let (result, _src, cpu_dst) = convert_img(
3167 &mut CPUProcessor::new(),
3168 src,
3169 cpu_dst,
3170 Rotation::None,
3171 Flip::None,
3172 Crop::no_crop(),
3173 );
3174 result.unwrap();
3175
3176 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
3177 }
3178
3179 #[test]
3180 #[cfg(target_os = "linux")]
3181 fn test_rgba_to_yuyv_resize_g2d() {
3182 if !is_g2d_available() {
3183 eprintln!(
3184 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
3185 );
3186 return;
3187 }
3188 if !is_dma_available() {
3189 eprintln!(
3190 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3191 );
3192 return;
3193 }
3194
3195 let src = load_bytes_to_tensor(
3196 1280,
3197 720,
3198 PixelFormat::Rgba,
3199 Some(TensorMemory::Dma),
3200 include_bytes!(concat!(
3201 env!("CARGO_MANIFEST_DIR"),
3202 "/../../testdata/camera720p.rgba"
3203 )),
3204 )
3205 .unwrap();
3206
3207 let (dst_width, dst_height) = (1280, 720);
3208
3209 let cpu_dst = TensorDyn::image(
3210 dst_width,
3211 dst_height,
3212 PixelFormat::Yuyv,
3213 DType::U8,
3214 Some(TensorMemory::Dma),
3215 )
3216 .unwrap();
3217
3218 let g2d_dst = TensorDyn::image(
3219 dst_width,
3220 dst_height,
3221 PixelFormat::Yuyv,
3222 DType::U8,
3223 Some(TensorMemory::Dma),
3224 )
3225 .unwrap();
3226
3227 let mut g2d_converter = G2DProcessor::new().unwrap();
3228 let crop = Crop {
3229 src_rect: None,
3230 dst_rect: Some(Rect::new(100, 100, 2, 2)),
3231 dst_color: None,
3232 };
3233
3234 g2d_dst
3235 .as_u8()
3236 .unwrap()
3237 .map()
3238 .unwrap()
3239 .as_mut_slice()
3240 .fill(128);
3241 let (result, src, g2d_dst) = convert_img(
3242 &mut g2d_converter,
3243 src,
3244 g2d_dst,
3245 Rotation::None,
3246 Flip::None,
3247 crop,
3248 );
3249 result.unwrap();
3250
3251 let cpu_dst_img = cpu_dst;
3252 cpu_dst_img
3253 .as_u8()
3254 .unwrap()
3255 .map()
3256 .unwrap()
3257 .as_mut_slice()
3258 .fill(128);
3259 let (result, _src, cpu_dst) = convert_img(
3260 &mut CPUProcessor::new(),
3261 src,
3262 cpu_dst_img,
3263 Rotation::None,
3264 Flip::None,
3265 crop,
3266 );
3267 result.unwrap();
3268
3269 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
3270 }
3271
3272 #[test]
3273 fn test_yuyv_to_rgba_cpu() {
3274 let file = include_bytes!(concat!(
3275 env!("CARGO_MANIFEST_DIR"),
3276 "/../../testdata/camera720p.yuyv"
3277 ))
3278 .to_vec();
3279 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
3280 src.as_u8()
3281 .unwrap()
3282 .map()
3283 .unwrap()
3284 .as_mut_slice()
3285 .copy_from_slice(&file);
3286
3287 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3288 let mut cpu_converter = CPUProcessor::new();
3289
3290 let (result, _src, dst) = convert_img(
3291 &mut cpu_converter,
3292 src,
3293 dst,
3294 Rotation::None,
3295 Flip::None,
3296 Crop::no_crop(),
3297 );
3298 result.unwrap();
3299
3300 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3301 target_image
3302 .as_u8()
3303 .unwrap()
3304 .map()
3305 .unwrap()
3306 .as_mut_slice()
3307 .copy_from_slice(include_bytes!(concat!(
3308 env!("CARGO_MANIFEST_DIR"),
3309 "/../../testdata/camera720p.rgba"
3310 )));
3311
3312 compare_images(&dst, &target_image, 0.98, function!());
3313 }
3314
3315 #[test]
3316 fn test_yuyv_to_rgb_cpu() {
3317 let file = include_bytes!(concat!(
3318 env!("CARGO_MANIFEST_DIR"),
3319 "/../../testdata/camera720p.yuyv"
3320 ))
3321 .to_vec();
3322 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
3323 src.as_u8()
3324 .unwrap()
3325 .map()
3326 .unwrap()
3327 .as_mut_slice()
3328 .copy_from_slice(&file);
3329
3330 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3331 let mut cpu_converter = CPUProcessor::new();
3332
3333 let (result, _src, dst) = convert_img(
3334 &mut cpu_converter,
3335 src,
3336 dst,
3337 Rotation::None,
3338 Flip::None,
3339 Crop::no_crop(),
3340 );
3341 result.unwrap();
3342
3343 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3344 target_image
3345 .as_u8()
3346 .unwrap()
3347 .map()
3348 .unwrap()
3349 .as_mut_slice()
3350 .as_chunks_mut::<3>()
3351 .0
3352 .iter_mut()
3353 .zip(
3354 include_bytes!(concat!(
3355 env!("CARGO_MANIFEST_DIR"),
3356 "/../../testdata/camera720p.rgba"
3357 ))
3358 .as_chunks::<4>()
3359 .0,
3360 )
3361 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
3362
3363 compare_images(&dst, &target_image, 0.98, function!());
3364 }
3365
3366 #[test]
3367 #[cfg(target_os = "linux")]
3368 fn test_yuyv_to_rgba_g2d() {
3369 if !is_g2d_available() {
3370 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
3371 return;
3372 }
3373 if !is_dma_available() {
3374 eprintln!(
3375 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3376 );
3377 return;
3378 }
3379
3380 let src = load_bytes_to_tensor(
3381 1280,
3382 720,
3383 PixelFormat::Yuyv,
3384 None,
3385 include_bytes!(concat!(
3386 env!("CARGO_MANIFEST_DIR"),
3387 "/../../testdata/camera720p.yuyv"
3388 )),
3389 )
3390 .unwrap();
3391
3392 let dst = TensorDyn::image(
3393 1280,
3394 720,
3395 PixelFormat::Rgba,
3396 DType::U8,
3397 Some(TensorMemory::Dma),
3398 )
3399 .unwrap();
3400 let mut g2d_converter = G2DProcessor::new().unwrap();
3401
3402 let (result, _src, dst) = convert_img(
3403 &mut g2d_converter,
3404 src,
3405 dst,
3406 Rotation::None,
3407 Flip::None,
3408 Crop::no_crop(),
3409 );
3410 result.unwrap();
3411
3412 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3413 target_image
3414 .as_u8()
3415 .unwrap()
3416 .map()
3417 .unwrap()
3418 .as_mut_slice()
3419 .copy_from_slice(include_bytes!(concat!(
3420 env!("CARGO_MANIFEST_DIR"),
3421 "/../../testdata/camera720p.rgba"
3422 )));
3423
3424 compare_images(&dst, &target_image, 0.98, function!());
3425 }
3426
3427 #[test]
3428 #[cfg(target_os = "linux")]
3429 #[cfg(feature = "opengl")]
3430 fn test_yuyv_to_rgba_opengl() {
3431 if !is_opengl_available() {
3432 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3433 return;
3434 }
3435 if !is_dma_available() {
3436 eprintln!(
3437 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3438 function!()
3439 );
3440 return;
3441 }
3442
3443 let src = load_bytes_to_tensor(
3444 1280,
3445 720,
3446 PixelFormat::Yuyv,
3447 Some(TensorMemory::Dma),
3448 include_bytes!(concat!(
3449 env!("CARGO_MANIFEST_DIR"),
3450 "/../../testdata/camera720p.yuyv"
3451 )),
3452 )
3453 .unwrap();
3454
3455 let dst = TensorDyn::image(
3456 1280,
3457 720,
3458 PixelFormat::Rgba,
3459 DType::U8,
3460 Some(TensorMemory::Dma),
3461 )
3462 .unwrap();
3463 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3464
3465 let (result, _src, dst) = convert_img(
3466 &mut gl_converter,
3467 src,
3468 dst,
3469 Rotation::None,
3470 Flip::None,
3471 Crop::no_crop(),
3472 );
3473 result.unwrap();
3474
3475 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3476 target_image
3477 .as_u8()
3478 .unwrap()
3479 .map()
3480 .unwrap()
3481 .as_mut_slice()
3482 .copy_from_slice(include_bytes!(concat!(
3483 env!("CARGO_MANIFEST_DIR"),
3484 "/../../testdata/camera720p.rgba"
3485 )));
3486
3487 compare_images(&dst, &target_image, 0.98, function!());
3488 }
3489
3490 #[test]
3491 #[cfg(target_os = "linux")]
3492 fn test_yuyv_to_rgb_g2d() {
3493 if !is_g2d_available() {
3494 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
3495 return;
3496 }
3497 if !is_dma_available() {
3498 eprintln!(
3499 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3500 );
3501 return;
3502 }
3503
3504 let src = load_bytes_to_tensor(
3505 1280,
3506 720,
3507 PixelFormat::Yuyv,
3508 None,
3509 include_bytes!(concat!(
3510 env!("CARGO_MANIFEST_DIR"),
3511 "/../../testdata/camera720p.yuyv"
3512 )),
3513 )
3514 .unwrap();
3515
3516 let g2d_dst = TensorDyn::image(
3517 1280,
3518 720,
3519 PixelFormat::Rgb,
3520 DType::U8,
3521 Some(TensorMemory::Dma),
3522 )
3523 .unwrap();
3524 let mut g2d_converter = G2DProcessor::new().unwrap();
3525
3526 let (result, src, g2d_dst) = convert_img(
3527 &mut g2d_converter,
3528 src,
3529 g2d_dst,
3530 Rotation::None,
3531 Flip::None,
3532 Crop::no_crop(),
3533 );
3534 result.unwrap();
3535
3536 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3537 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3538
3539 let (result, _src, cpu_dst) = convert_img(
3540 &mut cpu_converter,
3541 src,
3542 cpu_dst,
3543 Rotation::None,
3544 Flip::None,
3545 Crop::no_crop(),
3546 );
3547 result.unwrap();
3548
3549 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3550 }
3551
3552 #[test]
3553 #[cfg(target_os = "linux")]
3554 fn test_yuyv_to_yuyv_resize_g2d() {
3555 if !is_g2d_available() {
3556 eprintln!(
3557 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
3558 );
3559 return;
3560 }
3561 if !is_dma_available() {
3562 eprintln!(
3563 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3564 );
3565 return;
3566 }
3567
3568 let src = load_bytes_to_tensor(
3569 1280,
3570 720,
3571 PixelFormat::Yuyv,
3572 None,
3573 include_bytes!(concat!(
3574 env!("CARGO_MANIFEST_DIR"),
3575 "/../../testdata/camera720p.yuyv"
3576 )),
3577 )
3578 .unwrap();
3579
3580 let g2d_dst = TensorDyn::image(
3581 600,
3582 400,
3583 PixelFormat::Yuyv,
3584 DType::U8,
3585 Some(TensorMemory::Dma),
3586 )
3587 .unwrap();
3588 let mut g2d_converter = G2DProcessor::new().unwrap();
3589
3590 let (result, src, g2d_dst) = convert_img(
3591 &mut g2d_converter,
3592 src,
3593 g2d_dst,
3594 Rotation::None,
3595 Flip::None,
3596 Crop::no_crop(),
3597 );
3598 result.unwrap();
3599
3600 let cpu_dst = TensorDyn::image(600, 400, PixelFormat::Yuyv, DType::U8, None).unwrap();
3601 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
3602
3603 let (result, _src, cpu_dst) = convert_img(
3604 &mut cpu_converter,
3605 src,
3606 cpu_dst,
3607 Rotation::None,
3608 Flip::None,
3609 Crop::no_crop(),
3610 );
3611 result.unwrap();
3612
3613 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.98, function!());
3615 }
3616
3617 #[test]
3618 fn test_yuyv_to_rgba_resize_cpu() {
3619 let src = load_bytes_to_tensor(
3620 1280,
3621 720,
3622 PixelFormat::Yuyv,
3623 None,
3624 include_bytes!(concat!(
3625 env!("CARGO_MANIFEST_DIR"),
3626 "/../../testdata/camera720p.yuyv"
3627 )),
3628 )
3629 .unwrap();
3630
3631 let (dst_width, dst_height) = (960, 540);
3632
3633 let dst =
3634 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3635 let mut cpu_converter = CPUProcessor::new();
3636
3637 let (result, _src, dst) = convert_img(
3638 &mut cpu_converter,
3639 src,
3640 dst,
3641 Rotation::None,
3642 Flip::None,
3643 Crop::no_crop(),
3644 );
3645 result.unwrap();
3646
3647 let dst_target =
3648 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3649 let src_target = load_bytes_to_tensor(
3650 1280,
3651 720,
3652 PixelFormat::Rgba,
3653 None,
3654 include_bytes!(concat!(
3655 env!("CARGO_MANIFEST_DIR"),
3656 "/../../testdata/camera720p.rgba"
3657 )),
3658 )
3659 .unwrap();
3660 let (result, _src_target, dst_target) = convert_img(
3661 &mut cpu_converter,
3662 src_target,
3663 dst_target,
3664 Rotation::None,
3665 Flip::None,
3666 Crop::no_crop(),
3667 );
3668 result.unwrap();
3669
3670 compare_images(&dst, &dst_target, 0.98, function!());
3671 }
3672
3673 #[test]
3674 #[cfg(target_os = "linux")]
3675 fn test_yuyv_to_rgba_crop_flip_g2d() {
3676 if !is_g2d_available() {
3677 eprintln!(
3678 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
3679 );
3680 return;
3681 }
3682 if !is_dma_available() {
3683 eprintln!(
3684 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3685 );
3686 return;
3687 }
3688
3689 let src = load_bytes_to_tensor(
3690 1280,
3691 720,
3692 PixelFormat::Yuyv,
3693 Some(TensorMemory::Dma),
3694 include_bytes!(concat!(
3695 env!("CARGO_MANIFEST_DIR"),
3696 "/../../testdata/camera720p.yuyv"
3697 )),
3698 )
3699 .unwrap();
3700
3701 let (dst_width, dst_height) = (640, 640);
3702
3703 let dst_g2d = TensorDyn::image(
3704 dst_width,
3705 dst_height,
3706 PixelFormat::Rgba,
3707 DType::U8,
3708 Some(TensorMemory::Dma),
3709 )
3710 .unwrap();
3711 let mut g2d_converter = G2DProcessor::new().unwrap();
3712 let crop = Crop {
3713 src_rect: Some(Rect {
3714 left: 20,
3715 top: 15,
3716 width: 400,
3717 height: 300,
3718 }),
3719 dst_rect: None,
3720 dst_color: None,
3721 };
3722
3723 let (result, src, dst_g2d) = convert_img(
3724 &mut g2d_converter,
3725 src,
3726 dst_g2d,
3727 Rotation::None,
3728 Flip::Horizontal,
3729 crop,
3730 );
3731 result.unwrap();
3732
3733 let dst_cpu = TensorDyn::image(
3734 dst_width,
3735 dst_height,
3736 PixelFormat::Rgba,
3737 DType::U8,
3738 Some(TensorMemory::Dma),
3739 )
3740 .unwrap();
3741 let mut cpu_converter = CPUProcessor::new();
3742
3743 let (result, _src, dst_cpu) = convert_img(
3744 &mut cpu_converter,
3745 src,
3746 dst_cpu,
3747 Rotation::None,
3748 Flip::Horizontal,
3749 crop,
3750 );
3751 result.unwrap();
3752 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
3753 }
3754
3755 #[test]
3756 #[cfg(target_os = "linux")]
3757 #[cfg(feature = "opengl")]
3758 fn test_yuyv_to_rgba_crop_flip_opengl() {
3759 if !is_opengl_available() {
3760 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3761 return;
3762 }
3763
3764 if !is_dma_available() {
3765 eprintln!(
3766 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3767 function!()
3768 );
3769 return;
3770 }
3771
3772 let src = load_bytes_to_tensor(
3773 1280,
3774 720,
3775 PixelFormat::Yuyv,
3776 Some(TensorMemory::Dma),
3777 include_bytes!(concat!(
3778 env!("CARGO_MANIFEST_DIR"),
3779 "/../../testdata/camera720p.yuyv"
3780 )),
3781 )
3782 .unwrap();
3783
3784 let (dst_width, dst_height) = (640, 640);
3785
3786 let dst_gl = TensorDyn::image(
3787 dst_width,
3788 dst_height,
3789 PixelFormat::Rgba,
3790 DType::U8,
3791 Some(TensorMemory::Dma),
3792 )
3793 .unwrap();
3794 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3795 let crop = Crop {
3796 src_rect: Some(Rect {
3797 left: 20,
3798 top: 15,
3799 width: 400,
3800 height: 300,
3801 }),
3802 dst_rect: None,
3803 dst_color: None,
3804 };
3805
3806 let (result, src, dst_gl) = convert_img(
3807 &mut gl_converter,
3808 src,
3809 dst_gl,
3810 Rotation::None,
3811 Flip::Horizontal,
3812 crop,
3813 );
3814 result.unwrap();
3815
3816 let dst_cpu = TensorDyn::image(
3817 dst_width,
3818 dst_height,
3819 PixelFormat::Rgba,
3820 DType::U8,
3821 Some(TensorMemory::Dma),
3822 )
3823 .unwrap();
3824 let mut cpu_converter = CPUProcessor::new();
3825
3826 let (result, _src, dst_cpu) = convert_img(
3827 &mut cpu_converter,
3828 src,
3829 dst_cpu,
3830 Rotation::None,
3831 Flip::Horizontal,
3832 crop,
3833 );
3834 result.unwrap();
3835 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
3836 }
3837
3838 #[test]
3839 fn test_vyuy_to_rgba_cpu() {
3840 let file = include_bytes!(concat!(
3841 env!("CARGO_MANIFEST_DIR"),
3842 "/../../testdata/camera720p.vyuy"
3843 ))
3844 .to_vec();
3845 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
3846 src.as_u8()
3847 .unwrap()
3848 .map()
3849 .unwrap()
3850 .as_mut_slice()
3851 .copy_from_slice(&file);
3852
3853 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3854 let mut cpu_converter = CPUProcessor::new();
3855
3856 let (result, _src, dst) = convert_img(
3857 &mut cpu_converter,
3858 src,
3859 dst,
3860 Rotation::None,
3861 Flip::None,
3862 Crop::no_crop(),
3863 );
3864 result.unwrap();
3865
3866 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3867 target_image
3868 .as_u8()
3869 .unwrap()
3870 .map()
3871 .unwrap()
3872 .as_mut_slice()
3873 .copy_from_slice(include_bytes!(concat!(
3874 env!("CARGO_MANIFEST_DIR"),
3875 "/../../testdata/camera720p.rgba"
3876 )));
3877
3878 compare_images(&dst, &target_image, 0.98, function!());
3879 }
3880
3881 #[test]
3882 fn test_vyuy_to_rgb_cpu() {
3883 let file = include_bytes!(concat!(
3884 env!("CARGO_MANIFEST_DIR"),
3885 "/../../testdata/camera720p.vyuy"
3886 ))
3887 .to_vec();
3888 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
3889 src.as_u8()
3890 .unwrap()
3891 .map()
3892 .unwrap()
3893 .as_mut_slice()
3894 .copy_from_slice(&file);
3895
3896 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3897 let mut cpu_converter = CPUProcessor::new();
3898
3899 let (result, _src, dst) = convert_img(
3900 &mut cpu_converter,
3901 src,
3902 dst,
3903 Rotation::None,
3904 Flip::None,
3905 Crop::no_crop(),
3906 );
3907 result.unwrap();
3908
3909 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
3910 target_image
3911 .as_u8()
3912 .unwrap()
3913 .map()
3914 .unwrap()
3915 .as_mut_slice()
3916 .as_chunks_mut::<3>()
3917 .0
3918 .iter_mut()
3919 .zip(
3920 include_bytes!(concat!(
3921 env!("CARGO_MANIFEST_DIR"),
3922 "/../../testdata/camera720p.rgba"
3923 ))
3924 .as_chunks::<4>()
3925 .0,
3926 )
3927 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
3928
3929 compare_images(&dst, &target_image, 0.98, function!());
3930 }
3931
3932 #[test]
3933 #[cfg(target_os = "linux")]
3934 fn test_vyuy_to_rgba_g2d() {
3935 if !is_g2d_available() {
3936 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
3937 return;
3938 }
3939 if !is_dma_available() {
3940 eprintln!(
3941 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3942 );
3943 return;
3944 }
3945
3946 let src = load_bytes_to_tensor(
3947 1280,
3948 720,
3949 PixelFormat::Vyuy,
3950 None,
3951 include_bytes!(concat!(
3952 env!("CARGO_MANIFEST_DIR"),
3953 "/../../testdata/camera720p.vyuy"
3954 )),
3955 )
3956 .unwrap();
3957
3958 let dst = TensorDyn::image(
3959 1280,
3960 720,
3961 PixelFormat::Rgba,
3962 DType::U8,
3963 Some(TensorMemory::Dma),
3964 )
3965 .unwrap();
3966 let mut g2d_converter = G2DProcessor::new().unwrap();
3967
3968 let (result, _src, dst) = convert_img(
3969 &mut g2d_converter,
3970 src,
3971 dst,
3972 Rotation::None,
3973 Flip::None,
3974 Crop::no_crop(),
3975 );
3976 match result {
3977 Err(Error::G2D(_)) => {
3978 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
3979 return;
3980 }
3981 r => r.unwrap(),
3982 }
3983
3984 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3985 target_image
3986 .as_u8()
3987 .unwrap()
3988 .map()
3989 .unwrap()
3990 .as_mut_slice()
3991 .copy_from_slice(include_bytes!(concat!(
3992 env!("CARGO_MANIFEST_DIR"),
3993 "/../../testdata/camera720p.rgba"
3994 )));
3995
3996 compare_images(&dst, &target_image, 0.98, function!());
3997 }
3998
3999 #[test]
4000 #[cfg(target_os = "linux")]
4001 fn test_vyuy_to_rgb_g2d() {
4002 if !is_g2d_available() {
4003 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
4004 return;
4005 }
4006 if !is_dma_available() {
4007 eprintln!(
4008 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4009 );
4010 return;
4011 }
4012
4013 let src = load_bytes_to_tensor(
4014 1280,
4015 720,
4016 PixelFormat::Vyuy,
4017 None,
4018 include_bytes!(concat!(
4019 env!("CARGO_MANIFEST_DIR"),
4020 "/../../testdata/camera720p.vyuy"
4021 )),
4022 )
4023 .unwrap();
4024
4025 let g2d_dst = TensorDyn::image(
4026 1280,
4027 720,
4028 PixelFormat::Rgb,
4029 DType::U8,
4030 Some(TensorMemory::Dma),
4031 )
4032 .unwrap();
4033 let mut g2d_converter = G2DProcessor::new().unwrap();
4034
4035 let (result, src, g2d_dst) = convert_img(
4036 &mut g2d_converter,
4037 src,
4038 g2d_dst,
4039 Rotation::None,
4040 Flip::None,
4041 Crop::no_crop(),
4042 );
4043 match result {
4044 Err(Error::G2D(_)) => {
4045 eprintln!(
4046 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
4047 );
4048 return;
4049 }
4050 r => r.unwrap(),
4051 }
4052
4053 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4054 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4055
4056 let (result, _src, cpu_dst) = convert_img(
4057 &mut cpu_converter,
4058 src,
4059 cpu_dst,
4060 Rotation::None,
4061 Flip::None,
4062 Crop::no_crop(),
4063 );
4064 result.unwrap();
4065
4066 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4067 }
4068
4069 #[test]
4070 #[cfg(target_os = "linux")]
4071 #[cfg(feature = "opengl")]
4072 fn test_vyuy_to_rgba_opengl() {
4073 if !is_opengl_available() {
4074 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4075 return;
4076 }
4077 if !is_dma_available() {
4078 eprintln!(
4079 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4080 function!()
4081 );
4082 return;
4083 }
4084
4085 let src = load_bytes_to_tensor(
4086 1280,
4087 720,
4088 PixelFormat::Vyuy,
4089 Some(TensorMemory::Dma),
4090 include_bytes!(concat!(
4091 env!("CARGO_MANIFEST_DIR"),
4092 "/../../testdata/camera720p.vyuy"
4093 )),
4094 )
4095 .unwrap();
4096
4097 let dst = TensorDyn::image(
4098 1280,
4099 720,
4100 PixelFormat::Rgba,
4101 DType::U8,
4102 Some(TensorMemory::Dma),
4103 )
4104 .unwrap();
4105 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4106
4107 let (result, _src, dst) = convert_img(
4108 &mut gl_converter,
4109 src,
4110 dst,
4111 Rotation::None,
4112 Flip::None,
4113 Crop::no_crop(),
4114 );
4115 match result {
4116 Err(Error::NotSupported(_)) => {
4117 eprintln!(
4118 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
4119 function!()
4120 );
4121 return;
4122 }
4123 r => r.unwrap(),
4124 }
4125
4126 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4127 target_image
4128 .as_u8()
4129 .unwrap()
4130 .map()
4131 .unwrap()
4132 .as_mut_slice()
4133 .copy_from_slice(include_bytes!(concat!(
4134 env!("CARGO_MANIFEST_DIR"),
4135 "/../../testdata/camera720p.rgba"
4136 )));
4137
4138 compare_images(&dst, &target_image, 0.98, function!());
4139 }
4140
4141 #[test]
4142 fn test_nv12_to_rgba_cpu() {
4143 let file = include_bytes!(concat!(
4144 env!("CARGO_MANIFEST_DIR"),
4145 "/../../testdata/zidane.nv12"
4146 ))
4147 .to_vec();
4148 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4149 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4150 .copy_from_slice(&file);
4151
4152 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4153 let mut cpu_converter = CPUProcessor::new();
4154
4155 let (result, _src, dst) = convert_img(
4156 &mut cpu_converter,
4157 src,
4158 dst,
4159 Rotation::None,
4160 Flip::None,
4161 Crop::no_crop(),
4162 );
4163 result.unwrap();
4164
4165 let target_image = crate::load_image(
4166 include_bytes!(concat!(
4167 env!("CARGO_MANIFEST_DIR"),
4168 "/../../testdata/zidane.jpg"
4169 )),
4170 Some(PixelFormat::Rgba),
4171 None,
4172 )
4173 .unwrap();
4174
4175 compare_images(&dst, &target_image, 0.98, function!());
4176 }
4177
4178 #[test]
4179 fn test_nv12_to_rgb_cpu() {
4180 let file = include_bytes!(concat!(
4181 env!("CARGO_MANIFEST_DIR"),
4182 "/../../testdata/zidane.nv12"
4183 ))
4184 .to_vec();
4185 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4186 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4187 .copy_from_slice(&file);
4188
4189 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4190 let mut cpu_converter = CPUProcessor::new();
4191
4192 let (result, _src, dst) = convert_img(
4193 &mut cpu_converter,
4194 src,
4195 dst,
4196 Rotation::None,
4197 Flip::None,
4198 Crop::no_crop(),
4199 );
4200 result.unwrap();
4201
4202 let target_image = crate::load_image(
4203 include_bytes!(concat!(
4204 env!("CARGO_MANIFEST_DIR"),
4205 "/../../testdata/zidane.jpg"
4206 )),
4207 Some(PixelFormat::Rgb),
4208 None,
4209 )
4210 .unwrap();
4211
4212 compare_images(&dst, &target_image, 0.98, function!());
4213 }
4214
4215 #[test]
4216 fn test_nv12_to_grey_cpu() {
4217 let file = include_bytes!(concat!(
4218 env!("CARGO_MANIFEST_DIR"),
4219 "/../../testdata/zidane.nv12"
4220 ))
4221 .to_vec();
4222 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4223 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4224 .copy_from_slice(&file);
4225
4226 let dst = TensorDyn::image(1280, 720, PixelFormat::Grey, DType::U8, None).unwrap();
4227 let mut cpu_converter = CPUProcessor::new();
4228
4229 let (result, _src, dst) = convert_img(
4230 &mut cpu_converter,
4231 src,
4232 dst,
4233 Rotation::None,
4234 Flip::None,
4235 Crop::no_crop(),
4236 );
4237 result.unwrap();
4238
4239 let target_image = crate::load_image(
4240 include_bytes!(concat!(
4241 env!("CARGO_MANIFEST_DIR"),
4242 "/../../testdata/zidane.jpg"
4243 )),
4244 Some(PixelFormat::Grey),
4245 None,
4246 )
4247 .unwrap();
4248
4249 compare_images(&dst, &target_image, 0.98, function!());
4250 }
4251
4252 #[test]
4253 fn test_nv12_to_yuyv_cpu() {
4254 let file = include_bytes!(concat!(
4255 env!("CARGO_MANIFEST_DIR"),
4256 "/../../testdata/zidane.nv12"
4257 ))
4258 .to_vec();
4259 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
4260 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
4261 .copy_from_slice(&file);
4262
4263 let dst = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
4264 let mut cpu_converter = CPUProcessor::new();
4265
4266 let (result, _src, dst) = convert_img(
4267 &mut cpu_converter,
4268 src,
4269 dst,
4270 Rotation::None,
4271 Flip::None,
4272 Crop::no_crop(),
4273 );
4274 result.unwrap();
4275
4276 let target_image = crate::load_image(
4277 include_bytes!(concat!(
4278 env!("CARGO_MANIFEST_DIR"),
4279 "/../../testdata/zidane.jpg"
4280 )),
4281 Some(PixelFormat::Rgb),
4282 None,
4283 )
4284 .unwrap();
4285
4286 compare_images_convert_to_rgb(&dst, &target_image, 0.98, function!());
4287 }
4288
4289 #[test]
4290 fn test_cpu_resize_planar_rgb() {
4291 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
4292 #[rustfmt::skip]
4293 let src_image = [
4294 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
4295 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4296 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
4297 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4298 ];
4299 src.as_u8()
4300 .unwrap()
4301 .map()
4302 .unwrap()
4303 .as_mut_slice()
4304 .copy_from_slice(&src_image);
4305
4306 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
4307 let mut cpu_converter = CPUProcessor::new();
4308
4309 let (result, _src, cpu_dst) = convert_img(
4310 &mut cpu_converter,
4311 src,
4312 cpu_dst,
4313 Rotation::None,
4314 Flip::None,
4315 Crop::new()
4316 .with_dst_rect(Some(Rect {
4317 left: 1,
4318 top: 1,
4319 width: 4,
4320 height: 4,
4321 }))
4322 .with_dst_color(Some([114, 114, 114, 255])),
4323 );
4324 result.unwrap();
4325
4326 #[rustfmt::skip]
4327 let expected_dst = [
4328 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,
4329 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,
4330 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,
4331 ];
4332
4333 assert_eq!(
4334 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
4335 &expected_dst
4336 );
4337 }
4338
4339 #[test]
4340 fn test_cpu_resize_planar_rgba() {
4341 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
4342 #[rustfmt::skip]
4343 let src_image = [
4344 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
4345 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4346 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
4347 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
4348 ];
4349 src.as_u8()
4350 .unwrap()
4351 .map()
4352 .unwrap()
4353 .as_mut_slice()
4354 .copy_from_slice(&src_image);
4355
4356 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgba, DType::U8, None).unwrap();
4357 let mut cpu_converter = CPUProcessor::new();
4358
4359 let (result, _src, cpu_dst) = convert_img(
4360 &mut cpu_converter,
4361 src,
4362 cpu_dst,
4363 Rotation::None,
4364 Flip::None,
4365 Crop::new()
4366 .with_dst_rect(Some(Rect {
4367 left: 1,
4368 top: 1,
4369 width: 4,
4370 height: 4,
4371 }))
4372 .with_dst_color(Some([114, 114, 114, 255])),
4373 );
4374 result.unwrap();
4375
4376 #[rustfmt::skip]
4377 let expected_dst = [
4378 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,
4379 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,
4380 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,
4381 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,
4382 ];
4383
4384 assert_eq!(
4385 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
4386 &expected_dst
4387 );
4388 }
4389
4390 #[test]
4391 #[cfg(target_os = "linux")]
4392 #[cfg(feature = "opengl")]
4393 fn test_opengl_resize_planar_rgb() {
4394 if !is_opengl_available() {
4395 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4396 return;
4397 }
4398
4399 if !is_dma_available() {
4400 eprintln!(
4401 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4402 function!()
4403 );
4404 return;
4405 }
4406
4407 let dst_width = 640;
4408 let dst_height = 640;
4409 let file = include_bytes!(concat!(
4410 env!("CARGO_MANIFEST_DIR"),
4411 "/../../testdata/test_image.jpg"
4412 ))
4413 .to_vec();
4414 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
4415
4416 let cpu_dst = TensorDyn::image(
4417 dst_width,
4418 dst_height,
4419 PixelFormat::PlanarRgb,
4420 DType::U8,
4421 None,
4422 )
4423 .unwrap();
4424 let mut cpu_converter = CPUProcessor::new();
4425 let (result, src, cpu_dst) = convert_img(
4426 &mut cpu_converter,
4427 src,
4428 cpu_dst,
4429 Rotation::None,
4430 Flip::None,
4431 Crop::no_crop(),
4432 );
4433 result.unwrap();
4434 let crop_letterbox = Crop::new()
4435 .with_dst_rect(Some(Rect {
4436 left: 102,
4437 top: 102,
4438 width: 440,
4439 height: 440,
4440 }))
4441 .with_dst_color(Some([114, 114, 114, 114]));
4442 let (result, src, cpu_dst) = convert_img(
4443 &mut cpu_converter,
4444 src,
4445 cpu_dst,
4446 Rotation::None,
4447 Flip::None,
4448 crop_letterbox,
4449 );
4450 result.unwrap();
4451
4452 let gl_dst = TensorDyn::image(
4453 dst_width,
4454 dst_height,
4455 PixelFormat::PlanarRgb,
4456 DType::U8,
4457 None,
4458 )
4459 .unwrap();
4460 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4461
4462 let (result, _src, gl_dst) = convert_img(
4463 &mut gl_converter,
4464 src,
4465 gl_dst,
4466 Rotation::None,
4467 Flip::None,
4468 crop_letterbox,
4469 );
4470 result.unwrap();
4471 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
4472 }
4473
4474 #[test]
4475 fn test_cpu_resize_nv16() {
4476 let file = include_bytes!(concat!(
4477 env!("CARGO_MANIFEST_DIR"),
4478 "/../../testdata/zidane.jpg"
4479 ))
4480 .to_vec();
4481 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
4482
4483 let cpu_nv16_dst = TensorDyn::image(640, 640, PixelFormat::Nv16, DType::U8, None).unwrap();
4484 let cpu_rgb_dst = TensorDyn::image(640, 640, PixelFormat::Rgb, DType::U8, None).unwrap();
4485 let mut cpu_converter = CPUProcessor::new();
4486 let crop = Crop::new()
4487 .with_dst_rect(Some(Rect {
4488 left: 20,
4489 top: 140,
4490 width: 600,
4491 height: 360,
4492 }))
4493 .with_dst_color(Some([255, 128, 0, 255]));
4494
4495 let (result, src, cpu_nv16_dst) = convert_img(
4496 &mut cpu_converter,
4497 src,
4498 cpu_nv16_dst,
4499 Rotation::None,
4500 Flip::None,
4501 crop,
4502 );
4503 result.unwrap();
4504
4505 let (result, _src, cpu_rgb_dst) = convert_img(
4506 &mut cpu_converter,
4507 src,
4508 cpu_rgb_dst,
4509 Rotation::None,
4510 Flip::None,
4511 crop,
4512 );
4513 result.unwrap();
4514 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
4515 }
4516
4517 fn load_bytes_to_tensor(
4518 width: usize,
4519 height: usize,
4520 format: PixelFormat,
4521 memory: Option<TensorMemory>,
4522 bytes: &[u8],
4523 ) -> Result<TensorDyn, Error> {
4524 let src = TensorDyn::image(width, height, format, DType::U8, memory)?;
4525 src.as_u8()
4526 .unwrap()
4527 .map()?
4528 .as_mut_slice()
4529 .copy_from_slice(bytes);
4530 Ok(src)
4531 }
4532
4533 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
4534 assert_eq!(img1.height(), img2.height(), "Heights differ");
4535 assert_eq!(img1.width(), img2.width(), "Widths differ");
4536 assert_eq!(
4537 img1.format().unwrap(),
4538 img2.format().unwrap(),
4539 "PixelFormat differ"
4540 );
4541 assert!(
4542 matches!(
4543 img1.format().unwrap(),
4544 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
4545 ),
4546 "format must be Rgb or Rgba for comparison"
4547 );
4548
4549 let image1 = match img1.format().unwrap() {
4550 PixelFormat::Rgb => image::RgbImage::from_vec(
4551 img1.width().unwrap() as u32,
4552 img1.height().unwrap() as u32,
4553 img1.as_u8().unwrap().map().unwrap().to_vec(),
4554 )
4555 .unwrap(),
4556 PixelFormat::Rgba => image::RgbaImage::from_vec(
4557 img1.width().unwrap() as u32,
4558 img1.height().unwrap() as u32,
4559 img1.as_u8().unwrap().map().unwrap().to_vec(),
4560 )
4561 .unwrap()
4562 .convert(),
4563 PixelFormat::Grey => image::GrayImage::from_vec(
4564 img1.width().unwrap() as u32,
4565 img1.height().unwrap() as u32,
4566 img1.as_u8().unwrap().map().unwrap().to_vec(),
4567 )
4568 .unwrap()
4569 .convert(),
4570 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
4571 img1.width().unwrap() as u32,
4572 (img1.height().unwrap() * 3) as u32,
4573 img1.as_u8().unwrap().map().unwrap().to_vec(),
4574 )
4575 .unwrap()
4576 .convert(),
4577 _ => return,
4578 };
4579
4580 let image2 = match img2.format().unwrap() {
4581 PixelFormat::Rgb => image::RgbImage::from_vec(
4582 img2.width().unwrap() as u32,
4583 img2.height().unwrap() as u32,
4584 img2.as_u8().unwrap().map().unwrap().to_vec(),
4585 )
4586 .unwrap(),
4587 PixelFormat::Rgba => image::RgbaImage::from_vec(
4588 img2.width().unwrap() as u32,
4589 img2.height().unwrap() as u32,
4590 img2.as_u8().unwrap().map().unwrap().to_vec(),
4591 )
4592 .unwrap()
4593 .convert(),
4594 PixelFormat::Grey => image::GrayImage::from_vec(
4595 img2.width().unwrap() as u32,
4596 img2.height().unwrap() as u32,
4597 img2.as_u8().unwrap().map().unwrap().to_vec(),
4598 )
4599 .unwrap()
4600 .convert(),
4601 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
4602 img2.width().unwrap() as u32,
4603 (img2.height().unwrap() * 3) as u32,
4604 img2.as_u8().unwrap().map().unwrap().to_vec(),
4605 )
4606 .unwrap()
4607 .convert(),
4608 _ => return,
4609 };
4610
4611 let similarity = image_compare::rgb_similarity_structure(
4612 &image_compare::Algorithm::RootMeanSquared,
4613 &image1,
4614 &image2,
4615 )
4616 .expect("Image Comparison failed");
4617 if similarity.score < threshold {
4618 similarity
4621 .image
4622 .to_color_map()
4623 .save(format!("{name}.png"))
4624 .unwrap();
4625 panic!(
4626 "{name}: converted image and target image have similarity score too low: {} < {}",
4627 similarity.score, threshold
4628 )
4629 }
4630 }
4631
4632 fn compare_images_convert_to_rgb(
4633 img1: &TensorDyn,
4634 img2: &TensorDyn,
4635 threshold: f64,
4636 name: &str,
4637 ) {
4638 assert_eq!(img1.height(), img2.height(), "Heights differ");
4639 assert_eq!(img1.width(), img2.width(), "Widths differ");
4640
4641 let mut img_rgb1 = TensorDyn::image(
4642 img1.width().unwrap(),
4643 img1.height().unwrap(),
4644 PixelFormat::Rgb,
4645 DType::U8,
4646 Some(TensorMemory::Mem),
4647 )
4648 .unwrap();
4649 let mut img_rgb2 = TensorDyn::image(
4650 img1.width().unwrap(),
4651 img1.height().unwrap(),
4652 PixelFormat::Rgb,
4653 DType::U8,
4654 Some(TensorMemory::Mem),
4655 )
4656 .unwrap();
4657 let mut __cv = CPUProcessor::default();
4658 let r1 = __cv.convert(
4659 img1,
4660 &mut img_rgb1,
4661 crate::Rotation::None,
4662 crate::Flip::None,
4663 crate::Crop::default(),
4664 );
4665 let r2 = __cv.convert(
4666 img2,
4667 &mut img_rgb2,
4668 crate::Rotation::None,
4669 crate::Flip::None,
4670 crate::Crop::default(),
4671 );
4672 if r1.is_err() || r2.is_err() {
4673 let w = img1.width().unwrap() as u32;
4675 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
4676 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
4677 let h1 = (data1.len() as u32) / w;
4678 let h2 = (data2.len() as u32) / w;
4679 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
4680 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
4681 let similarity = image_compare::gray_similarity_structure(
4682 &image_compare::Algorithm::RootMeanSquared,
4683 &g1,
4684 &g2,
4685 )
4686 .expect("Image Comparison failed");
4687 if similarity.score < threshold {
4688 panic!(
4689 "{name}: converted image and target image have similarity score too low: {} < {}",
4690 similarity.score, threshold
4691 )
4692 }
4693 return;
4694 }
4695
4696 let image1 = image::RgbImage::from_vec(
4697 img_rgb1.width().unwrap() as u32,
4698 img_rgb1.height().unwrap() as u32,
4699 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
4700 )
4701 .unwrap();
4702
4703 let image2 = image::RgbImage::from_vec(
4704 img_rgb2.width().unwrap() as u32,
4705 img_rgb2.height().unwrap() as u32,
4706 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
4707 )
4708 .unwrap();
4709
4710 let similarity = image_compare::rgb_similarity_structure(
4711 &image_compare::Algorithm::RootMeanSquared,
4712 &image1,
4713 &image2,
4714 )
4715 .expect("Image Comparison failed");
4716 if similarity.score < threshold {
4717 similarity
4720 .image
4721 .to_color_map()
4722 .save(format!("{name}.png"))
4723 .unwrap();
4724 panic!(
4725 "{name}: converted image and target image have similarity score too low: {} < {}",
4726 similarity.score, threshold
4727 )
4728 }
4729 }
4730
4731 #[test]
4736 fn test_nv12_image_creation() {
4737 let width = 640;
4738 let height = 480;
4739 let img = TensorDyn::image(width, height, PixelFormat::Nv12, DType::U8, None).unwrap();
4740
4741 assert_eq!(img.width(), Some(width));
4742 assert_eq!(img.height(), Some(height));
4743 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
4744 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
4746 }
4747
4748 #[test]
4749 fn test_nv12_channels() {
4750 let img = TensorDyn::image(640, 480, PixelFormat::Nv12, DType::U8, None).unwrap();
4751 assert_eq!(img.format().unwrap().channels(), 1);
4753 }
4754
4755 #[test]
4760 fn test_tensor_set_format_planar() {
4761 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
4762 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
4763 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
4764 assert_eq!(tensor.width(), Some(640));
4765 assert_eq!(tensor.height(), Some(480));
4766 }
4767
4768 #[test]
4769 fn test_tensor_set_format_interleaved() {
4770 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
4771 tensor.set_format(PixelFormat::Rgba).unwrap();
4772 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
4773 assert_eq!(tensor.width(), Some(640));
4774 assert_eq!(tensor.height(), Some(480));
4775 }
4776
4777 #[test]
4778 fn test_tensordyn_image_rgb() {
4779 let img = TensorDyn::image(640, 480, PixelFormat::Rgb, DType::U8, None).unwrap();
4780 assert_eq!(img.width(), Some(640));
4781 assert_eq!(img.height(), Some(480));
4782 assert_eq!(img.format(), Some(PixelFormat::Rgb));
4783 }
4784
4785 #[test]
4786 fn test_tensordyn_image_planar_rgb() {
4787 let img = TensorDyn::image(640, 480, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
4788 assert_eq!(img.width(), Some(640));
4789 assert_eq!(img.height(), Some(480));
4790 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
4791 }
4792
4793 #[test]
4794 fn test_rgb_int8_format() {
4795 let img = TensorDyn::image(
4797 1280,
4798 720,
4799 PixelFormat::Rgb,
4800 DType::I8,
4801 Some(TensorMemory::Mem),
4802 )
4803 .unwrap();
4804 assert_eq!(img.width(), Some(1280));
4805 assert_eq!(img.height(), Some(720));
4806 assert_eq!(img.format(), Some(PixelFormat::Rgb));
4807 assert_eq!(img.dtype(), DType::I8);
4808 }
4809
4810 #[test]
4811 fn test_planar_rgb_int8_format() {
4812 let img = TensorDyn::image(
4813 1280,
4814 720,
4815 PixelFormat::PlanarRgb,
4816 DType::I8,
4817 Some(TensorMemory::Mem),
4818 )
4819 .unwrap();
4820 assert_eq!(img.width(), Some(1280));
4821 assert_eq!(img.height(), Some(720));
4822 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
4823 assert_eq!(img.dtype(), DType::I8);
4824 }
4825
4826 #[test]
4827 fn test_rgb_from_tensor() {
4828 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
4829 tensor.set_format(PixelFormat::Rgb).unwrap();
4830 let img = TensorDyn::from(tensor);
4831 assert_eq!(img.width(), Some(1280));
4832 assert_eq!(img.height(), Some(720));
4833 assert_eq!(img.format(), Some(PixelFormat::Rgb));
4834 }
4835
4836 #[test]
4837 fn test_planar_rgb_from_tensor() {
4838 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
4839 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
4840 let img = TensorDyn::from(tensor);
4841 assert_eq!(img.width(), Some(1280));
4842 assert_eq!(img.height(), Some(720));
4843 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
4844 }
4845
4846 #[test]
4847 fn test_dtype_determines_int8() {
4848 let u8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::U8, None).unwrap();
4850 let i8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::I8, None).unwrap();
4851 assert_eq!(u8_img.dtype(), DType::U8);
4852 assert_eq!(i8_img.dtype(), DType::I8);
4853 }
4854
4855 #[test]
4856 fn test_pixel_layout_packed_vs_planar() {
4857 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
4859 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
4860 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
4861 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
4862 }
4863
4864 #[cfg(target_os = "linux")]
4869 #[cfg(feature = "opengl")]
4870 #[test]
4871 fn test_convert_pbo_to_pbo() {
4872 let mut converter = ImageProcessor::new().unwrap();
4873
4874 let is_pbo = converter
4876 .opengl
4877 .as_ref()
4878 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
4879 if !is_pbo {
4880 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
4881 return;
4882 }
4883
4884 let src_w = 640;
4885 let src_h = 480;
4886 let dst_w = 320;
4887 let dst_h = 240;
4888
4889 let pbo_src = converter
4891 .create_image(src_w, src_h, PixelFormat::Rgba, DType::U8, None)
4892 .unwrap();
4893 assert_eq!(
4894 pbo_src.as_u8().unwrap().memory(),
4895 TensorMemory::Pbo,
4896 "create_image should produce a PBO tensor"
4897 );
4898
4899 let file = include_bytes!(concat!(
4901 env!("CARGO_MANIFEST_DIR"),
4902 "/../../testdata/zidane.jpg"
4903 ))
4904 .to_vec();
4905 let jpeg_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
4906
4907 let mem_src = TensorDyn::image(
4909 src_w,
4910 src_h,
4911 PixelFormat::Rgba,
4912 DType::U8,
4913 Some(TensorMemory::Mem),
4914 )
4915 .unwrap();
4916 let (result, _jpeg_src, mem_src) = convert_img(
4917 &mut CPUProcessor::new(),
4918 jpeg_src,
4919 mem_src,
4920 Rotation::None,
4921 Flip::None,
4922 Crop::no_crop(),
4923 );
4924 result.unwrap();
4925
4926 {
4928 let src_data = mem_src.as_u8().unwrap().map().unwrap();
4929 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
4930 pbo_map.copy_from_slice(&src_data);
4931 }
4932
4933 let pbo_dst = converter
4935 .create_image(dst_w, dst_h, PixelFormat::Rgba, DType::U8, None)
4936 .unwrap();
4937 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
4938
4939 let mut pbo_dst = pbo_dst;
4941 let result = converter.convert(
4942 &pbo_src,
4943 &mut pbo_dst,
4944 Rotation::None,
4945 Flip::None,
4946 Crop::no_crop(),
4947 );
4948 result.unwrap();
4949
4950 let cpu_dst = TensorDyn::image(
4952 dst_w,
4953 dst_h,
4954 PixelFormat::Rgba,
4955 DType::U8,
4956 Some(TensorMemory::Mem),
4957 )
4958 .unwrap();
4959 let (result, _mem_src, cpu_dst) = convert_img(
4960 &mut CPUProcessor::new(),
4961 mem_src,
4962 cpu_dst,
4963 Rotation::None,
4964 Flip::None,
4965 Crop::no_crop(),
4966 );
4967 result.unwrap();
4968
4969 let pbo_dst_img = {
4970 let mut __t = pbo_dst.into_u8().unwrap();
4971 __t.set_format(PixelFormat::Rgba).unwrap();
4972 TensorDyn::from(__t)
4973 };
4974 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
4975 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
4976 }
4977
4978 #[test]
4979 fn test_image_bgra() {
4980 let img = TensorDyn::image(
4981 640,
4982 480,
4983 PixelFormat::Bgra,
4984 DType::U8,
4985 Some(edgefirst_tensor::TensorMemory::Mem),
4986 )
4987 .unwrap();
4988 assert_eq!(img.width(), Some(640));
4989 assert_eq!(img.height(), Some(480));
4990 assert_eq!(img.format().unwrap().channels(), 4);
4991 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
4992 }
4993
4994 #[test]
4999 fn test_force_backend_cpu() {
5000 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5001 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5002 let result = ImageProcessor::new();
5003 match original {
5004 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5005 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5006 }
5007 let converter = result.unwrap();
5008 assert!(converter.cpu.is_some());
5009 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
5010 }
5011
5012 #[test]
5013 fn test_force_backend_invalid() {
5014 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5015 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
5016 let result = ImageProcessor::new();
5017 match original {
5018 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5019 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5020 }
5021 assert!(
5022 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
5023 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
5024 );
5025 }
5026
5027 #[test]
5028 fn test_force_backend_unset() {
5029 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5030 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
5031 let result = ImageProcessor::new();
5032 match original {
5033 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5034 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5035 }
5036 let converter = result.unwrap();
5037 assert!(converter.forced_backend.is_none());
5038 }
5039
5040 #[test]
5045 fn test_draw_masks_proto_no_cpu_returns_error() {
5046 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
5048 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
5049 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
5050 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
5051 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
5052 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
5053
5054 let result = ImageProcessor::new();
5055
5056 match original_cpu {
5057 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
5058 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
5059 }
5060 match original_gl {
5061 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
5062 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
5063 }
5064 match original_g2d {
5065 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
5066 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
5067 }
5068
5069 let mut converter = result.unwrap();
5070 assert!(converter.cpu.is_none(), "CPU should be disabled");
5071
5072 let dst = TensorDyn::image(
5073 640,
5074 480,
5075 PixelFormat::Rgba,
5076 DType::U8,
5077 Some(TensorMemory::Mem),
5078 )
5079 .unwrap();
5080 let mut dst_dyn = dst;
5081 let det = [DetectBox {
5082 bbox: edgefirst_decoder::BoundingBox {
5083 xmin: 0.1,
5084 ymin: 0.1,
5085 xmax: 0.5,
5086 ymax: 0.5,
5087 },
5088 score: 0.9,
5089 label: 0,
5090 }];
5091 let proto_data = ProtoData {
5092 mask_coefficients: vec![vec![0.5; 4]],
5093 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
5094 };
5095 let result = converter.draw_masks_proto(&mut dst_dyn, &det, &proto_data);
5096 assert!(
5097 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
5098 "draw_masks_proto without CPU should return Internal error: {result:?}"
5099 );
5100 }
5101
5102 #[test]
5103 fn test_draw_masks_proto_cpu_fallback_works() {
5104 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5106 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5107 let result = ImageProcessor::new();
5108 match original {
5109 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5110 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5111 }
5112
5113 let mut converter = result.unwrap();
5114 assert!(converter.cpu.is_some());
5115
5116 let dst = TensorDyn::image(
5117 64,
5118 64,
5119 PixelFormat::Rgba,
5120 DType::U8,
5121 Some(TensorMemory::Mem),
5122 )
5123 .unwrap();
5124 let mut dst_dyn = dst;
5125 let det = [DetectBox {
5126 bbox: edgefirst_decoder::BoundingBox {
5127 xmin: 0.1,
5128 ymin: 0.1,
5129 xmax: 0.5,
5130 ymax: 0.5,
5131 },
5132 score: 0.9,
5133 label: 0,
5134 }];
5135 let proto_data = ProtoData {
5136 mask_coefficients: vec![vec![0.5; 4]],
5137 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
5138 };
5139 let result = converter.draw_masks_proto(&mut dst_dyn, &det, &proto_data);
5140 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
5141 }
5142}