1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
63
64pub const GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES: usize = 64;
77
78pub fn align_width_for_gpu_pitch(width: usize, bpp: usize) -> usize {
116 if bpp == 0 || width == 0 {
117 return width;
118 }
119
120 let Some(lcm_alignment) = checked_num_integer_lcm(GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES, bpp)
129 else {
130 log::warn!(
131 "align_width_for_gpu_pitch: lcm({GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES}, {bpp}) \
132 overflows usize, returning unaligned width {width}"
133 );
134 return width;
135 };
136 if lcm_alignment == 0 {
137 return width;
138 }
139
140 debug_assert_eq!(lcm_alignment % bpp, 0);
141 let width_alignment = lcm_alignment / bpp;
142 if width_alignment == 0 {
143 return width;
144 }
145
146 let remainder = width % width_alignment;
147 if remainder == 0 {
148 return width;
149 }
150
151 let pad = width_alignment - remainder;
152 match width.checked_add(pad) {
153 Some(aligned) => aligned,
154 None => {
155 log::warn!(
156 "align_width_for_gpu_pitch: width {width} + pad {pad} overflows usize, \
157 returning unaligned (caller should use a smaller width or pre-aligned size)"
158 );
159 width
160 }
161 }
162}
163
164#[cfg(target_os = "linux")]
173pub(crate) fn align_pitch_bytes_to_gpu_alignment(min_pitch_bytes: usize) -> Option<usize> {
174 let alignment = GPU_DMA_BUF_PITCH_ALIGNMENT_BYTES;
175 if min_pitch_bytes == 0 {
176 return Some(0);
177 }
178 let remainder = min_pitch_bytes % alignment;
179 if remainder == 0 {
180 return Some(min_pitch_bytes);
181 }
182 min_pitch_bytes.checked_add(alignment - remainder)
183}
184
185fn checked_num_integer_lcm(a: usize, b: usize) -> Option<usize> {
188 if a == 0 || b == 0 {
189 return Some(0);
190 }
191 let g = num_integer_gcd(a, b);
192 (a / g).checked_mul(b)
195}
196
197fn num_integer_gcd(a: usize, b: usize) -> usize {
198 if b == 0 {
199 a
200 } else {
201 num_integer_gcd(b, a % b)
202 }
203}
204
205pub fn primary_plane_bpp(format: PixelFormat, elem: usize) -> Option<usize> {
221 use edgefirst_tensor::PixelLayout;
222 match format.layout() {
223 PixelLayout::Packed => Some(format.channels() * elem),
224 PixelLayout::Planar => Some(elem),
225 PixelLayout::SemiPlanar => Some(elem),
229 _ => None,
232 }
233}
234
235use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
236use edgefirst_tensor::{
237 DType, PixelFormat, PixelLayout, Tensor, TensorDyn, TensorMemory, TensorTrait as _,
238};
239use enum_dispatch::enum_dispatch;
240use std::{fmt::Display, time::Instant};
241use zune_jpeg::{
242 zune_core::{colorspace::ColorSpace, options::DecoderOptions},
243 JpegDecoder,
244};
245use zune_png::PngDecoder;
246
247pub use cpu::CPUProcessor;
248pub use error::{Error, Result};
249#[cfg(target_os = "linux")]
250pub use g2d::G2DProcessor;
251#[cfg(target_os = "linux")]
252#[cfg(feature = "opengl")]
253pub use opengl_headless::GLProcessorThreaded;
254#[cfg(target_os = "linux")]
255#[cfg(feature = "opengl")]
256pub use opengl_headless::Int8InterpolationMode;
257#[cfg(target_os = "linux")]
258#[cfg(feature = "opengl")]
259pub use opengl_headless::{probe_egl_displays, EglDisplayInfo, EglDisplayKind};
260
261mod cpu;
262mod error;
263mod g2d;
264#[path = "gl/mod.rs"]
265mod opengl_headless;
266
267fn rotate_flip_to_dyn(
272 src: &Tensor<u8>,
273 src_fmt: PixelFormat,
274 rotation: Rotation,
275 flip: Flip,
276 memory: Option<TensorMemory>,
277) -> Result<TensorDyn, Error> {
278 let src_w = src.width().unwrap();
279 let src_h = src.height().unwrap();
280 let channels = src_fmt.channels();
281
282 let (dst_w, dst_h) = match rotation {
283 Rotation::None | Rotation::Rotate180 => (src_w, src_h),
284 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (src_h, src_w),
285 };
286
287 let dst = Tensor::<u8>::image(dst_w, dst_h, src_fmt, memory)?;
288 let src_map = src.map()?;
289 let mut dst_map = dst.map()?;
290
291 CPUProcessor::flip_rotate_ndarray_pf(
292 &src_map,
293 &mut dst_map,
294 dst_w,
295 dst_h,
296 channels,
297 rotation,
298 flip,
299 )?;
300 drop(dst_map);
301 drop(src_map);
302
303 Ok(TensorDyn::from(dst))
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum Rotation {
308 None = 0,
309 Clockwise90 = 1,
310 Rotate180 = 2,
311 CounterClockwise90 = 3,
312}
313impl Rotation {
314 pub fn from_degrees_clockwise(angle: usize) -> Rotation {
327 match angle.rem_euclid(360) {
328 0 => Rotation::None,
329 90 => Rotation::Clockwise90,
330 180 => Rotation::Rotate180,
331 270 => Rotation::CounterClockwise90,
332 _ => panic!("rotation angle is not a multiple of 90"),
333 }
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum Flip {
339 None = 0,
340 Vertical = 1,
341 Horizontal = 2,
342}
343
344#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
346pub enum ColorMode {
347 #[default]
352 Class,
353 Instance,
358 Track,
361}
362
363impl ColorMode {
364 #[inline]
366 pub fn index(self, idx: usize, label: usize) -> usize {
367 match self {
368 ColorMode::Class => label,
369 ColorMode::Instance | ColorMode::Track => idx,
370 }
371 }
372}
373
374#[derive(Debug, Clone, Copy)]
386pub struct MaskOverlay<'a> {
387 pub background: Option<&'a TensorDyn>,
388 pub opacity: f32,
389 pub letterbox: Option<[f32; 4]>,
399 pub color_mode: ColorMode,
400}
401
402impl Default for MaskOverlay<'_> {
403 fn default() -> Self {
404 Self {
405 background: None,
406 opacity: 1.0,
407 letterbox: None,
408 color_mode: ColorMode::Class,
409 }
410 }
411}
412
413impl<'a> MaskOverlay<'a> {
414 pub fn new() -> Self {
415 Self::default()
416 }
417
418 pub fn with_background(mut self, bg: &'a TensorDyn) -> Self {
419 self.background = Some(bg);
420 self
421 }
422
423 pub fn with_opacity(mut self, opacity: f32) -> Self {
424 self.opacity = opacity.clamp(0.0, 1.0);
425 self
426 }
427
428 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
429 self.color_mode = mode;
430 self
431 }
432
433 pub fn with_letterbox_crop(mut self, crop: &Crop, model_w: usize, model_h: usize) -> Self {
443 if let Some(r) = crop.dst_rect {
444 self.letterbox = Some([
445 r.left as f32 / model_w as f32,
446 r.top as f32 / model_h as f32,
447 (r.left + r.width) as f32 / model_w as f32,
448 (r.top + r.height) as f32 / model_h as f32,
449 ]);
450 }
451 self
452 }
453
454 fn apply_background(&self, dst: &mut TensorDyn) -> Result<MaskOverlay<'static>> {
457 use edgefirst_tensor::TensorMapTrait;
458 if let Some(bg) = self.background {
459 if bg.shape() != dst.shape() {
460 return Err(Error::InvalidShape(
461 "background shape does not match dst".into(),
462 ));
463 }
464 if bg.format() != dst.format() {
465 return Err(Error::InvalidShape(
466 "background pixel format does not match dst".into(),
467 ));
468 }
469 let bg_u8 = bg.as_u8().ok_or(Error::NotAnImage)?;
470 let dst_u8 = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
471 let bg_map = bg_u8.map()?;
472 let mut dst_map = dst_u8.map()?;
473 let bg_slice = bg_map.as_slice();
474 let dst_slice = dst_map.as_mut_slice();
475 if bg_slice.len() != dst_slice.len() {
476 return Err(Error::InvalidShape(
477 "background buffer size does not match dst".into(),
478 ));
479 }
480 dst_slice.copy_from_slice(bg_slice);
481 }
482 Ok(MaskOverlay {
483 background: None,
484 opacity: self.opacity.clamp(0.0, 1.0),
485 letterbox: self.letterbox,
486 color_mode: self.color_mode,
487 })
488 }
489}
490
491#[inline]
500fn unletter_bbox(bbox: DetectBox, lb: [f32; 4]) -> DetectBox {
501 let b = bbox.bbox.to_canonical();
502 let [lx0, ly0, lx1, ly1] = lb;
503 let inv_w = if lx1 > lx0 { 1.0 / (lx1 - lx0) } else { 1.0 };
504 let inv_h = if ly1 > ly0 { 1.0 / (ly1 - ly0) } else { 1.0 };
505 DetectBox {
506 bbox: edgefirst_decoder::BoundingBox {
507 xmin: ((b.xmin - lx0) * inv_w).clamp(0.0, 1.0),
508 ymin: ((b.ymin - ly0) * inv_h).clamp(0.0, 1.0),
509 xmax: ((b.xmax - lx0) * inv_w).clamp(0.0, 1.0),
510 ymax: ((b.ymax - ly0) * inv_h).clamp(0.0, 1.0),
511 },
512 ..bbox
513 }
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub struct Crop {
518 pub src_rect: Option<Rect>,
519 pub dst_rect: Option<Rect>,
520 pub dst_color: Option<[u8; 4]>,
521}
522
523impl Default for Crop {
524 fn default() -> Self {
525 Crop::new()
526 }
527}
528impl Crop {
529 pub fn new() -> Self {
531 Crop {
532 src_rect: None,
533 dst_rect: None,
534 dst_color: None,
535 }
536 }
537
538 pub fn with_src_rect(mut self, src_rect: Option<Rect>) -> Self {
540 self.src_rect = src_rect;
541 self
542 }
543
544 pub fn with_dst_rect(mut self, dst_rect: Option<Rect>) -> Self {
546 self.dst_rect = dst_rect;
547 self
548 }
549
550 pub fn with_dst_color(mut self, dst_color: Option<[u8; 4]>) -> Self {
552 self.dst_color = dst_color;
553 self
554 }
555
556 pub fn no_crop() -> Self {
558 Crop::new()
559 }
560
561 pub(crate) fn check_crop_dims(
563 &self,
564 src_w: usize,
565 src_h: usize,
566 dst_w: usize,
567 dst_h: usize,
568 ) -> Result<(), Error> {
569 let src_ok = self
570 .src_rect
571 .is_none_or(|r| r.left + r.width <= src_w && r.top + r.height <= src_h);
572 let dst_ok = self
573 .dst_rect
574 .is_none_or(|r| r.left + r.width <= dst_w && r.top + r.height <= dst_h);
575 match (src_ok, dst_ok) {
576 (true, true) => Ok(()),
577 (true, false) => Err(Error::CropInvalid(format!(
578 "Dest crop invalid: {:?}",
579 self.dst_rect
580 ))),
581 (false, true) => Err(Error::CropInvalid(format!(
582 "Src crop invalid: {:?}",
583 self.src_rect
584 ))),
585 (false, false) => Err(Error::CropInvalid(format!(
586 "Dest and Src crop invalid: {:?} {:?}",
587 self.dst_rect, self.src_rect
588 ))),
589 }
590 }
591
592 pub fn check_crop_dyn(
594 &self,
595 src: &edgefirst_tensor::TensorDyn,
596 dst: &edgefirst_tensor::TensorDyn,
597 ) -> Result<(), Error> {
598 self.check_crop_dims(
599 src.width().unwrap_or(0),
600 src.height().unwrap_or(0),
601 dst.width().unwrap_or(0),
602 dst.height().unwrap_or(0),
603 )
604 }
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq)]
608pub struct Rect {
609 pub left: usize,
610 pub top: usize,
611 pub width: usize,
612 pub height: usize,
613}
614
615impl Rect {
616 pub fn new(left: usize, top: usize, width: usize, height: usize) -> Self {
618 Self {
619 left,
620 top,
621 width,
622 height,
623 }
624 }
625
626 pub fn check_rect_dyn(&self, image: &TensorDyn) -> bool {
628 let w = image.width().unwrap_or(0);
629 let h = image.height().unwrap_or(0);
630 self.left + self.width <= w && self.top + self.height <= h
631 }
632}
633
634#[enum_dispatch(ImageProcessor)]
635pub trait ImageProcessorTrait {
636 fn convert(
652 &mut self,
653 src: &TensorDyn,
654 dst: &mut TensorDyn,
655 rotation: Rotation,
656 flip: Flip,
657 crop: Crop,
658 ) -> Result<()>;
659
660 fn draw_decoded_masks(
680 &mut self,
681 dst: &mut TensorDyn,
682 detect: &[DetectBox],
683 segmentation: &[Segmentation],
684 overlay: MaskOverlay<'_>,
685 ) -> Result<()>;
686
687 fn draw_proto_masks(
704 &mut self,
705 dst: &mut TensorDyn,
706 detect: &[DetectBox],
707 proto_data: &ProtoData,
708 overlay: MaskOverlay<'_>,
709 ) -> Result<()>;
710
711 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()>;
714}
715
716#[derive(Debug, Clone, Default)]
722pub struct ImageProcessorConfig {
723 #[cfg(target_os = "linux")]
731 #[cfg(feature = "opengl")]
732 pub egl_display: Option<EglDisplayKind>,
733
734 pub backend: ComputeBackend,
746}
747
748#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
755pub enum ComputeBackend {
756 #[default]
758 Auto,
759 Cpu,
761 G2d,
763 OpenGl,
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
773pub(crate) enum ForcedBackend {
774 Cpu,
775 G2d,
776 OpenGl,
777}
778
779#[derive(Debug)]
782pub struct ImageProcessor {
783 pub cpu: Option<CPUProcessor>,
786
787 #[cfg(target_os = "linux")]
788 pub g2d: Option<G2DProcessor>,
792 #[cfg(target_os = "linux")]
793 #[cfg(feature = "opengl")]
794 pub opengl: Option<GLProcessorThreaded>,
798
799 pub(crate) forced_backend: Option<ForcedBackend>,
801}
802
803unsafe impl Send for ImageProcessor {}
804unsafe impl Sync for ImageProcessor {}
805
806impl ImageProcessor {
807 pub fn new() -> Result<Self> {
825 Self::with_config(ImageProcessorConfig::default())
826 }
827
828 #[allow(unused_variables)]
837 pub fn with_config(config: ImageProcessorConfig) -> Result<Self> {
838 match config.backend {
842 ComputeBackend::Cpu => {
843 log::info!("ComputeBackend::Cpu — CPU only");
844 return Ok(Self {
845 cpu: Some(CPUProcessor::new()),
846 #[cfg(target_os = "linux")]
847 g2d: None,
848 #[cfg(target_os = "linux")]
849 #[cfg(feature = "opengl")]
850 opengl: None,
851 forced_backend: None,
852 });
853 }
854 ComputeBackend::G2d => {
855 log::info!("ComputeBackend::G2d — G2D + CPU fallback");
856 #[cfg(target_os = "linux")]
857 {
858 let g2d = match G2DProcessor::new() {
859 Ok(g) => Some(g),
860 Err(e) => {
861 log::warn!("G2D requested but failed to initialize: {e:?}");
862 None
863 }
864 };
865 return Ok(Self {
866 cpu: Some(CPUProcessor::new()),
867 g2d,
868 #[cfg(feature = "opengl")]
869 opengl: None,
870 forced_backend: None,
871 });
872 }
873 #[cfg(not(target_os = "linux"))]
874 {
875 log::warn!("G2D requested but not available on this platform, using CPU");
876 return Ok(Self {
877 cpu: Some(CPUProcessor::new()),
878 forced_backend: None,
879 });
880 }
881 }
882 ComputeBackend::OpenGl => {
883 log::info!("ComputeBackend::OpenGl — OpenGL + CPU fallback");
884 #[cfg(target_os = "linux")]
885 {
886 #[cfg(feature = "opengl")]
887 let opengl = match GLProcessorThreaded::new(config.egl_display) {
888 Ok(gl) => Some(gl),
889 Err(e) => {
890 log::warn!("OpenGL requested but failed to initialize: {e:?}");
891 None
892 }
893 };
894 return Ok(Self {
895 cpu: Some(CPUProcessor::new()),
896 g2d: None,
897 #[cfg(feature = "opengl")]
898 opengl,
899 forced_backend: None,
900 });
901 }
902 #[cfg(not(target_os = "linux"))]
903 {
904 log::warn!("OpenGL requested but not available on this platform, using CPU");
905 return Ok(Self {
906 cpu: Some(CPUProcessor::new()),
907 forced_backend: None,
908 });
909 }
910 }
911 ComputeBackend::Auto => { }
912 }
913
914 if let Ok(val) = std::env::var("EDGEFIRST_FORCE_BACKEND") {
919 let val_lower = val.to_lowercase();
920 let forced = match val_lower.as_str() {
921 "cpu" => ForcedBackend::Cpu,
922 "g2d" => ForcedBackend::G2d,
923 "opengl" => ForcedBackend::OpenGl,
924 other => {
925 return Err(Error::ForcedBackendUnavailable(format!(
926 "unknown EDGEFIRST_FORCE_BACKEND value: {other:?} (expected cpu, g2d, or opengl)"
927 )));
928 }
929 };
930
931 log::info!("EDGEFIRST_FORCE_BACKEND={val} — only initializing {val_lower} backend");
932
933 return match forced {
934 ForcedBackend::Cpu => Ok(Self {
935 cpu: Some(CPUProcessor::new()),
936 #[cfg(target_os = "linux")]
937 g2d: None,
938 #[cfg(target_os = "linux")]
939 #[cfg(feature = "opengl")]
940 opengl: None,
941 forced_backend: Some(ForcedBackend::Cpu),
942 }),
943 ForcedBackend::G2d => {
944 #[cfg(target_os = "linux")]
945 {
946 let g2d = G2DProcessor::new().map_err(|e| {
947 Error::ForcedBackendUnavailable(format!(
948 "g2d forced but failed to initialize: {e:?}"
949 ))
950 })?;
951 Ok(Self {
952 cpu: None,
953 g2d: Some(g2d),
954 #[cfg(feature = "opengl")]
955 opengl: None,
956 forced_backend: Some(ForcedBackend::G2d),
957 })
958 }
959 #[cfg(not(target_os = "linux"))]
960 {
961 Err(Error::ForcedBackendUnavailable(
962 "g2d backend is only available on Linux".into(),
963 ))
964 }
965 }
966 ForcedBackend::OpenGl => {
967 #[cfg(target_os = "linux")]
968 #[cfg(feature = "opengl")]
969 {
970 let opengl = GLProcessorThreaded::new(config.egl_display).map_err(|e| {
971 Error::ForcedBackendUnavailable(format!(
972 "opengl forced but failed to initialize: {e:?}"
973 ))
974 })?;
975 Ok(Self {
976 cpu: None,
977 g2d: None,
978 opengl: Some(opengl),
979 forced_backend: Some(ForcedBackend::OpenGl),
980 })
981 }
982 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
983 {
984 Err(Error::ForcedBackendUnavailable(
985 "opengl backend requires Linux with the 'opengl' feature enabled"
986 .into(),
987 ))
988 }
989 }
990 };
991 }
992
993 #[cfg(target_os = "linux")]
995 let g2d = if std::env::var("EDGEFIRST_DISABLE_G2D")
996 .map(|x| x != "0" && x.to_lowercase() != "false")
997 .unwrap_or(false)
998 {
999 log::debug!("EDGEFIRST_DISABLE_G2D is set");
1000 None
1001 } else {
1002 match G2DProcessor::new() {
1003 Ok(g2d_converter) => Some(g2d_converter),
1004 Err(err) => {
1005 log::warn!("Failed to initialize G2D converter: {err:?}");
1006 None
1007 }
1008 }
1009 };
1010
1011 #[cfg(target_os = "linux")]
1012 #[cfg(feature = "opengl")]
1013 let opengl = if std::env::var("EDGEFIRST_DISABLE_GL")
1014 .map(|x| x != "0" && x.to_lowercase() != "false")
1015 .unwrap_or(false)
1016 {
1017 log::debug!("EDGEFIRST_DISABLE_GL is set");
1018 None
1019 } else {
1020 match GLProcessorThreaded::new(config.egl_display) {
1021 Ok(gl_converter) => Some(gl_converter),
1022 Err(err) => {
1023 log::warn!("Failed to initialize GL converter: {err:?}");
1024 None
1025 }
1026 }
1027 };
1028
1029 let cpu = if std::env::var("EDGEFIRST_DISABLE_CPU")
1030 .map(|x| x != "0" && x.to_lowercase() != "false")
1031 .unwrap_or(false)
1032 {
1033 log::debug!("EDGEFIRST_DISABLE_CPU is set");
1034 None
1035 } else {
1036 Some(CPUProcessor::new())
1037 };
1038 Ok(Self {
1039 cpu,
1040 #[cfg(target_os = "linux")]
1041 g2d,
1042 #[cfg(target_os = "linux")]
1043 #[cfg(feature = "opengl")]
1044 opengl,
1045 forced_backend: None,
1046 })
1047 }
1048
1049 #[cfg(target_os = "linux")]
1052 #[cfg(feature = "opengl")]
1053 pub fn set_int8_interpolation_mode(&mut self, mode: Int8InterpolationMode) -> Result<()> {
1054 if let Some(ref mut gl) = self.opengl {
1055 gl.set_int8_interpolation_mode(mode)?;
1056 }
1057 Ok(())
1058 }
1059
1060 pub fn create_image(
1117 &self,
1118 width: usize,
1119 height: usize,
1120 format: PixelFormat,
1121 dtype: DType,
1122 memory: Option<TensorMemory>,
1123 ) -> Result<TensorDyn> {
1124 #[cfg(target_os = "linux")]
1135 let dma_stride_bytes: Option<usize> = primary_plane_bpp(format, dtype.size())
1136 .and_then(|bpp| width.checked_mul(bpp))
1137 .and_then(align_pitch_bytes_to_gpu_alignment);
1138
1139 #[cfg(target_os = "linux")]
1143 let try_dma = || -> Result<TensorDyn> {
1144 let packed = format.layout() == edgefirst_tensor::PixelLayout::Packed;
1152 match dma_stride_bytes {
1153 Some(stride)
1154 if packed
1155 && primary_plane_bpp(format, dtype.size())
1156 .and_then(|bpp| width.checked_mul(bpp))
1157 .is_some_and(|natural| stride > natural) =>
1158 {
1159 log::debug!(
1160 "create_image: padding row stride for {format:?} {width}x{height} \
1161 from natural pitch to {stride} bytes for GPU alignment"
1162 );
1163 Ok(TensorDyn::image_with_stride(
1164 width,
1165 height,
1166 format,
1167 dtype,
1168 stride,
1169 Some(edgefirst_tensor::TensorMemory::Dma),
1170 )?)
1171 }
1172 _ => Ok(TensorDyn::image(
1173 width,
1174 height,
1175 format,
1176 dtype,
1177 Some(edgefirst_tensor::TensorMemory::Dma),
1178 )?),
1179 }
1180 };
1181
1182 match memory {
1186 #[cfg(target_os = "linux")]
1187 Some(TensorMemory::Dma) => {
1188 return try_dma();
1189 }
1190 Some(mem) => {
1191 return Ok(TensorDyn::image(width, height, format, dtype, Some(mem))?);
1192 }
1193 None => {}
1194 }
1195
1196 #[cfg(target_os = "linux")]
1199 {
1200 #[cfg(feature = "opengl")]
1201 let gl_uses_pbo = self
1202 .opengl
1203 .as_ref()
1204 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
1205 #[cfg(not(feature = "opengl"))]
1206 let gl_uses_pbo = false;
1207
1208 if !gl_uses_pbo {
1209 if let Ok(img) = try_dma() {
1210 return Ok(img);
1211 }
1212 }
1213 }
1214
1215 #[cfg(target_os = "linux")]
1219 #[cfg(feature = "opengl")]
1220 if dtype.size() == 1 {
1221 if let Some(gl) = &self.opengl {
1222 match gl.create_pbo_image(width, height, format) {
1223 Ok(t) => {
1224 if dtype == DType::I8 {
1225 debug_assert!(
1233 t.chroma().is_none(),
1234 "PBO i8 transmute requires chroma == None"
1235 );
1236 let t_i8: Tensor<i8> = unsafe { std::mem::transmute(t) };
1237 return Ok(TensorDyn::from(t_i8));
1238 }
1239 return Ok(TensorDyn::from(t));
1240 }
1241 Err(e) => log::debug!("PBO image creation failed, falling back to Mem: {e:?}"),
1242 }
1243 }
1244 }
1245
1246 Ok(TensorDyn::image(
1248 width,
1249 height,
1250 format,
1251 dtype,
1252 Some(edgefirst_tensor::TensorMemory::Mem),
1253 )?)
1254 }
1255
1256 #[cfg(target_os = "linux")]
1308 pub fn import_image(
1309 &self,
1310 image: edgefirst_tensor::PlaneDescriptor,
1311 chroma: Option<edgefirst_tensor::PlaneDescriptor>,
1312 width: usize,
1313 height: usize,
1314 format: PixelFormat,
1315 dtype: DType,
1316 ) -> Result<TensorDyn> {
1317 use edgefirst_tensor::{Tensor, TensorMemory};
1318
1319 let image_stride = image.stride();
1321 let image_offset = image.offset();
1322 let chroma_stride = chroma.as_ref().and_then(|c| c.stride());
1323 let chroma_offset = chroma.as_ref().and_then(|c| c.offset());
1324
1325 if let Some(chroma_pd) = chroma {
1326 if dtype != DType::U8 && dtype != DType::I8 {
1331 return Err(Error::NotSupported(format!(
1332 "multiplane import only supports U8/I8, got {dtype:?}"
1333 )));
1334 }
1335 if format.layout() != PixelLayout::SemiPlanar {
1336 return Err(Error::NotSupported(format!(
1337 "import_image with chroma requires a semi-planar format, got {format:?}"
1338 )));
1339 }
1340
1341 let chroma_h = match format {
1342 PixelFormat::Nv12 => {
1343 if !height.is_multiple_of(2) {
1344 return Err(Error::InvalidShape(format!(
1345 "NV12 requires even height, got {height}"
1346 )));
1347 }
1348 height / 2
1349 }
1350 PixelFormat::Nv16 => {
1353 return Err(Error::NotSupported(
1354 "multiplane NV16 is not yet supported; use contiguous NV16 instead".into(),
1355 ))
1356 }
1357 _ => {
1358 return Err(Error::NotSupported(format!(
1359 "unsupported semi-planar format: {format:?}"
1360 )))
1361 }
1362 };
1363
1364 let luma = Tensor::<u8>::from_fd(image.into_fd(), &[height, width], Some("luma"))?;
1365 if luma.memory() != TensorMemory::Dma {
1366 return Err(Error::NotSupported(format!(
1367 "luma fd must be DMA-backed, got {:?}",
1368 luma.memory()
1369 )));
1370 }
1371
1372 let chroma_tensor =
1373 Tensor::<u8>::from_fd(chroma_pd.into_fd(), &[chroma_h, width], Some("chroma"))?;
1374 if chroma_tensor.memory() != TensorMemory::Dma {
1375 return Err(Error::NotSupported(format!(
1376 "chroma fd must be DMA-backed, got {:?}",
1377 chroma_tensor.memory()
1378 )));
1379 }
1380
1381 let mut tensor = Tensor::<u8>::from_planes(luma, chroma_tensor, format)?;
1384
1385 if let Some(s) = image_stride {
1387 tensor.set_row_stride(s)?;
1388 }
1389 if let Some(o) = image_offset {
1390 tensor.set_plane_offset(o);
1391 }
1392
1393 if let Some(chroma_ref) = tensor.chroma_mut() {
1398 if let Some(s) = chroma_stride {
1399 if s < width {
1400 return Err(Error::InvalidShape(format!(
1401 "chroma stride {s} < minimum {width} for {format:?}"
1402 )));
1403 }
1404 chroma_ref.set_row_stride_unchecked(s);
1405 }
1406 if let Some(o) = chroma_offset {
1407 chroma_ref.set_plane_offset(o);
1408 }
1409 }
1410
1411 if dtype == DType::I8 {
1412 const {
1416 assert!(std::mem::size_of::<Tensor<u8>>() == std::mem::size_of::<Tensor<i8>>());
1417 assert!(
1418 std::mem::align_of::<Tensor<u8>>() == std::mem::align_of::<Tensor<i8>>()
1419 );
1420 }
1421 let tensor_i8: Tensor<i8> = unsafe { std::mem::transmute(tensor) };
1422 return Ok(TensorDyn::from(tensor_i8));
1423 }
1424 Ok(TensorDyn::from(tensor))
1425 } else {
1426 let shape = match format.layout() {
1428 PixelLayout::Packed => vec![height, width, format.channels()],
1429 PixelLayout::Planar => vec![format.channels(), height, width],
1430 PixelLayout::SemiPlanar => {
1431 let total_h = match format {
1432 PixelFormat::Nv12 => {
1433 if !height.is_multiple_of(2) {
1434 return Err(Error::InvalidShape(format!(
1435 "NV12 requires even height, got {height}"
1436 )));
1437 }
1438 height * 3 / 2
1439 }
1440 PixelFormat::Nv16 => height * 2,
1441 _ => {
1442 return Err(Error::InvalidShape(format!(
1443 "unknown semi-planar height multiplier for {format:?}"
1444 )))
1445 }
1446 };
1447 vec![total_h, width]
1448 }
1449 _ => {
1450 return Err(Error::NotSupported(format!(
1451 "unsupported pixel layout for import_image: {:?}",
1452 format.layout()
1453 )));
1454 }
1455 };
1456 let tensor = TensorDyn::from_fd(image.into_fd(), &shape, dtype, None)?;
1457 if tensor.memory() != TensorMemory::Dma {
1458 return Err(Error::NotSupported(format!(
1459 "import_image requires DMA-backed fd, got {:?}",
1460 tensor.memory()
1461 )));
1462 }
1463 let mut tensor = tensor.with_format(format)?;
1464 if let Some(s) = image_stride {
1465 tensor.set_row_stride(s)?;
1466 }
1467 if let Some(o) = image_offset {
1468 tensor.set_plane_offset(o);
1469 }
1470 Ok(tensor)
1471 }
1472 }
1473
1474 pub fn draw_masks(
1482 &mut self,
1483 decoder: &edgefirst_decoder::Decoder,
1484 outputs: &[&TensorDyn],
1485 dst: &mut TensorDyn,
1486 overlay: MaskOverlay<'_>,
1487 ) -> Result<Vec<DetectBox>> {
1488 let mut output_boxes = Vec::with_capacity(100);
1489
1490 let proto_result = decoder
1492 .decode_proto(outputs, &mut output_boxes)
1493 .map_err(|e| Error::Internal(format!("decode_proto: {e:#?}")))?;
1494
1495 if let Some(proto_data) = proto_result {
1496 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
1497 } else {
1498 let mut output_masks = Vec::with_capacity(100);
1500 decoder
1501 .decode(outputs, &mut output_boxes, &mut output_masks)
1502 .map_err(|e| Error::Internal(format!("decode: {e:#?}")))?;
1503 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
1504 }
1505 Ok(output_boxes)
1506 }
1507
1508 #[cfg(feature = "tracker")]
1516 pub fn draw_masks_tracked<TR: edgefirst_tracker::Tracker<DetectBox>>(
1517 &mut self,
1518 decoder: &edgefirst_decoder::Decoder,
1519 tracker: &mut TR,
1520 timestamp: u64,
1521 outputs: &[&TensorDyn],
1522 dst: &mut TensorDyn,
1523 overlay: MaskOverlay<'_>,
1524 ) -> Result<(Vec<DetectBox>, Vec<edgefirst_tracker::TrackInfo>)> {
1525 let mut output_boxes = Vec::with_capacity(100);
1526 let mut output_tracks = Vec::new();
1527
1528 let proto_result = decoder
1529 .decode_proto_tracked(
1530 tracker,
1531 timestamp,
1532 outputs,
1533 &mut output_boxes,
1534 &mut output_tracks,
1535 )
1536 .map_err(|e| Error::Internal(format!("decode_proto_tracked: {e:#?}")))?;
1537
1538 if let Some(proto_data) = proto_result {
1539 self.draw_proto_masks(dst, &output_boxes, &proto_data, overlay)?;
1540 } else {
1541 let mut output_masks = Vec::with_capacity(100);
1545 decoder
1546 .decode_tracked(
1547 tracker,
1548 timestamp,
1549 outputs,
1550 &mut output_boxes,
1551 &mut output_masks,
1552 &mut output_tracks,
1553 )
1554 .map_err(|e| Error::Internal(format!("decode_tracked: {e:#?}")))?;
1555 self.draw_decoded_masks(dst, &output_boxes, &output_masks, overlay)?;
1556 }
1557 Ok((output_boxes, output_tracks))
1558 }
1559
1560 pub fn materialize_masks(
1584 &self,
1585 detect: &[DetectBox],
1586 proto_data: &ProtoData,
1587 letterbox: Option<[f32; 4]>,
1588 ) -> Result<Vec<Segmentation>> {
1589 let cpu = self.cpu.as_ref().ok_or(Error::NoConverter)?;
1590 cpu.materialize_segmentations(detect, proto_data, letterbox)
1591 }
1592}
1593
1594impl ImageProcessorTrait for ImageProcessor {
1595 fn convert(
1601 &mut self,
1602 src: &TensorDyn,
1603 dst: &mut TensorDyn,
1604 rotation: Rotation,
1605 flip: Flip,
1606 crop: Crop,
1607 ) -> Result<()> {
1608 let start = Instant::now();
1609 let src_fmt = src.format();
1610 let dst_fmt = dst.format();
1611 log::trace!(
1612 "convert: {src_fmt:?}({:?}/{:?}) → {dst_fmt:?}({:?}/{:?}), \
1613 rotation={rotation:?}, flip={flip:?}, backend={:?}",
1614 src.dtype(),
1615 src.memory(),
1616 dst.dtype(),
1617 dst.memory(),
1618 self.forced_backend,
1619 );
1620
1621 if let Some(forced) = self.forced_backend {
1623 return match forced {
1624 ForcedBackend::Cpu => {
1625 if let Some(cpu) = self.cpu.as_mut() {
1626 let r = cpu.convert(src, dst, rotation, flip, crop);
1627 log::trace!(
1628 "convert: forced=cpu result={} ({:?})",
1629 if r.is_ok() { "ok" } else { "err" },
1630 start.elapsed()
1631 );
1632 return r;
1633 }
1634 Err(Error::ForcedBackendUnavailable("cpu".into()))
1635 }
1636 ForcedBackend::G2d => {
1637 #[cfg(target_os = "linux")]
1638 if let Some(g2d) = self.g2d.as_mut() {
1639 let r = g2d.convert(src, dst, rotation, flip, crop);
1640 log::trace!(
1641 "convert: forced=g2d result={} ({:?})",
1642 if r.is_ok() { "ok" } else { "err" },
1643 start.elapsed()
1644 );
1645 return r;
1646 }
1647 Err(Error::ForcedBackendUnavailable("g2d".into()))
1648 }
1649 ForcedBackend::OpenGl => {
1650 #[cfg(target_os = "linux")]
1651 #[cfg(feature = "opengl")]
1652 if let Some(opengl) = self.opengl.as_mut() {
1653 let r = opengl.convert(src, dst, rotation, flip, crop);
1654 log::trace!(
1655 "convert: forced=opengl result={} ({:?})",
1656 if r.is_ok() { "ok" } else { "err" },
1657 start.elapsed()
1658 );
1659 return r;
1660 }
1661 Err(Error::ForcedBackendUnavailable("opengl".into()))
1662 }
1663 };
1664 }
1665
1666 #[cfg(target_os = "linux")]
1668 #[cfg(feature = "opengl")]
1669 if let Some(opengl) = self.opengl.as_mut() {
1670 match opengl.convert(src, dst, rotation, flip, crop) {
1671 Ok(_) => {
1672 log::trace!(
1673 "convert: auto selected=opengl for {src_fmt:?}→{dst_fmt:?} ({:?})",
1674 start.elapsed()
1675 );
1676 return Ok(());
1677 }
1678 Err(e) => {
1679 log::trace!("convert: auto opengl declined {src_fmt:?}→{dst_fmt:?}: {e}");
1680 }
1681 }
1682 }
1683
1684 #[cfg(target_os = "linux")]
1685 if let Some(g2d) = self.g2d.as_mut() {
1686 match g2d.convert(src, dst, rotation, flip, crop) {
1687 Ok(_) => {
1688 log::trace!(
1689 "convert: auto selected=g2d for {src_fmt:?}→{dst_fmt:?} ({:?})",
1690 start.elapsed()
1691 );
1692 return Ok(());
1693 }
1694 Err(e) => {
1695 log::trace!("convert: auto g2d declined {src_fmt:?}→{dst_fmt:?}: {e}");
1696 }
1697 }
1698 }
1699
1700 if let Some(cpu) = self.cpu.as_mut() {
1701 match cpu.convert(src, dst, rotation, flip, crop) {
1702 Ok(_) => {
1703 log::trace!(
1704 "convert: auto selected=cpu for {src_fmt:?}→{dst_fmt:?} ({:?})",
1705 start.elapsed()
1706 );
1707 return Ok(());
1708 }
1709 Err(e) => {
1710 log::trace!("convert: auto cpu failed {src_fmt:?}→{dst_fmt:?}: {e}");
1711 return Err(e);
1712 }
1713 }
1714 }
1715 Err(Error::NoConverter)
1716 }
1717
1718 fn draw_decoded_masks(
1719 &mut self,
1720 dst: &mut TensorDyn,
1721 detect: &[DetectBox],
1722 segmentation: &[Segmentation],
1723 overlay: MaskOverlay<'_>,
1724 ) -> Result<()> {
1725 let start = Instant::now();
1726
1727 if detect.is_empty() && segmentation.is_empty() {
1728 return Ok(());
1729 }
1730
1731 let lb_boxes: Vec<DetectBox>;
1734 let lb_segs: Vec<Segmentation>;
1735 let (detect, segmentation) = if let Some(lb) = overlay.letterbox {
1736 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
1737 lb_segs = if segmentation.len() == lb_boxes.len() {
1740 segmentation
1741 .iter()
1742 .zip(lb_boxes.iter())
1743 .map(|(s, d)| Segmentation {
1744 xmin: d.bbox.xmin,
1745 ymin: d.bbox.ymin,
1746 xmax: d.bbox.xmax,
1747 ymax: d.bbox.ymax,
1748 segmentation: s.segmentation.clone(),
1749 })
1750 .collect()
1751 } else {
1752 segmentation.to_vec()
1753 };
1754 (lb_boxes.as_slice(), lb_segs.as_slice())
1755 } else {
1756 (detect, segmentation)
1757 };
1758
1759 if let Some(forced) = self.forced_backend {
1761 return match forced {
1762 ForcedBackend::Cpu => {
1763 let overlay = overlay.apply_background(dst)?;
1765 if let Some(cpu) = self.cpu.as_mut() {
1766 return cpu.draw_decoded_masks(dst, detect, segmentation, overlay);
1767 }
1768 Err(Error::ForcedBackendUnavailable("cpu".into()))
1769 }
1770 ForcedBackend::G2d => Err(Error::NotSupported(
1771 "g2d does not support draw_decoded_masks".into(),
1772 )),
1773 ForcedBackend::OpenGl => {
1774 #[cfg(target_os = "linux")]
1776 #[cfg(feature = "opengl")]
1777 if let Some(opengl) = self.opengl.as_mut() {
1778 return opengl.draw_decoded_masks(dst, detect, segmentation, overlay);
1779 }
1780 Err(Error::ForcedBackendUnavailable("opengl".into()))
1781 }
1782 };
1783 }
1784
1785 #[cfg(target_os = "linux")]
1789 #[cfg(feature = "opengl")]
1790 if let Some(opengl) = self.opengl.as_mut() {
1791 log::trace!(
1792 "draw_decoded_masks started with opengl in {:?}",
1793 start.elapsed()
1794 );
1795 match opengl.draw_decoded_masks(dst, detect, segmentation, overlay) {
1796 Ok(_) => {
1797 log::trace!("draw_decoded_masks with opengl in {:?}", start.elapsed());
1798 return Ok(());
1799 }
1800 Err(e) => {
1801 log::trace!("draw_decoded_masks didn't work with opengl: {e:?}")
1802 }
1803 }
1804 }
1805
1806 let overlay = overlay.apply_background(dst)?;
1808 log::trace!(
1809 "draw_decoded_masks started with cpu in {:?}",
1810 start.elapsed()
1811 );
1812 if let Some(cpu) = self.cpu.as_mut() {
1813 match cpu.draw_decoded_masks(dst, detect, segmentation, overlay) {
1814 Ok(_) => {
1815 log::trace!("draw_decoded_masks with cpu in {:?}", start.elapsed());
1816 return Ok(());
1817 }
1818 Err(e) => {
1819 log::trace!("draw_decoded_masks didn't work with cpu: {e:?}");
1820 return Err(e);
1821 }
1822 }
1823 }
1824 Err(Error::NoConverter)
1825 }
1826
1827 fn draw_proto_masks(
1828 &mut self,
1829 dst: &mut TensorDyn,
1830 detect: &[DetectBox],
1831 proto_data: &ProtoData,
1832 overlay: MaskOverlay<'_>,
1833 ) -> Result<()> {
1834 let start = Instant::now();
1835
1836 if detect.is_empty() {
1837 return Ok(());
1838 }
1839
1840 let lb_boxes: Vec<DetectBox>;
1846 let render_detect = if let Some(lb) = overlay.letterbox {
1847 lb_boxes = detect.iter().map(|&d| unletter_bbox(d, lb)).collect();
1848 lb_boxes.as_slice()
1849 } else {
1850 detect
1851 };
1852
1853 if let Some(forced) = self.forced_backend {
1855 return match forced {
1856 ForcedBackend::Cpu => {
1857 let overlay = overlay.apply_background(dst)?;
1858 if let Some(cpu) = self.cpu.as_mut() {
1859 return cpu.draw_proto_masks(dst, render_detect, proto_data, overlay);
1860 }
1861 Err(Error::ForcedBackendUnavailable("cpu".into()))
1862 }
1863 ForcedBackend::G2d => Err(Error::NotSupported(
1864 "g2d does not support draw_proto_masks".into(),
1865 )),
1866 ForcedBackend::OpenGl => {
1867 #[cfg(target_os = "linux")]
1868 #[cfg(feature = "opengl")]
1869 if let Some(opengl) = self.opengl.as_mut() {
1870 return opengl.draw_proto_masks(dst, render_detect, proto_data, overlay);
1871 }
1872 Err(Error::ForcedBackendUnavailable("opengl".into()))
1873 }
1874 };
1875 }
1876
1877 #[cfg(target_os = "linux")]
1884 #[cfg(feature = "opengl")]
1885 if let Some(opengl) = self.opengl.as_mut() {
1886 let Some(cpu) = self.cpu.as_ref() else {
1887 return Err(Error::Internal(
1888 "draw_proto_masks requires CPU backend for hybrid path".into(),
1889 ));
1890 };
1891 log::trace!(
1892 "draw_proto_masks started with hybrid (cpu+opengl) in {:?}",
1893 start.elapsed()
1894 );
1895 let segmentation =
1896 cpu.materialize_segmentations(detect, proto_data, overlay.letterbox)?;
1897 match opengl.draw_decoded_masks(dst, render_detect, &segmentation, overlay) {
1898 Ok(_) => {
1899 log::trace!(
1900 "draw_proto_masks with hybrid (cpu+opengl) in {:?}",
1901 start.elapsed()
1902 );
1903 return Ok(());
1904 }
1905 Err(e) => {
1906 log::trace!("draw_proto_masks hybrid path failed, falling back to cpu: {e:?}");
1907 }
1908 }
1909 }
1910
1911 let overlay = overlay.apply_background(dst)?;
1913 let Some(cpu) = self.cpu.as_mut() else {
1914 return Err(Error::Internal(
1915 "draw_proto_masks requires CPU backend for fallback path".into(),
1916 ));
1917 };
1918 log::trace!("draw_proto_masks started with cpu in {:?}", start.elapsed());
1919 cpu.draw_proto_masks(dst, render_detect, proto_data, overlay)
1920 }
1921
1922 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
1923 let start = Instant::now();
1924
1925 if let Some(forced) = self.forced_backend {
1927 return match forced {
1928 ForcedBackend::Cpu => {
1929 if let Some(cpu) = self.cpu.as_mut() {
1930 return cpu.set_class_colors(colors);
1931 }
1932 Err(Error::ForcedBackendUnavailable("cpu".into()))
1933 }
1934 ForcedBackend::G2d => Err(Error::NotSupported(
1935 "g2d does not support set_class_colors".into(),
1936 )),
1937 ForcedBackend::OpenGl => {
1938 #[cfg(target_os = "linux")]
1939 #[cfg(feature = "opengl")]
1940 if let Some(opengl) = self.opengl.as_mut() {
1941 return opengl.set_class_colors(colors);
1942 }
1943 Err(Error::ForcedBackendUnavailable("opengl".into()))
1944 }
1945 };
1946 }
1947
1948 #[cfg(target_os = "linux")]
1951 #[cfg(feature = "opengl")]
1952 if let Some(opengl) = self.opengl.as_mut() {
1953 log::trace!("image started with opengl in {:?}", start.elapsed());
1954 match opengl.set_class_colors(colors) {
1955 Ok(_) => {
1956 log::trace!("colors set with opengl in {:?}", start.elapsed());
1957 return Ok(());
1958 }
1959 Err(e) => {
1960 log::trace!("colors didn't set with opengl: {e:?}")
1961 }
1962 }
1963 }
1964 log::trace!("image started with cpu in {:?}", start.elapsed());
1965 if let Some(cpu) = self.cpu.as_mut() {
1966 match cpu.set_class_colors(colors) {
1967 Ok(_) => {
1968 log::trace!("colors set with cpu in {:?}", start.elapsed());
1969 return Ok(());
1970 }
1971 Err(e) => {
1972 log::trace!("colors didn't set with cpu: {e:?}");
1973 return Err(e);
1974 }
1975 }
1976 }
1977 Err(Error::NoConverter)
1978 }
1979}
1980
1981fn read_exif_orientation(exif_bytes: &[u8]) -> (Rotation, Flip) {
1987 let exifreader = exif::Reader::new();
1988 let Ok(exif_) = exifreader.read_raw(exif_bytes.to_vec()) else {
1989 return (Rotation::None, Flip::None);
1990 };
1991 let Some(orientation) = exif_.get_field(exif::Tag::Orientation, exif::In::PRIMARY) else {
1992 return (Rotation::None, Flip::None);
1993 };
1994 match orientation.value.get_uint(0) {
1995 Some(1) => (Rotation::None, Flip::None),
1996 Some(2) => (Rotation::None, Flip::Horizontal),
1997 Some(3) => (Rotation::Rotate180, Flip::None),
1998 Some(4) => (Rotation::Rotate180, Flip::Horizontal),
1999 Some(5) => (Rotation::Clockwise90, Flip::Horizontal),
2000 Some(6) => (Rotation::Clockwise90, Flip::None),
2001 Some(7) => (Rotation::CounterClockwise90, Flip::Horizontal),
2002 Some(8) => (Rotation::CounterClockwise90, Flip::None),
2003 Some(v) => {
2004 log::warn!("broken orientation EXIF value: {v}");
2005 (Rotation::None, Flip::None)
2006 }
2007 None => (Rotation::None, Flip::None),
2008 }
2009}
2010
2011fn pixelfmt_to_colorspace(fmt: PixelFormat) -> Option<ColorSpace> {
2014 match fmt {
2015 PixelFormat::Rgb => Some(ColorSpace::RGB),
2016 PixelFormat::Rgba => Some(ColorSpace::RGBA),
2017 PixelFormat::Grey => Some(ColorSpace::Luma),
2018 _ => None,
2019 }
2020}
2021
2022fn colorspace_to_pixelfmt(cs: ColorSpace) -> Option<PixelFormat> {
2024 match cs {
2025 ColorSpace::RGB => Some(PixelFormat::Rgb),
2026 ColorSpace::RGBA => Some(PixelFormat::Rgba),
2027 ColorSpace::Luma => Some(PixelFormat::Grey),
2028 _ => None,
2029 }
2030}
2031
2032fn load_jpeg(
2034 image: &[u8],
2035 format: Option<PixelFormat>,
2036 memory: Option<TensorMemory>,
2037) -> Result<TensorDyn> {
2038 let colour = match format {
2039 Some(f) => pixelfmt_to_colorspace(f)
2040 .ok_or_else(|| Error::NotSupported(format!("Unsupported image format {f:?}")))?,
2041 None => ColorSpace::RGB,
2042 };
2043 let options = DecoderOptions::default().jpeg_set_out_colorspace(colour);
2044 let mut decoder = JpegDecoder::new_with_options(image, options);
2045 decoder.decode_headers()?;
2046
2047 let image_info = decoder.info().ok_or(Error::Internal(
2048 "JPEG did not return decoded image info".to_string(),
2049 ))?;
2050
2051 let converted_cs = decoder
2052 .get_output_colorspace()
2053 .ok_or(Error::Internal("No output colorspace".to_string()))?;
2054
2055 let converted_fmt = colorspace_to_pixelfmt(converted_cs).ok_or(Error::NotSupported(
2056 "Unsupported JPEG decoder output".to_string(),
2057 ))?;
2058
2059 let dest_fmt = format.unwrap_or(converted_fmt);
2060
2061 let (rotation, flip) = decoder
2062 .exif()
2063 .map(|x| read_exif_orientation(x))
2064 .unwrap_or((Rotation::None, Flip::None));
2065
2066 let w = image_info.width as usize;
2067 let h = image_info.height as usize;
2068
2069 if (rotation, flip) == (Rotation::None, Flip::None) {
2070 let mut img = Tensor::<u8>::image(w, h, dest_fmt, memory)?;
2071
2072 if converted_fmt != dest_fmt {
2073 let tmp = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
2074 decoder.decode_into(&mut tmp.map()?)?;
2075 CPUProcessor::convert_format_pf(&tmp, &mut img, converted_fmt, dest_fmt)?;
2076 return Ok(TensorDyn::from(img));
2077 }
2078 decoder.decode_into(&mut img.map()?)?;
2079 return Ok(TensorDyn::from(img));
2080 }
2081
2082 let mut tmp = Tensor::<u8>::image(w, h, dest_fmt, Some(TensorMemory::Mem))?;
2083
2084 if converted_fmt != dest_fmt {
2085 let tmp2 = Tensor::<u8>::image(w, h, converted_fmt, Some(TensorMemory::Mem))?;
2086 decoder.decode_into(&mut tmp2.map()?)?;
2087 CPUProcessor::convert_format_pf(&tmp2, &mut tmp, converted_fmt, dest_fmt)?;
2088 } else {
2089 decoder.decode_into(&mut tmp.map()?)?;
2090 }
2091
2092 rotate_flip_to_dyn(&tmp, dest_fmt, rotation, flip, memory)
2093}
2094
2095fn load_png(
2097 image: &[u8],
2098 format: Option<PixelFormat>,
2099 memory: Option<TensorMemory>,
2100) -> Result<TensorDyn> {
2101 let fmt = format.unwrap_or(PixelFormat::Rgb);
2102 let alpha = match fmt {
2103 PixelFormat::Rgb => false,
2104 PixelFormat::Rgba => true,
2105 _ => {
2106 return Err(Error::NotImplemented(
2107 "Unsupported image format".to_string(),
2108 ));
2109 }
2110 };
2111
2112 let options = DecoderOptions::default()
2113 .png_set_add_alpha_channel(alpha)
2114 .png_set_decode_animated(false);
2115 let mut decoder = PngDecoder::new_with_options(image, options);
2116 decoder.decode_headers()?;
2117 let image_info = decoder.get_info().ok_or(Error::Internal(
2118 "PNG did not return decoded image info".to_string(),
2119 ))?;
2120
2121 let (rotation, flip) = image_info
2122 .exif
2123 .as_ref()
2124 .map(|x| read_exif_orientation(x))
2125 .unwrap_or((Rotation::None, Flip::None));
2126
2127 if (rotation, flip) == (Rotation::None, Flip::None) {
2128 let img = Tensor::<u8>::image(image_info.width, image_info.height, fmt, memory)?;
2129 decoder.decode_into(&mut img.map()?)?;
2130 return Ok(TensorDyn::from(img));
2131 }
2132
2133 let tmp = Tensor::<u8>::image(
2134 image_info.width,
2135 image_info.height,
2136 fmt,
2137 Some(TensorMemory::Mem),
2138 )?;
2139 decoder.decode_into(&mut tmp.map()?)?;
2140
2141 rotate_flip_to_dyn(&tmp, fmt, rotation, flip, memory)
2142}
2143
2144pub fn load_image(
2163 image: &[u8],
2164 format: Option<PixelFormat>,
2165 memory: Option<TensorMemory>,
2166) -> Result<TensorDyn> {
2167 if let Ok(i) = load_jpeg(image, format, memory) {
2168 return Ok(i);
2169 }
2170 if let Ok(i) = load_png(image, format, memory) {
2171 return Ok(i);
2172 }
2173 Err(Error::NotSupported(
2174 "Could not decode as jpeg or png".to_string(),
2175 ))
2176}
2177
2178pub fn save_jpeg(tensor: &TensorDyn, path: impl AsRef<std::path::Path>, quality: u8) -> Result<()> {
2182 let t = tensor.as_u8().ok_or(Error::UnsupportedFormat(
2183 "save_jpeg requires u8 tensor".to_string(),
2184 ))?;
2185 let fmt = t.format().ok_or(Error::NotAnImage)?;
2186 if fmt.layout() != PixelLayout::Packed {
2187 return Err(Error::NotImplemented(
2188 "Saving planar images is not supported".to_string(),
2189 ));
2190 }
2191
2192 let colour = match fmt {
2193 PixelFormat::Rgb => jpeg_encoder::ColorType::Rgb,
2194 PixelFormat::Rgba => jpeg_encoder::ColorType::Rgba,
2195 _ => {
2196 return Err(Error::NotImplemented(
2197 "Unsupported image format for saving".to_string(),
2198 ));
2199 }
2200 };
2201
2202 let w = t.width().ok_or(Error::NotAnImage)?;
2203 let h = t.height().ok_or(Error::NotAnImage)?;
2204 let encoder = jpeg_encoder::Encoder::new_file(path, quality)?;
2205 let tensor_map = t.map()?;
2206
2207 encoder.encode(&tensor_map, w as u16, h as u16, colour)?;
2208
2209 Ok(())
2210}
2211
2212pub(crate) struct FunctionTimer<T: Display> {
2213 name: T,
2214 start: std::time::Instant,
2215}
2216
2217impl<T: Display> FunctionTimer<T> {
2218 pub fn new(name: T) -> Self {
2219 Self {
2220 name,
2221 start: std::time::Instant::now(),
2222 }
2223 }
2224}
2225
2226impl<T: Display> Drop for FunctionTimer<T> {
2227 fn drop(&mut self) {
2228 log::trace!("{} elapsed: {:?}", self.name, self.start.elapsed())
2229 }
2230}
2231
2232const DEFAULT_COLORS: [[f32; 4]; 20] = [
2233 [0., 1., 0., 0.7],
2234 [1., 0.5568628, 0., 0.7],
2235 [0.25882353, 0.15294118, 0.13333333, 0.7],
2236 [0.8, 0.7647059, 0.78039216, 0.7],
2237 [0.3137255, 0.3137255, 0.3137255, 0.7],
2238 [0.1411765, 0.3098039, 0.1215686, 0.7],
2239 [1., 0.95686275, 0.5137255, 0.7],
2240 [0.3529412, 0.32156863, 0., 0.7],
2241 [0.4235294, 0.6235294, 0.6509804, 0.7],
2242 [0.5098039, 0.5098039, 0.7294118, 0.7],
2243 [0.00784314, 0.18823529, 0.29411765, 0.7],
2244 [0.0, 0.2706, 1.0, 0.7],
2245 [0.0, 0.0, 0.0, 0.7],
2246 [0.0, 0.5, 0.0, 0.7],
2247 [1.0, 0.0, 0.0, 0.7],
2248 [0.0, 0.0, 1.0, 0.7],
2249 [1.0, 0.5, 0.5, 0.7],
2250 [0.1333, 0.5451, 0.1333, 0.7],
2251 [0.1176, 0.4118, 0.8235, 0.7],
2252 [1., 1., 1., 0.7],
2253];
2254
2255const fn denorm<const M: usize, const N: usize>(a: [[f32; M]; N]) -> [[u8; M]; N] {
2256 let mut result = [[0; M]; N];
2257 let mut i = 0;
2258 while i < N {
2259 let mut j = 0;
2260 while j < M {
2261 result[i][j] = (a[i][j] * 255.0).round() as u8;
2262 j += 1;
2263 }
2264 i += 1;
2265 }
2266 result
2267}
2268
2269const DEFAULT_COLORS_U8: [[u8; 4]; 20] = denorm(DEFAULT_COLORS);
2270
2271#[cfg(test)]
2272#[cfg_attr(coverage_nightly, coverage(off))]
2273mod alignment_tests {
2274 use super::*;
2275
2276 #[test]
2277 fn align_width_rgba8_common_widths() {
2278 assert_eq!(align_width_for_gpu_pitch(640, 4), 640); assert_eq!(align_width_for_gpu_pitch(1280, 4), 1280); assert_eq!(align_width_for_gpu_pitch(1920, 4), 1920); assert_eq!(align_width_for_gpu_pitch(3840, 4), 3840); assert_eq!(align_width_for_gpu_pitch(3004, 4), 3008); assert_eq!(align_width_for_gpu_pitch(3000, 4), 3008); assert_eq!(align_width_for_gpu_pitch(17, 4), 32); assert_eq!(align_width_for_gpu_pitch(1, 4), 16); }
2289
2290 #[test]
2291 fn align_width_rgb888_packed() {
2292 assert_eq!(align_width_for_gpu_pitch(64, 3), 64); assert_eq!(align_width_for_gpu_pitch(640, 3), 640); assert_eq!(align_width_for_gpu_pitch(1, 3), 64); assert_eq!(align_width_for_gpu_pitch(65, 3), 128); for w in [3004usize, 1281, 100, 17] {
2299 let padded = align_width_for_gpu_pitch(w, 3);
2300 assert!(padded >= w);
2301 assert_eq!((padded * 3) % 64, 0);
2302 assert_eq!((padded * 3) % 3, 0);
2303 }
2304 }
2305
2306 #[test]
2307 fn align_width_grey_u8() {
2308 assert_eq!(align_width_for_gpu_pitch(64, 1), 64);
2310 assert_eq!(align_width_for_gpu_pitch(640, 1), 640);
2311 assert_eq!(align_width_for_gpu_pitch(1, 1), 64);
2312 assert_eq!(align_width_for_gpu_pitch(65, 1), 128);
2313 }
2314
2315 #[test]
2316 fn align_width_zero_inputs() {
2317 assert_eq!(align_width_for_gpu_pitch(0, 4), 0);
2318 assert_eq!(align_width_for_gpu_pitch(640, 0), 640);
2319 }
2320
2321 #[test]
2322 fn align_width_never_returns_smaller_than_input() {
2323 for &bpp in &[1usize, 2, 3, 4, 8] {
2327 for &w in &[
2328 1usize,
2329 17,
2330 64,
2331 65,
2332 100,
2333 1280,
2334 1281,
2335 1920,
2336 3004,
2337 3072,
2338 3840,
2339 usize::MAX / 8,
2340 usize::MAX / 4,
2341 usize::MAX / 2,
2342 usize::MAX - 1,
2343 usize::MAX,
2344 ] {
2345 let aligned = align_width_for_gpu_pitch(w, bpp);
2346 assert!(
2347 aligned >= w,
2348 "align_width_for_gpu_pitch({w}, {bpp}) = {aligned} < {w}"
2349 );
2350 }
2351 }
2352 }
2353
2354 #[test]
2355 fn align_width_overflow_returns_unaligned_not_smaller() {
2356 let aligned_extreme = usize::MAX - 15; assert_eq!(
2362 align_width_for_gpu_pitch(aligned_extreme, 4),
2363 aligned_extreme
2364 );
2365 let misaligned_extreme = usize::MAX - 1;
2368 let result = align_width_for_gpu_pitch(misaligned_extreme, 4);
2369 assert!(
2370 result == misaligned_extreme || result >= misaligned_extreme,
2371 "extreme misaligned width must not be rounded down to {result}"
2372 );
2373 }
2374
2375 #[test]
2376 fn checked_lcm_basic_and_overflow() {
2377 assert_eq!(checked_num_integer_lcm(64, 4), Some(64));
2378 assert_eq!(checked_num_integer_lcm(64, 3), Some(192));
2379 assert_eq!(checked_num_integer_lcm(64, 1), Some(64));
2380 assert_eq!(checked_num_integer_lcm(0, 4), Some(0));
2381 assert_eq!(checked_num_integer_lcm(64, 0), Some(0));
2382 assert_eq!(
2384 checked_num_integer_lcm(usize::MAX, usize::MAX - 1),
2385 None,
2386 "coprime extreme values must overflow detect, not panic"
2387 );
2388 }
2389
2390 #[test]
2391 fn primary_plane_bpp_known_formats() {
2392 assert_eq!(primary_plane_bpp(PixelFormat::Rgba, 1), Some(4));
2394 assert_eq!(primary_plane_bpp(PixelFormat::Bgra, 1), Some(4));
2395 assert_eq!(primary_plane_bpp(PixelFormat::Rgb, 1), Some(3));
2396 assert_eq!(primary_plane_bpp(PixelFormat::Grey, 1), Some(1));
2397 assert_eq!(primary_plane_bpp(PixelFormat::Nv12, 1), Some(1));
2399 }
2400}
2401
2402#[cfg(test)]
2403#[cfg_attr(coverage_nightly, coverage(off))]
2404mod image_tests {
2405 use super::*;
2406 use crate::{CPUProcessor, Rotation};
2407 #[cfg(target_os = "linux")]
2408 use edgefirst_tensor::is_dma_available;
2409 use edgefirst_tensor::{TensorMapTrait, TensorMemory, TensorTrait};
2410 use image::buffer::ConvertBuffer;
2411
2412 fn convert_img(
2418 proc: &mut dyn ImageProcessorTrait,
2419 src: TensorDyn,
2420 dst: TensorDyn,
2421 rotation: Rotation,
2422 flip: Flip,
2423 crop: Crop,
2424 ) -> (Result<()>, TensorDyn, TensorDyn) {
2425 let src_fourcc = src.format().unwrap();
2426 let dst_fourcc = dst.format().unwrap();
2427 let src_dyn = src;
2428 let mut dst_dyn = dst;
2429 let result = proc.convert(&src_dyn, &mut dst_dyn, rotation, flip, crop);
2430 let src_back = {
2431 let mut __t = src_dyn.into_u8().unwrap();
2432 __t.set_format(src_fourcc).unwrap();
2433 TensorDyn::from(__t)
2434 };
2435 let dst_back = {
2436 let mut __t = dst_dyn.into_u8().unwrap();
2437 __t.set_format(dst_fourcc).unwrap();
2438 TensorDyn::from(__t)
2439 };
2440 (result, src_back, dst_back)
2441 }
2442
2443 #[ctor::ctor]
2444 fn init() {
2445 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
2446 }
2447
2448 macro_rules! function {
2449 () => {{
2450 fn f() {}
2451 fn type_name_of<T>(_: T) -> &'static str {
2452 std::any::type_name::<T>()
2453 }
2454 let name = type_name_of(f);
2455
2456 match &name[..name.len() - 3].rfind(':') {
2458 Some(pos) => &name[pos + 1..name.len() - 3],
2459 None => &name[..name.len() - 3],
2460 }
2461 }};
2462 }
2463
2464 #[test]
2465 fn test_invalid_crop() {
2466 let src = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
2467 let dst = TensorDyn::image(100, 100, PixelFormat::Rgb, DType::U8, None).unwrap();
2468
2469 let crop = Crop::new()
2470 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
2471 .with_dst_rect(Some(Rect::new(0, 0, 150, 150)));
2472
2473 let result = crop.check_crop_dyn(&src, &dst);
2474 assert!(matches!(
2475 result,
2476 Err(Error::CropInvalid(e)) if e.starts_with("Dest and Src crop invalid")
2477 ));
2478
2479 let crop = crop.with_src_rect(Some(Rect::new(0, 0, 10, 10)));
2480 let result = crop.check_crop_dyn(&src, &dst);
2481 assert!(matches!(
2482 result,
2483 Err(Error::CropInvalid(e)) if e.starts_with("Dest crop invalid")
2484 ));
2485
2486 let crop = crop
2487 .with_src_rect(Some(Rect::new(50, 50, 60, 60)))
2488 .with_dst_rect(Some(Rect::new(0, 0, 50, 50)));
2489 let result = crop.check_crop_dyn(&src, &dst);
2490 assert!(matches!(
2491 result,
2492 Err(Error::CropInvalid(e)) if e.starts_with("Src crop invalid")
2493 ));
2494
2495 let crop = crop.with_src_rect(Some(Rect::new(50, 50, 50, 50)));
2496
2497 let result = crop.check_crop_dyn(&src, &dst);
2498 assert!(result.is_ok());
2499 }
2500
2501 #[test]
2502 fn test_invalid_tensor_format() -> Result<(), Error> {
2503 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4, 1], None, None)?;
2505 let result = tensor.set_format(PixelFormat::Rgb);
2506 assert!(result.is_err(), "4D tensor should reject set_format");
2507
2508 let mut tensor = Tensor::<u8>::new(&[720, 1280, 4], None, None)?;
2510 let result = tensor.set_format(PixelFormat::Rgb);
2511 assert!(result.is_err(), "4-channel tensor should reject RGB format");
2512
2513 Ok(())
2514 }
2515
2516 #[test]
2517 fn test_invalid_image_file() -> Result<(), Error> {
2518 let result = crate::load_image(&[123; 5000], None, None);
2519 assert!(matches!(
2520 result,
2521 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
2522
2523 Ok(())
2524 }
2525
2526 #[test]
2527 fn test_invalid_jpeg_format() -> Result<(), Error> {
2528 let result = crate::load_image(&[123; 5000], Some(PixelFormat::Yuyv), None);
2529 assert!(matches!(
2530 result,
2531 Err(Error::NotSupported(e)) if e == "Could not decode as jpeg or png"));
2532
2533 Ok(())
2534 }
2535
2536 #[test]
2537 fn test_load_resize_save() {
2538 let file = include_bytes!(concat!(
2539 env!("CARGO_MANIFEST_DIR"),
2540 "/../../testdata/zidane.jpg"
2541 ));
2542 let img = crate::load_image(file, Some(PixelFormat::Rgba), None).unwrap();
2543 assert_eq!(img.width(), Some(1280));
2544 assert_eq!(img.height(), Some(720));
2545
2546 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None).unwrap();
2547 let mut converter = CPUProcessor::new();
2548 let (result, _img, dst) = convert_img(
2549 &mut converter,
2550 img,
2551 dst,
2552 Rotation::None,
2553 Flip::None,
2554 Crop::no_crop(),
2555 );
2556 result.unwrap();
2557 assert_eq!(dst.width(), Some(640));
2558 assert_eq!(dst.height(), Some(360));
2559
2560 crate::save_jpeg(&dst, "zidane_resized.jpg", 80).unwrap();
2561
2562 let file = std::fs::read("zidane_resized.jpg").unwrap();
2563 let img = crate::load_image(&file, None, None).unwrap();
2564 assert_eq!(img.width(), Some(640));
2565 assert_eq!(img.height(), Some(360));
2566 assert_eq!(img.format().unwrap(), PixelFormat::Rgb);
2567 }
2568
2569 #[test]
2570 fn test_from_tensor_planar() -> Result<(), Error> {
2571 let mut tensor = Tensor::new(&[3, 720, 1280], None, None)?;
2572 tensor.map()?.copy_from_slice(include_bytes!(concat!(
2573 env!("CARGO_MANIFEST_DIR"),
2574 "/../../testdata/camera720p.8bps"
2575 )));
2576 let planar = {
2577 tensor
2578 .set_format(PixelFormat::PlanarRgb)
2579 .map_err(|e| crate::Error::Internal(e.to_string()))?;
2580 TensorDyn::from(tensor)
2581 };
2582
2583 let rbga = load_bytes_to_tensor(
2584 1280,
2585 720,
2586 PixelFormat::Rgba,
2587 None,
2588 include_bytes!(concat!(
2589 env!("CARGO_MANIFEST_DIR"),
2590 "/../../testdata/camera720p.rgba"
2591 )),
2592 )?;
2593 compare_images_convert_to_rgb(&planar, &rbga, 0.98, function!());
2594
2595 Ok(())
2596 }
2597
2598 #[test]
2599 fn test_from_tensor_invalid_format() {
2600 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
2603 }
2604
2605 #[test]
2606 #[should_panic(expected = "Failed to save planar RGB image")]
2607 fn test_save_planar() {
2608 let planar_img = load_bytes_to_tensor(
2609 1280,
2610 720,
2611 PixelFormat::PlanarRgb,
2612 None,
2613 include_bytes!(concat!(
2614 env!("CARGO_MANIFEST_DIR"),
2615 "/../../testdata/camera720p.8bps"
2616 )),
2617 )
2618 .unwrap();
2619
2620 let save_path = "/tmp/planar_rgb.jpg";
2621 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save planar RGB image");
2622 }
2623
2624 #[test]
2625 #[should_panic(expected = "Failed to save YUYV image")]
2626 fn test_save_yuyv() {
2627 let planar_img = load_bytes_to_tensor(
2628 1280,
2629 720,
2630 PixelFormat::Yuyv,
2631 None,
2632 include_bytes!(concat!(
2633 env!("CARGO_MANIFEST_DIR"),
2634 "/../../testdata/camera720p.yuyv"
2635 )),
2636 )
2637 .unwrap();
2638
2639 let save_path = "/tmp/yuyv.jpg";
2640 crate::save_jpeg(&planar_img, save_path, 90).expect("Failed to save YUYV image");
2641 }
2642
2643 #[test]
2644 fn test_rotation_angle() {
2645 assert_eq!(Rotation::from_degrees_clockwise(0), Rotation::None);
2646 assert_eq!(Rotation::from_degrees_clockwise(90), Rotation::Clockwise90);
2647 assert_eq!(Rotation::from_degrees_clockwise(180), Rotation::Rotate180);
2648 assert_eq!(
2649 Rotation::from_degrees_clockwise(270),
2650 Rotation::CounterClockwise90
2651 );
2652 assert_eq!(Rotation::from_degrees_clockwise(360), Rotation::None);
2653 assert_eq!(Rotation::from_degrees_clockwise(450), Rotation::Clockwise90);
2654 assert_eq!(Rotation::from_degrees_clockwise(540), Rotation::Rotate180);
2655 assert_eq!(
2656 Rotation::from_degrees_clockwise(630),
2657 Rotation::CounterClockwise90
2658 );
2659 }
2660
2661 #[test]
2662 #[should_panic(expected = "rotation angle is not a multiple of 90")]
2663 fn test_rotation_angle_panic() {
2664 Rotation::from_degrees_clockwise(361);
2665 }
2666
2667 #[test]
2668 fn test_disable_env_var() -> Result<(), Error> {
2669 let saved_force = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
2673 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
2674
2675 #[cfg(target_os = "linux")]
2676 {
2677 let original = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
2678 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
2679 let converter = ImageProcessor::new()?;
2680 match original {
2681 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
2682 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
2683 }
2684 assert!(converter.g2d.is_none());
2685 }
2686
2687 #[cfg(target_os = "linux")]
2688 #[cfg(feature = "opengl")]
2689 {
2690 let original = std::env::var("EDGEFIRST_DISABLE_GL").ok();
2691 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
2692 let converter = ImageProcessor::new()?;
2693 match original {
2694 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
2695 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
2696 }
2697 assert!(converter.opengl.is_none());
2698 }
2699
2700 let original = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
2701 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
2702 let converter = ImageProcessor::new()?;
2703 match original {
2704 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
2705 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
2706 }
2707 assert!(converter.cpu.is_none());
2708
2709 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
2710 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
2711 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
2712 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
2713 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
2714 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
2715 let mut converter = ImageProcessor::new()?;
2716
2717 let src = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
2718 let dst = TensorDyn::image(640, 360, PixelFormat::Rgba, DType::U8, None)?;
2719 let (result, _src, _dst) = convert_img(
2720 &mut converter,
2721 src,
2722 dst,
2723 Rotation::None,
2724 Flip::None,
2725 Crop::no_crop(),
2726 );
2727 assert!(matches!(result, Err(Error::NoConverter)));
2728
2729 match original_cpu {
2730 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
2731 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
2732 }
2733 match original_gl {
2734 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
2735 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
2736 }
2737 match original_g2d {
2738 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
2739 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
2740 }
2741 match saved_force {
2742 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
2743 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
2744 }
2745
2746 Ok(())
2747 }
2748
2749 #[test]
2750 fn test_unsupported_conversion() {
2751 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
2752 let dst = TensorDyn::image(640, 360, PixelFormat::Nv12, DType::U8, None).unwrap();
2753 let mut converter = ImageProcessor::new().unwrap();
2754 let (result, _src, _dst) = convert_img(
2755 &mut converter,
2756 src,
2757 dst,
2758 Rotation::None,
2759 Flip::None,
2760 Crop::no_crop(),
2761 );
2762 log::debug!("result: {:?}", result);
2763 assert!(matches!(
2764 result,
2765 Err(Error::NotSupported(e)) if e.starts_with("Conversion from NV12 to NV12")
2766 ));
2767 }
2768
2769 #[test]
2770 fn test_load_grey() {
2771 let grey_img = crate::load_image(
2772 include_bytes!(concat!(
2773 env!("CARGO_MANIFEST_DIR"),
2774 "/../../testdata/grey.jpg"
2775 )),
2776 Some(PixelFormat::Rgba),
2777 None,
2778 )
2779 .unwrap();
2780
2781 let grey_but_rgb_img = crate::load_image(
2782 include_bytes!(concat!(
2783 env!("CARGO_MANIFEST_DIR"),
2784 "/../../testdata/grey-rgb.jpg"
2785 )),
2786 Some(PixelFormat::Rgba),
2787 None,
2788 )
2789 .unwrap();
2790
2791 compare_images(&grey_img, &grey_but_rgb_img, 0.99, function!());
2792 }
2793
2794 #[test]
2795 fn test_new_nv12() {
2796 let nv12 = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
2797 assert_eq!(nv12.height(), Some(720));
2798 assert_eq!(nv12.width(), Some(1280));
2799 assert_eq!(nv12.format().unwrap(), PixelFormat::Nv12);
2800 assert_eq!(nv12.format().unwrap().channels(), 1);
2802 assert!(nv12.format().is_some_and(
2803 |f| f.layout() == PixelLayout::Planar || f.layout() == PixelLayout::SemiPlanar
2804 ))
2805 }
2806
2807 #[test]
2808 #[cfg(target_os = "linux")]
2809 fn test_new_image_converter() {
2810 let dst_width = 640;
2811 let dst_height = 360;
2812 let file = include_bytes!(concat!(
2813 env!("CARGO_MANIFEST_DIR"),
2814 "/../../testdata/zidane.jpg"
2815 ))
2816 .to_vec();
2817 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2818
2819 let mut converter = ImageProcessor::new().unwrap();
2820 let converter_dst = converter
2821 .create_image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
2822 .unwrap();
2823 let (result, src, converter_dst) = convert_img(
2824 &mut converter,
2825 src,
2826 converter_dst,
2827 Rotation::None,
2828 Flip::None,
2829 Crop::no_crop(),
2830 );
2831 result.unwrap();
2832
2833 let cpu_dst =
2834 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
2835 let mut cpu_converter = CPUProcessor::new();
2836 let (result, _src, cpu_dst) = convert_img(
2837 &mut cpu_converter,
2838 src,
2839 cpu_dst,
2840 Rotation::None,
2841 Flip::None,
2842 Crop::no_crop(),
2843 );
2844 result.unwrap();
2845
2846 compare_images(&converter_dst, &cpu_dst, 0.98, function!());
2847 }
2848
2849 #[test]
2850 #[cfg(target_os = "linux")]
2851 fn test_create_image_dtype_i8() {
2852 let mut converter = ImageProcessor::new().unwrap();
2853
2854 let dst = converter
2856 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2857 .unwrap();
2858 assert_eq!(dst.dtype(), DType::I8);
2859 assert!(dst.width() == Some(320));
2860 assert!(dst.height() == Some(240));
2861 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
2862
2863 let dst_u8 = converter
2865 .create_image(320, 240, PixelFormat::Rgb, DType::U8, None)
2866 .unwrap();
2867 assert_eq!(dst_u8.dtype(), DType::U8);
2868
2869 let file = include_bytes!(concat!(
2871 env!("CARGO_MANIFEST_DIR"),
2872 "/../../testdata/zidane.jpg"
2873 ))
2874 .to_vec();
2875 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2876 let mut dst_i8 = converter
2877 .create_image(320, 240, PixelFormat::Rgb, DType::I8, None)
2878 .unwrap();
2879 converter
2880 .convert(
2881 &src,
2882 &mut dst_i8,
2883 Rotation::None,
2884 Flip::None,
2885 Crop::no_crop(),
2886 )
2887 .unwrap();
2888 }
2889
2890 #[test]
2891 #[cfg(target_os = "linux")]
2892 fn test_create_image_nv12_dma_non_aligned_width() {
2893 let converter = ImageProcessor::new().unwrap();
2899
2900 let result = converter.create_image(
2904 100,
2905 64,
2906 PixelFormat::Nv12,
2907 DType::U8,
2908 Some(TensorMemory::Dma),
2909 );
2910
2911 match result {
2912 Ok(img) => {
2913 assert_eq!(img.width(), Some(100));
2914 assert_eq!(img.height(), Some(64));
2915 assert_eq!(img.format(), Some(PixelFormat::Nv12));
2916 assert!(
2918 img.row_stride().is_none(),
2919 "NV12 must not be stride-padded by create_image",
2920 );
2921 }
2922 Err(e) => {
2923 let msg = format!("{e}");
2926 assert!(
2927 !msg.contains("image_with_stride"),
2928 "NV12 should not hit the stride-padded path: {msg}",
2929 );
2930 }
2931 }
2932 }
2933
2934 #[test]
2935 #[ignore] fn test_crop_skip() {
2939 let file = include_bytes!(concat!(
2940 env!("CARGO_MANIFEST_DIR"),
2941 "/../../testdata/zidane.jpg"
2942 ))
2943 .to_vec();
2944 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
2945
2946 let mut converter = ImageProcessor::new().unwrap();
2947 let converter_dst = converter
2948 .create_image(1280, 720, PixelFormat::Rgba, DType::U8, None)
2949 .unwrap();
2950 let crop = Crop::new()
2951 .with_src_rect(Some(Rect::new(0, 0, 640, 640)))
2952 .with_dst_rect(Some(Rect::new(0, 0, 640, 640)));
2953 let (result, src, converter_dst) = convert_img(
2954 &mut converter,
2955 src,
2956 converter_dst,
2957 Rotation::None,
2958 Flip::None,
2959 crop,
2960 );
2961 result.unwrap();
2962
2963 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
2964 let mut cpu_converter = CPUProcessor::new();
2965 let (result, _src, cpu_dst) = convert_img(
2966 &mut cpu_converter,
2967 src,
2968 cpu_dst,
2969 Rotation::None,
2970 Flip::None,
2971 crop,
2972 );
2973 result.unwrap();
2974
2975 compare_images(&converter_dst, &cpu_dst, 0.99999, function!());
2976 }
2977
2978 #[test]
2979 fn test_invalid_pixel_format() {
2980 assert!(PixelFormat::from_fourcc(u32::from_le_bytes(*b"TEST")).is_none());
2983 }
2984
2985 #[cfg(target_os = "linux")]
2987 static G2D_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2988
2989 #[cfg(target_os = "linux")]
2990 fn is_g2d_available() -> bool {
2991 *G2D_AVAILABLE.get_or_init(|| G2DProcessor::new().is_ok())
2992 }
2993
2994 #[cfg(target_os = "linux")]
2995 #[cfg(feature = "opengl")]
2996 static GL_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2997
2998 #[cfg(target_os = "linux")]
2999 #[cfg(feature = "opengl")]
3000 fn is_opengl_available() -> bool {
3002 #[cfg(all(target_os = "linux", feature = "opengl"))]
3003 {
3004 *GL_AVAILABLE.get_or_init(|| GLProcessorThreaded::new(None).is_ok())
3005 }
3006
3007 #[cfg(not(all(target_os = "linux", feature = "opengl")))]
3008 {
3009 false
3010 }
3011 }
3012
3013 #[test]
3014 fn test_load_jpeg_with_exif() {
3015 let file = include_bytes!(concat!(
3016 env!("CARGO_MANIFEST_DIR"),
3017 "/../../testdata/zidane_rotated_exif.jpg"
3018 ))
3019 .to_vec();
3020 let loaded = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3021
3022 assert_eq!(loaded.height(), Some(1280));
3023 assert_eq!(loaded.width(), Some(720));
3024
3025 let file = include_bytes!(concat!(
3026 env!("CARGO_MANIFEST_DIR"),
3027 "/../../testdata/zidane.jpg"
3028 ))
3029 .to_vec();
3030 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3031
3032 let (dst_width, dst_height) = (cpu_src.height().unwrap(), cpu_src.width().unwrap());
3033
3034 let cpu_dst =
3035 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3036 let mut cpu_converter = CPUProcessor::new();
3037
3038 let (result, _cpu_src, cpu_dst) = convert_img(
3039 &mut cpu_converter,
3040 cpu_src,
3041 cpu_dst,
3042 Rotation::Clockwise90,
3043 Flip::None,
3044 Crop::no_crop(),
3045 );
3046 result.unwrap();
3047
3048 compare_images(&loaded, &cpu_dst, 0.98, function!());
3049 }
3050
3051 #[test]
3052 fn test_load_png_with_exif() {
3053 let file = include_bytes!(concat!(
3054 env!("CARGO_MANIFEST_DIR"),
3055 "/../../testdata/zidane_rotated_exif_180.png"
3056 ))
3057 .to_vec();
3058 let loaded = crate::load_png(&file, Some(PixelFormat::Rgba), None).unwrap();
3059
3060 assert_eq!(loaded.height(), Some(720));
3061 assert_eq!(loaded.width(), Some(1280));
3062
3063 let file = include_bytes!(concat!(
3064 env!("CARGO_MANIFEST_DIR"),
3065 "/../../testdata/zidane.jpg"
3066 ))
3067 .to_vec();
3068 let cpu_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3069
3070 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
3071 let mut cpu_converter = CPUProcessor::new();
3072
3073 let (result, _cpu_src, cpu_dst) = convert_img(
3074 &mut cpu_converter,
3075 cpu_src,
3076 cpu_dst,
3077 Rotation::Rotate180,
3078 Flip::None,
3079 Crop::no_crop(),
3080 );
3081 result.unwrap();
3082
3083 compare_images(&loaded, &cpu_dst, 0.98, function!());
3084 }
3085
3086 #[test]
3087 #[cfg(target_os = "linux")]
3088 fn test_g2d_resize() {
3089 if !is_g2d_available() {
3090 eprintln!("SKIPPED: test_g2d_resize - G2D library (libg2d.so.2) not available");
3091 return;
3092 }
3093 if !is_dma_available() {
3094 eprintln!(
3095 "SKIPPED: test_g2d_resize - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3096 );
3097 return;
3098 }
3099
3100 let dst_width = 640;
3101 let dst_height = 360;
3102 let file = include_bytes!(concat!(
3103 env!("CARGO_MANIFEST_DIR"),
3104 "/../../testdata/zidane.jpg"
3105 ))
3106 .to_vec();
3107 let src =
3108 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
3109
3110 let g2d_dst = TensorDyn::image(
3111 dst_width,
3112 dst_height,
3113 PixelFormat::Rgba,
3114 DType::U8,
3115 Some(TensorMemory::Dma),
3116 )
3117 .unwrap();
3118 let mut g2d_converter = G2DProcessor::new().unwrap();
3119 let (result, src, g2d_dst) = convert_img(
3120 &mut g2d_converter,
3121 src,
3122 g2d_dst,
3123 Rotation::None,
3124 Flip::None,
3125 Crop::no_crop(),
3126 );
3127 result.unwrap();
3128
3129 let cpu_dst =
3130 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3131 let mut cpu_converter = CPUProcessor::new();
3132 let (result, _src, cpu_dst) = convert_img(
3133 &mut cpu_converter,
3134 src,
3135 cpu_dst,
3136 Rotation::None,
3137 Flip::None,
3138 Crop::no_crop(),
3139 );
3140 result.unwrap();
3141
3142 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3143 }
3144
3145 #[test]
3146 #[cfg(target_os = "linux")]
3147 #[cfg(feature = "opengl")]
3148 fn test_opengl_resize() {
3149 if !is_opengl_available() {
3150 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3151 return;
3152 }
3153
3154 let dst_width = 640;
3155 let dst_height = 360;
3156 let file = include_bytes!(concat!(
3157 env!("CARGO_MANIFEST_DIR"),
3158 "/../../testdata/zidane.jpg"
3159 ))
3160 .to_vec();
3161 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3162
3163 let cpu_dst =
3164 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3165 let mut cpu_converter = CPUProcessor::new();
3166 let (result, src, cpu_dst) = convert_img(
3167 &mut cpu_converter,
3168 src,
3169 cpu_dst,
3170 Rotation::None,
3171 Flip::None,
3172 Crop::no_crop(),
3173 );
3174 result.unwrap();
3175
3176 let mut src = src;
3177 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3178
3179 for _ in 0..5 {
3180 let gl_dst =
3181 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None)
3182 .unwrap();
3183 let (result, src_back, gl_dst) = convert_img(
3184 &mut gl_converter,
3185 src,
3186 gl_dst,
3187 Rotation::None,
3188 Flip::None,
3189 Crop::no_crop(),
3190 );
3191 result.unwrap();
3192 src = src_back;
3193
3194 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3195 }
3196 }
3197
3198 #[test]
3199 #[cfg(target_os = "linux")]
3200 #[cfg(feature = "opengl")]
3201 fn test_opengl_10_threads() {
3202 if !is_opengl_available() {
3203 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3204 return;
3205 }
3206
3207 let handles: Vec<_> = (0..10)
3208 .map(|i| {
3209 std::thread::Builder::new()
3210 .name(format!("Thread {i}"))
3211 .spawn(test_opengl_resize)
3212 .unwrap()
3213 })
3214 .collect();
3215 handles.into_iter().for_each(|h| {
3216 if let Err(e) = h.join() {
3217 std::panic::resume_unwind(e)
3218 }
3219 });
3220 }
3221
3222 #[test]
3223 #[cfg(target_os = "linux")]
3224 #[cfg(feature = "opengl")]
3225 fn test_opengl_grey() {
3226 if !is_opengl_available() {
3227 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3228 return;
3229 }
3230
3231 let img = crate::load_image(
3232 include_bytes!(concat!(
3233 env!("CARGO_MANIFEST_DIR"),
3234 "/../../testdata/grey.jpg"
3235 )),
3236 Some(PixelFormat::Grey),
3237 None,
3238 )
3239 .unwrap();
3240
3241 let gl_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
3242 let cpu_dst = TensorDyn::image(640, 640, PixelFormat::Grey, DType::U8, None).unwrap();
3243
3244 let mut converter = CPUProcessor::new();
3245
3246 let (result, img, cpu_dst) = convert_img(
3247 &mut converter,
3248 img,
3249 cpu_dst,
3250 Rotation::None,
3251 Flip::None,
3252 Crop::no_crop(),
3253 );
3254 result.unwrap();
3255
3256 let mut gl = GLProcessorThreaded::new(None).unwrap();
3257 let (result, _img, gl_dst) = convert_img(
3258 &mut gl,
3259 img,
3260 gl_dst,
3261 Rotation::None,
3262 Flip::None,
3263 Crop::no_crop(),
3264 );
3265 result.unwrap();
3266
3267 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3268 }
3269
3270 #[test]
3271 #[cfg(target_os = "linux")]
3272 fn test_g2d_src_crop() {
3273 if !is_g2d_available() {
3274 eprintln!("SKIPPED: test_g2d_src_crop - G2D library (libg2d.so.2) not available");
3275 return;
3276 }
3277 if !is_dma_available() {
3278 eprintln!(
3279 "SKIPPED: test_g2d_src_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3280 );
3281 return;
3282 }
3283
3284 let dst_width = 640;
3285 let dst_height = 640;
3286 let file = include_bytes!(concat!(
3287 env!("CARGO_MANIFEST_DIR"),
3288 "/../../testdata/zidane.jpg"
3289 ))
3290 .to_vec();
3291 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3292
3293 let cpu_dst =
3294 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3295 let mut cpu_converter = CPUProcessor::new();
3296 let crop = Crop {
3297 src_rect: Some(Rect {
3298 left: 0,
3299 top: 0,
3300 width: 640,
3301 height: 360,
3302 }),
3303 dst_rect: None,
3304 dst_color: None,
3305 };
3306 let (result, src, cpu_dst) = convert_img(
3307 &mut cpu_converter,
3308 src,
3309 cpu_dst,
3310 Rotation::None,
3311 Flip::None,
3312 crop,
3313 );
3314 result.unwrap();
3315
3316 let g2d_dst =
3317 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3318 let mut g2d_converter = G2DProcessor::new().unwrap();
3319 let (result, _src, g2d_dst) = convert_img(
3320 &mut g2d_converter,
3321 src,
3322 g2d_dst,
3323 Rotation::None,
3324 Flip::None,
3325 crop,
3326 );
3327 result.unwrap();
3328
3329 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3330 }
3331
3332 #[test]
3333 #[cfg(target_os = "linux")]
3334 fn test_g2d_dst_crop() {
3335 if !is_g2d_available() {
3336 eprintln!("SKIPPED: test_g2d_dst_crop - G2D library (libg2d.so.2) not available");
3337 return;
3338 }
3339 if !is_dma_available() {
3340 eprintln!(
3341 "SKIPPED: test_g2d_dst_crop - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3342 );
3343 return;
3344 }
3345
3346 let dst_width = 640;
3347 let dst_height = 640;
3348 let file = include_bytes!(concat!(
3349 env!("CARGO_MANIFEST_DIR"),
3350 "/../../testdata/zidane.jpg"
3351 ))
3352 .to_vec();
3353 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3354
3355 let cpu_dst =
3356 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3357 let mut cpu_converter = CPUProcessor::new();
3358 let crop = Crop {
3359 src_rect: None,
3360 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3361 dst_color: None,
3362 };
3363 let (result, src, cpu_dst) = convert_img(
3364 &mut cpu_converter,
3365 src,
3366 cpu_dst,
3367 Rotation::None,
3368 Flip::None,
3369 crop,
3370 );
3371 result.unwrap();
3372
3373 let g2d_dst =
3374 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3375 let mut g2d_converter = G2DProcessor::new().unwrap();
3376 let (result, _src, g2d_dst) = convert_img(
3377 &mut g2d_converter,
3378 src,
3379 g2d_dst,
3380 Rotation::None,
3381 Flip::None,
3382 crop,
3383 );
3384 result.unwrap();
3385
3386 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3387 }
3388
3389 #[test]
3390 #[cfg(target_os = "linux")]
3391 fn test_g2d_all_rgba() {
3392 if !is_g2d_available() {
3393 eprintln!("SKIPPED: test_g2d_all_rgba - G2D library (libg2d.so.2) not available");
3394 return;
3395 }
3396 if !is_dma_available() {
3397 eprintln!(
3398 "SKIPPED: test_g2d_all_rgba - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3399 );
3400 return;
3401 }
3402
3403 let dst_width = 640;
3404 let dst_height = 640;
3405 let file = include_bytes!(concat!(
3406 env!("CARGO_MANIFEST_DIR"),
3407 "/../../testdata/zidane.jpg"
3408 ))
3409 .to_vec();
3410 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3411 let src_dyn = src;
3412
3413 let mut cpu_dst =
3414 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3415 let mut cpu_converter = CPUProcessor::new();
3416 let mut g2d_dst =
3417 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3418 let mut g2d_converter = G2DProcessor::new().unwrap();
3419
3420 let crop = Crop {
3421 src_rect: Some(Rect::new(50, 120, 1024, 576)),
3422 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3423 dst_color: None,
3424 };
3425
3426 for rot in [
3427 Rotation::None,
3428 Rotation::Clockwise90,
3429 Rotation::Rotate180,
3430 Rotation::CounterClockwise90,
3431 ] {
3432 cpu_dst
3433 .as_u8()
3434 .unwrap()
3435 .map()
3436 .unwrap()
3437 .as_mut_slice()
3438 .fill(114);
3439 g2d_dst
3440 .as_u8()
3441 .unwrap()
3442 .map()
3443 .unwrap()
3444 .as_mut_slice()
3445 .fill(114);
3446 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
3447 let mut cpu_dst_dyn = cpu_dst;
3448 cpu_converter
3449 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
3450 .unwrap();
3451 cpu_dst = {
3452 let mut __t = cpu_dst_dyn.into_u8().unwrap();
3453 __t.set_format(PixelFormat::Rgba).unwrap();
3454 TensorDyn::from(__t)
3455 };
3456
3457 let mut g2d_dst_dyn = g2d_dst;
3458 g2d_converter
3459 .convert(&src_dyn, &mut g2d_dst_dyn, Rotation::None, Flip::None, crop)
3460 .unwrap();
3461 g2d_dst = {
3462 let mut __t = g2d_dst_dyn.into_u8().unwrap();
3463 __t.set_format(PixelFormat::Rgba).unwrap();
3464 TensorDyn::from(__t)
3465 };
3466
3467 compare_images(
3468 &g2d_dst,
3469 &cpu_dst,
3470 0.98,
3471 &format!("{} {:?} {:?}", function!(), rot, flip),
3472 );
3473 }
3474 }
3475 }
3476
3477 #[test]
3478 #[cfg(target_os = "linux")]
3479 #[cfg(feature = "opengl")]
3480 fn test_opengl_src_crop() {
3481 if !is_opengl_available() {
3482 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3483 return;
3484 }
3485
3486 let dst_width = 640;
3487 let dst_height = 360;
3488 let file = include_bytes!(concat!(
3489 env!("CARGO_MANIFEST_DIR"),
3490 "/../../testdata/zidane.jpg"
3491 ))
3492 .to_vec();
3493 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3494 let crop = Crop {
3495 src_rect: Some(Rect {
3496 left: 320,
3497 top: 180,
3498 width: 1280 - 320,
3499 height: 720 - 180,
3500 }),
3501 dst_rect: None,
3502 dst_color: None,
3503 };
3504
3505 let cpu_dst =
3506 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3507 let mut cpu_converter = CPUProcessor::new();
3508 let (result, src, cpu_dst) = convert_img(
3509 &mut cpu_converter,
3510 src,
3511 cpu_dst,
3512 Rotation::None,
3513 Flip::None,
3514 crop,
3515 );
3516 result.unwrap();
3517
3518 let gl_dst =
3519 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3520 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3521 let (result, _src, gl_dst) = convert_img(
3522 &mut gl_converter,
3523 src,
3524 gl_dst,
3525 Rotation::None,
3526 Flip::None,
3527 crop,
3528 );
3529 result.unwrap();
3530
3531 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3532 }
3533
3534 #[test]
3535 #[cfg(target_os = "linux")]
3536 #[cfg(feature = "opengl")]
3537 fn test_opengl_dst_crop() {
3538 if !is_opengl_available() {
3539 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3540 return;
3541 }
3542
3543 let dst_width = 640;
3544 let dst_height = 640;
3545 let file = include_bytes!(concat!(
3546 env!("CARGO_MANIFEST_DIR"),
3547 "/../../testdata/zidane.jpg"
3548 ))
3549 .to_vec();
3550 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3551
3552 let cpu_dst =
3553 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3554 let mut cpu_converter = CPUProcessor::new();
3555 let crop = Crop {
3556 src_rect: None,
3557 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3558 dst_color: None,
3559 };
3560 let (result, src, cpu_dst) = convert_img(
3561 &mut cpu_converter,
3562 src,
3563 cpu_dst,
3564 Rotation::None,
3565 Flip::None,
3566 crop,
3567 );
3568 result.unwrap();
3569
3570 let gl_dst =
3571 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3572 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3573 let (result, _src, gl_dst) = convert_img(
3574 &mut gl_converter,
3575 src,
3576 gl_dst,
3577 Rotation::None,
3578 Flip::None,
3579 crop,
3580 );
3581 result.unwrap();
3582
3583 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3584 }
3585
3586 #[test]
3587 #[cfg(target_os = "linux")]
3588 #[cfg(feature = "opengl")]
3589 fn test_opengl_all_rgba() {
3590 if !is_opengl_available() {
3591 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3592 return;
3593 }
3594
3595 let dst_width = 640;
3596 let dst_height = 640;
3597 let file = include_bytes!(concat!(
3598 env!("CARGO_MANIFEST_DIR"),
3599 "/../../testdata/zidane.jpg"
3600 ))
3601 .to_vec();
3602
3603 let mut cpu_converter = CPUProcessor::new();
3604
3605 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3606
3607 let mut mem = vec![None, Some(TensorMemory::Mem), Some(TensorMemory::Shm)];
3608 if is_dma_available() {
3609 mem.push(Some(TensorMemory::Dma));
3610 }
3611 let crop = Crop {
3612 src_rect: Some(Rect::new(50, 120, 1024, 576)),
3613 dst_rect: Some(Rect::new(100, 100, 512, 288)),
3614 dst_color: None,
3615 };
3616 for m in mem {
3617 let src = crate::load_image(&file, Some(PixelFormat::Rgba), m).unwrap();
3618 let src_dyn = src;
3619
3620 for rot in [
3621 Rotation::None,
3622 Rotation::Clockwise90,
3623 Rotation::Rotate180,
3624 Rotation::CounterClockwise90,
3625 ] {
3626 for flip in [Flip::None, Flip::Horizontal, Flip::Vertical] {
3627 let cpu_dst =
3628 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
3629 .unwrap();
3630 let gl_dst =
3631 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, m)
3632 .unwrap();
3633 cpu_dst
3634 .as_u8()
3635 .unwrap()
3636 .map()
3637 .unwrap()
3638 .as_mut_slice()
3639 .fill(114);
3640 gl_dst
3641 .as_u8()
3642 .unwrap()
3643 .map()
3644 .unwrap()
3645 .as_mut_slice()
3646 .fill(114);
3647
3648 let mut cpu_dst_dyn = cpu_dst;
3649 cpu_converter
3650 .convert(&src_dyn, &mut cpu_dst_dyn, Rotation::None, Flip::None, crop)
3651 .unwrap();
3652 let cpu_dst = {
3653 let mut __t = cpu_dst_dyn.into_u8().unwrap();
3654 __t.set_format(PixelFormat::Rgba).unwrap();
3655 TensorDyn::from(__t)
3656 };
3657
3658 let mut gl_dst_dyn = gl_dst;
3659 gl_converter
3660 .convert(&src_dyn, &mut gl_dst_dyn, Rotation::None, Flip::None, crop)
3661 .map_err(|e| {
3662 log::error!("error mem {m:?} rot {rot:?} error: {e:?}");
3663 e
3664 })
3665 .unwrap();
3666 let gl_dst = {
3667 let mut __t = gl_dst_dyn.into_u8().unwrap();
3668 __t.set_format(PixelFormat::Rgba).unwrap();
3669 TensorDyn::from(__t)
3670 };
3671
3672 compare_images(
3673 &gl_dst,
3674 &cpu_dst,
3675 0.98,
3676 &format!("{} {:?} {:?}", function!(), rot, flip),
3677 );
3678 }
3679 }
3680 }
3681 }
3682
3683 #[test]
3684 #[cfg(target_os = "linux")]
3685 fn test_cpu_rotate() {
3686 for rot in [
3687 Rotation::Clockwise90,
3688 Rotation::Rotate180,
3689 Rotation::CounterClockwise90,
3690 ] {
3691 test_cpu_rotate_(rot);
3692 }
3693 }
3694
3695 #[cfg(target_os = "linux")]
3696 fn test_cpu_rotate_(rot: Rotation) {
3697 let file = include_bytes!(concat!(
3701 env!("CARGO_MANIFEST_DIR"),
3702 "/../../testdata/zidane.jpg"
3703 ))
3704 .to_vec();
3705
3706 let unchanged_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3707 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
3708
3709 let (dst_width, dst_height) = match rot {
3710 Rotation::None | Rotation::Rotate180 => (src.width().unwrap(), src.height().unwrap()),
3711 Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
3712 (src.height().unwrap(), src.width().unwrap())
3713 }
3714 };
3715
3716 let cpu_dst =
3717 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3718 let mut cpu_converter = CPUProcessor::new();
3719
3720 let (result, src, cpu_dst) = convert_img(
3723 &mut cpu_converter,
3724 src,
3725 cpu_dst,
3726 rot,
3727 Flip::None,
3728 Crop::no_crop(),
3729 );
3730 result.unwrap();
3731
3732 let (result, cpu_dst, src) = convert_img(
3733 &mut cpu_converter,
3734 cpu_dst,
3735 src,
3736 rot,
3737 Flip::None,
3738 Crop::no_crop(),
3739 );
3740 result.unwrap();
3741
3742 let (result, src, cpu_dst) = convert_img(
3743 &mut cpu_converter,
3744 src,
3745 cpu_dst,
3746 rot,
3747 Flip::None,
3748 Crop::no_crop(),
3749 );
3750 result.unwrap();
3751
3752 let (result, _cpu_dst, src) = convert_img(
3753 &mut cpu_converter,
3754 cpu_dst,
3755 src,
3756 rot,
3757 Flip::None,
3758 Crop::no_crop(),
3759 );
3760 result.unwrap();
3761
3762 compare_images(&src, &unchanged_src, 0.98, function!());
3763 }
3764
3765 #[test]
3766 #[cfg(target_os = "linux")]
3767 #[cfg(feature = "opengl")]
3768 fn test_opengl_rotate() {
3769 if !is_opengl_available() {
3770 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3771 return;
3772 }
3773
3774 let size = (1280, 720);
3775 let mut mem = vec![None, Some(TensorMemory::Shm), Some(TensorMemory::Mem)];
3776
3777 if is_dma_available() {
3778 mem.push(Some(TensorMemory::Dma));
3779 }
3780 for m in mem {
3781 for rot in [
3782 Rotation::Clockwise90,
3783 Rotation::Rotate180,
3784 Rotation::CounterClockwise90,
3785 ] {
3786 test_opengl_rotate_(size, rot, m);
3787 }
3788 }
3789 }
3790
3791 #[cfg(target_os = "linux")]
3792 #[cfg(feature = "opengl")]
3793 fn test_opengl_rotate_(
3794 size: (usize, usize),
3795 rot: Rotation,
3796 tensor_memory: Option<TensorMemory>,
3797 ) {
3798 let (dst_width, dst_height) = match rot {
3799 Rotation::None | Rotation::Rotate180 => size,
3800 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
3801 };
3802
3803 let file = include_bytes!(concat!(
3804 env!("CARGO_MANIFEST_DIR"),
3805 "/../../testdata/zidane.jpg"
3806 ))
3807 .to_vec();
3808 let src = crate::load_image(&file, Some(PixelFormat::Rgba), tensor_memory).unwrap();
3809
3810 let cpu_dst =
3811 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3812 let mut cpu_converter = CPUProcessor::new();
3813
3814 let (result, mut src, cpu_dst) = convert_img(
3815 &mut cpu_converter,
3816 src,
3817 cpu_dst,
3818 rot,
3819 Flip::None,
3820 Crop::no_crop(),
3821 );
3822 result.unwrap();
3823
3824 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
3825
3826 for _ in 0..5 {
3827 let gl_dst = TensorDyn::image(
3828 dst_width,
3829 dst_height,
3830 PixelFormat::Rgba,
3831 DType::U8,
3832 tensor_memory,
3833 )
3834 .unwrap();
3835 let (result, src_back, gl_dst) = convert_img(
3836 &mut gl_converter,
3837 src,
3838 gl_dst,
3839 rot,
3840 Flip::None,
3841 Crop::no_crop(),
3842 );
3843 result.unwrap();
3844 src = src_back;
3845 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
3846 }
3847 }
3848
3849 #[test]
3850 #[cfg(target_os = "linux")]
3851 fn test_g2d_rotate() {
3852 if !is_g2d_available() {
3853 eprintln!("SKIPPED: test_g2d_rotate - G2D library (libg2d.so.2) not available");
3854 return;
3855 }
3856 if !is_dma_available() {
3857 eprintln!(
3858 "SKIPPED: test_g2d_rotate - DMA memory allocation not available (permission denied or no DMA-BUF support)"
3859 );
3860 return;
3861 }
3862
3863 let size = (1280, 720);
3864 for rot in [
3865 Rotation::Clockwise90,
3866 Rotation::Rotate180,
3867 Rotation::CounterClockwise90,
3868 ] {
3869 test_g2d_rotate_(size, rot);
3870 }
3871 }
3872
3873 #[cfg(target_os = "linux")]
3874 fn test_g2d_rotate_(size: (usize, usize), rot: Rotation) {
3875 let (dst_width, dst_height) = match rot {
3876 Rotation::None | Rotation::Rotate180 => size,
3877 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (size.1, size.0),
3878 };
3879
3880 let file = include_bytes!(concat!(
3881 env!("CARGO_MANIFEST_DIR"),
3882 "/../../testdata/zidane.jpg"
3883 ))
3884 .to_vec();
3885 let src =
3886 crate::load_image(&file, Some(PixelFormat::Rgba), Some(TensorMemory::Dma)).unwrap();
3887
3888 let cpu_dst =
3889 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3890 let mut cpu_converter = CPUProcessor::new();
3891
3892 let (result, src, cpu_dst) = convert_img(
3893 &mut cpu_converter,
3894 src,
3895 cpu_dst,
3896 rot,
3897 Flip::None,
3898 Crop::no_crop(),
3899 );
3900 result.unwrap();
3901
3902 let g2d_dst = TensorDyn::image(
3903 dst_width,
3904 dst_height,
3905 PixelFormat::Rgba,
3906 DType::U8,
3907 Some(TensorMemory::Dma),
3908 )
3909 .unwrap();
3910 let mut g2d_converter = G2DProcessor::new().unwrap();
3911
3912 let (result, _src, g2d_dst) = convert_img(
3913 &mut g2d_converter,
3914 src,
3915 g2d_dst,
3916 rot,
3917 Flip::None,
3918 Crop::no_crop(),
3919 );
3920 result.unwrap();
3921
3922 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
3923 }
3924
3925 #[test]
3926 fn test_rgba_to_yuyv_resize_cpu() {
3927 let src = load_bytes_to_tensor(
3928 1280,
3929 720,
3930 PixelFormat::Rgba,
3931 None,
3932 include_bytes!(concat!(
3933 env!("CARGO_MANIFEST_DIR"),
3934 "/../../testdata/camera720p.rgba"
3935 )),
3936 )
3937 .unwrap();
3938
3939 let (dst_width, dst_height) = (640, 360);
3940
3941 let dst =
3942 TensorDyn::image(dst_width, dst_height, PixelFormat::Yuyv, DType::U8, None).unwrap();
3943
3944 let dst_through_yuyv =
3945 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3946 let dst_direct =
3947 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
3948
3949 let mut cpu_converter = CPUProcessor::new();
3950
3951 let (result, src, dst) = convert_img(
3952 &mut cpu_converter,
3953 src,
3954 dst,
3955 Rotation::None,
3956 Flip::None,
3957 Crop::no_crop(),
3958 );
3959 result.unwrap();
3960
3961 let (result, _dst, dst_through_yuyv) = convert_img(
3962 &mut cpu_converter,
3963 dst,
3964 dst_through_yuyv,
3965 Rotation::None,
3966 Flip::None,
3967 Crop::no_crop(),
3968 );
3969 result.unwrap();
3970
3971 let (result, _src, dst_direct) = convert_img(
3972 &mut cpu_converter,
3973 src,
3974 dst_direct,
3975 Rotation::None,
3976 Flip::None,
3977 Crop::no_crop(),
3978 );
3979 result.unwrap();
3980
3981 compare_images(&dst_through_yuyv, &dst_direct, 0.98, function!());
3982 }
3983
3984 #[test]
3985 #[cfg(target_os = "linux")]
3986 #[cfg(feature = "opengl")]
3987 #[ignore = "opengl doesn't support rendering to PixelFormat::Yuyv texture"]
3988 fn test_rgba_to_yuyv_resize_opengl() {
3989 if !is_opengl_available() {
3990 eprintln!("SKIPPED: {} - OpenGL not available", function!());
3991 return;
3992 }
3993
3994 if !is_dma_available() {
3995 eprintln!(
3996 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
3997 function!()
3998 );
3999 return;
4000 }
4001
4002 let src = load_bytes_to_tensor(
4003 1280,
4004 720,
4005 PixelFormat::Rgba,
4006 None,
4007 include_bytes!(concat!(
4008 env!("CARGO_MANIFEST_DIR"),
4009 "/../../testdata/camera720p.rgba"
4010 )),
4011 )
4012 .unwrap();
4013
4014 let (dst_width, dst_height) = (640, 360);
4015
4016 let dst = TensorDyn::image(
4017 dst_width,
4018 dst_height,
4019 PixelFormat::Yuyv,
4020 DType::U8,
4021 Some(TensorMemory::Dma),
4022 )
4023 .unwrap();
4024
4025 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4026
4027 let (result, src, dst) = convert_img(
4028 &mut gl_converter,
4029 src,
4030 dst,
4031 Rotation::None,
4032 Flip::None,
4033 Crop::new()
4034 .with_dst_rect(Some(Rect::new(100, 100, 100, 100)))
4035 .with_dst_color(Some([255, 255, 255, 255])),
4036 );
4037 result.unwrap();
4038
4039 std::fs::write(
4040 "rgba_to_yuyv_opengl.yuyv",
4041 dst.as_u8().unwrap().map().unwrap().as_slice(),
4042 )
4043 .unwrap();
4044 let cpu_dst = TensorDyn::image(
4045 dst_width,
4046 dst_height,
4047 PixelFormat::Yuyv,
4048 DType::U8,
4049 Some(TensorMemory::Dma),
4050 )
4051 .unwrap();
4052 let (result, _src, cpu_dst) = convert_img(
4053 &mut CPUProcessor::new(),
4054 src,
4055 cpu_dst,
4056 Rotation::None,
4057 Flip::None,
4058 Crop::no_crop(),
4059 );
4060 result.unwrap();
4061
4062 compare_images_convert_to_rgb(&dst, &cpu_dst, 0.98, function!());
4063 }
4064
4065 #[test]
4066 #[cfg(target_os = "linux")]
4067 fn test_rgba_to_yuyv_resize_g2d() {
4068 if !is_g2d_available() {
4069 eprintln!(
4070 "SKIPPED: test_rgba_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
4071 );
4072 return;
4073 }
4074 if !is_dma_available() {
4075 eprintln!(
4076 "SKIPPED: test_rgba_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4077 );
4078 return;
4079 }
4080
4081 let src = load_bytes_to_tensor(
4082 1280,
4083 720,
4084 PixelFormat::Rgba,
4085 Some(TensorMemory::Dma),
4086 include_bytes!(concat!(
4087 env!("CARGO_MANIFEST_DIR"),
4088 "/../../testdata/camera720p.rgba"
4089 )),
4090 )
4091 .unwrap();
4092
4093 let (dst_width, dst_height) = (1280, 720);
4094
4095 let cpu_dst = TensorDyn::image(
4096 dst_width,
4097 dst_height,
4098 PixelFormat::Yuyv,
4099 DType::U8,
4100 Some(TensorMemory::Dma),
4101 )
4102 .unwrap();
4103
4104 let g2d_dst = TensorDyn::image(
4105 dst_width,
4106 dst_height,
4107 PixelFormat::Yuyv,
4108 DType::U8,
4109 Some(TensorMemory::Dma),
4110 )
4111 .unwrap();
4112
4113 let mut g2d_converter = G2DProcessor::new().unwrap();
4114 let crop = Crop {
4115 src_rect: None,
4116 dst_rect: Some(Rect::new(100, 100, 2, 2)),
4117 dst_color: None,
4118 };
4119
4120 g2d_dst
4121 .as_u8()
4122 .unwrap()
4123 .map()
4124 .unwrap()
4125 .as_mut_slice()
4126 .fill(128);
4127 let (result, src, g2d_dst) = convert_img(
4128 &mut g2d_converter,
4129 src,
4130 g2d_dst,
4131 Rotation::None,
4132 Flip::None,
4133 crop,
4134 );
4135 result.unwrap();
4136
4137 let cpu_dst_img = cpu_dst;
4138 cpu_dst_img
4139 .as_u8()
4140 .unwrap()
4141 .map()
4142 .unwrap()
4143 .as_mut_slice()
4144 .fill(128);
4145 let (result, _src, cpu_dst) = convert_img(
4146 &mut CPUProcessor::new(),
4147 src,
4148 cpu_dst_img,
4149 Rotation::None,
4150 Flip::None,
4151 crop,
4152 );
4153 result.unwrap();
4154
4155 compare_images_convert_to_rgb(&cpu_dst, &g2d_dst, 0.98, function!());
4156 }
4157
4158 #[test]
4159 fn test_yuyv_to_rgba_cpu() {
4160 let file = include_bytes!(concat!(
4161 env!("CARGO_MANIFEST_DIR"),
4162 "/../../testdata/camera720p.yuyv"
4163 ))
4164 .to_vec();
4165 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
4166 src.as_u8()
4167 .unwrap()
4168 .map()
4169 .unwrap()
4170 .as_mut_slice()
4171 .copy_from_slice(&file);
4172
4173 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4174 let mut cpu_converter = CPUProcessor::new();
4175
4176 let (result, _src, dst) = convert_img(
4177 &mut cpu_converter,
4178 src,
4179 dst,
4180 Rotation::None,
4181 Flip::None,
4182 Crop::no_crop(),
4183 );
4184 result.unwrap();
4185
4186 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4187 target_image
4188 .as_u8()
4189 .unwrap()
4190 .map()
4191 .unwrap()
4192 .as_mut_slice()
4193 .copy_from_slice(include_bytes!(concat!(
4194 env!("CARGO_MANIFEST_DIR"),
4195 "/../../testdata/camera720p.rgba"
4196 )));
4197
4198 compare_images(&dst, &target_image, 0.98, function!());
4199 }
4200
4201 #[test]
4202 fn test_yuyv_to_rgb_cpu() {
4203 let file = include_bytes!(concat!(
4204 env!("CARGO_MANIFEST_DIR"),
4205 "/../../testdata/camera720p.yuyv"
4206 ))
4207 .to_vec();
4208 let src = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
4209 src.as_u8()
4210 .unwrap()
4211 .map()
4212 .unwrap()
4213 .as_mut_slice()
4214 .copy_from_slice(&file);
4215
4216 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4217 let mut cpu_converter = CPUProcessor::new();
4218
4219 let (result, _src, dst) = convert_img(
4220 &mut cpu_converter,
4221 src,
4222 dst,
4223 Rotation::None,
4224 Flip::None,
4225 Crop::no_crop(),
4226 );
4227 result.unwrap();
4228
4229 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4230 target_image
4231 .as_u8()
4232 .unwrap()
4233 .map()
4234 .unwrap()
4235 .as_mut_slice()
4236 .as_chunks_mut::<3>()
4237 .0
4238 .iter_mut()
4239 .zip(
4240 include_bytes!(concat!(
4241 env!("CARGO_MANIFEST_DIR"),
4242 "/../../testdata/camera720p.rgba"
4243 ))
4244 .as_chunks::<4>()
4245 .0,
4246 )
4247 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
4248
4249 compare_images(&dst, &target_image, 0.98, function!());
4250 }
4251
4252 #[test]
4253 #[cfg(target_os = "linux")]
4254 fn test_yuyv_to_rgba_g2d() {
4255 if !is_g2d_available() {
4256 eprintln!("SKIPPED: test_yuyv_to_rgba_g2d - G2D library (libg2d.so.2) not available");
4257 return;
4258 }
4259 if !is_dma_available() {
4260 eprintln!(
4261 "SKIPPED: test_yuyv_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4262 );
4263 return;
4264 }
4265
4266 let src = load_bytes_to_tensor(
4267 1280,
4268 720,
4269 PixelFormat::Yuyv,
4270 None,
4271 include_bytes!(concat!(
4272 env!("CARGO_MANIFEST_DIR"),
4273 "/../../testdata/camera720p.yuyv"
4274 )),
4275 )
4276 .unwrap();
4277
4278 let dst = TensorDyn::image(
4279 1280,
4280 720,
4281 PixelFormat::Rgba,
4282 DType::U8,
4283 Some(TensorMemory::Dma),
4284 )
4285 .unwrap();
4286 let mut g2d_converter = G2DProcessor::new().unwrap();
4287
4288 let (result, _src, dst) = convert_img(
4289 &mut g2d_converter,
4290 src,
4291 dst,
4292 Rotation::None,
4293 Flip::None,
4294 Crop::no_crop(),
4295 );
4296 result.unwrap();
4297
4298 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4299 target_image
4300 .as_u8()
4301 .unwrap()
4302 .map()
4303 .unwrap()
4304 .as_mut_slice()
4305 .copy_from_slice(include_bytes!(concat!(
4306 env!("CARGO_MANIFEST_DIR"),
4307 "/../../testdata/camera720p.rgba"
4308 )));
4309
4310 compare_images(&dst, &target_image, 0.98, function!());
4311 }
4312
4313 #[test]
4314 #[cfg(target_os = "linux")]
4315 #[cfg(feature = "opengl")]
4316 fn test_yuyv_to_rgba_opengl() {
4317 if !is_opengl_available() {
4318 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4319 return;
4320 }
4321 if !is_dma_available() {
4322 eprintln!(
4323 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4324 function!()
4325 );
4326 return;
4327 }
4328
4329 let src = load_bytes_to_tensor(
4330 1280,
4331 720,
4332 PixelFormat::Yuyv,
4333 Some(TensorMemory::Dma),
4334 include_bytes!(concat!(
4335 env!("CARGO_MANIFEST_DIR"),
4336 "/../../testdata/camera720p.yuyv"
4337 )),
4338 )
4339 .unwrap();
4340
4341 let dst = TensorDyn::image(
4342 1280,
4343 720,
4344 PixelFormat::Rgba,
4345 DType::U8,
4346 Some(TensorMemory::Dma),
4347 )
4348 .unwrap();
4349 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4350
4351 let (result, _src, dst) = convert_img(
4352 &mut gl_converter,
4353 src,
4354 dst,
4355 Rotation::None,
4356 Flip::None,
4357 Crop::no_crop(),
4358 );
4359 result.unwrap();
4360
4361 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4362 target_image
4363 .as_u8()
4364 .unwrap()
4365 .map()
4366 .unwrap()
4367 .as_mut_slice()
4368 .copy_from_slice(include_bytes!(concat!(
4369 env!("CARGO_MANIFEST_DIR"),
4370 "/../../testdata/camera720p.rgba"
4371 )));
4372
4373 compare_images(&dst, &target_image, 0.98, function!());
4374 }
4375
4376 #[test]
4377 #[cfg(target_os = "linux")]
4378 fn test_yuyv_to_rgb_g2d() {
4379 if !is_g2d_available() {
4380 eprintln!("SKIPPED: test_yuyv_to_rgb_g2d - G2D library (libg2d.so.2) not available");
4381 return;
4382 }
4383 if !is_dma_available() {
4384 eprintln!(
4385 "SKIPPED: test_yuyv_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4386 );
4387 return;
4388 }
4389
4390 let src = load_bytes_to_tensor(
4391 1280,
4392 720,
4393 PixelFormat::Yuyv,
4394 None,
4395 include_bytes!(concat!(
4396 env!("CARGO_MANIFEST_DIR"),
4397 "/../../testdata/camera720p.yuyv"
4398 )),
4399 )
4400 .unwrap();
4401
4402 let g2d_dst = TensorDyn::image(
4403 1280,
4404 720,
4405 PixelFormat::Rgb,
4406 DType::U8,
4407 Some(TensorMemory::Dma),
4408 )
4409 .unwrap();
4410 let mut g2d_converter = G2DProcessor::new().unwrap();
4411
4412 let (result, src, g2d_dst) = convert_img(
4413 &mut g2d_converter,
4414 src,
4415 g2d_dst,
4416 Rotation::None,
4417 Flip::None,
4418 Crop::no_crop(),
4419 );
4420 result.unwrap();
4421
4422 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4423 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4424
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
4435 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4436 }
4437
4438 #[test]
4439 #[cfg(target_os = "linux")]
4440 fn test_yuyv_to_yuyv_resize_g2d() {
4441 if !is_g2d_available() {
4442 eprintln!(
4443 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - G2D library (libg2d.so.2) not available"
4444 );
4445 return;
4446 }
4447 if !is_dma_available() {
4448 eprintln!(
4449 "SKIPPED: test_yuyv_to_yuyv_resize_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4450 );
4451 return;
4452 }
4453
4454 let src = load_bytes_to_tensor(
4455 1280,
4456 720,
4457 PixelFormat::Yuyv,
4458 None,
4459 include_bytes!(concat!(
4460 env!("CARGO_MANIFEST_DIR"),
4461 "/../../testdata/camera720p.yuyv"
4462 )),
4463 )
4464 .unwrap();
4465
4466 let g2d_dst = TensorDyn::image(
4467 600,
4468 400,
4469 PixelFormat::Yuyv,
4470 DType::U8,
4471 Some(TensorMemory::Dma),
4472 )
4473 .unwrap();
4474 let mut g2d_converter = G2DProcessor::new().unwrap();
4475
4476 let (result, src, g2d_dst) = convert_img(
4477 &mut g2d_converter,
4478 src,
4479 g2d_dst,
4480 Rotation::None,
4481 Flip::None,
4482 Crop::no_crop(),
4483 );
4484 result.unwrap();
4485
4486 let cpu_dst = TensorDyn::image(600, 400, PixelFormat::Yuyv, DType::U8, None).unwrap();
4487 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4488
4489 let (result, _src, cpu_dst) = convert_img(
4490 &mut cpu_converter,
4491 src,
4492 cpu_dst,
4493 Rotation::None,
4494 Flip::None,
4495 Crop::no_crop(),
4496 );
4497 result.unwrap();
4498
4499 compare_images_convert_to_rgb(&g2d_dst, &cpu_dst, 0.98, function!());
4501 }
4502
4503 #[test]
4504 fn test_yuyv_to_rgba_resize_cpu() {
4505 let src = load_bytes_to_tensor(
4506 1280,
4507 720,
4508 PixelFormat::Yuyv,
4509 None,
4510 include_bytes!(concat!(
4511 env!("CARGO_MANIFEST_DIR"),
4512 "/../../testdata/camera720p.yuyv"
4513 )),
4514 )
4515 .unwrap();
4516
4517 let (dst_width, dst_height) = (960, 540);
4518
4519 let dst =
4520 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
4521 let mut cpu_converter = CPUProcessor::new();
4522
4523 let (result, _src, dst) = convert_img(
4524 &mut cpu_converter,
4525 src,
4526 dst,
4527 Rotation::None,
4528 Flip::None,
4529 Crop::no_crop(),
4530 );
4531 result.unwrap();
4532
4533 let dst_target =
4534 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgba, DType::U8, None).unwrap();
4535 let src_target = load_bytes_to_tensor(
4536 1280,
4537 720,
4538 PixelFormat::Rgba,
4539 None,
4540 include_bytes!(concat!(
4541 env!("CARGO_MANIFEST_DIR"),
4542 "/../../testdata/camera720p.rgba"
4543 )),
4544 )
4545 .unwrap();
4546 let (result, _src_target, dst_target) = convert_img(
4547 &mut cpu_converter,
4548 src_target,
4549 dst_target,
4550 Rotation::None,
4551 Flip::None,
4552 Crop::no_crop(),
4553 );
4554 result.unwrap();
4555
4556 compare_images(&dst, &dst_target, 0.98, function!());
4557 }
4558
4559 #[test]
4560 #[cfg(target_os = "linux")]
4561 fn test_yuyv_to_rgba_crop_flip_g2d() {
4562 if !is_g2d_available() {
4563 eprintln!(
4564 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - G2D library (libg2d.so.2) not available"
4565 );
4566 return;
4567 }
4568 if !is_dma_available() {
4569 eprintln!(
4570 "SKIPPED: test_yuyv_to_rgba_crop_flip_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4571 );
4572 return;
4573 }
4574
4575 let src = load_bytes_to_tensor(
4576 1280,
4577 720,
4578 PixelFormat::Yuyv,
4579 Some(TensorMemory::Dma),
4580 include_bytes!(concat!(
4581 env!("CARGO_MANIFEST_DIR"),
4582 "/../../testdata/camera720p.yuyv"
4583 )),
4584 )
4585 .unwrap();
4586
4587 let (dst_width, dst_height) = (640, 640);
4588
4589 let dst_g2d = TensorDyn::image(
4590 dst_width,
4591 dst_height,
4592 PixelFormat::Rgba,
4593 DType::U8,
4594 Some(TensorMemory::Dma),
4595 )
4596 .unwrap();
4597 let mut g2d_converter = G2DProcessor::new().unwrap();
4598 let crop = Crop {
4599 src_rect: Some(Rect {
4600 left: 20,
4601 top: 15,
4602 width: 400,
4603 height: 300,
4604 }),
4605 dst_rect: None,
4606 dst_color: None,
4607 };
4608
4609 let (result, src, dst_g2d) = convert_img(
4610 &mut g2d_converter,
4611 src,
4612 dst_g2d,
4613 Rotation::None,
4614 Flip::Horizontal,
4615 crop,
4616 );
4617 result.unwrap();
4618
4619 let dst_cpu = TensorDyn::image(
4620 dst_width,
4621 dst_height,
4622 PixelFormat::Rgba,
4623 DType::U8,
4624 Some(TensorMemory::Dma),
4625 )
4626 .unwrap();
4627 let mut cpu_converter = CPUProcessor::new();
4628
4629 let (result, _src, dst_cpu) = convert_img(
4630 &mut cpu_converter,
4631 src,
4632 dst_cpu,
4633 Rotation::None,
4634 Flip::Horizontal,
4635 crop,
4636 );
4637 result.unwrap();
4638 compare_images(&dst_g2d, &dst_cpu, 0.98, function!());
4639 }
4640
4641 #[test]
4642 #[cfg(target_os = "linux")]
4643 #[cfg(feature = "opengl")]
4644 fn test_yuyv_to_rgba_crop_flip_opengl() {
4645 if !is_opengl_available() {
4646 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4647 return;
4648 }
4649
4650 if !is_dma_available() {
4651 eprintln!(
4652 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4653 function!()
4654 );
4655 return;
4656 }
4657
4658 let src = load_bytes_to_tensor(
4659 1280,
4660 720,
4661 PixelFormat::Yuyv,
4662 Some(TensorMemory::Dma),
4663 include_bytes!(concat!(
4664 env!("CARGO_MANIFEST_DIR"),
4665 "/../../testdata/camera720p.yuyv"
4666 )),
4667 )
4668 .unwrap();
4669
4670 let (dst_width, dst_height) = (640, 640);
4671
4672 let dst_gl = TensorDyn::image(
4673 dst_width,
4674 dst_height,
4675 PixelFormat::Rgba,
4676 DType::U8,
4677 Some(TensorMemory::Dma),
4678 )
4679 .unwrap();
4680 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4681 let crop = Crop {
4682 src_rect: Some(Rect {
4683 left: 20,
4684 top: 15,
4685 width: 400,
4686 height: 300,
4687 }),
4688 dst_rect: None,
4689 dst_color: None,
4690 };
4691
4692 let (result, src, dst_gl) = convert_img(
4693 &mut gl_converter,
4694 src,
4695 dst_gl,
4696 Rotation::None,
4697 Flip::Horizontal,
4698 crop,
4699 );
4700 result.unwrap();
4701
4702 let dst_cpu = TensorDyn::image(
4703 dst_width,
4704 dst_height,
4705 PixelFormat::Rgba,
4706 DType::U8,
4707 Some(TensorMemory::Dma),
4708 )
4709 .unwrap();
4710 let mut cpu_converter = CPUProcessor::new();
4711
4712 let (result, _src, dst_cpu) = convert_img(
4713 &mut cpu_converter,
4714 src,
4715 dst_cpu,
4716 Rotation::None,
4717 Flip::Horizontal,
4718 crop,
4719 );
4720 result.unwrap();
4721 compare_images(&dst_gl, &dst_cpu, 0.98, function!());
4722 }
4723
4724 #[test]
4725 fn test_vyuy_to_rgba_cpu() {
4726 let file = include_bytes!(concat!(
4727 env!("CARGO_MANIFEST_DIR"),
4728 "/../../testdata/camera720p.vyuy"
4729 ))
4730 .to_vec();
4731 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
4732 src.as_u8()
4733 .unwrap()
4734 .map()
4735 .unwrap()
4736 .as_mut_slice()
4737 .copy_from_slice(&file);
4738
4739 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4740 let mut cpu_converter = CPUProcessor::new();
4741
4742 let (result, _src, dst) = convert_img(
4743 &mut cpu_converter,
4744 src,
4745 dst,
4746 Rotation::None,
4747 Flip::None,
4748 Crop::no_crop(),
4749 );
4750 result.unwrap();
4751
4752 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4753 target_image
4754 .as_u8()
4755 .unwrap()
4756 .map()
4757 .unwrap()
4758 .as_mut_slice()
4759 .copy_from_slice(include_bytes!(concat!(
4760 env!("CARGO_MANIFEST_DIR"),
4761 "/../../testdata/camera720p.rgba"
4762 )));
4763
4764 compare_images(&dst, &target_image, 0.98, function!());
4765 }
4766
4767 #[test]
4768 fn test_vyuy_to_rgb_cpu() {
4769 let file = include_bytes!(concat!(
4770 env!("CARGO_MANIFEST_DIR"),
4771 "/../../testdata/camera720p.vyuy"
4772 ))
4773 .to_vec();
4774 let src = TensorDyn::image(1280, 720, PixelFormat::Vyuy, DType::U8, None).unwrap();
4775 src.as_u8()
4776 .unwrap()
4777 .map()
4778 .unwrap()
4779 .as_mut_slice()
4780 .copy_from_slice(&file);
4781
4782 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4783 let mut cpu_converter = CPUProcessor::new();
4784
4785 let (result, _src, dst) = convert_img(
4786 &mut cpu_converter,
4787 src,
4788 dst,
4789 Rotation::None,
4790 Flip::None,
4791 Crop::no_crop(),
4792 );
4793 result.unwrap();
4794
4795 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4796 target_image
4797 .as_u8()
4798 .unwrap()
4799 .map()
4800 .unwrap()
4801 .as_mut_slice()
4802 .as_chunks_mut::<3>()
4803 .0
4804 .iter_mut()
4805 .zip(
4806 include_bytes!(concat!(
4807 env!("CARGO_MANIFEST_DIR"),
4808 "/../../testdata/camera720p.rgba"
4809 ))
4810 .as_chunks::<4>()
4811 .0,
4812 )
4813 .for_each(|(dst, src)| *dst = [src[0], src[1], src[2]]);
4814
4815 compare_images(&dst, &target_image, 0.98, function!());
4816 }
4817
4818 #[test]
4819 #[cfg(target_os = "linux")]
4820 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
4821 fn test_vyuy_to_rgba_g2d() {
4822 if !is_g2d_available() {
4823 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D library (libg2d.so.2) not available");
4824 return;
4825 }
4826 if !is_dma_available() {
4827 eprintln!(
4828 "SKIPPED: test_vyuy_to_rgba_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4829 );
4830 return;
4831 }
4832
4833 let src = load_bytes_to_tensor(
4834 1280,
4835 720,
4836 PixelFormat::Vyuy,
4837 None,
4838 include_bytes!(concat!(
4839 env!("CARGO_MANIFEST_DIR"),
4840 "/../../testdata/camera720p.vyuy"
4841 )),
4842 )
4843 .unwrap();
4844
4845 let dst = TensorDyn::image(
4846 1280,
4847 720,
4848 PixelFormat::Rgba,
4849 DType::U8,
4850 Some(TensorMemory::Dma),
4851 )
4852 .unwrap();
4853 let mut g2d_converter = G2DProcessor::new().unwrap();
4854
4855 let (result, _src, dst) = convert_img(
4856 &mut g2d_converter,
4857 src,
4858 dst,
4859 Rotation::None,
4860 Flip::None,
4861 Crop::no_crop(),
4862 );
4863 match result {
4864 Err(Error::G2D(_)) => {
4865 eprintln!("SKIPPED: test_vyuy_to_rgba_g2d - G2D does not support PixelFormat::Vyuy format");
4866 return;
4867 }
4868 r => r.unwrap(),
4869 }
4870
4871 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
4872 target_image
4873 .as_u8()
4874 .unwrap()
4875 .map()
4876 .unwrap()
4877 .as_mut_slice()
4878 .copy_from_slice(include_bytes!(concat!(
4879 env!("CARGO_MANIFEST_DIR"),
4880 "/../../testdata/camera720p.rgba"
4881 )));
4882
4883 compare_images(&dst, &target_image, 0.98, function!());
4884 }
4885
4886 #[test]
4887 #[cfg(target_os = "linux")]
4888 #[ignore = "G2D does not support VYUY; re-enable when hardware support is added"]
4889 fn test_vyuy_to_rgb_g2d() {
4890 if !is_g2d_available() {
4891 eprintln!("SKIPPED: test_vyuy_to_rgb_g2d - G2D library (libg2d.so.2) not available");
4892 return;
4893 }
4894 if !is_dma_available() {
4895 eprintln!(
4896 "SKIPPED: test_vyuy_to_rgb_g2d - DMA memory allocation not available (permission denied or no DMA-BUF support)"
4897 );
4898 return;
4899 }
4900
4901 let src = load_bytes_to_tensor(
4902 1280,
4903 720,
4904 PixelFormat::Vyuy,
4905 None,
4906 include_bytes!(concat!(
4907 env!("CARGO_MANIFEST_DIR"),
4908 "/../../testdata/camera720p.vyuy"
4909 )),
4910 )
4911 .unwrap();
4912
4913 let g2d_dst = TensorDyn::image(
4914 1280,
4915 720,
4916 PixelFormat::Rgb,
4917 DType::U8,
4918 Some(TensorMemory::Dma),
4919 )
4920 .unwrap();
4921 let mut g2d_converter = G2DProcessor::new().unwrap();
4922
4923 let (result, src, g2d_dst) = convert_img(
4924 &mut g2d_converter,
4925 src,
4926 g2d_dst,
4927 Rotation::None,
4928 Flip::None,
4929 Crop::no_crop(),
4930 );
4931 match result {
4932 Err(Error::G2D(_)) => {
4933 eprintln!(
4934 "SKIPPED: test_vyuy_to_rgb_g2d - G2D does not support PixelFormat::Vyuy format"
4935 );
4936 return;
4937 }
4938 r => r.unwrap(),
4939 }
4940
4941 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
4942 let mut cpu_converter: CPUProcessor = CPUProcessor::new();
4943
4944 let (result, _src, cpu_dst) = convert_img(
4945 &mut cpu_converter,
4946 src,
4947 cpu_dst,
4948 Rotation::None,
4949 Flip::None,
4950 Crop::no_crop(),
4951 );
4952 result.unwrap();
4953
4954 compare_images(&g2d_dst, &cpu_dst, 0.98, function!());
4955 }
4956
4957 #[test]
4958 #[cfg(target_os = "linux")]
4959 #[cfg(feature = "opengl")]
4960 fn test_vyuy_to_rgba_opengl() {
4961 if !is_opengl_available() {
4962 eprintln!("SKIPPED: {} - OpenGL not available", function!());
4963 return;
4964 }
4965 if !is_dma_available() {
4966 eprintln!(
4967 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
4968 function!()
4969 );
4970 return;
4971 }
4972
4973 let src = load_bytes_to_tensor(
4974 1280,
4975 720,
4976 PixelFormat::Vyuy,
4977 Some(TensorMemory::Dma),
4978 include_bytes!(concat!(
4979 env!("CARGO_MANIFEST_DIR"),
4980 "/../../testdata/camera720p.vyuy"
4981 )),
4982 )
4983 .unwrap();
4984
4985 let dst = TensorDyn::image(
4986 1280,
4987 720,
4988 PixelFormat::Rgba,
4989 DType::U8,
4990 Some(TensorMemory::Dma),
4991 )
4992 .unwrap();
4993 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
4994
4995 let (result, _src, dst) = convert_img(
4996 &mut gl_converter,
4997 src,
4998 dst,
4999 Rotation::None,
5000 Flip::None,
5001 Crop::no_crop(),
5002 );
5003 match result {
5004 Err(Error::NotSupported(_)) => {
5005 eprintln!(
5006 "SKIPPED: {} - OpenGL does not support PixelFormat::Vyuy DMA format",
5007 function!()
5008 );
5009 return;
5010 }
5011 r => r.unwrap(),
5012 }
5013
5014 let target_image = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
5015 target_image
5016 .as_u8()
5017 .unwrap()
5018 .map()
5019 .unwrap()
5020 .as_mut_slice()
5021 .copy_from_slice(include_bytes!(concat!(
5022 env!("CARGO_MANIFEST_DIR"),
5023 "/../../testdata/camera720p.rgba"
5024 )));
5025
5026 compare_images(&dst, &target_image, 0.98, function!());
5027 }
5028
5029 #[test]
5030 fn test_nv12_to_rgba_cpu() {
5031 let file = include_bytes!(concat!(
5032 env!("CARGO_MANIFEST_DIR"),
5033 "/../../testdata/zidane.nv12"
5034 ))
5035 .to_vec();
5036 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
5037 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
5038 .copy_from_slice(&file);
5039
5040 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None).unwrap();
5041 let mut cpu_converter = CPUProcessor::new();
5042
5043 let (result, _src, dst) = convert_img(
5044 &mut cpu_converter,
5045 src,
5046 dst,
5047 Rotation::None,
5048 Flip::None,
5049 Crop::no_crop(),
5050 );
5051 result.unwrap();
5052
5053 let target_image = crate::load_image(
5054 include_bytes!(concat!(
5055 env!("CARGO_MANIFEST_DIR"),
5056 "/../../testdata/zidane.jpg"
5057 )),
5058 Some(PixelFormat::Rgba),
5059 None,
5060 )
5061 .unwrap();
5062
5063 compare_images(&dst, &target_image, 0.98, function!());
5064 }
5065
5066 #[test]
5067 fn test_nv12_to_rgb_cpu() {
5068 let file = include_bytes!(concat!(
5069 env!("CARGO_MANIFEST_DIR"),
5070 "/../../testdata/zidane.nv12"
5071 ))
5072 .to_vec();
5073 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
5074 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
5075 .copy_from_slice(&file);
5076
5077 let dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None).unwrap();
5078 let mut cpu_converter = CPUProcessor::new();
5079
5080 let (result, _src, dst) = convert_img(
5081 &mut cpu_converter,
5082 src,
5083 dst,
5084 Rotation::None,
5085 Flip::None,
5086 Crop::no_crop(),
5087 );
5088 result.unwrap();
5089
5090 let target_image = crate::load_image(
5091 include_bytes!(concat!(
5092 env!("CARGO_MANIFEST_DIR"),
5093 "/../../testdata/zidane.jpg"
5094 )),
5095 Some(PixelFormat::Rgb),
5096 None,
5097 )
5098 .unwrap();
5099
5100 compare_images(&dst, &target_image, 0.98, function!());
5101 }
5102
5103 #[test]
5104 fn test_nv12_to_grey_cpu() {
5105 let file = include_bytes!(concat!(
5106 env!("CARGO_MANIFEST_DIR"),
5107 "/../../testdata/zidane.nv12"
5108 ))
5109 .to_vec();
5110 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
5111 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
5112 .copy_from_slice(&file);
5113
5114 let dst = TensorDyn::image(1280, 720, PixelFormat::Grey, DType::U8, None).unwrap();
5115 let mut cpu_converter = CPUProcessor::new();
5116
5117 let (result, _src, dst) = convert_img(
5118 &mut cpu_converter,
5119 src,
5120 dst,
5121 Rotation::None,
5122 Flip::None,
5123 Crop::no_crop(),
5124 );
5125 result.unwrap();
5126
5127 let target_image = crate::load_image(
5128 include_bytes!(concat!(
5129 env!("CARGO_MANIFEST_DIR"),
5130 "/../../testdata/zidane.jpg"
5131 )),
5132 Some(PixelFormat::Grey),
5133 None,
5134 )
5135 .unwrap();
5136
5137 compare_images(&dst, &target_image, 0.98, function!());
5138 }
5139
5140 #[test]
5141 fn test_nv12_to_yuyv_cpu() {
5142 let file = include_bytes!(concat!(
5143 env!("CARGO_MANIFEST_DIR"),
5144 "/../../testdata/zidane.nv12"
5145 ))
5146 .to_vec();
5147 let src = TensorDyn::image(1280, 720, PixelFormat::Nv12, DType::U8, None).unwrap();
5148 src.as_u8().unwrap().map().unwrap().as_mut_slice()[0..(1280 * 720 * 3 / 2)]
5149 .copy_from_slice(&file);
5150
5151 let dst = TensorDyn::image(1280, 720, PixelFormat::Yuyv, DType::U8, None).unwrap();
5152 let mut cpu_converter = CPUProcessor::new();
5153
5154 let (result, _src, dst) = convert_img(
5155 &mut cpu_converter,
5156 src,
5157 dst,
5158 Rotation::None,
5159 Flip::None,
5160 Crop::no_crop(),
5161 );
5162 result.unwrap();
5163
5164 let target_image = crate::load_image(
5165 include_bytes!(concat!(
5166 env!("CARGO_MANIFEST_DIR"),
5167 "/../../testdata/zidane.jpg"
5168 )),
5169 Some(PixelFormat::Rgb),
5170 None,
5171 )
5172 .unwrap();
5173
5174 compare_images_convert_to_rgb(&dst, &target_image, 0.98, function!());
5175 }
5176
5177 #[test]
5178 fn test_cpu_resize_planar_rgb() {
5179 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
5180 #[rustfmt::skip]
5181 let src_image = [
5182 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
5183 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
5184 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
5185 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
5186 ];
5187 src.as_u8()
5188 .unwrap()
5189 .map()
5190 .unwrap()
5191 .as_mut_slice()
5192 .copy_from_slice(&src_image);
5193
5194 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
5195 let mut cpu_converter = CPUProcessor::new();
5196
5197 let (result, _src, cpu_dst) = convert_img(
5198 &mut cpu_converter,
5199 src,
5200 cpu_dst,
5201 Rotation::None,
5202 Flip::None,
5203 Crop::new()
5204 .with_dst_rect(Some(Rect {
5205 left: 1,
5206 top: 1,
5207 width: 4,
5208 height: 4,
5209 }))
5210 .with_dst_color(Some([114, 114, 114, 255])),
5211 );
5212 result.unwrap();
5213
5214 #[rustfmt::skip]
5215 let expected_dst = [
5216 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,
5217 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,
5218 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,
5219 ];
5220
5221 assert_eq!(
5222 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
5223 &expected_dst
5224 );
5225 }
5226
5227 #[test]
5228 fn test_cpu_resize_planar_rgba() {
5229 let src = TensorDyn::image(4, 4, PixelFormat::Rgba, DType::U8, None).unwrap();
5230 #[rustfmt::skip]
5231 let src_image = [
5232 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
5233 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
5234 0, 0, 255, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255,
5235 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 255, 0, 255, 255,
5236 ];
5237 src.as_u8()
5238 .unwrap()
5239 .map()
5240 .unwrap()
5241 .as_mut_slice()
5242 .copy_from_slice(&src_image);
5243
5244 let cpu_dst = TensorDyn::image(5, 5, PixelFormat::PlanarRgba, DType::U8, None).unwrap();
5245 let mut cpu_converter = CPUProcessor::new();
5246
5247 let (result, _src, cpu_dst) = convert_img(
5248 &mut cpu_converter,
5249 src,
5250 cpu_dst,
5251 Rotation::None,
5252 Flip::None,
5253 Crop::new()
5254 .with_dst_rect(Some(Rect {
5255 left: 1,
5256 top: 1,
5257 width: 4,
5258 height: 4,
5259 }))
5260 .with_dst_color(Some([114, 114, 114, 255])),
5261 );
5262 result.unwrap();
5263
5264 #[rustfmt::skip]
5265 let expected_dst = [
5266 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,
5267 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,
5268 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,
5269 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,
5270 ];
5271
5272 assert_eq!(
5273 cpu_dst.as_u8().unwrap().map().unwrap().as_slice(),
5274 &expected_dst
5275 );
5276 }
5277
5278 #[test]
5279 #[cfg(target_os = "linux")]
5280 #[cfg(feature = "opengl")]
5281 fn test_opengl_resize_planar_rgb() {
5282 if !is_opengl_available() {
5283 eprintln!("SKIPPED: {} - OpenGL not available", function!());
5284 return;
5285 }
5286
5287 if !is_dma_available() {
5288 eprintln!(
5289 "SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
5290 function!()
5291 );
5292 return;
5293 }
5294
5295 let dst_width = 640;
5296 let dst_height = 640;
5297 let file = include_bytes!(concat!(
5298 env!("CARGO_MANIFEST_DIR"),
5299 "/../../testdata/test_image.jpg"
5300 ))
5301 .to_vec();
5302 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
5303
5304 let cpu_dst = TensorDyn::image(
5305 dst_width,
5306 dst_height,
5307 PixelFormat::PlanarRgb,
5308 DType::U8,
5309 None,
5310 )
5311 .unwrap();
5312 let mut cpu_converter = CPUProcessor::new();
5313 let (result, src, cpu_dst) = convert_img(
5314 &mut cpu_converter,
5315 src,
5316 cpu_dst,
5317 Rotation::None,
5318 Flip::None,
5319 Crop::no_crop(),
5320 );
5321 result.unwrap();
5322 let crop_letterbox = Crop::new()
5323 .with_dst_rect(Some(Rect {
5324 left: 102,
5325 top: 102,
5326 width: 440,
5327 height: 440,
5328 }))
5329 .with_dst_color(Some([114, 114, 114, 114]));
5330 let (result, src, cpu_dst) = convert_img(
5331 &mut cpu_converter,
5332 src,
5333 cpu_dst,
5334 Rotation::None,
5335 Flip::None,
5336 crop_letterbox,
5337 );
5338 result.unwrap();
5339
5340 let gl_dst = TensorDyn::image(
5341 dst_width,
5342 dst_height,
5343 PixelFormat::PlanarRgb,
5344 DType::U8,
5345 None,
5346 )
5347 .unwrap();
5348 let mut gl_converter = GLProcessorThreaded::new(None).unwrap();
5349
5350 let (result, _src, gl_dst) = convert_img(
5351 &mut gl_converter,
5352 src,
5353 gl_dst,
5354 Rotation::None,
5355 Flip::None,
5356 crop_letterbox,
5357 );
5358 result.unwrap();
5359 compare_images(&gl_dst, &cpu_dst, 0.98, function!());
5360 }
5361
5362 #[test]
5363 fn test_cpu_resize_nv16() {
5364 let file = include_bytes!(concat!(
5365 env!("CARGO_MANIFEST_DIR"),
5366 "/../../testdata/zidane.jpg"
5367 ))
5368 .to_vec();
5369 let src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
5370
5371 let cpu_nv16_dst = TensorDyn::image(640, 640, PixelFormat::Nv16, DType::U8, None).unwrap();
5372 let cpu_rgb_dst = TensorDyn::image(640, 640, PixelFormat::Rgb, DType::U8, None).unwrap();
5373 let mut cpu_converter = CPUProcessor::new();
5374 let crop = Crop::new()
5375 .with_dst_rect(Some(Rect {
5376 left: 20,
5377 top: 140,
5378 width: 600,
5379 height: 360,
5380 }))
5381 .with_dst_color(Some([255, 128, 0, 255]));
5382
5383 let (result, src, cpu_nv16_dst) = convert_img(
5384 &mut cpu_converter,
5385 src,
5386 cpu_nv16_dst,
5387 Rotation::None,
5388 Flip::None,
5389 crop,
5390 );
5391 result.unwrap();
5392
5393 let (result, _src, cpu_rgb_dst) = convert_img(
5394 &mut cpu_converter,
5395 src,
5396 cpu_rgb_dst,
5397 Rotation::None,
5398 Flip::None,
5399 crop,
5400 );
5401 result.unwrap();
5402 compare_images_convert_to_rgb(&cpu_nv16_dst, &cpu_rgb_dst, 0.99, function!());
5403 }
5404
5405 fn load_bytes_to_tensor(
5406 width: usize,
5407 height: usize,
5408 format: PixelFormat,
5409 memory: Option<TensorMemory>,
5410 bytes: &[u8],
5411 ) -> Result<TensorDyn, Error> {
5412 let src = TensorDyn::image(width, height, format, DType::U8, memory)?;
5413 src.as_u8()
5414 .unwrap()
5415 .map()?
5416 .as_mut_slice()
5417 .copy_from_slice(bytes);
5418 Ok(src)
5419 }
5420
5421 fn compare_images(img1: &TensorDyn, img2: &TensorDyn, threshold: f64, name: &str) {
5422 assert_eq!(img1.height(), img2.height(), "Heights differ");
5423 assert_eq!(img1.width(), img2.width(), "Widths differ");
5424 assert_eq!(
5425 img1.format().unwrap(),
5426 img2.format().unwrap(),
5427 "PixelFormat differ"
5428 );
5429 assert!(
5430 matches!(
5431 img1.format().unwrap(),
5432 PixelFormat::Rgb | PixelFormat::Rgba | PixelFormat::Grey | PixelFormat::PlanarRgb
5433 ),
5434 "format must be Rgb or Rgba for comparison"
5435 );
5436
5437 let image1 = match img1.format().unwrap() {
5438 PixelFormat::Rgb => image::RgbImage::from_vec(
5439 img1.width().unwrap() as u32,
5440 img1.height().unwrap() as u32,
5441 img1.as_u8().unwrap().map().unwrap().to_vec(),
5442 )
5443 .unwrap(),
5444 PixelFormat::Rgba => image::RgbaImage::from_vec(
5445 img1.width().unwrap() as u32,
5446 img1.height().unwrap() as u32,
5447 img1.as_u8().unwrap().map().unwrap().to_vec(),
5448 )
5449 .unwrap()
5450 .convert(),
5451 PixelFormat::Grey => image::GrayImage::from_vec(
5452 img1.width().unwrap() as u32,
5453 img1.height().unwrap() as u32,
5454 img1.as_u8().unwrap().map().unwrap().to_vec(),
5455 )
5456 .unwrap()
5457 .convert(),
5458 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
5459 img1.width().unwrap() as u32,
5460 (img1.height().unwrap() * 3) as u32,
5461 img1.as_u8().unwrap().map().unwrap().to_vec(),
5462 )
5463 .unwrap()
5464 .convert(),
5465 _ => return,
5466 };
5467
5468 let image2 = match img2.format().unwrap() {
5469 PixelFormat::Rgb => image::RgbImage::from_vec(
5470 img2.width().unwrap() as u32,
5471 img2.height().unwrap() as u32,
5472 img2.as_u8().unwrap().map().unwrap().to_vec(),
5473 )
5474 .unwrap(),
5475 PixelFormat::Rgba => image::RgbaImage::from_vec(
5476 img2.width().unwrap() as u32,
5477 img2.height().unwrap() as u32,
5478 img2.as_u8().unwrap().map().unwrap().to_vec(),
5479 )
5480 .unwrap()
5481 .convert(),
5482 PixelFormat::Grey => image::GrayImage::from_vec(
5483 img2.width().unwrap() as u32,
5484 img2.height().unwrap() as u32,
5485 img2.as_u8().unwrap().map().unwrap().to_vec(),
5486 )
5487 .unwrap()
5488 .convert(),
5489 PixelFormat::PlanarRgb => image::GrayImage::from_vec(
5490 img2.width().unwrap() as u32,
5491 (img2.height().unwrap() * 3) as u32,
5492 img2.as_u8().unwrap().map().unwrap().to_vec(),
5493 )
5494 .unwrap()
5495 .convert(),
5496 _ => return,
5497 };
5498
5499 let similarity = image_compare::rgb_similarity_structure(
5500 &image_compare::Algorithm::RootMeanSquared,
5501 &image1,
5502 &image2,
5503 )
5504 .expect("Image Comparison failed");
5505 if similarity.score < threshold {
5506 similarity
5509 .image
5510 .to_color_map()
5511 .save(format!("{name}.png"))
5512 .unwrap();
5513 panic!(
5514 "{name}: converted image and target image have similarity score too low: {} < {}",
5515 similarity.score, threshold
5516 )
5517 }
5518 }
5519
5520 fn compare_images_convert_to_rgb(
5521 img1: &TensorDyn,
5522 img2: &TensorDyn,
5523 threshold: f64,
5524 name: &str,
5525 ) {
5526 assert_eq!(img1.height(), img2.height(), "Heights differ");
5527 assert_eq!(img1.width(), img2.width(), "Widths differ");
5528
5529 let mut img_rgb1 = TensorDyn::image(
5530 img1.width().unwrap(),
5531 img1.height().unwrap(),
5532 PixelFormat::Rgb,
5533 DType::U8,
5534 Some(TensorMemory::Mem),
5535 )
5536 .unwrap();
5537 let mut img_rgb2 = TensorDyn::image(
5538 img1.width().unwrap(),
5539 img1.height().unwrap(),
5540 PixelFormat::Rgb,
5541 DType::U8,
5542 Some(TensorMemory::Mem),
5543 )
5544 .unwrap();
5545 let mut __cv = CPUProcessor::default();
5546 let r1 = __cv.convert(
5547 img1,
5548 &mut img_rgb1,
5549 crate::Rotation::None,
5550 crate::Flip::None,
5551 crate::Crop::default(),
5552 );
5553 let r2 = __cv.convert(
5554 img2,
5555 &mut img_rgb2,
5556 crate::Rotation::None,
5557 crate::Flip::None,
5558 crate::Crop::default(),
5559 );
5560 if r1.is_err() || r2.is_err() {
5561 let w = img1.width().unwrap() as u32;
5563 let data1 = img1.as_u8().unwrap().map().unwrap().to_vec();
5564 let data2 = img2.as_u8().unwrap().map().unwrap().to_vec();
5565 let h1 = (data1.len() as u32) / w;
5566 let h2 = (data2.len() as u32) / w;
5567 let g1 = image::GrayImage::from_vec(w, h1, data1).unwrap();
5568 let g2 = image::GrayImage::from_vec(w, h2, data2).unwrap();
5569 let similarity = image_compare::gray_similarity_structure(
5570 &image_compare::Algorithm::RootMeanSquared,
5571 &g1,
5572 &g2,
5573 )
5574 .expect("Image Comparison failed");
5575 if similarity.score < threshold {
5576 panic!(
5577 "{name}: converted image and target image have similarity score too low: {} < {}",
5578 similarity.score, threshold
5579 )
5580 }
5581 return;
5582 }
5583
5584 let image1 = image::RgbImage::from_vec(
5585 img_rgb1.width().unwrap() as u32,
5586 img_rgb1.height().unwrap() as u32,
5587 img_rgb1.as_u8().unwrap().map().unwrap().to_vec(),
5588 )
5589 .unwrap();
5590
5591 let image2 = image::RgbImage::from_vec(
5592 img_rgb2.width().unwrap() as u32,
5593 img_rgb2.height().unwrap() as u32,
5594 img_rgb2.as_u8().unwrap().map().unwrap().to_vec(),
5595 )
5596 .unwrap();
5597
5598 let similarity = image_compare::rgb_similarity_structure(
5599 &image_compare::Algorithm::RootMeanSquared,
5600 &image1,
5601 &image2,
5602 )
5603 .expect("Image Comparison failed");
5604 if similarity.score < threshold {
5605 similarity
5608 .image
5609 .to_color_map()
5610 .save(format!("{name}.png"))
5611 .unwrap();
5612 panic!(
5613 "{name}: converted image and target image have similarity score too low: {} < {}",
5614 similarity.score, threshold
5615 )
5616 }
5617 }
5618
5619 #[test]
5624 fn test_nv12_image_creation() {
5625 let width = 640;
5626 let height = 480;
5627 let img = TensorDyn::image(width, height, PixelFormat::Nv12, DType::U8, None).unwrap();
5628
5629 assert_eq!(img.width(), Some(width));
5630 assert_eq!(img.height(), Some(height));
5631 assert_eq!(img.format().unwrap(), PixelFormat::Nv12);
5632 assert_eq!(img.as_u8().unwrap().shape(), &[height * 3 / 2, width]);
5634 }
5635
5636 #[test]
5637 fn test_nv12_channels() {
5638 let img = TensorDyn::image(640, 480, PixelFormat::Nv12, DType::U8, None).unwrap();
5639 assert_eq!(img.format().unwrap().channels(), 1);
5641 }
5642
5643 #[test]
5648 fn test_tensor_set_format_planar() {
5649 let mut tensor = Tensor::<u8>::new(&[3, 480, 640], None, None).unwrap();
5650 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
5651 assert_eq!(tensor.format(), Some(PixelFormat::PlanarRgb));
5652 assert_eq!(tensor.width(), Some(640));
5653 assert_eq!(tensor.height(), Some(480));
5654 }
5655
5656 #[test]
5657 fn test_tensor_set_format_interleaved() {
5658 let mut tensor = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
5659 tensor.set_format(PixelFormat::Rgba).unwrap();
5660 assert_eq!(tensor.format(), Some(PixelFormat::Rgba));
5661 assert_eq!(tensor.width(), Some(640));
5662 assert_eq!(tensor.height(), Some(480));
5663 }
5664
5665 #[test]
5666 fn test_tensordyn_image_rgb() {
5667 let img = TensorDyn::image(640, 480, PixelFormat::Rgb, DType::U8, None).unwrap();
5668 assert_eq!(img.width(), Some(640));
5669 assert_eq!(img.height(), Some(480));
5670 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5671 }
5672
5673 #[test]
5674 fn test_tensordyn_image_planar_rgb() {
5675 let img = TensorDyn::image(640, 480, PixelFormat::PlanarRgb, DType::U8, None).unwrap();
5676 assert_eq!(img.width(), Some(640));
5677 assert_eq!(img.height(), Some(480));
5678 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5679 }
5680
5681 #[test]
5682 fn test_rgb_int8_format() {
5683 let img = TensorDyn::image(
5685 1280,
5686 720,
5687 PixelFormat::Rgb,
5688 DType::I8,
5689 Some(TensorMemory::Mem),
5690 )
5691 .unwrap();
5692 assert_eq!(img.width(), Some(1280));
5693 assert_eq!(img.height(), Some(720));
5694 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5695 assert_eq!(img.dtype(), DType::I8);
5696 }
5697
5698 #[test]
5699 fn test_planar_rgb_int8_format() {
5700 let img = TensorDyn::image(
5701 1280,
5702 720,
5703 PixelFormat::PlanarRgb,
5704 DType::I8,
5705 Some(TensorMemory::Mem),
5706 )
5707 .unwrap();
5708 assert_eq!(img.width(), Some(1280));
5709 assert_eq!(img.height(), Some(720));
5710 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5711 assert_eq!(img.dtype(), DType::I8);
5712 }
5713
5714 #[test]
5715 fn test_rgb_from_tensor() {
5716 let mut tensor = Tensor::<u8>::new(&[720, 1280, 3], None, None).unwrap();
5717 tensor.set_format(PixelFormat::Rgb).unwrap();
5718 let img = TensorDyn::from(tensor);
5719 assert_eq!(img.width(), Some(1280));
5720 assert_eq!(img.height(), Some(720));
5721 assert_eq!(img.format(), Some(PixelFormat::Rgb));
5722 }
5723
5724 #[test]
5725 fn test_planar_rgb_from_tensor() {
5726 let mut tensor = Tensor::<u8>::new(&[3, 720, 1280], None, None).unwrap();
5727 tensor.set_format(PixelFormat::PlanarRgb).unwrap();
5728 let img = TensorDyn::from(tensor);
5729 assert_eq!(img.width(), Some(1280));
5730 assert_eq!(img.height(), Some(720));
5731 assert_eq!(img.format(), Some(PixelFormat::PlanarRgb));
5732 }
5733
5734 #[test]
5735 fn test_dtype_determines_int8() {
5736 let u8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::U8, None).unwrap();
5738 let i8_img = TensorDyn::image(64, 64, PixelFormat::Rgb, DType::I8, None).unwrap();
5739 assert_eq!(u8_img.dtype(), DType::U8);
5740 assert_eq!(i8_img.dtype(), DType::I8);
5741 }
5742
5743 #[test]
5744 fn test_pixel_layout_packed_vs_planar() {
5745 assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
5747 assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
5748 assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
5749 assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
5750 }
5751
5752 #[cfg(target_os = "linux")]
5757 #[cfg(feature = "opengl")]
5758 #[test]
5759 fn test_convert_pbo_to_pbo() {
5760 let mut converter = ImageProcessor::new().unwrap();
5761
5762 let is_pbo = converter
5764 .opengl
5765 .as_ref()
5766 .is_some_and(|gl| gl.transfer_backend() == opengl_headless::TransferBackend::Pbo);
5767 if !is_pbo {
5768 eprintln!("Skipping test_convert_pbo_to_pbo: backend is not PBO");
5769 return;
5770 }
5771
5772 let src_w = 640;
5773 let src_h = 480;
5774 let dst_w = 320;
5775 let dst_h = 240;
5776
5777 let pbo_src = converter
5779 .create_image(src_w, src_h, PixelFormat::Rgba, DType::U8, None)
5780 .unwrap();
5781 assert_eq!(
5782 pbo_src.as_u8().unwrap().memory(),
5783 TensorMemory::Pbo,
5784 "create_image should produce a PBO tensor"
5785 );
5786
5787 let file = include_bytes!(concat!(
5789 env!("CARGO_MANIFEST_DIR"),
5790 "/../../testdata/zidane.jpg"
5791 ))
5792 .to_vec();
5793 let jpeg_src = crate::load_image(&file, Some(PixelFormat::Rgba), None).unwrap();
5794
5795 let mem_src = TensorDyn::image(
5797 src_w,
5798 src_h,
5799 PixelFormat::Rgba,
5800 DType::U8,
5801 Some(TensorMemory::Mem),
5802 )
5803 .unwrap();
5804 let (result, _jpeg_src, mem_src) = convert_img(
5805 &mut CPUProcessor::new(),
5806 jpeg_src,
5807 mem_src,
5808 Rotation::None,
5809 Flip::None,
5810 Crop::no_crop(),
5811 );
5812 result.unwrap();
5813
5814 {
5816 let src_data = mem_src.as_u8().unwrap().map().unwrap();
5817 let mut pbo_map = pbo_src.as_u8().unwrap().map().unwrap();
5818 pbo_map.copy_from_slice(&src_data);
5819 }
5820
5821 let pbo_dst = converter
5823 .create_image(dst_w, dst_h, PixelFormat::Rgba, DType::U8, None)
5824 .unwrap();
5825 assert_eq!(pbo_dst.as_u8().unwrap().memory(), TensorMemory::Pbo);
5826
5827 let mut pbo_dst = pbo_dst;
5829 let result = converter.convert(
5830 &pbo_src,
5831 &mut pbo_dst,
5832 Rotation::None,
5833 Flip::None,
5834 Crop::no_crop(),
5835 );
5836 result.unwrap();
5837
5838 let cpu_dst = TensorDyn::image(
5840 dst_w,
5841 dst_h,
5842 PixelFormat::Rgba,
5843 DType::U8,
5844 Some(TensorMemory::Mem),
5845 )
5846 .unwrap();
5847 let (result, _mem_src, cpu_dst) = convert_img(
5848 &mut CPUProcessor::new(),
5849 mem_src,
5850 cpu_dst,
5851 Rotation::None,
5852 Flip::None,
5853 Crop::no_crop(),
5854 );
5855 result.unwrap();
5856
5857 let pbo_dst_img = {
5858 let mut __t = pbo_dst.into_u8().unwrap();
5859 __t.set_format(PixelFormat::Rgba).unwrap();
5860 TensorDyn::from(__t)
5861 };
5862 compare_images(&pbo_dst_img, &cpu_dst, 0.95, function!());
5863 log::info!("test_convert_pbo_to_pbo: PASS — PBO-to-PBO convert matches CPU reference");
5864 }
5865
5866 #[test]
5867 fn test_image_bgra() {
5868 let img = TensorDyn::image(
5869 640,
5870 480,
5871 PixelFormat::Bgra,
5872 DType::U8,
5873 Some(edgefirst_tensor::TensorMemory::Mem),
5874 )
5875 .unwrap();
5876 assert_eq!(img.width(), Some(640));
5877 assert_eq!(img.height(), Some(480));
5878 assert_eq!(img.format().unwrap().channels(), 4);
5879 assert_eq!(img.format().unwrap(), PixelFormat::Bgra);
5880 }
5881
5882 #[test]
5887 fn test_force_backend_cpu() {
5888 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5889 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5890 let result = ImageProcessor::new();
5891 match original {
5892 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5893 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5894 }
5895 let converter = result.unwrap();
5896 assert!(converter.cpu.is_some());
5897 assert_eq!(converter.forced_backend, Some(ForcedBackend::Cpu));
5898 }
5899
5900 #[test]
5901 fn test_force_backend_invalid() {
5902 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5903 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "invalid") };
5904 let result = ImageProcessor::new();
5905 match original {
5906 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5907 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5908 }
5909 assert!(
5910 matches!(&result, Err(Error::ForcedBackendUnavailable(s)) if s.contains("unknown")),
5911 "invalid backend value should return ForcedBackendUnavailable error: {result:?}"
5912 );
5913 }
5914
5915 #[test]
5916 fn test_force_backend_unset() {
5917 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5918 unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") };
5919 let result = ImageProcessor::new();
5920 match original {
5921 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5922 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
5923 }
5924 let converter = result.unwrap();
5925 assert!(converter.forced_backend.is_none());
5926 }
5927
5928 #[test]
5933 fn test_draw_proto_masks_no_cpu_returns_error() {
5934 let original_cpu = std::env::var("EDGEFIRST_DISABLE_CPU").ok();
5936 unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", "1") };
5937 let original_gl = std::env::var("EDGEFIRST_DISABLE_GL").ok();
5938 unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", "1") };
5939 let original_g2d = std::env::var("EDGEFIRST_DISABLE_G2D").ok();
5940 unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", "1") };
5941
5942 let result = ImageProcessor::new();
5943
5944 match original_cpu {
5945 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_CPU", s) },
5946 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_CPU") },
5947 }
5948 match original_gl {
5949 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_GL", s) },
5950 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_GL") },
5951 }
5952 match original_g2d {
5953 Some(s) => unsafe { std::env::set_var("EDGEFIRST_DISABLE_G2D", s) },
5954 None => unsafe { std::env::remove_var("EDGEFIRST_DISABLE_G2D") },
5955 }
5956
5957 let mut converter = result.unwrap();
5958 assert!(converter.cpu.is_none(), "CPU should be disabled");
5959
5960 let dst = TensorDyn::image(
5961 640,
5962 480,
5963 PixelFormat::Rgba,
5964 DType::U8,
5965 Some(TensorMemory::Mem),
5966 )
5967 .unwrap();
5968 let mut dst_dyn = dst;
5969 let det = [DetectBox {
5970 bbox: edgefirst_decoder::BoundingBox {
5971 xmin: 0.1,
5972 ymin: 0.1,
5973 xmax: 0.5,
5974 ymax: 0.5,
5975 },
5976 score: 0.9,
5977 label: 0,
5978 }];
5979 let proto_data = ProtoData {
5980 mask_coefficients: vec![vec![0.5; 4]],
5981 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
5982 };
5983 let result =
5984 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
5985 assert!(
5986 matches!(&result, Err(Error::Internal(s)) if s.contains("CPU backend")),
5987 "draw_proto_masks without CPU should return Internal error: {result:?}"
5988 );
5989 }
5990
5991 #[test]
5992 fn test_draw_proto_masks_cpu_fallback_works() {
5993 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
5995 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
5996 let result = ImageProcessor::new();
5997 match original {
5998 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
5999 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
6000 }
6001
6002 let mut converter = result.unwrap();
6003 assert!(converter.cpu.is_some());
6004
6005 let dst = TensorDyn::image(
6006 64,
6007 64,
6008 PixelFormat::Rgba,
6009 DType::U8,
6010 Some(TensorMemory::Mem),
6011 )
6012 .unwrap();
6013 let mut dst_dyn = dst;
6014 let det = [DetectBox {
6015 bbox: edgefirst_decoder::BoundingBox {
6016 xmin: 0.1,
6017 ymin: 0.1,
6018 xmax: 0.5,
6019 ymax: 0.5,
6020 },
6021 score: 0.9,
6022 label: 0,
6023 }];
6024 let proto_data = ProtoData {
6025 mask_coefficients: vec![vec![0.5; 4]],
6026 protos: edgefirst_decoder::ProtoTensor::Float(ndarray::Array3::<f32>::zeros((8, 8, 4))),
6027 };
6028 let result =
6029 converter.draw_proto_masks(&mut dst_dyn, &det, &proto_data, Default::default());
6030 assert!(result.is_ok(), "CPU fallback path should work: {result:?}");
6031 }
6032
6033 #[test]
6034 fn test_set_format_then_cpu_convert() {
6035 let original = std::env::var("EDGEFIRST_FORCE_BACKEND").ok();
6037 unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", "cpu") };
6038 let mut processor = ImageProcessor::new().unwrap();
6039 match original {
6040 Some(s) => unsafe { std::env::set_var("EDGEFIRST_FORCE_BACKEND", s) },
6041 None => unsafe { std::env::remove_var("EDGEFIRST_FORCE_BACKEND") },
6042 }
6043
6044 let image = include_bytes!(concat!(
6046 env!("CARGO_MANIFEST_DIR"),
6047 "/../../testdata/zidane.jpg"
6048 ));
6049 let src = load_image(image, Some(PixelFormat::Rgba), None).unwrap();
6050
6051 let mut dst =
6053 TensorDyn::new(&[640, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
6054 dst.set_format(PixelFormat::Rgb).unwrap();
6055
6056 processor
6058 .convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6059 .unwrap();
6060
6061 assert_eq!(dst.format(), Some(PixelFormat::Rgb));
6063 assert_eq!(dst.width(), Some(640));
6064 assert_eq!(dst.height(), Some(640));
6065 }
6066
6067 #[test]
6073 fn test_multiple_image_processors_same_thread() {
6074 let mut processors: Vec<ImageProcessor> = (0..4)
6075 .map(|_| ImageProcessor::new().expect("ImageProcessor::new() failed"))
6076 .collect();
6077
6078 for proc in &mut processors {
6079 let src = proc
6080 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
6081 .expect("create src failed");
6082 let mut dst = proc
6083 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
6084 .expect("create dst failed");
6085 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6086 .expect("convert failed");
6087 assert_eq!(dst.width(), Some(64));
6088 assert_eq!(dst.height(), Some(64));
6089 }
6090 }
6091
6092 #[test]
6099 fn test_multiple_image_processors_separate_threads() {
6100 use std::sync::mpsc;
6101 use std::time::Duration;
6102
6103 const TIMEOUT: Duration = Duration::from_secs(60);
6104
6105 let (tx, rx) = mpsc::channel::<()>();
6106
6107 std::thread::spawn(move || {
6108 let handles: Vec<_> = (0..4)
6109 .map(|i| {
6110 std::thread::spawn(move || {
6111 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
6112 panic!("ImageProcessor::new() failed on thread {i}: {e}")
6113 });
6114 let src = proc
6115 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
6116 .unwrap_or_else(|e| panic!("create src failed on thread {i}: {e}"));
6117 let mut dst = proc
6118 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
6119 .unwrap_or_else(|e| panic!("create dst failed on thread {i}: {e}"));
6120 proc.convert(&src, &mut dst, Rotation::None, Flip::None, Crop::default())
6121 .unwrap_or_else(|e| panic!("convert failed on thread {i}: {e}"));
6122 assert_eq!(dst.width(), Some(64));
6123 assert_eq!(dst.height(), Some(64));
6124 })
6125 })
6126 .collect();
6127
6128 for (i, h) in handles.into_iter().enumerate() {
6129 h.join()
6130 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
6131 }
6132
6133 let _ = tx.send(());
6134 });
6135
6136 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
6137 panic!("test_multiple_image_processors_separate_threads timed out after {TIMEOUT:?}")
6138 });
6139 }
6140
6141 #[test]
6148 fn test_image_processors_concurrent_operations() {
6149 use std::sync::{mpsc, Arc, Barrier};
6150 use std::time::Duration;
6151
6152 const N: usize = 4;
6153 const ROUNDS: usize = 10;
6154 const TIMEOUT: Duration = Duration::from_secs(60);
6155
6156 let (tx, rx) = mpsc::channel::<()>();
6157
6158 std::thread::spawn(move || {
6159 let barrier = Arc::new(Barrier::new(N));
6160
6161 let handles: Vec<_> = (0..N)
6162 .map(|i| {
6163 let barrier = Arc::clone(&barrier);
6164 std::thread::spawn(move || {
6165 let mut proc = ImageProcessor::new().unwrap_or_else(|e| {
6166 panic!("ImageProcessor::new() failed on thread {i}: {e}")
6167 });
6168
6169 barrier.wait();
6171
6172 for round in 0..ROUNDS {
6174 let src = proc
6175 .create_image(128, 128, PixelFormat::Rgb, DType::U8, None)
6176 .unwrap_or_else(|e| {
6177 panic!("create src failed on thread {i} round {round}: {e}")
6178 });
6179 let mut dst = proc
6180 .create_image(64, 64, PixelFormat::Rgb, DType::U8, None)
6181 .unwrap_or_else(|e| {
6182 panic!("create dst failed on thread {i} round {round}: {e}")
6183 });
6184 proc.convert(
6185 &src,
6186 &mut dst,
6187 Rotation::None,
6188 Flip::None,
6189 Crop::default(),
6190 )
6191 .unwrap_or_else(|e| {
6192 panic!("convert failed on thread {i} round {round}: {e}")
6193 });
6194 assert_eq!(dst.width(), Some(64));
6195 assert_eq!(dst.height(), Some(64));
6196 }
6197 })
6198 })
6199 .collect();
6200
6201 for (i, h) in handles.into_iter().enumerate() {
6202 h.join()
6203 .unwrap_or_else(|e| panic!("thread {i} panicked: {e:?}"));
6204 }
6205
6206 let _ = tx.send(());
6207 });
6208
6209 rx.recv_timeout(TIMEOUT).unwrap_or_else(|_| {
6210 panic!("test_image_processors_concurrent_operations timed out after {TIMEOUT:?}")
6211 });
6212 }
6213}