1#![cfg(target_os = "linux")]
5
6use crate::{CPUProcessor, Crop, Error, Flip, ImageProcessorTrait, Result, Rotation};
7use edgefirst_tensor::{DType, PixelFormat, Tensor, TensorDyn, TensorMapTrait, TensorTrait};
8use four_char_code::FourCharCode;
9use g2d_sys::{G2DFormat, G2DPhysical, G2DSurface, G2D};
10use std::{os::fd::AsRawFd, time::Instant};
11
12fn pixelfmt_to_fourcc(fmt: PixelFormat) -> FourCharCode {
14 use four_char_code::four_char_code;
15 match fmt {
16 PixelFormat::Rgb => four_char_code!("RGB "),
17 PixelFormat::Rgba => four_char_code!("RGBA"),
18 PixelFormat::Bgra => four_char_code!("BGRA"),
19 PixelFormat::Grey => four_char_code!("Y800"),
20 PixelFormat::Yuyv => four_char_code!("YUYV"),
21 PixelFormat::Vyuy => four_char_code!("VYUY"),
22 PixelFormat::Nv12 => four_char_code!("NV12"),
23 PixelFormat::Nv16 => four_char_code!("NV16"),
24 _ => four_char_code!("RGBA"),
26 }
27}
28
29#[derive(Debug)]
32pub struct G2DProcessor {
33 g2d: G2D,
34}
35
36unsafe impl Send for G2DProcessor {}
37unsafe impl Sync for G2DProcessor {}
38
39impl G2DProcessor {
40 pub fn new() -> Result<Self> {
42 let mut g2d = G2D::new("libg2d.so.2")?;
43 g2d.set_bt709_colorspace()?;
44
45 log::debug!("G2DConverter created with version {:?}", g2d.version());
46 Ok(Self { g2d })
47 }
48
49 pub fn version(&self) -> g2d_sys::Version {
52 self.g2d.version()
53 }
54
55 fn convert_impl(
56 &mut self,
57 src_dyn: &TensorDyn,
58 dst_dyn: &mut TensorDyn,
59 rotation: Rotation,
60 flip: Flip,
61 crop: Crop,
62 ) -> Result<()> {
63 if log::log_enabled!(log::Level::Trace) {
64 log::trace!(
65 "G2D convert: {:?}({:?}/{:?}) → {:?}({:?}/{:?})",
66 src_dyn.format(),
67 src_dyn.dtype(),
68 src_dyn.memory(),
69 dst_dyn.format(),
70 dst_dyn.dtype(),
71 dst_dyn.memory(),
72 );
73 }
74
75 if src_dyn.dtype() != DType::U8 {
76 return Err(Error::NotSupported(
77 "G2D only supports u8 source tensors".to_string(),
78 ));
79 }
80 let is_int8_dst = dst_dyn.dtype() == DType::I8;
81 if dst_dyn.dtype() != DType::U8 && !is_int8_dst {
82 return Err(Error::NotSupported(
83 "G2D only supports u8 or i8 destination tensors".to_string(),
84 ));
85 }
86
87 let src_fmt = src_dyn.format().ok_or(Error::NotAnImage)?;
88 let dst_fmt = dst_dyn.format().ok_or(Error::NotAnImage)?;
89
90 use PixelFormat::*;
92 match (src_fmt, dst_fmt) {
93 (Rgba, Rgba) => {}
94 (Rgba, Yuyv) => {}
95 (Rgba, Rgb) => {}
96 (Yuyv, Rgba) => {}
97 (Yuyv, Yuyv) => {}
98 (Yuyv, Rgb) => {}
99 (Nv12, Rgba) => {}
102 (Nv12, Yuyv) => {}
103 (Nv12, Rgb) => {}
104 (Rgba, Bgra) => {}
105 (Yuyv, Bgra) => {}
106 (Nv12, Bgra) => {}
107 (Bgra, Bgra) => {}
108 (s, d) => {
109 return Err(Error::NotSupported(format!(
110 "G2D does not support {} to {} conversion",
111 s, d
112 )));
113 }
114 }
115
116 crop.check_crop_dyn(src_dyn, dst_dyn)?;
117
118 let src = src_dyn.as_u8().unwrap();
119 let dst = if is_int8_dst {
122 let i8_tensor = dst_dyn.as_i8_mut().unwrap();
130 unsafe { &mut *(i8_tensor as *mut Tensor<i8> as *mut Tensor<u8>) }
131 } else {
132 dst_dyn.as_u8_mut().unwrap()
133 };
134
135 let mut src_surface = tensor_to_g2d_surface(src)?;
136 let mut dst_surface = tensor_to_g2d_surface(dst)?;
137
138 src_surface.rot = match flip {
139 Flip::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
140 Flip::Vertical => g2d_sys::g2d_rotation_G2D_FLIP_V,
141 Flip::Horizontal => g2d_sys::g2d_rotation_G2D_FLIP_H,
142 };
143
144 dst_surface.rot = match rotation {
145 Rotation::None => g2d_sys::g2d_rotation_G2D_ROTATION_0,
146 Rotation::Clockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_90,
147 Rotation::Rotate180 => g2d_sys::g2d_rotation_G2D_ROTATION_180,
148 Rotation::CounterClockwise90 => g2d_sys::g2d_rotation_G2D_ROTATION_270,
149 };
150
151 if let Some(crop_rect) = crop.src_rect {
152 src_surface.left = crop_rect.left as i32;
153 src_surface.top = crop_rect.top as i32;
154 src_surface.right = (crop_rect.left + crop_rect.width) as i32;
155 src_surface.bottom = (crop_rect.top + crop_rect.height) as i32;
156 }
157
158 let dst_w = dst.width().unwrap();
159 let dst_h = dst.height().unwrap();
160
161 let needs_clear = crop.dst_color.is_some()
167 && crop.dst_rect.is_some_and(|dst_rect| {
168 dst_rect.left != 0
169 || dst_rect.top != 0
170 || dst_rect.width != dst_w
171 || dst_rect.height != dst_h
172 });
173
174 if needs_clear && dst_fmt != Rgb {
175 if let Some(dst_color) = crop.dst_color {
176 let start = Instant::now();
177 self.g2d.clear(&mut dst_surface, dst_color)?;
178 log::trace!("g2d clear takes {:?}", start.elapsed());
179 }
180 }
181
182 if let Some(crop_rect) = crop.dst_rect {
183 dst_surface.planes[0] += ((crop_rect.top * dst_surface.stride as usize
187 + crop_rect.left)
188 * dst_fmt.channels()) as u64;
189
190 dst_surface.right = crop_rect.width as i32;
191 dst_surface.bottom = crop_rect.height as i32;
192 dst_surface.width = crop_rect.width as i32;
193 dst_surface.height = crop_rect.height as i32;
194 }
195
196 log::trace!("G2D blit: {src_fmt}→{dst_fmt} int8={is_int8_dst}");
197 self.g2d.blit(&src_surface, &dst_surface)?;
198 self.g2d.finish()?;
199 log::trace!("G2D blit complete");
200
201 if needs_clear && dst_fmt == Rgb {
203 if let (Some(dst_color), Some(dst_rect)) = (crop.dst_color, crop.dst_rect) {
204 let start = Instant::now();
205 CPUProcessor::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
206 log::trace!("cpu fill takes {:?}", start.elapsed());
207 }
208 }
209
210 if is_int8_dst {
215 let start = Instant::now();
216 let mut map = dst.map()?;
217 crate::cpu::apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
218 log::trace!("g2d int8 XOR 0x80 post-pass takes {:?}", start.elapsed());
219 }
220
221 Ok(())
222 }
223}
224
225impl ImageProcessorTrait for G2DProcessor {
226 fn convert(
227 &mut self,
228 src: &TensorDyn,
229 dst: &mut TensorDyn,
230 rotation: Rotation,
231 flip: Flip,
232 crop: Crop,
233 ) -> Result<()> {
234 self.convert_impl(src, dst, rotation, flip, crop)
235 }
236
237 fn draw_masks(
238 &mut self,
239 _dst: &mut TensorDyn,
240 _detect: &[crate::DetectBox],
241 _segmentation: &[crate::Segmentation],
242 ) -> Result<()> {
243 Err(Error::NotImplemented(
244 "G2D does not support drawing detection or segmentation overlays".to_string(),
245 ))
246 }
247
248 fn draw_masks_proto(
249 &mut self,
250 _dst: &mut TensorDyn,
251 _detect: &[crate::DetectBox],
252 _proto_data: &crate::ProtoData,
253 ) -> Result<()> {
254 Err(Error::NotImplemented(
255 "G2D does not support drawing detection or segmentation overlays".to_string(),
256 ))
257 }
258
259 fn decode_masks_atlas(
260 &mut self,
261 _detect: &[crate::DetectBox],
262 _proto_data: crate::ProtoData,
263 _output_width: usize,
264 _output_height: usize,
265 ) -> Result<(Vec<u8>, Vec<crate::MaskRegion>)> {
266 Err(Error::NotImplemented(
267 "G2D does not support decoding mask atlas".to_string(),
268 ))
269 }
270
271 fn set_class_colors(&mut self, _: &[[u8; 4]]) -> Result<()> {
272 Err(Error::NotImplemented(
273 "G2D does not support setting colors for rendering detection or segmentation overlays"
274 .to_string(),
275 ))
276 }
277}
278
279fn tensor_to_g2d_surface(img: &Tensor<u8>) -> Result<G2DSurface> {
283 let fmt = img.format().ok_or(Error::NotAnImage)?;
284 let dma = img
285 .as_dma()
286 .ok_or_else(|| Error::NotImplemented("g2d only supports Dma memory".to_string()))?;
287 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into()?;
288
289 let base_addr = phys.address();
296 let luma_offset = img.plane_offset().unwrap_or(0) as u64;
297 let planes = if fmt == PixelFormat::Nv12 {
298 if img.is_multiplane() {
299 let chroma = img.chroma().unwrap();
301 let chroma_dma = chroma.as_dma().ok_or_else(|| {
302 Error::NotImplemented("g2d multiplane chroma must be DMA-backed".to_string())
303 })?;
304 let uv_phys: G2DPhysical = chroma_dma.fd.as_raw_fd().try_into()?;
305 let chroma_offset = img.chroma().and_then(|c| c.plane_offset()).unwrap_or(0) as u64;
306 [
307 base_addr + luma_offset,
308 uv_phys.address() + chroma_offset,
309 0,
310 ]
311 } else {
312 let w = img.width().unwrap();
313 let h = img.height().unwrap();
314 let stride = img.effective_row_stride().unwrap_or(w);
315 let uv_offset = (luma_offset as usize + stride * h) as u64;
316 [base_addr + luma_offset, base_addr + uv_offset, 0]
317 }
318 } else {
319 [base_addr + luma_offset, 0, 0]
320 };
321
322 let w = img.width().unwrap();
323 let h = img.height().unwrap();
324 let fourcc = pixelfmt_to_fourcc(fmt);
325
326 let stride_pixels = match img.effective_row_stride() {
329 Some(s) => {
330 let channels = fmt.channels();
331 if s % channels != 0 {
332 return Err(Error::NotImplemented(
333 "g2d requires row stride to be a multiple of bytes-per-pixel".to_string(),
334 ));
335 }
336 s / channels
337 }
338 None => w,
339 };
340
341 Ok(G2DSurface {
342 planes,
343 format: G2DFormat::try_from(fourcc)?.format(),
344 left: 0,
345 top: 0,
346 right: w as i32,
347 bottom: h as i32,
348 stride: stride_pixels as i32,
349 width: w as i32,
350 height: h as i32,
351 blendfunc: 0,
352 clrcolor: 0,
353 rot: 0,
354 global_alpha: 0,
355 })
356}
357
358#[cfg(feature = "g2d_test_formats")]
359#[cfg(test)]
360mod g2d_tests {
361 use super::*;
362 use crate::{CPUProcessor, Flip, G2DProcessor, ImageProcessorTrait, Rect, Rotation};
363 use edgefirst_tensor::{
364 is_dma_available, DType, PixelFormat, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
365 };
366 use image::buffer::ConvertBuffer;
367
368 #[test]
369 #[cfg(target_os = "linux")]
370 fn test_g2d_formats_no_resize() {
371 for i in [
372 PixelFormat::Rgba,
373 PixelFormat::Yuyv,
374 PixelFormat::Rgb,
375 PixelFormat::Grey,
376 PixelFormat::Nv12,
377 ] {
378 for o in [
379 PixelFormat::Rgba,
380 PixelFormat::Yuyv,
381 PixelFormat::Rgb,
382 PixelFormat::Grey,
383 ] {
384 let res = test_g2d_format_no_resize_(i, o);
385 if let Err(e) = res {
386 println!("{i} to {o} failed: {e:?}");
387 } else {
388 println!("{i} to {o} success");
389 }
390 }
391 }
392 }
393
394 fn test_g2d_format_no_resize_(
395 g2d_in_fmt: PixelFormat,
396 g2d_out_fmt: PixelFormat,
397 ) -> Result<(), crate::Error> {
398 let dst_width = 1280;
399 let dst_height = 720;
400 let file = include_bytes!(concat!(
401 env!("CARGO_MANIFEST_DIR"),
402 "/../../testdata/zidane.jpg"
403 ))
404 .to_vec();
405 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
406
407 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
409
410 let mut cpu_converter = CPUProcessor::new();
411
412 if g2d_in_fmt == PixelFormat::Nv12 {
414 let nv12_bytes = include_bytes!(concat!(
415 env!("CARGO_MANIFEST_DIR"),
416 "/../../testdata/zidane.nv12"
417 ));
418 src2.as_u8()
419 .unwrap()
420 .map()?
421 .as_mut_slice()
422 .copy_from_slice(nv12_bytes);
423 } else {
424 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
425 }
426
427 let mut g2d_dst = TensorDyn::image(
428 dst_width,
429 dst_height,
430 g2d_out_fmt,
431 DType::U8,
432 Some(TensorMemory::Dma),
433 )?;
434 let mut g2d_converter = G2DProcessor::new()?;
435 let src2_dyn = src2;
436 let mut g2d_dst_dyn = g2d_dst;
437 g2d_converter.convert(
438 &src2_dyn,
439 &mut g2d_dst_dyn,
440 Rotation::None,
441 Flip::None,
442 Crop::no_crop(),
443 )?;
444 g2d_dst = {
445 let mut __t = g2d_dst_dyn.into_u8().unwrap();
446 __t.set_format(g2d_out_fmt)
447 .map_err(|e| crate::Error::Internal(e.to_string()))?;
448 TensorDyn::from(__t)
449 };
450
451 let mut cpu_dst =
452 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
453 cpu_converter.convert(
454 &g2d_dst,
455 &mut cpu_dst,
456 Rotation::None,
457 Flip::None,
458 Crop::no_crop(),
459 )?;
460
461 compare_images(
462 &src,
463 &cpu_dst,
464 0.98,
465 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}"),
466 )
467 }
468
469 #[test]
470 #[cfg(target_os = "linux")]
471 fn test_g2d_formats_with_resize() {
472 for i in [
473 PixelFormat::Rgba,
474 PixelFormat::Yuyv,
475 PixelFormat::Rgb,
476 PixelFormat::Grey,
477 PixelFormat::Nv12,
478 ] {
479 for o in [
480 PixelFormat::Rgba,
481 PixelFormat::Yuyv,
482 PixelFormat::Rgb,
483 PixelFormat::Grey,
484 ] {
485 let res = test_g2d_format_with_resize_(i, o);
486 if let Err(e) = res {
487 println!("{i} to {o} failed: {e:?}");
488 } else {
489 println!("{i} to {o} success");
490 }
491 }
492 }
493 }
494
495 #[test]
496 #[cfg(target_os = "linux")]
497 fn test_g2d_formats_with_resize_dst_crop() {
498 for i in [
499 PixelFormat::Rgba,
500 PixelFormat::Yuyv,
501 PixelFormat::Rgb,
502 PixelFormat::Grey,
503 PixelFormat::Nv12,
504 ] {
505 for o in [
506 PixelFormat::Rgba,
507 PixelFormat::Yuyv,
508 PixelFormat::Rgb,
509 PixelFormat::Grey,
510 ] {
511 let res = test_g2d_format_with_resize_dst_crop(i, o);
512 if let Err(e) = res {
513 println!("{i} to {o} failed: {e:?}");
514 } else {
515 println!("{i} to {o} success");
516 }
517 }
518 }
519 }
520
521 fn test_g2d_format_with_resize_(
522 g2d_in_fmt: PixelFormat,
523 g2d_out_fmt: PixelFormat,
524 ) -> Result<(), crate::Error> {
525 let dst_width = 600;
526 let dst_height = 400;
527 let file = include_bytes!(concat!(
528 env!("CARGO_MANIFEST_DIR"),
529 "/../../testdata/zidane.jpg"
530 ))
531 .to_vec();
532 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
533
534 let mut cpu_converter = CPUProcessor::new();
535
536 let mut reference = TensorDyn::image(
537 dst_width,
538 dst_height,
539 PixelFormat::Rgb,
540 DType::U8,
541 Some(TensorMemory::Dma),
542 )?;
543 cpu_converter.convert(
544 &src,
545 &mut reference,
546 Rotation::None,
547 Flip::None,
548 Crop::no_crop(),
549 )?;
550
551 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
553
554 if g2d_in_fmt == PixelFormat::Nv12 {
556 let nv12_bytes = include_bytes!(concat!(
557 env!("CARGO_MANIFEST_DIR"),
558 "/../../testdata/zidane.nv12"
559 ));
560 src2.as_u8()
561 .unwrap()
562 .map()?
563 .as_mut_slice()
564 .copy_from_slice(nv12_bytes);
565 } else {
566 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
567 }
568
569 let mut g2d_dst = TensorDyn::image(
570 dst_width,
571 dst_height,
572 g2d_out_fmt,
573 DType::U8,
574 Some(TensorMemory::Dma),
575 )?;
576 let mut g2d_converter = G2DProcessor::new()?;
577 let src2_dyn = src2;
578 let mut g2d_dst_dyn = g2d_dst;
579 g2d_converter.convert(
580 &src2_dyn,
581 &mut g2d_dst_dyn,
582 Rotation::None,
583 Flip::None,
584 Crop::no_crop(),
585 )?;
586 g2d_dst = {
587 let mut __t = g2d_dst_dyn.into_u8().unwrap();
588 __t.set_format(g2d_out_fmt)
589 .map_err(|e| crate::Error::Internal(e.to_string()))?;
590 TensorDyn::from(__t)
591 };
592
593 let mut cpu_dst =
594 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
595 cpu_converter.convert(
596 &g2d_dst,
597 &mut cpu_dst,
598 Rotation::None,
599 Flip::None,
600 Crop::no_crop(),
601 )?;
602
603 compare_images(
604 &reference,
605 &cpu_dst,
606 0.98,
607 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized"),
608 )
609 }
610
611 fn test_g2d_format_with_resize_dst_crop(
612 g2d_in_fmt: PixelFormat,
613 g2d_out_fmt: PixelFormat,
614 ) -> Result<(), crate::Error> {
615 let dst_width = 600;
616 let dst_height = 400;
617 let crop = Crop {
618 src_rect: None,
619 dst_rect: Some(Rect {
620 top: 100,
621 left: 100,
622 height: 100,
623 width: 200,
624 }),
625 dst_color: None,
626 };
627 let file = include_bytes!(concat!(
628 env!("CARGO_MANIFEST_DIR"),
629 "/../../testdata/zidane.jpg"
630 ))
631 .to_vec();
632 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
633
634 let mut cpu_converter = CPUProcessor::new();
635
636 let mut reference = TensorDyn::image(
637 dst_width,
638 dst_height,
639 PixelFormat::Rgb,
640 DType::U8,
641 Some(TensorMemory::Dma),
642 )?;
643 reference
644 .as_u8()
645 .unwrap()
646 .map()
647 .unwrap()
648 .as_mut_slice()
649 .fill(128);
650 cpu_converter.convert(&src, &mut reference, Rotation::None, Flip::None, crop)?;
651
652 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
654
655 if g2d_in_fmt == PixelFormat::Nv12 {
657 let nv12_bytes = include_bytes!(concat!(
658 env!("CARGO_MANIFEST_DIR"),
659 "/../../testdata/zidane.nv12"
660 ));
661 src2.as_u8()
662 .unwrap()
663 .map()?
664 .as_mut_slice()
665 .copy_from_slice(nv12_bytes);
666 } else {
667 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
668 }
669
670 let mut g2d_dst = TensorDyn::image(
671 dst_width,
672 dst_height,
673 g2d_out_fmt,
674 DType::U8,
675 Some(TensorMemory::Dma),
676 )?;
677 g2d_dst
678 .as_u8()
679 .unwrap()
680 .map()
681 .unwrap()
682 .as_mut_slice()
683 .fill(128);
684 let mut g2d_converter = G2DProcessor::new()?;
685 let src2_dyn = src2;
686 let mut g2d_dst_dyn = g2d_dst;
687 g2d_converter.convert(
688 &src2_dyn,
689 &mut g2d_dst_dyn,
690 Rotation::None,
691 Flip::None,
692 crop,
693 )?;
694 g2d_dst = {
695 let mut __t = g2d_dst_dyn.into_u8().unwrap();
696 __t.set_format(g2d_out_fmt)
697 .map_err(|e| crate::Error::Internal(e.to_string()))?;
698 TensorDyn::from(__t)
699 };
700
701 let mut cpu_dst =
702 TensorDyn::image(dst_width, dst_height, PixelFormat::Rgb, DType::U8, None)?;
703 cpu_converter.convert(
704 &g2d_dst,
705 &mut cpu_dst,
706 Rotation::None,
707 Flip::None,
708 Crop::no_crop(),
709 )?;
710
711 compare_images(
712 &reference,
713 &cpu_dst,
714 0.98,
715 &format!("{g2d_in_fmt}_to_{g2d_out_fmt}_resized_dst_crop"),
716 )
717 }
718
719 fn compare_images(
720 img1: &TensorDyn,
721 img2: &TensorDyn,
722 threshold: f64,
723 name: &str,
724 ) -> Result<(), crate::Error> {
725 assert_eq!(img1.height(), img2.height(), "Heights differ");
726 assert_eq!(img1.width(), img2.width(), "Widths differ");
727 assert_eq!(
728 img1.format().unwrap(),
729 img2.format().unwrap(),
730 "PixelFormat differ"
731 );
732 assert!(
733 matches!(img1.format().unwrap(), PixelFormat::Rgb | PixelFormat::Rgba),
734 "format must be Rgb or Rgba for comparison"
735 );
736 let image1 = match img1.format().unwrap() {
737 PixelFormat::Rgb => image::RgbImage::from_vec(
738 img1.width().unwrap() as u32,
739 img1.height().unwrap() as u32,
740 img1.as_u8().unwrap().map().unwrap().to_vec(),
741 )
742 .unwrap(),
743 PixelFormat::Rgba => image::RgbaImage::from_vec(
744 img1.width().unwrap() as u32,
745 img1.height().unwrap() as u32,
746 img1.as_u8().unwrap().map().unwrap().to_vec(),
747 )
748 .unwrap()
749 .convert(),
750
751 _ => unreachable!(),
752 };
753
754 let image2 = match img2.format().unwrap() {
755 PixelFormat::Rgb => image::RgbImage::from_vec(
756 img2.width().unwrap() as u32,
757 img2.height().unwrap() as u32,
758 img2.as_u8().unwrap().map().unwrap().to_vec(),
759 )
760 .unwrap(),
761 PixelFormat::Rgba => image::RgbaImage::from_vec(
762 img2.width().unwrap() as u32,
763 img2.height().unwrap() as u32,
764 img2.as_u8().unwrap().map().unwrap().to_vec(),
765 )
766 .unwrap()
767 .convert(),
768
769 _ => unreachable!(),
770 };
771
772 let similarity = image_compare::rgb_similarity_structure(
773 &image_compare::Algorithm::RootMeanSquared,
774 &image1,
775 &image2,
776 )
777 .expect("Image Comparison failed");
778
779 if similarity.score < threshold {
780 image1.save(format!("{name}_1.png")).unwrap();
781 image2.save(format!("{name}_2.png")).unwrap();
782 return Err(Error::Internal(format!(
783 "{name}: converted image and target image have similarity score too low: {} < {}",
784 similarity.score, threshold
785 )));
786 }
787 Ok(())
788 }
789
790 fn load_raw_image(
796 width: usize,
797 height: usize,
798 format: PixelFormat,
799 memory: Option<TensorMemory>,
800 bytes: &[u8],
801 ) -> Result<TensorDyn, crate::Error> {
802 let img = TensorDyn::image(width, height, format, DType::U8, memory)?;
803 let mut map = img.as_u8().unwrap().map()?;
804 map.as_mut_slice()[..bytes.len()].copy_from_slice(bytes);
805 Ok(img)
806 }
807
808 #[test]
810 #[cfg(target_os = "linux")]
811 fn test_g2d_nv12_to_rgba_reference() -> Result<(), crate::Error> {
812 if !is_dma_available() {
813 return Ok(());
814 }
815 let src = load_raw_image(
817 1280,
818 720,
819 PixelFormat::Nv12,
820 Some(TensorMemory::Dma),
821 include_bytes!(concat!(
822 env!("CARGO_MANIFEST_DIR"),
823 "/../../testdata/camera720p.nv12"
824 )),
825 )?;
826
827 let reference = load_raw_image(
829 1280,
830 720,
831 PixelFormat::Rgba,
832 None,
833 include_bytes!(concat!(
834 env!("CARGO_MANIFEST_DIR"),
835 "/../../testdata/camera720p.rgba"
836 )),
837 )?;
838
839 let mut dst = TensorDyn::image(
841 1280,
842 720,
843 PixelFormat::Rgba,
844 DType::U8,
845 Some(TensorMemory::Dma),
846 )?;
847 let mut g2d = G2DProcessor::new()?;
848 let src_dyn = src;
849 let mut dst_dyn = dst;
850 g2d.convert(
851 &src_dyn,
852 &mut dst_dyn,
853 Rotation::None,
854 Flip::None,
855 Crop::no_crop(),
856 )?;
857 dst = {
858 let mut __t = dst_dyn.into_u8().unwrap();
859 __t.set_format(PixelFormat::Rgba)
860 .map_err(|e| crate::Error::Internal(e.to_string()))?;
861 TensorDyn::from(__t)
862 };
863
864 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
866 cpu_dst
867 .as_u8()
868 .unwrap()
869 .map()?
870 .as_mut_slice()
871 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
872
873 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgba_reference")
874 }
875
876 #[test]
878 #[cfg(target_os = "linux")]
879 fn test_g2d_nv12_to_rgb_reference() -> Result<(), crate::Error> {
880 if !is_dma_available() {
881 return Ok(());
882 }
883 let src = load_raw_image(
885 1280,
886 720,
887 PixelFormat::Nv12,
888 Some(TensorMemory::Dma),
889 include_bytes!(concat!(
890 env!("CARGO_MANIFEST_DIR"),
891 "/../../testdata/camera720p.nv12"
892 )),
893 )?;
894
895 let reference = load_raw_image(
897 1280,
898 720,
899 PixelFormat::Rgb,
900 None,
901 include_bytes!(concat!(
902 env!("CARGO_MANIFEST_DIR"),
903 "/../../testdata/camera720p.rgb"
904 )),
905 )?;
906
907 let mut dst = TensorDyn::image(
909 1280,
910 720,
911 PixelFormat::Rgb,
912 DType::U8,
913 Some(TensorMemory::Dma),
914 )?;
915 let mut g2d = G2DProcessor::new()?;
916 let src_dyn = src;
917 let mut dst_dyn = dst;
918 g2d.convert(
919 &src_dyn,
920 &mut dst_dyn,
921 Rotation::None,
922 Flip::None,
923 Crop::no_crop(),
924 )?;
925 dst = {
926 let mut __t = dst_dyn.into_u8().unwrap();
927 __t.set_format(PixelFormat::Rgb)
928 .map_err(|e| crate::Error::Internal(e.to_string()))?;
929 TensorDyn::from(__t)
930 };
931
932 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
934 cpu_dst
935 .as_u8()
936 .unwrap()
937 .map()?
938 .as_mut_slice()
939 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
940
941 compare_images(&reference, &cpu_dst, 0.98, "g2d_nv12_to_rgb_reference")
942 }
943
944 #[test]
946 #[cfg(target_os = "linux")]
947 fn test_g2d_yuyv_to_rgba_reference() -> Result<(), crate::Error> {
948 if !is_dma_available() {
949 return Ok(());
950 }
951 let src = load_raw_image(
953 1280,
954 720,
955 PixelFormat::Yuyv,
956 Some(TensorMemory::Dma),
957 include_bytes!(concat!(
958 env!("CARGO_MANIFEST_DIR"),
959 "/../../testdata/camera720p.yuyv"
960 )),
961 )?;
962
963 let reference = load_raw_image(
965 1280,
966 720,
967 PixelFormat::Rgba,
968 None,
969 include_bytes!(concat!(
970 env!("CARGO_MANIFEST_DIR"),
971 "/../../testdata/camera720p.rgba"
972 )),
973 )?;
974
975 let mut dst = TensorDyn::image(
977 1280,
978 720,
979 PixelFormat::Rgba,
980 DType::U8,
981 Some(TensorMemory::Dma),
982 )?;
983 let mut g2d = G2DProcessor::new()?;
984 let src_dyn = src;
985 let mut dst_dyn = dst;
986 g2d.convert(
987 &src_dyn,
988 &mut dst_dyn,
989 Rotation::None,
990 Flip::None,
991 Crop::no_crop(),
992 )?;
993 dst = {
994 let mut __t = dst_dyn.into_u8().unwrap();
995 __t.set_format(PixelFormat::Rgba)
996 .map_err(|e| crate::Error::Internal(e.to_string()))?;
997 TensorDyn::from(__t)
998 };
999
1000 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1002 cpu_dst
1003 .as_u8()
1004 .unwrap()
1005 .map()?
1006 .as_mut_slice()
1007 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1008
1009 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgba_reference")
1010 }
1011
1012 #[test]
1014 #[cfg(target_os = "linux")]
1015 fn test_g2d_yuyv_to_rgb_reference() -> Result<(), crate::Error> {
1016 if !is_dma_available() {
1017 return Ok(());
1018 }
1019 let src = load_raw_image(
1021 1280,
1022 720,
1023 PixelFormat::Yuyv,
1024 Some(TensorMemory::Dma),
1025 include_bytes!(concat!(
1026 env!("CARGO_MANIFEST_DIR"),
1027 "/../../testdata/camera720p.yuyv"
1028 )),
1029 )?;
1030
1031 let reference = load_raw_image(
1033 1280,
1034 720,
1035 PixelFormat::Rgb,
1036 None,
1037 include_bytes!(concat!(
1038 env!("CARGO_MANIFEST_DIR"),
1039 "/../../testdata/camera720p.rgb"
1040 )),
1041 )?;
1042
1043 let mut dst = TensorDyn::image(
1045 1280,
1046 720,
1047 PixelFormat::Rgb,
1048 DType::U8,
1049 Some(TensorMemory::Dma),
1050 )?;
1051 let mut g2d = G2DProcessor::new()?;
1052 let src_dyn = src;
1053 let mut dst_dyn = dst;
1054 g2d.convert(
1055 &src_dyn,
1056 &mut dst_dyn,
1057 Rotation::None,
1058 Flip::None,
1059 Crop::no_crop(),
1060 )?;
1061 dst = {
1062 let mut __t = dst_dyn.into_u8().unwrap();
1063 __t.set_format(PixelFormat::Rgb)
1064 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1065 TensorDyn::from(__t)
1066 };
1067
1068 let cpu_dst = TensorDyn::image(1280, 720, PixelFormat::Rgb, DType::U8, None)?;
1070 cpu_dst
1071 .as_u8()
1072 .unwrap()
1073 .map()?
1074 .as_mut_slice()
1075 .copy_from_slice(dst.as_u8().unwrap().map()?.as_slice());
1076
1077 compare_images(&reference, &cpu_dst, 0.98, "g2d_yuyv_to_rgb_reference")
1078 }
1079
1080 #[test]
1083 #[cfg(target_os = "linux")]
1084 #[ignore = "G2D on i.MX 8MP rejects BGRA as destination format; re-enable when supported"]
1085 fn test_g2d_bgra_no_resize() {
1086 for src_fmt in [
1087 PixelFormat::Rgba,
1088 PixelFormat::Yuyv,
1089 PixelFormat::Nv12,
1090 PixelFormat::Bgra,
1091 ] {
1092 test_g2d_bgra_no_resize_(src_fmt).unwrap_or_else(|e| {
1093 panic!("{src_fmt} to PixelFormat::Bgra failed: {e:?}");
1094 });
1095 }
1096 }
1097
1098 fn test_g2d_bgra_no_resize_(g2d_in_fmt: PixelFormat) -> Result<(), crate::Error> {
1099 let file = include_bytes!(concat!(
1100 env!("CARGO_MANIFEST_DIR"),
1101 "/../../testdata/zidane.jpg"
1102 ))
1103 .to_vec();
1104 let src = crate::load_image(&file, Some(PixelFormat::Rgb), None)?;
1105
1106 let mut src2 = TensorDyn::image(1280, 720, g2d_in_fmt, DType::U8, Some(TensorMemory::Dma))?;
1108 let mut cpu_converter = CPUProcessor::new();
1109
1110 if g2d_in_fmt == PixelFormat::Nv12 {
1111 let nv12_bytes = include_bytes!(concat!(
1112 env!("CARGO_MANIFEST_DIR"),
1113 "/../../testdata/zidane.nv12"
1114 ));
1115 src2.as_u8()
1116 .unwrap()
1117 .map()?
1118 .as_mut_slice()
1119 .copy_from_slice(nv12_bytes);
1120 } else {
1121 cpu_converter.convert(&src, &mut src2, Rotation::None, Flip::None, Crop::no_crop())?;
1122 }
1123
1124 let mut g2d = G2DProcessor::new()?;
1125
1126 let mut bgra_dst = TensorDyn::image(
1128 1280,
1129 720,
1130 PixelFormat::Bgra,
1131 DType::U8,
1132 Some(TensorMemory::Dma),
1133 )?;
1134 let src2_dyn = src2;
1135 let mut bgra_dst_dyn = bgra_dst;
1136 g2d.convert(
1137 &src2_dyn,
1138 &mut bgra_dst_dyn,
1139 Rotation::None,
1140 Flip::None,
1141 Crop::no_crop(),
1142 )?;
1143 bgra_dst = {
1144 let mut __t = bgra_dst_dyn.into_u8().unwrap();
1145 __t.set_format(PixelFormat::Bgra)
1146 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1147 TensorDyn::from(__t)
1148 };
1149
1150 let src2 = {
1152 let mut __t = src2_dyn.into_u8().unwrap();
1153 __t.set_format(g2d_in_fmt)
1154 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1155 TensorDyn::from(__t)
1156 };
1157
1158 let mut rgba_dst = TensorDyn::image(
1160 1280,
1161 720,
1162 PixelFormat::Rgba,
1163 DType::U8,
1164 Some(TensorMemory::Dma),
1165 )?;
1166 let src2_dyn2 = src2;
1167 let mut rgba_dst_dyn = rgba_dst;
1168 g2d.convert(
1169 &src2_dyn2,
1170 &mut rgba_dst_dyn,
1171 Rotation::None,
1172 Flip::None,
1173 Crop::no_crop(),
1174 )?;
1175 rgba_dst = {
1176 let mut __t = rgba_dst_dyn.into_u8().unwrap();
1177 __t.set_format(PixelFormat::Rgba)
1178 .map_err(|e| crate::Error::Internal(e.to_string()))?;
1179 TensorDyn::from(__t)
1180 };
1181
1182 let bgra_cpu = TensorDyn::image(1280, 720, PixelFormat::Bgra, DType::U8, None)?;
1184 bgra_cpu
1185 .as_u8()
1186 .unwrap()
1187 .map()?
1188 .as_mut_slice()
1189 .copy_from_slice(bgra_dst.as_u8().unwrap().map()?.as_slice());
1190
1191 let rgba_cpu = TensorDyn::image(1280, 720, PixelFormat::Rgba, DType::U8, None)?;
1192 rgba_cpu
1193 .as_u8()
1194 .unwrap()
1195 .map()?
1196 .as_mut_slice()
1197 .copy_from_slice(rgba_dst.as_u8().unwrap().map()?.as_slice());
1198
1199 let bgra_map = bgra_cpu.as_u8().unwrap().map()?;
1201 let rgba_map = rgba_cpu.as_u8().unwrap().map()?;
1202 let bgra_buf = bgra_map.as_slice();
1203 let rgba_buf = rgba_map.as_slice();
1204
1205 assert_eq!(bgra_buf.len(), rgba_buf.len());
1206 for (i, (bc, rc)) in bgra_buf
1207 .chunks_exact(4)
1208 .zip(rgba_buf.chunks_exact(4))
1209 .enumerate()
1210 {
1211 assert_eq!(
1212 bc[0], rc[2],
1213 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} B mismatch",
1214 );
1215 assert_eq!(
1216 bc[1], rc[1],
1217 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} G mismatch",
1218 );
1219 assert_eq!(
1220 bc[2], rc[0],
1221 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} R mismatch",
1222 );
1223 assert_eq!(
1224 bc[3], rc[3],
1225 "{g2d_in_fmt} to PixelFormat::Bgra: pixel {i} A mismatch",
1226 );
1227 }
1228 Ok(())
1229 }
1230
1231 fn surface_for(
1242 width: usize,
1243 height: usize,
1244 fmt: PixelFormat,
1245 offset: Option<usize>,
1246 row_stride: Option<usize>,
1247 ) -> Result<G2DSurface, crate::Error> {
1248 use edgefirst_tensor::TensorMemory;
1249 let mut t = Tensor::<u8>::image(width, height, fmt, Some(TensorMemory::Dma))?;
1250 if let Some(o) = offset {
1251 t.set_plane_offset(o);
1252 }
1253 if let Some(s) = row_stride {
1254 t.set_row_stride_unchecked(s);
1255 }
1256 tensor_to_g2d_surface(&t)
1257 }
1258
1259 #[test]
1260 fn g2d_surface_single_plane_no_offset() {
1261 if !is_dma_available() {
1262 return;
1263 }
1264 let s = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1265 assert_ne!(s.planes[0], 0);
1267 assert_eq!(s.stride, 640);
1268 }
1269
1270 #[test]
1271 fn g2d_surface_single_plane_with_offset() {
1272 if !is_dma_available() {
1273 return;
1274 }
1275 use edgefirst_tensor::TensorMemory;
1276 let mut t =
1277 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1278 let s0 = tensor_to_g2d_surface(&t).unwrap();
1279 t.set_plane_offset(4096);
1280 let s1 = tensor_to_g2d_surface(&t).unwrap();
1281 assert_eq!(s1.planes[0], s0.planes[0] + 4096);
1282 }
1283
1284 #[test]
1285 fn g2d_surface_single_plane_zero_offset() {
1286 if !is_dma_available() {
1287 return;
1288 }
1289 use edgefirst_tensor::TensorMemory;
1290 let mut t =
1291 Tensor::<u8>::image(640, 480, PixelFormat::Rgba, Some(TensorMemory::Dma)).unwrap();
1292 let s_none = tensor_to_g2d_surface(&t).unwrap();
1293 t.set_plane_offset(0);
1294 let s_zero = tensor_to_g2d_surface(&t).unwrap();
1295 assert_eq!(s_none.planes[0], s_zero.planes[0]);
1297 }
1298
1299 #[test]
1300 fn g2d_surface_stride_rgba() {
1301 if !is_dma_available() {
1302 return;
1303 }
1304 let s_default = surface_for(640, 480, PixelFormat::Rgba, None, None).unwrap();
1306 assert_eq!(s_default.stride, 640);
1307
1308 let s_custom = surface_for(640, 480, PixelFormat::Rgba, None, Some(2816)).unwrap();
1310 assert_eq!(s_custom.stride, 704);
1311 }
1312
1313 #[test]
1314 fn g2d_surface_stride_rgb() {
1315 if !is_dma_available() {
1316 return;
1317 }
1318 let s_default = surface_for(640, 480, PixelFormat::Rgb, None, None).unwrap();
1319 assert_eq!(s_default.stride, 640);
1320
1321 let s_custom = surface_for(640, 480, PixelFormat::Rgb, None, Some(1980)).unwrap();
1323 assert_eq!(s_custom.stride, 660);
1324 }
1325
1326 #[test]
1327 fn g2d_surface_stride_grey() {
1328 if !is_dma_available() {
1329 return;
1330 }
1331 let s = match surface_for(640, 480, PixelFormat::Grey, None, Some(1024)) {
1333 Ok(s) => s,
1334 Err(crate::Error::G2D(..)) => return,
1335 Err(e) => panic!("unexpected error: {e:?}"),
1336 };
1337 assert_eq!(s.stride, 1024);
1339 }
1340
1341 #[test]
1342 fn g2d_surface_contiguous_nv12_offset() {
1343 if !is_dma_available() {
1344 return;
1345 }
1346 use edgefirst_tensor::TensorMemory;
1347 let mut t =
1348 Tensor::<u8>::image(640, 480, PixelFormat::Nv12, Some(TensorMemory::Dma)).unwrap();
1349 let s0 = tensor_to_g2d_surface(&t).unwrap();
1350
1351 t.set_plane_offset(8192);
1352 let s1 = tensor_to_g2d_surface(&t).unwrap();
1353
1354 assert_eq!(s1.planes[0], s0.planes[0] + 8192);
1356 assert_eq!(s1.planes[1], s0.planes[1] + 8192);
1360 }
1361
1362 #[test]
1363 fn g2d_surface_contiguous_nv12_stride() {
1364 if !is_dma_available() {
1365 return;
1366 }
1367 let s = surface_for(640, 480, PixelFormat::Nv12, None, None).unwrap();
1369 assert_eq!(s.stride, 640);
1370
1371 let s_padded = surface_for(640, 480, PixelFormat::Nv12, None, Some(1024)).unwrap();
1373 assert_eq!(s_padded.stride, 1024);
1374 }
1375
1376 #[test]
1377 fn g2d_surface_multiplane_nv12_offset() {
1378 if !is_dma_available() {
1379 return;
1380 }
1381 use edgefirst_tensor::TensorMemory;
1382
1383 let mut luma =
1385 Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Dma), Some("luma")).unwrap();
1386 let mut chroma =
1387 Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Dma), Some("chroma")).unwrap();
1388
1389 let luma_base = {
1391 let dma = luma.as_dma().unwrap();
1392 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1393 phys.address()
1394 };
1395 let chroma_base = {
1396 let dma = chroma.as_dma().unwrap();
1397 let phys: G2DPhysical = dma.fd.as_raw_fd().try_into().unwrap();
1398 phys.address()
1399 };
1400
1401 luma.set_plane_offset(4096);
1403 chroma.set_plane_offset(2048);
1404 let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1405 let s = tensor_to_g2d_surface(&combined).unwrap();
1406
1407 assert_eq!(s.planes[0], luma_base + 4096);
1409 assert_eq!(s.planes[1], chroma_base + 2048);
1411 }
1412}