1use crate::{
5 Error, Flip, FunctionTimer, ImageProcessorTrait, Rect, ResolvedCrop, Result, Rotation,
6};
7use edgefirst_tensor::{
8 DType, PixelFormat, Tensor, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
9};
10use edgefirst_tensor::{DetectBox, ProtoData, Segmentation};
11
12mod convert;
13mod masks;
14mod normalize;
15mod resize;
16mod simd;
17mod tests;
18
19#[derive(Debug, Clone, Copy)]
31pub(crate) struct ColorParams {
32 pub matrix: yuv::YuvStandardMatrix,
34 pub range: yuv::YuvRange,
36 pub encoding: edgefirst_tensor::ColorEncoding,
41 pub range_kind: edgefirst_tensor::ColorRange,
42 pub src_full_range: bool,
45 pub dst_full_range: bool,
48}
49
50#[derive(Debug)]
53pub struct CPUProcessor {
54 resizer: fast_image_resize::Resizer,
55 options: fast_image_resize::ResizeOptions,
56 colors: [[u8; 4]; 20],
57 widen_scratch: Option<TensorDyn>,
63 resize_destride_scratch: Vec<u8>,
71 resize_dst_destride_scratch: Vec<u8>,
82 nv_strip_scratch: Vec<u8>,
88 nv_strip_y_pack: Vec<u8>,
104 nv_strip_uv_pack: Vec<u8>,
106 #[cfg(test)]
113 fused_hits: u64,
114 #[cfg(test)]
121 last_tmp_dims: Option<(usize, usize)>,
122 convert_tmp: Option<Tensor<u8>>,
129 convert_tmp2: Option<Tensor<u8>>,
130 convert_src_sub: Option<Tensor<u8>>,
135}
136
137impl Clone for CPUProcessor {
143 fn clone(&self) -> Self {
144 Self {
145 resizer: self.resizer.clone(),
146 options: self.options,
147 colors: self.colors,
148 widen_scratch: None,
149 resize_destride_scratch: Vec::new(),
150 resize_dst_destride_scratch: Vec::new(),
151 nv_strip_scratch: Vec::new(),
152 nv_strip_y_pack: Vec::new(),
153 nv_strip_uv_pack: Vec::new(),
154 #[cfg(test)]
155 fused_hits: 0,
156 #[cfg(test)]
157 last_tmp_dims: None,
158 convert_tmp: None,
159 convert_tmp2: None,
160 convert_src_sub: None,
161 }
162 }
163}
164
165unsafe impl Send for CPUProcessor {}
166unsafe impl Sync for CPUProcessor {}
167
168impl Default for CPUProcessor {
169 fn default() -> Self {
170 Self::new_bilinear()
171 }
172}
173
174fn prepare_dst_base_cpu(dst: &mut TensorDyn, background: Option<&TensorDyn>) -> Result<()> {
185 match background {
186 Some(bg) => {
187 if bg.shape() != dst.shape() {
188 return Err(Error::InvalidShape(
189 "background shape does not match dst".into(),
190 ));
191 }
192 if bg.format() != dst.format() {
193 return Err(Error::InvalidShape(
194 "background pixel format does not match dst".into(),
195 ));
196 }
197 let bg_u8 = bg.as_typed::<u8>().ok_or(Error::NotAnImage)?;
198 let dst_u8 = dst.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
199 CPUProcessor::copy_image(bg_u8, dst_u8)?;
206 }
207 None => {
208 let dst_u8 = dst.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
209 let mut dst_map = dst_u8.map_mut()?;
210 dst_map.as_mut_slice().fill(0);
211 }
212 }
213 Ok(())
214}
215
216fn chroma_alignment_ok(fmt: PixelFormat, r: Rect) -> bool {
224 match fmt {
225 PixelFormat::Nv12 => r.left.is_multiple_of(2) && r.top.is_multiple_of(2),
226 PixelFormat::Nv16 => r.left.is_multiple_of(2),
227 PixelFormat::Nv24 => true,
228 _ => false,
229 }
230}
231
232fn row_stride_for(width: usize, fmt: PixelFormat) -> usize {
234 use edgefirst_tensor::PixelLayout;
235 match fmt.layout() {
236 PixelLayout::Packed => width * fmt.channels(),
237 PixelLayout::Planar | PixelLayout::SemiPlanar => width,
238 _ => width, }
240}
241
242fn tensor_row_stride(tensor: &Tensor<u8>) -> usize {
247 tensor.effective_row_stride().unwrap_or_else(|| {
248 let w = tensor.width().unwrap_or(0);
249 let fmt = tensor.format().unwrap_or(PixelFormat::Rgb);
250 row_stride_for(w, fmt)
251 })
252}
253
254fn logical_surface(tensor: &Tensor<u8>) -> Result<(usize, usize)> {
264 use edgefirst_tensor::PixelLayout;
265 let fmt = tensor.format().ok_or(Error::NotAnImage)?;
266 let w = tensor.width().ok_or(Error::NotAnImage)?;
267 let h = tensor.height().ok_or(Error::NotAnImage)?;
268 Ok(match fmt.layout() {
269 PixelLayout::Packed => (h, w * fmt.channels()),
270 PixelLayout::Planar => (fmt.channels() * h, w),
271 PixelLayout::SemiPlanar => (fmt.combined_plane_height(h).unwrap_or(h), w),
272 _ => (h, row_stride_for(w, fmt)),
275 })
276}
277
278fn packed_row_pairs<'s, 'd>(
284 src: &'s [u8],
285 src_stride: usize,
286 src_row_bytes: usize,
287 dst: &'d mut [u8],
288 dst_stride: usize,
289 dst_row_bytes: usize,
290 rows: usize,
291) -> impl Iterator<Item = (&'s [u8], &'d mut [u8])> {
292 src.chunks(src_stride)
293 .zip(dst.chunks_mut(dst_stride))
294 .take(rows)
295 .map(move |(s, d)| (&s[..src_row_bytes], &mut d[..dst_row_bytes]))
296}
297
298fn split_semi_planar(
304 bytes: &[u8],
305 stride: usize,
306 src_h: usize,
307 fmt: PixelFormat,
308) -> Result<(&[u8], &[u8])> {
309 let total_h = fmt.combined_plane_height(src_h).unwrap_or(src_h);
310 let need = stride.checked_mul(total_h).ok_or_else(|| {
311 Error::InvalidShape(format!(
312 "{fmt:?} plane size overflow (stride={stride}, h={src_h})"
313 ))
314 })?;
315 if bytes.len() < need {
316 return Err(Error::InvalidShape(format!(
317 "{fmt:?} source has {} bytes but needs {need} (stride={stride}, h={src_h})",
318 bytes.len()
319 )));
320 }
321 Ok(bytes.split_at(stride * src_h))
322}
323
324fn split_semi_planar_mut(
331 bytes: &mut [u8],
332 stride: usize,
333 dst_h: usize,
334 fmt: PixelFormat,
335) -> Result<(&mut [u8], &mut [u8])> {
336 let total_h = fmt.combined_plane_height(dst_h).unwrap_or(dst_h);
337 let need = stride.checked_mul(total_h).ok_or_else(|| {
338 Error::InvalidShape(format!(
339 "{fmt:?} plane size overflow (stride={stride}, combined_h={total_h})"
340 ))
341 })?;
342 if bytes.len() < need {
343 return Err(Error::InvalidShape(format!(
344 "{fmt:?} destination has {} bytes but needs {need} (stride={stride}, combined_h={total_h})",
345 bytes.len()
346 )));
347 }
348 Ok(bytes.split_at_mut(stride * dst_h))
349}
350
351fn guard_plane(
357 buf_len: usize,
358 stride: usize,
359 rows: usize,
360 row_bytes: usize,
361 what: &str,
362) -> Result<()> {
363 let need = stride.checked_mul(rows).ok_or_else(|| {
364 Error::InvalidShape(format!(
365 "{what} plane size overflow (stride={stride}, rows={rows})"
366 ))
367 })?;
368 if row_bytes > stride || buf_len < need {
369 return Err(Error::InvalidShape(format!(
370 "{what} buffer too small: {buf_len} bytes, need {need} (stride={stride}, rows={rows}, row_bytes={row_bytes})"
371 )));
372 }
373 Ok(())
374}
375
376pub(crate) fn apply_int8_xor_bias(data: &mut [u8], fmt: PixelFormat) {
381 use edgefirst_tensor::PixelLayout;
382 if !fmt.has_alpha() {
383 for b in data.iter_mut() {
384 *b ^= 0x80;
385 }
386 } else if fmt.layout() == PixelLayout::Planar {
387 let channels = fmt.channels();
389 let plane_size = data.len() / channels;
390 for b in data[..plane_size * (channels - 1)].iter_mut() {
391 *b ^= 0x80;
392 }
393 } else {
394 let channels = fmt.channels();
396 for pixel in data.chunks_exact_mut(channels) {
397 for b in &mut pixel[..channels - 1] {
398 *b ^= 0x80;
399 }
400 }
401 }
402}
403
404pub(crate) fn apply_int8_xor_bias_rows(tensor: &mut Tensor<u8>, fmt: PixelFormat) -> Result<()> {
410 use edgefirst_tensor::PixelLayout;
411 let (rows, row_bytes) = logical_surface(tensor)?;
412 let stride = tensor_row_stride(tensor);
413 let color_rows = if fmt.has_alpha() && fmt.layout() == PixelLayout::Planar {
416 rows / fmt.channels() * (fmt.channels() - 1)
417 } else {
418 rows
419 };
420 let row_fmt = if fmt.layout() == PixelLayout::Planar {
423 PixelFormat::Grey
424 } else {
425 fmt
426 };
427 let mut map = tensor.map_mut()?;
429 let buf = map.as_mut_slice();
430 guard_plane(buf.len(), stride, rows, row_bytes, "int8 bias dst")?;
431 for row in buf.chunks_mut(stride).take(color_rows) {
432 apply_int8_xor_bias(&mut row[..row_bytes], row_fmt);
433 }
434 Ok(())
435}
436
437impl CPUProcessor {
438 pub fn new() -> Self {
440 Self::new_bilinear()
441 }
442
443 fn resize_alg(mode: crate::Interpolation) -> fast_image_resize::ResizeAlg {
446 match mode.supported() {
447 crate::Interpolation::Nearest => fast_image_resize::ResizeAlg::Nearest,
448 _ => {
449 fast_image_resize::ResizeAlg::Interpolation(fast_image_resize::FilterType::Bilinear)
450 }
451 }
452 }
453
454 fn new_bilinear() -> Self {
456 Self::with_interpolation(crate::Interpolation::Bilinear)
457 }
458
459 fn with_interpolation(mode: crate::Interpolation) -> Self {
461 let resizer = fast_image_resize::Resizer::new();
462 let options = fast_image_resize::ResizeOptions::new()
463 .resize_alg(Self::resize_alg(mode))
464 .use_alpha(false);
465
466 log::debug!("CPUConverter created");
467 Self {
468 resizer,
469 options,
470 colors: crate::DEFAULT_COLORS_U8,
471 widen_scratch: None,
472 resize_destride_scratch: Vec::new(),
473 resize_dst_destride_scratch: Vec::new(),
474 nv_strip_scratch: Vec::new(),
475 nv_strip_y_pack: Vec::new(),
476 nv_strip_uv_pack: Vec::new(),
477 #[cfg(test)]
478 fused_hits: 0,
479 #[cfg(test)]
480 last_tmp_dims: None,
481 convert_tmp: None,
482 convert_tmp2: None,
483 convert_src_sub: None,
484 }
485 }
486
487 pub fn set_interpolation(&mut self, mode: crate::Interpolation) {
490 self.options = self.options.clone().resize_alg(Self::resize_alg(mode));
491 }
492
493 pub fn new_nearest() -> Self {
495 Self::with_interpolation(crate::Interpolation::Nearest)
496 }
497
498 #[cfg(test)]
500 pub(super) fn fused_hits(&self) -> u64 {
501 self.fused_hits
502 }
503
504 #[cfg(test)]
507 pub(super) fn last_tmp_dims(&self) -> Option<(usize, usize)> {
508 self.last_tmp_dims
509 }
510
511 pub(crate) fn support_conversion_pf(src: PixelFormat, dst: PixelFormat) -> bool {
512 use PixelFormat::*;
513 matches!(
514 (src, dst),
515 (Nv12, Rgb)
516 | (Nv12, Rgba)
517 | (Nv12, Grey)
518 | (Nv16, Rgb)
519 | (Nv16, Rgba)
520 | (Nv16, Bgra)
521 | (Nv24, Rgb)
522 | (Nv24, Rgba)
523 | (Nv24, Grey)
524 | (Nv24, Bgra)
525 | (Yuyv, Rgb)
526 | (Yuyv, Rgba)
527 | (Yuyv, Grey)
528 | (Yuyv, Yuyv)
529 | (Yuyv, PlanarRgb)
530 | (Yuyv, PlanarRgba)
531 | (Yuyv, Nv16)
532 | (Vyuy, Rgb)
533 | (Vyuy, Rgba)
534 | (Vyuy, Grey)
535 | (Vyuy, Vyuy)
536 | (Vyuy, PlanarRgb)
537 | (Vyuy, PlanarRgba)
538 | (Vyuy, Nv16)
539 | (Rgba, Rgb)
540 | (Rgba, Rgba)
541 | (Rgba, Grey)
542 | (Rgba, Yuyv)
543 | (Rgba, PlanarRgb)
544 | (Rgba, PlanarRgba)
545 | (Rgba, Nv16)
546 | (Rgb, Rgb)
547 | (Rgb, Rgba)
548 | (Rgb, Grey)
549 | (Rgb, Yuyv)
550 | (Rgb, PlanarRgb)
551 | (Rgb, PlanarRgba)
552 | (Rgb, Nv16)
553 | (Grey, Rgb)
554 | (Grey, Rgba)
555 | (Grey, Grey)
556 | (Grey, Yuyv)
557 | (Grey, PlanarRgb)
558 | (Grey, PlanarRgba)
559 | (Grey, Nv16)
560 | (Nv12, Bgra)
561 | (Yuyv, Bgra)
562 | (Vyuy, Bgra)
563 | (Rgba, Bgra)
564 | (Rgb, Bgra)
565 | (Grey, Bgra)
566 | (Bgra, Bgra)
567 | (PlanarRgb, Rgb)
568 | (PlanarRgb, Rgba)
569 | (PlanarRgba, Rgb)
570 | (PlanarRgba, Rgba)
571 | (PlanarRgb, Bgra)
572 | (PlanarRgba, Bgra)
573 )
574 }
575
576 pub(crate) fn convert_format_pf(
578 src: &Tensor<u8>,
579 dst: &mut Tensor<u8>,
580 src_fmt: PixelFormat,
581 dst_fmt: PixelFormat,
582 cp: ColorParams,
583 ) -> Result<()> {
584 let _timer = FunctionTimer::new(format!(
585 "ImageProcessor::convert_format {} to {}",
586 src_fmt, dst_fmt,
587 ));
588
589 use PixelFormat::*;
590 match (src_fmt, dst_fmt) {
591 (Nv12, Rgb) => Self::convert_nv12_to_rgb(src, dst, cp),
592 (Nv12, Rgba) => Self::convert_nv12_to_rgba(src, dst, cp),
593 (Nv12, Grey) => Self::convert_nv12_to_grey(src, dst, cp),
594 (Yuyv, Rgb) => Self::convert_yuyv_to_rgb(src, dst, cp),
595 (Yuyv, Rgba) => Self::convert_yuyv_to_rgba(src, dst, cp),
596 (Yuyv, Grey) => Self::convert_yuyv_to_grey(src, dst, cp),
597 (Yuyv, Yuyv) => Self::copy_image(src, dst),
598 (Yuyv, PlanarRgb) => Self::convert_yuyv_to_8bps(src, dst, cp),
599 (Yuyv, PlanarRgba) => Self::convert_yuyv_to_prgba(src, dst, cp),
600 (Yuyv, Nv16) => Self::convert_yuyv_to_nv16(src, dst),
601 (Vyuy, Rgb) => Self::convert_vyuy_to_rgb(src, dst, cp),
602 (Vyuy, Rgba) => Self::convert_vyuy_to_rgba(src, dst, cp),
603 (Vyuy, Grey) => Self::convert_vyuy_to_grey(src, dst, cp),
604 (Vyuy, Vyuy) => Self::copy_image(src, dst),
605 (Vyuy, PlanarRgb) => Self::convert_vyuy_to_8bps(src, dst, cp),
606 (Vyuy, PlanarRgba) => Self::convert_vyuy_to_prgba(src, dst, cp),
607 (Vyuy, Nv16) => Self::convert_vyuy_to_nv16(src, dst),
608 (Rgba, Rgb) => Self::convert_rgba_to_rgb(src, dst),
609 (Rgba, Rgba) => Self::copy_image(src, dst),
610 (Rgba, Grey) => Self::convert_rgba_to_grey(src, dst),
611 (Rgba, Yuyv) => Self::convert_rgba_to_yuyv(src, dst, cp),
612 (Rgba, PlanarRgb) => Self::convert_rgba_to_8bps(src, dst),
613 (Rgba, PlanarRgba) => Self::convert_rgba_to_prgba(src, dst),
614 (Rgba, Nv16) => Self::convert_rgba_to_nv16(src, dst, cp),
615 (Rgb, Rgb) => Self::copy_image(src, dst),
616 (Rgb, Rgba) => Self::convert_rgb_to_rgba(src, dst),
617 (Rgb, Grey) => Self::convert_rgb_to_grey(src, dst),
618 (Rgb, Yuyv) => Self::convert_rgb_to_yuyv(src, dst, cp),
619 (Rgb, PlanarRgb) => Self::convert_rgb_to_8bps(src, dst),
620 (Rgb, PlanarRgba) => Self::convert_rgb_to_prgba(src, dst),
621 (Rgb, Nv16) => Self::convert_rgb_to_nv16(src, dst, cp),
622 (Grey, Rgb) => Self::convert_grey_to_rgb(src, dst),
623 (Grey, Rgba) => Self::convert_grey_to_rgba(src, dst),
624 (Grey, Grey) => Self::copy_image(src, dst),
625 (Grey, Yuyv) => Self::convert_grey_to_yuyv(src, dst, cp),
626 (Grey, PlanarRgb) => Self::convert_grey_to_8bps(src, dst),
627 (Grey, PlanarRgba) => Self::convert_grey_to_prgba(src, dst),
628 (Grey, Nv16) => Self::convert_grey_to_nv16(src, dst, cp),
629
630 (Nv16, Rgb) => Self::convert_nv16_to_rgb(src, dst, cp),
632 (Nv16, Rgba) => Self::convert_nv16_to_rgba(src, dst, cp),
633 (Nv24, Rgb) => Self::convert_nv24_to_rgb(src, dst, cp),
634 (Nv24, Rgba) => Self::convert_nv24_to_rgba(src, dst, cp),
635 (Nv24, Grey) => Self::convert_nv24_to_grey(src, dst, cp),
636 (PlanarRgb, Rgb) => Self::convert_8bps_to_rgb(src, dst),
637 (PlanarRgb, Rgba) => Self::convert_8bps_to_rgba(src, dst),
638 (PlanarRgba, Rgb) => Self::convert_prgba_to_rgb(src, dst),
639 (PlanarRgba, Rgba) => Self::convert_prgba_to_rgba(src, dst),
640
641 (Bgra, Bgra) => Self::copy_image(src, dst),
643 (Nv12, Bgra) => {
644 Self::convert_nv12_to_rgba(src, dst, cp)?;
645 Self::swizzle_rb_4chan(dst)
646 }
647 (Nv16, Bgra) => {
648 Self::convert_nv16_to_rgba(src, dst, cp)?;
649 Self::swizzle_rb_4chan(dst)
650 }
651 (Nv24, Bgra) => {
652 Self::convert_nv24_to_rgba(src, dst, cp)?;
653 Self::swizzle_rb_4chan(dst)
654 }
655 (Yuyv, Bgra) => {
656 Self::convert_yuyv_to_rgba(src, dst, cp)?;
657 Self::swizzle_rb_4chan(dst)
658 }
659 (Vyuy, Bgra) => {
660 Self::convert_vyuy_to_rgba(src, dst, cp)?;
661 Self::swizzle_rb_4chan(dst)
662 }
663 (Rgba, Bgra) => {
664 Self::copy_image(src, dst)?;
665 Self::swizzle_rb_4chan(dst)
666 }
667 (Rgb, Bgra) => {
668 Self::convert_rgb_to_rgba(src, dst)?;
669 Self::swizzle_rb_4chan(dst)
670 }
671 (Grey, Bgra) => {
672 Self::convert_grey_to_rgba(src, dst)?;
673 Self::swizzle_rb_4chan(dst)
674 }
675 (PlanarRgb, Bgra) => {
676 Self::convert_8bps_to_rgba(src, dst)?;
677 Self::swizzle_rb_4chan(dst)
678 }
679 (PlanarRgba, Bgra) => {
680 Self::convert_prgba_to_rgba(src, dst)?;
681 Self::swizzle_rb_4chan(dst)
682 }
683
684 (s, d) => Err(Error::NotSupported(format!("Conversion from {s} to {d}",))),
685 }
686 }
687
688 pub(crate) fn fill_image_outside_crop_u8(
691 dst: &mut Tensor<u8>,
692 rgba: [u8; 4],
693 crop: Rect,
694 device_interior: bool,
695 ) -> Result<()> {
696 let dst_fmt = dst.format().unwrap();
697 let dst_w = dst.width().unwrap();
698 let dst_h = dst.height().unwrap();
699 let cm = crate::colorimetry::resolve_colorimetry(dst.colorimetry(), dst.height());
703 let cp = ColorParams {
704 matrix: crate::colorimetry::yuv_matrix(cm.encoding.unwrap()),
705 range: crate::colorimetry::yuv_range(cm.range.unwrap()),
706 encoding: cm.encoding.unwrap(),
707 range_kind: cm.range.unwrap(),
708 src_full_range: cm.range == Some(edgefirst_tensor::ColorRange::Full),
709 dst_full_range: cm.range == Some(edgefirst_tensor::ColorRange::Full),
710 };
711 let dst_stride = tensor_row_stride(dst);
712 let mut dst_map = if device_interior {
713 dst.map_for_partial_write()?
714 } else {
715 dst.map_for_write()?
716 };
717 let dst_tup = (dst_map.as_mut_slice(), dst_w, dst_h, dst_stride);
718 Self::fill_outside_crop_dispatch(dst_tup, dst_fmt, rgba, crop, cp)
719 }
720
721 fn fill_outside_crop_dispatch(
726 dst: (&mut [u8], usize, usize, usize),
727 fmt: PixelFormat,
728 rgba: [u8; 4],
729 crop: Rect,
730 cp: ColorParams,
731 ) -> Result<()> {
732 use PixelFormat::*;
733 match fmt {
734 Rgba | Bgra => Self::fill_image_outside_crop_(dst, rgba, crop),
735 Rgb => Self::fill_image_outside_crop_(dst, Self::rgba_to_rgb(rgba), crop),
736 Grey => Self::fill_image_outside_crop_(dst, Self::rgba_to_grey(rgba), crop),
737 Yuyv => {
738 let (bytes, w, h, stride) = dst;
739 let yuyv = Self::rgba_to_yuyv(rgba, cp);
740 Self::fill_image_outside_crop_(
743 (&mut *bytes, w / 2, h, stride),
744 yuyv,
745 Rect::new(crop.left / 2, crop.top, crop.width.div_ceil(2), crop.height),
746 )?;
747 if w % 2 == 1 {
755 let last = w - 1;
756 let col_outside = last < crop.left || last >= crop.left + crop.width;
757 for y in 0..h {
758 let row_outside = y < crop.top || y >= crop.top + crop.height;
759 if row_outside || col_outside {
760 let off = y * stride + last * 2;
761 bytes[off] = yuyv[0];
762 bytes[off + 1] = yuyv[1];
763 }
764 }
765 }
766 Ok(())
767 }
768 PlanarRgb => Self::fill_image_outside_crop_planar(dst, Self::rgba_to_rgb(rgba), crop),
769 PlanarRgba => Self::fill_image_outside_crop_planar(dst, rgba, crop),
770 Nv16 => {
771 let yuyv = Self::rgba_to_yuyv(rgba, cp);
772 Self::fill_image_outside_crop_yuv_semiplanar(dst, yuyv[0], [yuyv[1], yuyv[3]], crop)
773 }
774 _ => Err(Error::Internal(format!(
775 "Found unexpected destination {fmt}",
776 ))),
777 }
778 }
779}
780
781fn color_params(src: &TensorDyn, dst: &TensorDyn) -> (ColorParams, ColorParams) {
784 let src_cm = crate::colorimetry::effective_colorimetry(src);
789 let dst_cm = crate::colorimetry::effective_colorimetry(dst);
790 let src_full = src_cm.range == Some(edgefirst_tensor::ColorRange::Full);
791 let dst_full = dst_cm.range == Some(edgefirst_tensor::ColorRange::Full);
792 let src_params = ColorParams {
793 matrix: crate::colorimetry::yuv_matrix(src_cm.encoding.unwrap()),
794 range: crate::colorimetry::yuv_range(src_cm.range.unwrap()),
795 encoding: src_cm.encoding.unwrap(),
796 range_kind: src_cm.range.unwrap(),
797 src_full_range: src_full,
798 dst_full_range: dst_full,
799 };
800 let dst_params = ColorParams {
801 matrix: crate::colorimetry::yuv_matrix(dst_cm.encoding.unwrap()),
802 range: crate::colorimetry::yuv_range(dst_cm.range.unwrap()),
803 encoding: dst_cm.encoding.unwrap(),
804 range_kind: dst_cm.range.unwrap(),
805 src_full_range: src_full,
806 dst_full_range: dst_full,
807 };
808 (src_params, dst_params)
809}
810
811fn partial_dst_access(dst: &TensorDyn, crop: &ResolvedCrop) -> edgefirst_tensor::CpuAccess {
815 let full = |r: &Rect| {
816 r.left == 0 && r.top == 0 && Some(r.width) == dst.width() && Some(r.height) == dst.height()
817 };
818 let keeps_outside = crop.dst_color.is_none() && crop.dst_rect.is_some_and(|r| !full(&r));
819 if dst.is_view() || keeps_outside {
820 edgefirst_tensor::CpuAccess::ReadWrite
821 } else {
822 edgefirst_tensor::CpuAccess::None
823 }
824}
825
826pub(crate) fn convert_dst_access(
831 src: Option<PixelFormat>,
832 dst_fmt: PixelFormat,
833 dst_dtype: DType,
834) -> edgefirst_tensor::CpuAccess {
835 use edgefirst_tensor::CpuAccess;
836 let swizzles = dst_fmt == PixelFormat::Bgra && src != Some(PixelFormat::Bgra);
837 match dst_dtype {
838 DType::I8 => CpuAccess::ReadWrite,
839 DType::U8 if swizzles => CpuAccess::ReadWrite,
840 _ => CpuAccess::Write,
841 }
842}
843
844fn require_draw_access(dst: &TensorDyn, background: Option<&TensorDyn>) -> Result<()> {
847 dst.cpu_access()
848 .require(edgefirst_tensor::CpuAccess::ReadWrite)?;
849 if let Some(bg) = background {
850 bg.cpu_access().require(edgefirst_tensor::CpuAccess::Read)?;
851 }
852 Ok(())
853}
854
855impl ImageProcessorTrait for CPUProcessor {
856 fn convert(
857 &mut self,
858 src: &TensorDyn,
859 dst: &mut TensorDyn,
860 options: Option<crate::ConvertOptions>,
861 ) -> Result<()> {
862 src.cpu_access()
863 .require(edgefirst_tensor::CpuAccess::Read)?;
864 dst.cpu_access()
865 .require(edgefirst_tensor::CpuAccess::Write)?;
866 let options = options.unwrap_or_default();
867 let (rotation, flip, crop) = (
868 options.get_rotation(),
869 options.get_flip(),
870 options.get_crop(),
871 );
872 let crop = crop.resolve(
873 src.width().unwrap_or(0),
874 src.height().unwrap_or(0),
875 dst.width().unwrap_or(0),
876 dst.height().unwrap_or(0),
877 options.get_placement(),
878 )?;
879 dst.cpu_access().require(partial_dst_access(dst, &crop))?;
880 let output_map = crate::options::output_map_for(&options, dst, crop.dst_rect.is_some())?;
881 if output_map.is_none() {
882 if let Some(dst_fmt) = dst.format() {
883 dst.cpu_access()
884 .require(convert_dst_access(src.format(), dst_fmt, dst.dtype()))?;
885 }
886 }
887 let processor_options = self.options;
888 if let Some(mode) = options.get_interpolation() {
889 self.options = self.options.resize_alg(Self::resize_alg(mode));
890 }
891 let result = match output_map {
892 None => self.convert_impl(src, dst, rotation, flip, crop),
893 Some((map, pad_output)) => {
894 self.convert_mapped(src, dst, rotation, flip, crop, &map, pad_output)
895 }
896 };
897 self.options = processor_options;
898 result
899 }
900
901 fn draw_decoded_masks(
902 &mut self,
903 dst: &mut TensorDyn,
904 detect: &[DetectBox],
905 segmentation: &[Segmentation],
906 overlay: crate::MaskOverlay<'_>,
907 ) -> Result<()> {
908 require_draw_access(dst, overlay.background)?;
912 prepare_dst_base_cpu(dst, overlay.background)?;
913 let dst = dst.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
914 self.draw_decoded_masks_impl(
915 dst,
916 detect,
917 segmentation,
918 overlay.opacity,
919 overlay.color_mode,
920 )
921 }
922
923 fn draw_proto_masks(
924 &mut self,
925 dst: &mut TensorDyn,
926 detect: &[DetectBox],
927 proto_data: &ProtoData,
928 overlay: crate::MaskOverlay<'_>,
929 ) -> Result<()> {
930 require_draw_access(dst, overlay.background)?;
931 prepare_dst_base_cpu(dst, overlay.background)?;
932 let dst = dst.as_typed_mut::<u8>().ok_or(Error::NotAnImage)?;
933 self.draw_proto_masks_impl(
934 dst,
935 detect,
936 proto_data,
937 overlay.opacity,
938 overlay.content_region,
939 overlay.color_mode,
940 )
941 }
942
943 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
944 for (c, new_c) in self.colors.iter_mut().zip(colors.iter()) {
945 *c = *new_c;
946 }
947 Ok(())
948 }
949}
950
951impl CPUProcessor {
953 #[allow(clippy::too_many_arguments)]
956 fn convert_mapped(
957 &mut self,
958 src: &TensorDyn,
959 dst: &mut TensorDyn,
960 rotation: Rotation,
961 flip: Flip,
962 crop: ResolvedCrop,
963 map: &crate::options::OutputMap,
964 pad_output: Option<[f32; 4]>,
965 ) -> Result<()> {
966 let src_u8 = src
967 .as_typed::<u8>()
968 .ok_or_else(|| Error::NotSupported(format!("dtype {} source", src.dtype())))?;
969 let src_fmt = src.format().ok_or(Error::NotAnImage)?;
970 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
971 let dw = dst.width().ok_or(Error::NotAnImage)?;
972 let dh = dst.height().ok_or(Error::NotAnImage)?;
973 let mut tmp = self.take_scratch(dw, dh, dst_fmt)?;
974 let (src_params, dst_params) = color_params(src, dst);
975 let result = (|| {
976 let tmp_u8 = tmp.as_typed_mut::<u8>().unwrap();
977 self.convert_u8(
978 src_u8, tmp_u8, src_fmt, dst_fmt, rotation, flip, crop, src_params, dst_params,
979 )?;
980 normalize::write_mapped(tmp.as_typed::<u8>().unwrap(), dst, map)?;
981 if let (Some(pad), Some(rect)) = (pad_output, crop.dst_rect) {
982 let values = map.pad_values([0; 4], Some(pad));
983 normalize::fill_outside(dst, rect, values)?;
984 }
985 Ok(())
986 })();
987 self.widen_scratch = Some(tmp);
988 result
989 }
990
991 fn take_scratch(&mut self, w: usize, h: usize, fmt: PixelFormat) -> Result<TensorDyn> {
993 let matches = self.widen_scratch.as_ref().is_some_and(|t| {
994 t.width() == Some(w) && t.height() == Some(h) && t.format() == Some(fmt)
995 });
996 if matches {
997 return Ok(self.widen_scratch.take().unwrap());
998 }
999 Ok(TensorDyn::image(
1000 w,
1001 h,
1002 fmt,
1003 DType::U8,
1004 Some(TensorMemory::Mem),
1005 edgefirst_tensor::CpuAccess::ReadWrite,
1006 )?)
1007 }
1008
1009 pub(crate) fn convert_impl(
1011 &mut self,
1012 src: &TensorDyn,
1013 dst: &mut TensorDyn,
1014 rotation: Rotation,
1015 flip: Flip,
1016 crop: ResolvedCrop,
1017 ) -> Result<()> {
1018 let src_fmt = src.format().ok_or(Error::NotAnImage)?;
1019 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
1020
1021 let (src_params, dst_params) = color_params(src, dst);
1022 match (src.dtype(), dst.dtype()) {
1023 (DType::U8, DType::U8) => {
1024 let src = src.as_typed::<u8>().unwrap();
1025 let dst = dst.as_typed_mut::<u8>().unwrap();
1026 self.convert_u8(
1027 src, dst, src_fmt, dst_fmt, rotation, flip, crop, src_params, dst_params,
1028 )
1029 }
1030 (DType::U8, DType::I8) => {
1031 let src_u8 = src.as_typed::<u8>().unwrap();
1034 let dst_i8 = dst.as_typed_mut::<i8>().unwrap();
1035 let dst_u8 = unsafe { &mut *(dst_i8 as *mut Tensor<i8> as *mut Tensor<u8>) };
1039 self.convert_u8(
1040 src_u8, dst_u8, src_fmt, dst_fmt, rotation, flip, crop, src_params, dst_params,
1041 )?;
1042 apply_int8_xor_bias_rows(dst_u8, dst_fmt)
1044 }
1045 (DType::U8, d @ (DType::F32 | DType::F16)) => {
1046 let src_u8 = src.as_typed::<u8>().unwrap();
1047 let dw = dst.width().ok_or(Error::NotAnImage)?;
1048 let dh = dst.height().ok_or(Error::NotAnImage)?;
1049 let mut tmp = self.take_scratch(dw, dh, dst_fmt)?;
1054 {
1055 let tmp_u8 = tmp.as_typed_mut::<u8>().unwrap();
1056 self.convert_u8(
1057 src_u8, tmp_u8, src_fmt, dst_fmt, rotation, flip, crop, src_params,
1058 dst_params,
1059 )?;
1060 }
1061 {
1071 let tmp_u8 = tmp.as_typed::<u8>().unwrap();
1072 let (rows, row_len) = logical_surface(tmp_u8)?;
1073 let src_stride = tensor_row_stride(tmp_u8);
1074 let dst_stride_bytes = dst.effective_row_stride().ok_or(Error::NotAnImage)?;
1075 let src_map = tmp_u8.map_read()?;
1076 guard_plane(
1077 src_map.as_slice().len(),
1078 src_stride,
1079 rows,
1080 row_len,
1081 "widen src",
1082 )?;
1083 let src_rows = src_map.as_slice().chunks(src_stride).take(rows);
1084 let elem = d.size();
1085 if !dst_stride_bytes.is_multiple_of(elem) {
1086 return Err(Error::InvalidShape(format!(
1087 "{d} destination row stride {dst_stride_bytes} is not a multiple of \
1088 the element size {elem}"
1089 )));
1090 }
1091 let dst_stride = dst_stride_bytes / elem;
1092 match d {
1093 DType::F32 => {
1094 let dst_t = dst.as_typed_mut::<f32>().unwrap();
1095 let mut dst_map = dst_t.map_for_write()?;
1096 guard_plane(
1098 dst_map.as_slice().len(),
1099 dst_stride,
1100 rows,
1101 row_len,
1102 "widen f32 dst",
1103 )?;
1104 for (s, dr) in
1108 src_rows.zip(dst_map.as_mut_slice().chunks_mut(dst_stride))
1109 {
1110 simd::widen_u8_to_f32_norm(s, &mut dr[..row_len]);
1111 }
1112 }
1113 DType::F16 => {
1114 let dst_t = dst.as_typed_mut::<half::f16>().unwrap();
1115 let mut dst_map = dst_t.map_for_write()?;
1116 guard_plane(
1117 dst_map.as_slice().len(),
1118 dst_stride,
1119 rows,
1120 row_len,
1121 "widen f16 dst",
1122 )?;
1123 for (s, dr) in
1128 src_rows.zip(dst_map.as_mut_slice().chunks_mut(dst_stride))
1129 {
1130 simd::widen_u8_to_f16_norm(s, &mut dr[..row_len]);
1131 }
1132 }
1133 _ => unreachable!(),
1134 }
1135 }
1136 self.widen_scratch = Some(tmp);
1137 Ok(())
1138 }
1139 (s, d) => Err(Error::NotSupported(format!("dtype {s} -> {d}",))),
1140 }
1141 }
1142
1143 fn reuse_or_alloc_image(
1149 cached: Option<Tensor<u8>>,
1150 w: usize,
1151 h: usize,
1152 fmt: PixelFormat,
1153 ) -> Result<Tensor<u8>> {
1154 if let Some(t) = cached {
1155 if t.width() == Some(w) && t.height() == Some(h) && t.format() == Some(fmt) {
1156 return Ok(t);
1157 }
1158 }
1159 Ok(Tensor::<u8>::image(
1160 w,
1161 h,
1162 fmt,
1163 Some(TensorMemory::Mem),
1164 edgefirst_tensor::CpuAccess::ReadWrite,
1165 )?)
1166 }
1167
1168 fn pre_resize_region(
1189 &self,
1190 src_fmt: PixelFormat,
1191 (src_w, src_h): (usize, usize),
1192 (dst_w, dst_h): (usize, usize),
1193 rotation: Rotation,
1194 crop: ResolvedCrop,
1195 ) -> Option<Rect> {
1196 use PixelFormat::{Nv12, Nv16, Nv24};
1197
1198 if !matches!(src_fmt, Nv12 | Nv16 | Nv24) {
1199 return None;
1200 }
1201 let r = crop.src_rect?;
1202 let full_src = Rect {
1203 left: 0,
1204 top: 0,
1205 width: src_w,
1206 height: src_h,
1207 };
1208 if r == full_src {
1209 return None;
1210 }
1211
1212 let d = crop.dst_rect.unwrap_or(Rect {
1217 left: 0,
1218 top: 0,
1219 width: dst_w,
1220 height: dst_h,
1221 });
1222 let (dst_x, dst_y) = match rotation {
1223 Rotation::None | Rotation::Rotate180 => (d.width, d.height),
1224 Rotation::Clockwise90 | Rotation::CounterClockwise90 => (d.height, d.width),
1225 };
1226 let halo_x = self.filter_halo(r.width, dst_x)?;
1227 let halo_y = self.filter_halo(r.height, dst_y)?;
1228
1229 let mut left = r.left.saturating_sub(halo_x);
1230 let mut top = r.top.saturating_sub(halo_y);
1231 let mut right = (r.left + r.width + halo_x).min(src_w);
1232 let mut bottom = (r.top + r.height + halo_y).min(src_h);
1233
1234 let (align_x, align_y) = match src_fmt {
1240 Nv12 => (2, 2),
1241 Nv16 => (2, 1),
1242 _ => (1, 1),
1243 };
1244 left -= left % align_x;
1245 top -= top % align_y;
1246 right = right.next_multiple_of(align_x).min(src_w);
1247 bottom = bottom.next_multiple_of(align_y).min(src_h);
1248
1249 let grown = Rect {
1250 left,
1251 top,
1252 width: right - left,
1253 height: bottom - top,
1254 };
1255 (grown != full_src).then_some(grown)
1258 }
1259
1260 #[allow(clippy::too_many_arguments)]
1262 fn convert_u8(
1263 &mut self,
1264 src: &Tensor<u8>,
1265 dst: &mut Tensor<u8>,
1266 src_fmt: PixelFormat,
1267 dst_fmt: PixelFormat,
1268 rotation: Rotation,
1269 flip: Flip,
1270 crop: ResolvedCrop,
1271 src_params: ColorParams,
1272 dst_params: ColorParams,
1273 ) -> Result<()> {
1274 use PixelFormat::*;
1275
1276 #[cfg(test)]
1277 {
1278 self.last_tmp_dims = None;
1279 }
1280
1281 let src_w = src.width().unwrap();
1282 let src_h = src.height().unwrap();
1283 let dst_w = dst.width().unwrap();
1284 let dst_h = dst.height().unwrap();
1285
1286 crop.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
1287
1288 let intermediate = match (src_fmt, dst_fmt) {
1290 (Nv12, Rgb) => Rgb,
1291 (Nv12, Rgba) => Rgba,
1292 (Nv12, Grey) => Grey,
1293 (Nv12, Yuyv) => Rgba,
1294 (Nv12, Nv16) => Rgba,
1295 (Nv12, PlanarRgb) => Rgb,
1296 (Nv12, PlanarRgba) => Rgba,
1297 (Nv16, PlanarRgb) => Rgb,
1298 (Nv16, PlanarRgba) => Rgba,
1299 (Nv24, PlanarRgb) => Rgb,
1300 (Nv24, PlanarRgba) => Rgba,
1301 (Yuyv, Rgb) => Rgb,
1302 (Yuyv, Rgba) => Rgba,
1303 (Yuyv, Grey) => Grey,
1304 (Yuyv, Yuyv) => Rgba,
1305 (Yuyv, PlanarRgb) => Rgb,
1306 (Yuyv, PlanarRgba) => Rgba,
1307 (Yuyv, Nv16) => Rgba,
1308 (Vyuy, Rgb) => Rgb,
1309 (Vyuy, Rgba) => Rgba,
1310 (Vyuy, Grey) => Grey,
1311 (Vyuy, Vyuy) => Rgba,
1312 (Vyuy, PlanarRgb) => Rgb,
1313 (Vyuy, PlanarRgba) => Rgba,
1314 (Vyuy, Nv16) => Rgba,
1315 (Rgba, Rgb) => Rgba,
1316 (Rgba, Rgba) => Rgba,
1317 (Rgba, Grey) => Grey,
1318 (Rgba, Yuyv) => Rgba,
1319 (Rgba, PlanarRgb) => Rgba,
1320 (Rgba, PlanarRgba) => Rgba,
1321 (Rgba, Nv16) => Rgba,
1322 (Rgb, Rgb) => Rgb,
1323 (Rgb, Rgba) => Rgb,
1324 (Rgb, Grey) => Grey,
1325 (Rgb, Yuyv) => Rgb,
1326 (Rgb, PlanarRgb) => Rgb,
1327 (Rgb, PlanarRgba) => Rgb,
1328 (Rgb, Nv16) => Rgb,
1329 (Grey, Rgb) => Rgb,
1330 (Grey, Rgba) => Rgba,
1331 (Grey, Grey) => Grey,
1332 (Grey, Yuyv) => Grey,
1333 (Grey, PlanarRgb) => Grey,
1334 (Grey, PlanarRgba) => Grey,
1335 (Grey, Nv16) => Grey,
1336 (Nv12, Bgra) => Rgba,
1337 (Yuyv, Bgra) => Rgba,
1338 (Vyuy, Bgra) => Rgba,
1339 (Rgba, Bgra) => Rgba,
1340 (Rgb, Bgra) => Rgb,
1341 (Grey, Bgra) => Grey,
1342 (Bgra, Bgra) => Bgra,
1343 (Nv16, Rgb) => Rgb,
1344 (Nv16, Rgba) => Rgba,
1345 (Nv16, Bgra) => Rgba,
1346 (Nv24, Rgb) => Rgb,
1347 (Nv24, Rgba) => Rgba,
1348 (Nv24, Grey) => Grey,
1349 (Nv24, Bgra) => Rgba,
1350 (PlanarRgb, Rgb) => Rgb,
1351 (PlanarRgb, Rgba) => Rgb,
1352 (PlanarRgb, Bgra) => Rgb,
1353 (PlanarRgba, Rgb) => Rgba,
1354 (PlanarRgba, Rgba) => Rgba,
1355 (PlanarRgba, Bgra) => Rgba,
1356 (s, d) => {
1357 return Err(Error::NotSupported(format!("Conversion from {s} to {d}",)));
1358 }
1359 };
1360
1361 let need_resize_flip_rotation = rotation != Rotation::None
1362 || flip != Flip::None
1363 || src_w != dst_w
1364 || src_h != dst_h
1365 || crop.src_rect.is_some_and(|c| {
1366 c != Rect {
1367 left: 0,
1368 top: 0,
1369 width: src_w,
1370 height: src_h,
1371 }
1372 })
1373 || crop.dst_rect.is_some_and(|c| {
1374 c != Rect {
1375 left: 0,
1376 top: 0,
1377 width: dst_w,
1378 height: dst_h,
1379 }
1380 });
1381
1382 let direct_is_yuv_src = matches!(src_fmt, Nv12 | Nv16 | Nv24 | Yuyv | Vyuy);
1385 let direct_params = if direct_is_yuv_src {
1386 src_params
1387 } else {
1388 dst_params
1389 };
1390
1391 let full_dst_rect = Rect {
1408 left: 0,
1409 top: 0,
1410 width: dst_w,
1411 height: dst_h,
1412 };
1413 let fused_region = if rotation != Rotation::None || flip != Flip::None {
1414 None
1415 } else {
1416 match crop.src_rect {
1417 None if src_w == dst_w && src_h == dst_h => Some(None),
1418 None => None,
1419 Some(r)
1420 if r.width == dst_w
1421 && r.height == dst_h
1422 && crop.dst_rect.is_none_or(|d| d == full_dst_rect)
1423 && chroma_alignment_ok(src_fmt, r) =>
1424 {
1425 Some(Some(r))
1426 }
1427 Some(_) => None,
1428 }
1429 };
1430 if let Some(region) = fused_region {
1431 if matches!(src_fmt, Nv12 | Nv16 | Nv24) && matches!(dst_fmt, PlanarRgb | PlanarRgba) {
1432 #[cfg(test)]
1433 {
1434 self.fused_hits += 1;
1435 }
1436 return self.convert_nv_to_planar_fused(
1437 src,
1438 dst,
1439 src_fmt,
1440 dst_fmt,
1441 direct_params,
1442 region,
1443 );
1444 }
1445 }
1446
1447 if !need_resize_flip_rotation && Self::support_conversion_pf(src_fmt, dst_fmt) {
1449 return Self::convert_format_pf(src, dst, src_fmt, dst_fmt, direct_params);
1450 }
1451
1452 if dst_fmt == Yuyv && !dst_w.is_multiple_of(2) {
1454 return Err(Error::NotSupported(format!(
1455 "{} destination must have width divisible by 2",
1456 dst_fmt,
1457 )));
1458 }
1459
1460 let mut cached_tmp = self.convert_tmp.take();
1468 let mut cached_tmp2 = self.convert_tmp2.take();
1469
1470 let pre_region = if intermediate != src_fmt {
1475 self.pre_resize_region(src_fmt, (src_w, src_h), (dst_w, dst_h), rotation, crop)
1476 } else {
1477 None
1478 };
1479
1480 let tmp_holder: Option<Tensor<u8>> = if intermediate != src_fmt {
1482 let _s = tracing::trace_span!(
1483 "image.convert.cpu.format_convert",
1484 from = ?src_fmt,
1485 to = ?intermediate,
1486 pass = "pre_resize",
1487 )
1488 .entered();
1489 let (tmp_w, tmp_h) = pre_region.map_or((src_w, src_h), |g| (g.width, g.height));
1490 let mut t = Self::reuse_or_alloc_image(cached_tmp.take(), tmp_w, tmp_h, intermediate)?;
1491 #[cfg(test)]
1492 {
1493 self.last_tmp_dims = Some((tmp_w, tmp_h));
1494 }
1495 match pre_region {
1496 Some(g) => {
1497 let mut sub = Self::reuse_or_alloc_image(
1498 self.convert_src_sub.take(),
1499 g.width,
1500 g.height,
1501 src_fmt,
1502 )?;
1503 {
1504 let _s = tracing::trace_span!(
1505 "image.convert.cpu.extract_region",
1506 region_w = g.width,
1507 region_h = g.height,
1508 )
1509 .entered();
1510 Self::extract_nv_region(src, &mut sub, src_fmt, g)?;
1511 }
1512 Self::convert_format_pf(&sub, &mut t, src_fmt, intermediate, src_params)?;
1513 self.convert_src_sub = Some(sub);
1514 }
1515 None => Self::convert_format_pf(src, &mut t, src_fmt, intermediate, src_params)?,
1516 }
1517 Some(t)
1518 } else {
1519 None
1520 };
1521
1522 let crop = match (pre_region, crop.src_rect) {
1527 (Some(g), Some(r)) => ResolvedCrop {
1528 src_rect: Some(Rect {
1529 left: r.left - g.left,
1530 top: r.top - g.top,
1531 ..r
1532 }),
1533 ..crop
1534 },
1535 _ => crop,
1536 };
1537 let (tmp, tmp_fmt): (&Tensor<u8>, PixelFormat) = match &tmp_holder {
1538 Some(t) => (t, intermediate),
1539 None => (src, src_fmt),
1540 };
1541
1542 debug_assert!(matches!(tmp_fmt, Rgb | Rgba | Grey));
1544 if tmp_fmt == dst_fmt {
1545 let _s = tracing::trace_span!("image.convert.cpu.resize_flip_rotate").entered();
1546 self.resize_flip_rotate_pf(tmp, dst, dst_fmt, rotation, flip, crop)?;
1547 } else if !need_resize_flip_rotation {
1548 let _s = tracing::trace_span!(
1549 "image.convert.cpu.format_convert",
1550 from = ?tmp_fmt,
1551 to = ?dst_fmt,
1552 pass = "direct",
1553 )
1554 .entered();
1555 Self::convert_format_pf(tmp, dst, tmp_fmt, dst_fmt, dst_params)?;
1556 } else {
1557 let mut tmp2 = Self::reuse_or_alloc_image(cached_tmp2.take(), dst_w, dst_h, tmp_fmt)?;
1558 if crop.dst_rect.is_some_and(|c| {
1559 c != Rect {
1560 left: 0,
1561 top: 0,
1562 width: dst_w,
1563 height: dst_h,
1564 }
1565 }) && crop.dst_color.is_none()
1566 {
1567 Self::convert_format_pf(dst, &mut tmp2, dst_fmt, tmp_fmt, dst_params)?;
1568 }
1569 {
1570 let _s = tracing::trace_span!("image.convert.cpu.resize_flip_rotate").entered();
1571 self.resize_flip_rotate_pf(tmp, &mut tmp2, tmp_fmt, rotation, flip, crop)?;
1572 }
1573 {
1574 let _s = tracing::trace_span!(
1575 "image.convert.cpu.format_convert",
1576 from = ?tmp_fmt,
1577 to = ?dst_fmt,
1578 pass = "post_resize",
1579 )
1580 .entered();
1581 Self::convert_format_pf(&tmp2, dst, tmp_fmt, dst_fmt, dst_params)?;
1582 }
1583 cached_tmp2 = Some(tmp2);
1584 }
1585 if let Some(t) = tmp_holder {
1588 cached_tmp = Some(t);
1589 }
1590 self.convert_tmp = cached_tmp;
1591 self.convert_tmp2 = cached_tmp2;
1592
1593 if let (Some(dst_rect), Some(dst_color)) = (crop.dst_rect, crop.dst_color) {
1594 let full_rect = Rect {
1595 left: 0,
1596 top: 0,
1597 width: dst_w,
1598 height: dst_h,
1599 };
1600 if dst_rect != full_rect {
1601 Self::fill_image_outside_crop_u8(dst, dst_color, dst_rect, false)?;
1602 }
1603 }
1604
1605 Ok(())
1606 }
1607
1608 fn draw_decoded_masks_impl(
1609 &mut self,
1610 dst: &mut Tensor<u8>,
1611 detect: &[DetectBox],
1612 segmentation: &[Segmentation],
1613 opacity: f32,
1614 color_mode: crate::ColorMode,
1615 ) -> Result<()> {
1616 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
1617 if !matches!(dst_fmt, PixelFormat::Rgba | PixelFormat::Rgb) {
1618 return Err(crate::Error::NotSupported(
1619 "CPU image rendering only supports RGBA or RGB images".to_string(),
1620 ));
1621 }
1622
1623 let _timer = FunctionTimer::new("CPUProcessor::draw_decoded_masks");
1624
1625 let dst_w = dst.width().unwrap();
1626 let dst_h = dst.height().unwrap();
1627 let dst_rs = tensor_row_stride(dst);
1628 let dst_c = dst_fmt.channels();
1629
1630 let mut map = dst.map_mut()?;
1631 let dst_slice = map.as_mut_slice();
1632
1633 self.render_box(dst_w, dst_h, dst_rs, dst_c, dst_slice, detect, color_mode)?;
1634
1635 if segmentation.is_empty() {
1636 return Ok(());
1637 }
1638
1639 if let Some(bad) = segmentation
1646 .iter()
1647 .find(|s| s.segmentation.shape().len() != 3)
1648 {
1649 return Err(crate::Error::InvalidShape(format!(
1650 "segmentation must be [H, W, C], got {:?}",
1651 bad.segmentation.shape()
1652 )));
1653 }
1654
1655 let is_semantic = segmentation[0].segmentation.shape()[2] > 1;
1658
1659 if is_semantic {
1660 self.render_modelpack_segmentation(
1661 dst_w,
1662 dst_h,
1663 dst_rs,
1664 dst_c,
1665 dst_slice,
1666 &segmentation[0],
1667 opacity,
1668 )?;
1669 } else {
1670 for (idx, (seg, det)) in segmentation.iter().zip(detect).enumerate() {
1671 let color_index = color_mode.index(idx, det.label);
1672 self.render_yolo_segmentation(
1673 dst_w,
1674 dst_h,
1675 dst_rs,
1676 dst_c,
1677 dst_slice,
1678 seg,
1679 color_index,
1680 opacity,
1681 )?;
1682 }
1683 }
1684
1685 Ok(())
1686 }
1687
1688 fn draw_proto_masks_impl(
1689 &mut self,
1690 dst: &mut Tensor<u8>,
1691 detect: &[DetectBox],
1692 proto_data: &ProtoData,
1693 opacity: f32,
1694 content_region: Option<[f32; 4]>,
1695 color_mode: crate::ColorMode,
1696 ) -> Result<()> {
1697 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
1698 if !matches!(dst_fmt, PixelFormat::Rgba | PixelFormat::Rgb) {
1699 return Err(crate::Error::NotSupported(
1700 "CPU image rendering only supports RGBA or RGB images".to_string(),
1701 ));
1702 }
1703
1704 let _timer = FunctionTimer::new("CPUProcessor::draw_proto_masks");
1705
1706 let dst_w = dst.width().unwrap();
1707 let dst_h = dst.height().unwrap();
1708 let dst_rs = tensor_row_stride(dst);
1709 let channels = dst_fmt.channels();
1710
1711 let mut map = dst.map_mut()?;
1712 let dst_slice = map.as_mut_slice();
1713
1714 self.render_box(
1715 dst_w, dst_h, dst_rs, channels, dst_slice, detect, color_mode,
1716 )?;
1717
1718 if detect.is_empty() {
1719 return Ok(());
1720 }
1721 let proto_shape = proto_data.protos.shape();
1722 if proto_shape.len() != 3 {
1723 return Err(Error::InvalidShape(format!(
1724 "protos tensor must be rank-3, got {proto_shape:?}"
1725 )));
1726 }
1727 let proto_h = proto_shape[0];
1728 let proto_w = proto_shape[1];
1729 let num_protos = proto_shape[2];
1730 let coeff_shape = proto_data.mask_coefficients.shape();
1731 if coeff_shape.len() != 2 {
1732 return Err(Error::InvalidShape(format!(
1733 "mask_coefficients tensor must be rank-2, got {coeff_shape:?}"
1734 )));
1735 }
1736 if coeff_shape[0] == 0 {
1738 return Ok(());
1739 }
1740 if coeff_shape[1] != num_protos {
1741 return Err(Error::InvalidShape(format!(
1742 "mask_coefficients second dimension must match num_protos \
1743 ({num_protos}), got {coeff_shape:?}"
1744 )));
1745 }
1746
1747 let coeff_f32: Vec<f32> = match proto_data.mask_coefficients.dtype() {
1749 DType::F32 => {
1750 let t = proto_data.mask_coefficients.as_typed::<f32>().expect("F32");
1751 let m = t.map_read()?;
1752 m.as_slice().to_vec()
1753 }
1754 DType::F16 => {
1755 let t = proto_data
1756 .mask_coefficients
1757 .as_typed::<half::f16>()
1758 .expect("F16");
1759 let m = t.map_read()?;
1760 m.as_slice().iter().map(|v| v.to_f32()).collect()
1761 }
1762 DType::I8 => {
1763 let t = proto_data.mask_coefficients.as_typed::<i8>().expect("I8");
1764 let m = t.map_read()?;
1765 if let Some(q) = t.quantization() {
1766 use edgefirst_tensor::QuantMode;
1767 let (scale, zp) = match q.mode() {
1768 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1769 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1770 other => {
1771 return Err(Error::NotSupported(format!(
1772 "I8 mask_coefficients quantization mode {other:?} not supported"
1773 )));
1774 }
1775 };
1776 m.as_slice()
1777 .iter()
1778 .map(|&v| (v as f32 - zp) * scale)
1779 .collect()
1780 } else {
1781 m.as_slice().iter().map(|&v| v as f32).collect()
1782 }
1783 }
1784 DType::I16 => {
1785 let t = proto_data.mask_coefficients.as_typed::<i16>().expect("I16");
1786 let m = t.map_read()?;
1787 if let Some(q) = t.quantization() {
1788 use edgefirst_tensor::QuantMode;
1789 let (scale, zp) = match q.mode() {
1790 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1791 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1792 other => {
1793 return Err(Error::NotSupported(format!(
1794 "I16 mask_coefficients quantization mode {other:?} not supported"
1795 )));
1796 }
1797 };
1798 m.as_slice()
1799 .iter()
1800 .map(|&v| (v as f32 - zp) * scale)
1801 .collect()
1802 } else {
1803 m.as_slice().iter().map(|&v| v as f32).collect()
1804 }
1805 }
1806 other => {
1807 return Err(Error::InvalidShape(format!(
1808 "mask_coefficients dtype {other:?} not supported"
1809 )));
1810 }
1811 };
1812
1813 let (lx0, lx_range, ly0, ly_range) = match content_region {
1815 Some([lx0, ly0, lx1, ly1]) => (lx0, lx1 - lx0, ly0, ly1 - ly0),
1816 None => (0.0_f32, 1.0_f32, 0.0_f32, 1.0_f32),
1817 };
1818
1819 match proto_data.protos.dtype() {
1822 DType::F32 => {
1823 let t = proto_data.protos.as_typed::<f32>().expect("F32");
1824 let m = t.map_read()?;
1825 self.draw_proto_masks_inner(
1826 dst_slice,
1827 dst_w,
1828 dst_h,
1829 dst_rs,
1830 channels,
1831 detect,
1832 m.as_slice(),
1833 &coeff_f32,
1834 proto_h,
1835 proto_w,
1836 num_protos,
1837 opacity,
1838 (lx0, lx_range, ly0, ly_range),
1839 color_mode,
1840 0.0_f32,
1841 |p: &f32, _| *p,
1842 );
1843 }
1844 DType::F16 => {
1845 let t = proto_data.protos.as_typed::<half::f16>().expect("F16");
1846 let m = t.map_read()?;
1847 self.draw_proto_masks_inner(
1848 dst_slice,
1849 dst_w,
1850 dst_h,
1851 dst_rs,
1852 channels,
1853 detect,
1854 m.as_slice(),
1855 &coeff_f32,
1856 proto_h,
1857 proto_w,
1858 num_protos,
1859 opacity,
1860 (lx0, lx_range, ly0, ly_range),
1861 color_mode,
1862 0.0_f32,
1863 |p: &half::f16, _| p.to_f32(),
1864 );
1865 }
1866 DType::I8 => {
1867 use edgefirst_tensor::QuantMode;
1868 let t = proto_data.protos.as_typed::<i8>().expect("I8");
1869 let m = t.map_read()?;
1870 let quant = t.quantization().ok_or_else(|| {
1871 Error::InvalidShape("I8 protos require quantization metadata".into())
1872 })?;
1873 let (scale, zp) = match quant.mode() {
1874 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1875 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1876 QuantMode::PerChannel { axis, .. }
1877 | QuantMode::PerChannelSymmetric { axis, .. } => {
1878 return Err(Error::NotSupported(format!(
1879 "per-channel quantization (axis={axis}) in draw_proto_masks \
1880 CPU path not yet supported"
1881 )));
1882 }
1883 };
1884 self.draw_proto_masks_inner(
1885 dst_slice,
1886 dst_w,
1887 dst_h,
1888 dst_rs,
1889 channels,
1890 detect,
1891 m.as_slice(),
1892 &coeff_f32,
1893 proto_h,
1894 proto_w,
1895 num_protos,
1896 opacity,
1897 (lx0, lx_range, ly0, ly_range),
1898 color_mode,
1899 scale,
1900 move |p: &i8, _| (*p as f32) - zp,
1901 );
1902 }
1903 other => {
1904 return Err(Error::InvalidShape(format!(
1905 "proto tensor dtype {other:?} not supported"
1906 )));
1907 }
1908 }
1909
1910 Ok(())
1911 }
1912
1913 #[allow(clippy::too_many_arguments)]
1914 fn draw_proto_masks_inner<P: Copy>(
1915 &self,
1916 dst_slice: &mut [u8],
1917 dst_w: usize,
1918 dst_h: usize,
1919 dst_rs: usize,
1920 channels: usize,
1921 detect: &[DetectBox],
1922 protos: &[P],
1923 coeff_all_f32: &[f32],
1924 proto_h: usize,
1925 proto_w: usize,
1926 num_protos: usize,
1927 opacity: f32,
1928 letterbox_xy: (f32, f32, f32, f32),
1929 color_mode: crate::ColorMode,
1930 acc_scale: f32,
1931 load_f32: impl Fn(&P, f32) -> f32 + Copy,
1932 ) {
1933 let (lx0, lx_range, ly0, ly_range) = letterbox_xy;
1934 let stride_y = proto_w * num_protos;
1935 for (idx, det) in detect.iter().enumerate() {
1936 let coeff = &coeff_all_f32[idx * num_protos..(idx + 1) * num_protos];
1937 let color_index = color_mode.index(idx, det.label);
1938 let color = self.colors[color_index % self.colors.len()];
1939 let alpha = if opacity == 1.0 {
1940 color[3] as u16
1941 } else {
1942 (color[3] as f32 * opacity).round() as u16
1943 };
1944
1945 let start_x = (dst_w as f32 * det.bbox.xmin).round() as usize;
1946 let start_y = (dst_h as f32 * det.bbox.ymin).round() as usize;
1947 let end_x = ((dst_w as f32 * det.bbox.xmax).round() as usize).min(dst_w);
1948 let end_y = ((dst_h as f32 * det.bbox.ymax).round() as usize).min(dst_h);
1949
1950 for y in start_y..end_y {
1951 for x in start_x..end_x {
1952 let px = (lx0 + (x as f32 / dst_w as f32) * lx_range) * proto_w as f32 - 0.5;
1953 let py = (ly0 + (y as f32 / dst_h as f32) * ly_range) * proto_h as f32 - 0.5;
1954
1955 let x0 = (px.floor() as isize).clamp(0, proto_w as isize - 1) as usize;
1959 let y0 = (py.floor() as isize).clamp(0, proto_h as isize - 1) as usize;
1960 let x1 = (x0 + 1).min(proto_w - 1);
1961 let y1 = (y0 + 1).min(proto_h - 1);
1962 let fx = px - px.floor();
1963 let fy = py - py.floor();
1964 let w00 = (1.0 - fx) * (1.0 - fy);
1965 let w10 = fx * (1.0 - fy);
1966 let w01 = (1.0 - fx) * fy;
1967 let w11 = fx * fy;
1968 let b00 = y0 * stride_y + x0 * num_protos;
1969 let b10 = y0 * stride_y + x1 * num_protos;
1970 let b01 = y1 * stride_y + x0 * num_protos;
1971 let b11 = y1 * stride_y + x1 * num_protos;
1972 let mut acc = 0.0_f32;
1973 for p in 0..num_protos {
1974 let v00 = load_f32(&protos[b00 + p], 0.0);
1975 let v10 = load_f32(&protos[b10 + p], 0.0);
1976 let v01 = load_f32(&protos[b01 + p], 0.0);
1977 let v11 = load_f32(&protos[b11 + p], 0.0);
1978 let val = w00 * v00 + w10 * v10 + w01 * v01 + w11 * v11;
1979 acc += coeff[p] * val;
1980 }
1981 let final_acc = if acc_scale == 0.0 {
1982 acc
1983 } else {
1984 acc_scale * acc
1985 };
1986 let mask = 1.0 / (1.0 + (-final_acc).exp());
1990 if mask < 0.5 {
1991 continue;
1992 }
1993 let dst_index = y * dst_rs + x * channels;
1994 for c in 0..3 {
1995 dst_slice[dst_index + c] = ((color[c] as u16 * alpha
1996 + dst_slice[dst_index + c] as u16 * (255 - alpha))
1997 / 255) as u8;
1998 }
1999 }
2000 }
2001 }
2002 }
2003}