1use crate::{
5 Crop, Error, Flip, FunctionTimer, ImageProcessorTrait, Rect, ResolvedCrop, Result, Rotation,
6};
7use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
8use edgefirst_tensor::{
9 DType, PixelFormat, Tensor, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait,
10};
11
12mod convert;
13mod masks;
14mod resize;
15mod simd;
16mod tests;
17
18#[derive(Debug, Clone, Copy)]
30pub(crate) struct ColorParams {
31 pub matrix: yuv::YuvStandardMatrix,
33 pub range: yuv::YuvRange,
35 pub encoding: edgefirst_tensor::ColorEncoding,
40 pub range_kind: edgefirst_tensor::ColorRange,
41 pub src_full_range: bool,
44 pub dst_full_range: bool,
47}
48
49#[derive(Debug)]
52pub struct CPUProcessor {
53 resizer: fast_image_resize::Resizer,
54 options: fast_image_resize::ResizeOptions,
55 colors: [[u8; 4]; 20],
56 widen_scratch: Option<TensorDyn>,
62 resize_destride_scratch: Vec<u8>,
70 nv_strip_scratch: Vec<u8>,
76 convert_tmp: Option<Tensor<u8>>,
83 convert_tmp2: Option<Tensor<u8>>,
84}
85
86impl Clone for CPUProcessor {
92 fn clone(&self) -> Self {
93 Self {
94 resizer: self.resizer.clone(),
95 options: self.options,
96 colors: self.colors,
97 widen_scratch: None,
98 resize_destride_scratch: Vec::new(),
99 nv_strip_scratch: Vec::new(),
100 convert_tmp: None,
101 convert_tmp2: None,
102 }
103 }
104}
105
106unsafe impl Send for CPUProcessor {}
107unsafe impl Sync for CPUProcessor {}
108
109impl Default for CPUProcessor {
110 fn default() -> Self {
111 Self::new_bilinear()
112 }
113}
114
115fn prepare_dst_base_cpu(dst: &mut TensorDyn, background: Option<&TensorDyn>) -> Result<()> {
126 match background {
127 Some(bg) => {
128 if bg.shape() != dst.shape() {
129 return Err(Error::InvalidShape(
130 "background shape does not match dst".into(),
131 ));
132 }
133 if bg.format() != dst.format() {
134 return Err(Error::InvalidShape(
135 "background pixel format does not match dst".into(),
136 ));
137 }
138 let bg_u8 = bg.as_u8().ok_or(Error::NotAnImage)?;
139 let dst_u8 = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
140 let bg_map = bg_u8.map()?;
141 let mut dst_map = dst_u8.map()?;
142 let bg_slice = bg_map.as_slice();
143 let dst_slice = dst_map.as_mut_slice();
144 if bg_slice.len() != dst_slice.len() {
145 return Err(Error::InvalidShape(
146 "background buffer size does not match dst".into(),
147 ));
148 }
149 dst_slice.copy_from_slice(bg_slice);
150 }
151 None => {
152 let dst_u8 = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
153 let mut dst_map = dst_u8.map()?;
154 dst_map.as_mut_slice().fill(0);
155 }
156 }
157 Ok(())
158}
159
160fn row_stride_for(width: usize, fmt: PixelFormat) -> usize {
162 use edgefirst_tensor::PixelLayout;
163 match fmt.layout() {
164 PixelLayout::Packed => width * fmt.channels(),
165 PixelLayout::Planar | PixelLayout::SemiPlanar => width,
166 _ => width, }
168}
169
170fn tensor_row_stride(tensor: &Tensor<u8>) -> usize {
175 tensor.effective_row_stride().unwrap_or_else(|| {
176 let w = tensor.width().unwrap_or(0);
177 let fmt = tensor.format().unwrap_or(PixelFormat::Rgb);
178 row_stride_for(w, fmt)
179 })
180}
181
182fn split_semi_planar(
188 bytes: &[u8],
189 stride: usize,
190 src_h: usize,
191 fmt: PixelFormat,
192) -> Result<(&[u8], &[u8])> {
193 let total_h = fmt.combined_plane_height(src_h).unwrap_or(src_h);
194 let need = stride.checked_mul(total_h).ok_or_else(|| {
195 Error::InvalidShape(format!(
196 "{fmt:?} plane size overflow (stride={stride}, h={src_h})"
197 ))
198 })?;
199 if bytes.len() < need {
200 return Err(Error::InvalidShape(format!(
201 "{fmt:?} source has {} bytes but needs {need} (stride={stride}, h={src_h})",
202 bytes.len()
203 )));
204 }
205 Ok(bytes.split_at(stride * src_h))
206}
207
208fn split_semi_planar_mut(
215 bytes: &mut [u8],
216 stride: usize,
217 dst_h: usize,
218 fmt: PixelFormat,
219) -> Result<(&mut [u8], &mut [u8])> {
220 let total_h = fmt.combined_plane_height(dst_h).unwrap_or(dst_h);
221 let need = stride.checked_mul(total_h).ok_or_else(|| {
222 Error::InvalidShape(format!(
223 "{fmt:?} plane size overflow (stride={stride}, combined_h={total_h})"
224 ))
225 })?;
226 if bytes.len() < need {
227 return Err(Error::InvalidShape(format!(
228 "{fmt:?} destination has {} bytes but needs {need} (stride={stride}, combined_h={total_h})",
229 bytes.len()
230 )));
231 }
232 Ok(bytes.split_at_mut(stride * dst_h))
233}
234
235fn guard_plane(
241 buf_len: usize,
242 stride: usize,
243 rows: usize,
244 row_bytes: usize,
245 what: &str,
246) -> Result<()> {
247 let need = stride.checked_mul(rows).ok_or_else(|| {
248 Error::InvalidShape(format!(
249 "{what} plane size overflow (stride={stride}, rows={rows})"
250 ))
251 })?;
252 if row_bytes > stride || buf_len < need {
253 return Err(Error::InvalidShape(format!(
254 "{what} buffer too small: {buf_len} bytes, need {need} (stride={stride}, rows={rows}, row_bytes={row_bytes})"
255 )));
256 }
257 Ok(())
258}
259
260pub(crate) fn apply_int8_xor_bias(data: &mut [u8], fmt: PixelFormat) {
265 use edgefirst_tensor::PixelLayout;
266 if !fmt.has_alpha() {
267 for b in data.iter_mut() {
268 *b ^= 0x80;
269 }
270 } else if fmt.layout() == PixelLayout::Planar {
271 let channels = fmt.channels();
273 let plane_size = data.len() / channels;
274 for b in data[..plane_size * (channels - 1)].iter_mut() {
275 *b ^= 0x80;
276 }
277 } else {
278 let channels = fmt.channels();
280 for pixel in data.chunks_exact_mut(channels) {
281 for b in &mut pixel[..channels - 1] {
282 *b ^= 0x80;
283 }
284 }
285 }
286}
287
288impl CPUProcessor {
289 pub fn new() -> Self {
291 Self::new_bilinear()
292 }
293
294 fn new_bilinear() -> Self {
296 let resizer = fast_image_resize::Resizer::new();
297 let options = fast_image_resize::ResizeOptions::new()
298 .resize_alg(fast_image_resize::ResizeAlg::Convolution(
299 fast_image_resize::FilterType::Bilinear,
300 ))
301 .use_alpha(false);
302
303 log::debug!("CPUConverter created");
304 Self {
305 resizer,
306 options,
307 colors: crate::DEFAULT_COLORS_U8,
308 widen_scratch: None,
309 resize_destride_scratch: Vec::new(),
310 nv_strip_scratch: Vec::new(),
311 convert_tmp: None,
312 convert_tmp2: None,
313 }
314 }
315
316 pub fn new_nearest() -> Self {
318 let resizer = fast_image_resize::Resizer::new();
319 let options = fast_image_resize::ResizeOptions::new()
320 .resize_alg(fast_image_resize::ResizeAlg::Nearest)
321 .use_alpha(false);
322 log::debug!("CPUConverter created");
323 Self {
324 resizer,
325 options,
326 colors: crate::DEFAULT_COLORS_U8,
327 widen_scratch: None,
328 resize_destride_scratch: Vec::new(),
329 nv_strip_scratch: Vec::new(),
330 convert_tmp: None,
331 convert_tmp2: None,
332 }
333 }
334
335 pub(crate) fn support_conversion_pf(src: PixelFormat, dst: PixelFormat) -> bool {
336 use PixelFormat::*;
337 matches!(
338 (src, dst),
339 (Nv12, Rgb)
340 | (Nv12, Rgba)
341 | (Nv12, Grey)
342 | (Nv16, Rgb)
343 | (Nv16, Rgba)
344 | (Nv16, Bgra)
345 | (Nv24, Rgb)
346 | (Nv24, Rgba)
347 | (Nv24, Grey)
348 | (Nv24, Bgra)
349 | (Yuyv, Rgb)
350 | (Yuyv, Rgba)
351 | (Yuyv, Grey)
352 | (Yuyv, Yuyv)
353 | (Yuyv, PlanarRgb)
354 | (Yuyv, PlanarRgba)
355 | (Yuyv, Nv16)
356 | (Vyuy, Rgb)
357 | (Vyuy, Rgba)
358 | (Vyuy, Grey)
359 | (Vyuy, Vyuy)
360 | (Vyuy, PlanarRgb)
361 | (Vyuy, PlanarRgba)
362 | (Vyuy, Nv16)
363 | (Rgba, Rgb)
364 | (Rgba, Rgba)
365 | (Rgba, Grey)
366 | (Rgba, Yuyv)
367 | (Rgba, PlanarRgb)
368 | (Rgba, PlanarRgba)
369 | (Rgba, Nv16)
370 | (Rgb, Rgb)
371 | (Rgb, Rgba)
372 | (Rgb, Grey)
373 | (Rgb, Yuyv)
374 | (Rgb, PlanarRgb)
375 | (Rgb, PlanarRgba)
376 | (Rgb, Nv16)
377 | (Grey, Rgb)
378 | (Grey, Rgba)
379 | (Grey, Grey)
380 | (Grey, Yuyv)
381 | (Grey, PlanarRgb)
382 | (Grey, PlanarRgba)
383 | (Grey, Nv16)
384 | (Nv12, Bgra)
385 | (Yuyv, Bgra)
386 | (Vyuy, Bgra)
387 | (Rgba, Bgra)
388 | (Rgb, Bgra)
389 | (Grey, Bgra)
390 | (Bgra, Bgra)
391 | (PlanarRgb, Rgb)
392 | (PlanarRgb, Rgba)
393 | (PlanarRgba, Rgb)
394 | (PlanarRgba, Rgba)
395 | (PlanarRgb, Bgra)
396 | (PlanarRgba, Bgra)
397 )
398 }
399
400 pub(crate) fn convert_format_pf(
402 src: &Tensor<u8>,
403 dst: &mut Tensor<u8>,
404 src_fmt: PixelFormat,
405 dst_fmt: PixelFormat,
406 cp: ColorParams,
407 ) -> Result<()> {
408 let _timer = FunctionTimer::new(format!(
409 "ImageProcessor::convert_format {} to {}",
410 src_fmt, dst_fmt,
411 ));
412
413 use PixelFormat::*;
414 match (src_fmt, dst_fmt) {
415 (Nv12, Rgb) => Self::convert_nv12_to_rgb(src, dst, cp),
416 (Nv12, Rgba) => Self::convert_nv12_to_rgba(src, dst, cp),
417 (Nv12, Grey) => Self::convert_nv12_to_grey(src, dst, cp),
418 (Yuyv, Rgb) => Self::convert_yuyv_to_rgb(src, dst, cp),
419 (Yuyv, Rgba) => Self::convert_yuyv_to_rgba(src, dst, cp),
420 (Yuyv, Grey) => Self::convert_yuyv_to_grey(src, dst, cp),
421 (Yuyv, Yuyv) => Self::copy_image(src, dst),
422 (Yuyv, PlanarRgb) => Self::convert_yuyv_to_8bps(src, dst, cp),
423 (Yuyv, PlanarRgba) => Self::convert_yuyv_to_prgba(src, dst, cp),
424 (Yuyv, Nv16) => Self::convert_yuyv_to_nv16(src, dst),
425 (Vyuy, Rgb) => Self::convert_vyuy_to_rgb(src, dst, cp),
426 (Vyuy, Rgba) => Self::convert_vyuy_to_rgba(src, dst, cp),
427 (Vyuy, Grey) => Self::convert_vyuy_to_grey(src, dst, cp),
428 (Vyuy, Vyuy) => Self::copy_image(src, dst),
429 (Vyuy, PlanarRgb) => Self::convert_vyuy_to_8bps(src, dst, cp),
430 (Vyuy, PlanarRgba) => Self::convert_vyuy_to_prgba(src, dst, cp),
431 (Vyuy, Nv16) => Self::convert_vyuy_to_nv16(src, dst),
432 (Rgba, Rgb) => Self::convert_rgba_to_rgb(src, dst),
433 (Rgba, Rgba) => Self::copy_image(src, dst),
434 (Rgba, Grey) => Self::convert_rgba_to_grey(src, dst),
435 (Rgba, Yuyv) => Self::convert_rgba_to_yuyv(src, dst, cp),
436 (Rgba, PlanarRgb) => Self::convert_rgba_to_8bps(src, dst),
437 (Rgba, PlanarRgba) => Self::convert_rgba_to_prgba(src, dst),
438 (Rgba, Nv16) => Self::convert_rgba_to_nv16(src, dst, cp),
439 (Rgb, Rgb) => Self::copy_image(src, dst),
440 (Rgb, Rgba) => Self::convert_rgb_to_rgba(src, dst),
441 (Rgb, Grey) => Self::convert_rgb_to_grey(src, dst),
442 (Rgb, Yuyv) => Self::convert_rgb_to_yuyv(src, dst, cp),
443 (Rgb, PlanarRgb) => Self::convert_rgb_to_8bps(src, dst),
444 (Rgb, PlanarRgba) => Self::convert_rgb_to_prgba(src, dst),
445 (Rgb, Nv16) => Self::convert_rgb_to_nv16(src, dst, cp),
446 (Grey, Rgb) => Self::convert_grey_to_rgb(src, dst),
447 (Grey, Rgba) => Self::convert_grey_to_rgba(src, dst),
448 (Grey, Grey) => Self::copy_image(src, dst),
449 (Grey, Yuyv) => Self::convert_grey_to_yuyv(src, dst, cp),
450 (Grey, PlanarRgb) => Self::convert_grey_to_8bps(src, dst),
451 (Grey, PlanarRgba) => Self::convert_grey_to_prgba(src, dst),
452 (Grey, Nv16) => Self::convert_grey_to_nv16(src, dst, cp),
453
454 (Nv16, Rgb) => Self::convert_nv16_to_rgb(src, dst, cp),
456 (Nv16, Rgba) => Self::convert_nv16_to_rgba(src, dst, cp),
457 (Nv24, Rgb) => Self::convert_nv24_to_rgb(src, dst, cp),
458 (Nv24, Rgba) => Self::convert_nv24_to_rgba(src, dst, cp),
459 (Nv24, Grey) => Self::convert_nv24_to_grey(src, dst, cp),
460 (PlanarRgb, Rgb) => Self::convert_8bps_to_rgb(src, dst),
461 (PlanarRgb, Rgba) => Self::convert_8bps_to_rgba(src, dst),
462 (PlanarRgba, Rgb) => Self::convert_prgba_to_rgb(src, dst),
463 (PlanarRgba, Rgba) => Self::convert_prgba_to_rgba(src, dst),
464
465 (Bgra, Bgra) => Self::copy_image(src, dst),
467 (Nv12, Bgra) => {
468 Self::convert_nv12_to_rgba(src, dst, cp)?;
469 Self::swizzle_rb_4chan(dst)
470 }
471 (Nv16, Bgra) => {
472 Self::convert_nv16_to_rgba(src, dst, cp)?;
473 Self::swizzle_rb_4chan(dst)
474 }
475 (Nv24, Bgra) => {
476 Self::convert_nv24_to_rgba(src, dst, cp)?;
477 Self::swizzle_rb_4chan(dst)
478 }
479 (Yuyv, Bgra) => {
480 Self::convert_yuyv_to_rgba(src, dst, cp)?;
481 Self::swizzle_rb_4chan(dst)
482 }
483 (Vyuy, Bgra) => {
484 Self::convert_vyuy_to_rgba(src, dst, cp)?;
485 Self::swizzle_rb_4chan(dst)
486 }
487 (Rgba, Bgra) => {
488 dst.map()?.copy_from_slice(&src.map()?);
489 Self::swizzle_rb_4chan(dst)
490 }
491 (Rgb, Bgra) => {
492 Self::convert_rgb_to_rgba(src, dst)?;
493 Self::swizzle_rb_4chan(dst)
494 }
495 (Grey, Bgra) => {
496 Self::convert_grey_to_rgba(src, dst)?;
497 Self::swizzle_rb_4chan(dst)
498 }
499 (PlanarRgb, Bgra) => {
500 Self::convert_8bps_to_rgba(src, dst)?;
501 Self::swizzle_rb_4chan(dst)
502 }
503 (PlanarRgba, Bgra) => {
504 Self::convert_prgba_to_rgba(src, dst)?;
505 Self::swizzle_rb_4chan(dst)
506 }
507
508 (s, d) => Err(Error::NotSupported(format!("Conversion from {s} to {d}",))),
509 }
510 }
511
512 pub(crate) fn fill_image_outside_crop_u8(
514 dst: &mut Tensor<u8>,
515 rgba: [u8; 4],
516 crop: Rect,
517 ) -> Result<()> {
518 let dst_fmt = dst.format().unwrap();
519 let dst_w = dst.width().unwrap();
520 let dst_h = dst.height().unwrap();
521 let cm = crate::colorimetry::resolve_colorimetry(dst.colorimetry(), dst.height());
525 let cp = ColorParams {
526 matrix: crate::colorimetry::yuv_matrix(cm.encoding.unwrap()),
527 range: crate::colorimetry::yuv_range(cm.range.unwrap()),
528 encoding: cm.encoding.unwrap(),
529 range_kind: cm.range.unwrap(),
530 src_full_range: cm.range == Some(edgefirst_tensor::ColorRange::Full),
531 dst_full_range: cm.range == Some(edgefirst_tensor::ColorRange::Full),
532 };
533 let mut dst_map = dst.map()?;
534 let dst_tup = (dst_map.as_mut_slice(), dst_w, dst_h);
535 Self::fill_outside_crop_dispatch(dst_tup, dst_fmt, rgba, crop, cp)
536 }
537
538 fn fill_outside_crop_dispatch(
540 dst: (&mut [u8], usize, usize),
541 fmt: PixelFormat,
542 rgba: [u8; 4],
543 crop: Rect,
544 cp: ColorParams,
545 ) -> Result<()> {
546 use PixelFormat::*;
547 match fmt {
548 Rgba | Bgra => Self::fill_image_outside_crop_(dst, rgba, crop),
549 Rgb => Self::fill_image_outside_crop_(dst, Self::rgba_to_rgb(rgba), crop),
550 Grey => Self::fill_image_outside_crop_(dst, Self::rgba_to_grey(rgba), crop),
551 Yuyv => Self::fill_image_outside_crop_(
552 (dst.0, dst.1 / 2, dst.2),
553 Self::rgba_to_yuyv(rgba, cp),
554 Rect::new(crop.left / 2, crop.top, crop.width.div_ceil(2), crop.height),
555 ),
556 PlanarRgb => Self::fill_image_outside_crop_planar(dst, Self::rgba_to_rgb(rgba), crop),
557 PlanarRgba => Self::fill_image_outside_crop_planar(dst, rgba, crop),
558 Nv16 => {
559 let yuyv = Self::rgba_to_yuyv(rgba, cp);
560 Self::fill_image_outside_crop_yuv_semiplanar(dst, yuyv[0], [yuyv[1], yuyv[3]], crop)
561 }
562 _ => Err(Error::Internal(format!(
563 "Found unexpected destination {fmt}",
564 ))),
565 }
566 }
567}
568
569impl ImageProcessorTrait for CPUProcessor {
570 fn convert(
571 &mut self,
572 src: &TensorDyn,
573 dst: &mut TensorDyn,
574 rotation: Rotation,
575 flip: Flip,
576 crop: Crop,
577 ) -> Result<()> {
578 let crop = crop.resolve(
579 src.width().unwrap_or(0),
580 src.height().unwrap_or(0),
581 dst.width().unwrap_or(0),
582 dst.height().unwrap_or(0),
583 )?;
584 self.convert_impl(src, dst, rotation, flip, crop)
585 }
586
587 fn draw_decoded_masks(
588 &mut self,
589 dst: &mut TensorDyn,
590 detect: &[DetectBox],
591 segmentation: &[Segmentation],
592 overlay: crate::MaskOverlay<'_>,
593 ) -> Result<()> {
594 prepare_dst_base_cpu(dst, overlay.background)?;
598 let dst = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
599 self.draw_decoded_masks_impl(
600 dst,
601 detect,
602 segmentation,
603 overlay.opacity,
604 overlay.color_mode,
605 )
606 }
607
608 fn draw_proto_masks(
609 &mut self,
610 dst: &mut TensorDyn,
611 detect: &[DetectBox],
612 proto_data: &ProtoData,
613 overlay: crate::MaskOverlay<'_>,
614 ) -> Result<()> {
615 prepare_dst_base_cpu(dst, overlay.background)?;
616 let dst = dst.as_u8_mut().ok_or(Error::NotAnImage)?;
617 self.draw_proto_masks_impl(
618 dst,
619 detect,
620 proto_data,
621 overlay.opacity,
622 overlay.letterbox,
623 overlay.color_mode,
624 )
625 }
626
627 fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
628 for (c, new_c) in self.colors.iter_mut().zip(colors.iter()) {
629 *c = *new_c;
630 }
631 Ok(())
632 }
633}
634
635impl CPUProcessor {
637 pub(crate) fn convert_impl(
639 &mut self,
640 src: &TensorDyn,
641 dst: &mut TensorDyn,
642 rotation: Rotation,
643 flip: Flip,
644 crop: ResolvedCrop,
645 ) -> Result<()> {
646 let src_fmt = src.format().ok_or(Error::NotAnImage)?;
647 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
648
649 let src_cm = crate::colorimetry::effective_colorimetry(src);
654 let dst_cm = crate::colorimetry::effective_colorimetry(dst);
655 let src_full = src_cm.range == Some(edgefirst_tensor::ColorRange::Full);
656 let dst_full = dst_cm.range == Some(edgefirst_tensor::ColorRange::Full);
657 let src_params = ColorParams {
658 matrix: crate::colorimetry::yuv_matrix(src_cm.encoding.unwrap()),
659 range: crate::colorimetry::yuv_range(src_cm.range.unwrap()),
660 encoding: src_cm.encoding.unwrap(),
661 range_kind: src_cm.range.unwrap(),
662 src_full_range: src_full,
663 dst_full_range: dst_full,
664 };
665 let dst_params = ColorParams {
666 matrix: crate::colorimetry::yuv_matrix(dst_cm.encoding.unwrap()),
667 range: crate::colorimetry::yuv_range(dst_cm.range.unwrap()),
668 encoding: dst_cm.encoding.unwrap(),
669 range_kind: dst_cm.range.unwrap(),
670 src_full_range: src_full,
671 dst_full_range: dst_full,
672 };
673 match (src.dtype(), dst.dtype()) {
674 (DType::U8, DType::U8) => {
675 let src = src.as_u8().unwrap();
676 let dst = dst.as_u8_mut().unwrap();
677 self.convert_u8(
678 src, dst, src_fmt, dst_fmt, rotation, flip, crop, src_params, dst_params,
679 )
680 }
681 (DType::U8, DType::I8) => {
682 let src_u8 = src.as_u8().unwrap();
685 let dst_i8 = dst.as_i8_mut().unwrap();
686 let dst_u8 = unsafe { &mut *(dst_i8 as *mut Tensor<i8> as *mut Tensor<u8>) };
690 self.convert_u8(
691 src_u8, dst_u8, src_fmt, dst_fmt, rotation, flip, crop, src_params, dst_params,
692 )?;
693 let mut map = dst_u8.map()?;
695 apply_int8_xor_bias(map.as_mut_slice(), dst_fmt);
696 Ok(())
697 }
698 (DType::U8, d @ (DType::F32 | DType::F16)) => {
699 let src_u8 = src.as_u8().unwrap();
700 let dw = dst.width().ok_or(Error::NotAnImage)?;
701 let dh = dst.height().ok_or(Error::NotAnImage)?;
702 let scratch_matches = self.widen_scratch.as_ref().is_some_and(|t| {
707 t.width() == Some(dw) && t.height() == Some(dh) && t.format() == Some(dst_fmt)
708 });
709 let mut tmp = if scratch_matches {
710 self.widen_scratch.take().unwrap()
711 } else {
712 TensorDyn::image(dw, dh, dst_fmt, DType::U8, Some(TensorMemory::Mem))?
713 };
714 {
715 let tmp_u8 = tmp.as_u8_mut().unwrap();
716 self.convert_u8(
717 src_u8, tmp_u8, src_fmt, dst_fmt, rotation, flip, crop, src_params,
718 dst_params,
719 )?;
720 }
721 {
724 let tmp_u8 = tmp.as_u8().unwrap();
725 let src_map = tmp_u8.map()?;
726 match d {
727 DType::F32 => {
728 let dst_t = dst.as_f32_mut().unwrap();
729 let mut dst_map = dst_t.map()?;
730 debug_assert_eq!(src_map.as_slice().len(), dst_map.as_slice().len());
731 simd::widen_u8_to_f32_norm(src_map.as_slice(), dst_map.as_mut_slice());
735 }
736 DType::F16 => {
737 let dst_t = dst.as_f16_mut().unwrap();
738 let mut dst_map = dst_t.map()?;
739 debug_assert_eq!(src_map.as_slice().len(), dst_map.as_slice().len());
740 simd::widen_u8_to_f16_norm(src_map.as_slice(), dst_map.as_mut_slice());
745 }
746 _ => unreachable!(),
747 }
748 }
749 self.widen_scratch = Some(tmp);
750 Ok(())
751 }
752 (s, d) => Err(Error::NotSupported(format!("dtype {s} -> {d}",))),
753 }
754 }
755
756 fn reuse_or_alloc_image(
762 cached: Option<Tensor<u8>>,
763 w: usize,
764 h: usize,
765 fmt: PixelFormat,
766 ) -> Result<Tensor<u8>> {
767 if let Some(t) = cached {
768 if t.width() == Some(w) && t.height() == Some(h) && t.format() == Some(fmt) {
769 return Ok(t);
770 }
771 }
772 Ok(Tensor::<u8>::image(w, h, fmt, Some(TensorMemory::Mem))?)
773 }
774
775 #[allow(clippy::too_many_arguments)]
777 fn convert_u8(
778 &mut self,
779 src: &Tensor<u8>,
780 dst: &mut Tensor<u8>,
781 src_fmt: PixelFormat,
782 dst_fmt: PixelFormat,
783 rotation: Rotation,
784 flip: Flip,
785 crop: ResolvedCrop,
786 src_params: ColorParams,
787 dst_params: ColorParams,
788 ) -> Result<()> {
789 use PixelFormat::*;
790
791 let src_w = src.width().unwrap();
792 let src_h = src.height().unwrap();
793 let dst_w = dst.width().unwrap();
794 let dst_h = dst.height().unwrap();
795
796 crop.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
797
798 let intermediate = match (src_fmt, dst_fmt) {
800 (Nv12, Rgb) => Rgb,
801 (Nv12, Rgba) => Rgba,
802 (Nv12, Grey) => Grey,
803 (Nv12, Yuyv) => Rgba,
804 (Nv12, Nv16) => Rgba,
805 (Nv12, PlanarRgb) => Rgb,
806 (Nv12, PlanarRgba) => Rgba,
807 (Nv16, PlanarRgb) => Rgb,
808 (Nv16, PlanarRgba) => Rgba,
809 (Nv24, PlanarRgb) => Rgb,
810 (Nv24, PlanarRgba) => Rgba,
811 (Yuyv, Rgb) => Rgb,
812 (Yuyv, Rgba) => Rgba,
813 (Yuyv, Grey) => Grey,
814 (Yuyv, Yuyv) => Rgba,
815 (Yuyv, PlanarRgb) => Rgb,
816 (Yuyv, PlanarRgba) => Rgba,
817 (Yuyv, Nv16) => Rgba,
818 (Vyuy, Rgb) => Rgb,
819 (Vyuy, Rgba) => Rgba,
820 (Vyuy, Grey) => Grey,
821 (Vyuy, Vyuy) => Rgba,
822 (Vyuy, PlanarRgb) => Rgb,
823 (Vyuy, PlanarRgba) => Rgba,
824 (Vyuy, Nv16) => Rgba,
825 (Rgba, Rgb) => Rgba,
826 (Rgba, Rgba) => Rgba,
827 (Rgba, Grey) => Grey,
828 (Rgba, Yuyv) => Rgba,
829 (Rgba, PlanarRgb) => Rgba,
830 (Rgba, PlanarRgba) => Rgba,
831 (Rgba, Nv16) => Rgba,
832 (Rgb, Rgb) => Rgb,
833 (Rgb, Rgba) => Rgb,
834 (Rgb, Grey) => Grey,
835 (Rgb, Yuyv) => Rgb,
836 (Rgb, PlanarRgb) => Rgb,
837 (Rgb, PlanarRgba) => Rgb,
838 (Rgb, Nv16) => Rgb,
839 (Grey, Rgb) => Rgb,
840 (Grey, Rgba) => Rgba,
841 (Grey, Grey) => Grey,
842 (Grey, Yuyv) => Grey,
843 (Grey, PlanarRgb) => Grey,
844 (Grey, PlanarRgba) => Grey,
845 (Grey, Nv16) => Grey,
846 (Nv12, Bgra) => Rgba,
847 (Yuyv, Bgra) => Rgba,
848 (Vyuy, Bgra) => Rgba,
849 (Rgba, Bgra) => Rgba,
850 (Rgb, Bgra) => Rgb,
851 (Grey, Bgra) => Grey,
852 (Bgra, Bgra) => Bgra,
853 (Nv16, Rgb) => Rgb,
854 (Nv16, Rgba) => Rgba,
855 (Nv16, Bgra) => Rgba,
856 (Nv24, Rgb) => Rgb,
857 (Nv24, Rgba) => Rgba,
858 (Nv24, Grey) => Grey,
859 (Nv24, Bgra) => Rgba,
860 (PlanarRgb, Rgb) => Rgb,
861 (PlanarRgb, Rgba) => Rgb,
862 (PlanarRgb, Bgra) => Rgb,
863 (PlanarRgba, Rgb) => Rgba,
864 (PlanarRgba, Rgba) => Rgba,
865 (PlanarRgba, Bgra) => Rgba,
866 (s, d) => {
867 return Err(Error::NotSupported(format!("Conversion from {s} to {d}",)));
868 }
869 };
870
871 let need_resize_flip_rotation = rotation != Rotation::None
872 || flip != Flip::None
873 || src_w != dst_w
874 || src_h != dst_h
875 || crop.src_rect.is_some_and(|c| {
876 c != Rect {
877 left: 0,
878 top: 0,
879 width: src_w,
880 height: src_h,
881 }
882 })
883 || crop.dst_rect.is_some_and(|c| {
884 c != Rect {
885 left: 0,
886 top: 0,
887 width: dst_w,
888 height: dst_h,
889 }
890 });
891
892 let direct_is_yuv_src = matches!(src_fmt, Nv12 | Nv16 | Nv24 | Yuyv | Vyuy);
895 let direct_params = if direct_is_yuv_src {
896 src_params
897 } else {
898 dst_params
899 };
900
901 if !need_resize_flip_rotation
908 && matches!(src_fmt, Nv12 | Nv16 | Nv24)
909 && matches!(dst_fmt, PlanarRgb | PlanarRgba)
910 {
911 return self.convert_nv_to_planar_fused(src, dst, src_fmt, dst_fmt, direct_params);
912 }
913
914 if !need_resize_flip_rotation && Self::support_conversion_pf(src_fmt, dst_fmt) {
916 return Self::convert_format_pf(src, dst, src_fmt, dst_fmt, direct_params);
917 }
918
919 if dst_fmt == Yuyv && !dst_w.is_multiple_of(2) {
921 return Err(Error::NotSupported(format!(
922 "{} destination must have width divisible by 2",
923 dst_fmt,
924 )));
925 }
926
927 let mut cached_tmp = self.convert_tmp.take();
935 let mut cached_tmp2 = self.convert_tmp2.take();
936
937 let tmp_holder: Option<Tensor<u8>> = if intermediate != src_fmt {
939 let _s = tracing::trace_span!(
940 "image.convert.cpu.format_convert",
941 from = ?src_fmt,
942 to = ?intermediate,
943 pass = "pre_resize",
944 )
945 .entered();
946 let mut t = Self::reuse_or_alloc_image(cached_tmp.take(), src_w, src_h, intermediate)?;
947 Self::convert_format_pf(src, &mut t, src_fmt, intermediate, src_params)?;
948 Some(t)
949 } else {
950 None
951 };
952 let (tmp, tmp_fmt): (&Tensor<u8>, PixelFormat) = match &tmp_holder {
953 Some(t) => (t, intermediate),
954 None => (src, src_fmt),
955 };
956
957 debug_assert!(matches!(tmp_fmt, Rgb | Rgba | Grey));
959 if tmp_fmt == dst_fmt {
960 let _s = tracing::trace_span!("image.convert.cpu.resize_flip_rotate").entered();
961 self.resize_flip_rotate_pf(tmp, dst, dst_fmt, rotation, flip, crop)?;
962 } else if !need_resize_flip_rotation {
963 let _s = tracing::trace_span!(
964 "image.convert.cpu.format_convert",
965 from = ?tmp_fmt,
966 to = ?dst_fmt,
967 pass = "direct",
968 )
969 .entered();
970 Self::convert_format_pf(tmp, dst, tmp_fmt, dst_fmt, dst_params)?;
971 } else {
972 let mut tmp2 = Self::reuse_or_alloc_image(cached_tmp2.take(), dst_w, dst_h, tmp_fmt)?;
973 if crop.dst_rect.is_some_and(|c| {
974 c != Rect {
975 left: 0,
976 top: 0,
977 width: dst_w,
978 height: dst_h,
979 }
980 }) && crop.dst_color.is_none()
981 {
982 Self::convert_format_pf(dst, &mut tmp2, dst_fmt, tmp_fmt, dst_params)?;
983 }
984 {
985 let _s = tracing::trace_span!("image.convert.cpu.resize_flip_rotate").entered();
986 self.resize_flip_rotate_pf(tmp, &mut tmp2, tmp_fmt, rotation, flip, crop)?;
987 }
988 {
989 let _s = tracing::trace_span!(
990 "image.convert.cpu.format_convert",
991 from = ?tmp_fmt,
992 to = ?dst_fmt,
993 pass = "post_resize",
994 )
995 .entered();
996 Self::convert_format_pf(&tmp2, dst, tmp_fmt, dst_fmt, dst_params)?;
997 }
998 cached_tmp2 = Some(tmp2);
999 }
1000 if let Some(t) = tmp_holder {
1003 cached_tmp = Some(t);
1004 }
1005 self.convert_tmp = cached_tmp;
1006 self.convert_tmp2 = cached_tmp2;
1007
1008 if let (Some(dst_rect), Some(dst_color)) = (crop.dst_rect, crop.dst_color) {
1009 let full_rect = Rect {
1010 left: 0,
1011 top: 0,
1012 width: dst_w,
1013 height: dst_h,
1014 };
1015 if dst_rect != full_rect {
1016 Self::fill_image_outside_crop_u8(dst, dst_color, dst_rect)?;
1017 }
1018 }
1019
1020 Ok(())
1021 }
1022
1023 fn draw_decoded_masks_impl(
1024 &mut self,
1025 dst: &mut Tensor<u8>,
1026 detect: &[DetectBox],
1027 segmentation: &[Segmentation],
1028 opacity: f32,
1029 color_mode: crate::ColorMode,
1030 ) -> Result<()> {
1031 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
1032 if !matches!(dst_fmt, PixelFormat::Rgba | PixelFormat::Rgb) {
1033 return Err(crate::Error::NotSupported(
1034 "CPU image rendering only supports RGBA or RGB images".to_string(),
1035 ));
1036 }
1037
1038 let _timer = FunctionTimer::new("CPUProcessor::draw_decoded_masks");
1039
1040 let dst_w = dst.width().unwrap();
1041 let dst_h = dst.height().unwrap();
1042 let dst_rs = tensor_row_stride(dst);
1043 let dst_c = dst_fmt.channels();
1044
1045 let mut map = dst.map()?;
1046 let dst_slice = map.as_mut_slice();
1047
1048 self.render_box(dst_w, dst_h, dst_rs, dst_c, dst_slice, detect, color_mode)?;
1049
1050 if segmentation.is_empty() {
1051 return Ok(());
1052 }
1053
1054 let is_semantic = segmentation[0].segmentation.shape()[2] > 1;
1057
1058 if is_semantic {
1059 self.render_modelpack_segmentation(
1060 dst_w,
1061 dst_h,
1062 dst_rs,
1063 dst_c,
1064 dst_slice,
1065 &segmentation[0],
1066 opacity,
1067 )?;
1068 } else {
1069 for (idx, (seg, det)) in segmentation.iter().zip(detect).enumerate() {
1070 let color_index = color_mode.index(idx, det.label);
1071 self.render_yolo_segmentation(
1072 dst_w,
1073 dst_h,
1074 dst_rs,
1075 dst_c,
1076 dst_slice,
1077 seg,
1078 color_index,
1079 opacity,
1080 )?;
1081 }
1082 }
1083
1084 Ok(())
1085 }
1086
1087 fn draw_proto_masks_impl(
1088 &mut self,
1089 dst: &mut Tensor<u8>,
1090 detect: &[DetectBox],
1091 proto_data: &ProtoData,
1092 opacity: f32,
1093 letterbox: Option<[f32; 4]>,
1094 color_mode: crate::ColorMode,
1095 ) -> Result<()> {
1096 let dst_fmt = dst.format().ok_or(Error::NotAnImage)?;
1097 if !matches!(dst_fmt, PixelFormat::Rgba | PixelFormat::Rgb) {
1098 return Err(crate::Error::NotSupported(
1099 "CPU image rendering only supports RGBA or RGB images".to_string(),
1100 ));
1101 }
1102
1103 let _timer = FunctionTimer::new("CPUProcessor::draw_proto_masks");
1104
1105 let dst_w = dst.width().unwrap();
1106 let dst_h = dst.height().unwrap();
1107 let dst_rs = tensor_row_stride(dst);
1108 let channels = dst_fmt.channels();
1109
1110 let mut map = dst.map()?;
1111 let dst_slice = map.as_mut_slice();
1112
1113 self.render_box(
1114 dst_w, dst_h, dst_rs, channels, dst_slice, detect, color_mode,
1115 )?;
1116
1117 if detect.is_empty() {
1118 return Ok(());
1119 }
1120 let proto_shape = proto_data.protos.shape();
1121 if proto_shape.len() != 3 {
1122 return Err(Error::InvalidShape(format!(
1123 "protos tensor must be rank-3, got {proto_shape:?}"
1124 )));
1125 }
1126 let proto_h = proto_shape[0];
1127 let proto_w = proto_shape[1];
1128 let num_protos = proto_shape[2];
1129 let coeff_shape = proto_data.mask_coefficients.shape();
1130 if coeff_shape.len() != 2 {
1131 return Err(Error::InvalidShape(format!(
1132 "mask_coefficients tensor must be rank-2, got {coeff_shape:?}"
1133 )));
1134 }
1135 if coeff_shape[0] == 0 {
1137 return Ok(());
1138 }
1139 if coeff_shape[1] != num_protos {
1140 return Err(Error::InvalidShape(format!(
1141 "mask_coefficients second dimension must match num_protos \
1142 ({num_protos}), got {coeff_shape:?}"
1143 )));
1144 }
1145
1146 let coeff_f32: Vec<f32> = match proto_data.mask_coefficients.dtype() {
1148 DType::F32 => {
1149 let t = proto_data.mask_coefficients.as_f32().expect("F32");
1150 let m = t.map()?;
1151 m.as_slice().to_vec()
1152 }
1153 DType::F16 => {
1154 let t = proto_data.mask_coefficients.as_f16().expect("F16");
1155 let m = t.map()?;
1156 m.as_slice().iter().map(|v| v.to_f32()).collect()
1157 }
1158 DType::I8 => {
1159 let t = proto_data.mask_coefficients.as_i8().expect("I8");
1160 let m = t.map()?;
1161 if let Some(q) = t.quantization() {
1162 use edgefirst_tensor::QuantMode;
1163 let (scale, zp) = match q.mode() {
1164 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1165 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1166 other => {
1167 return Err(Error::NotSupported(format!(
1168 "I8 mask_coefficients quantization mode {other:?} not supported"
1169 )));
1170 }
1171 };
1172 m.as_slice()
1173 .iter()
1174 .map(|&v| (v as f32 - zp) * scale)
1175 .collect()
1176 } else {
1177 m.as_slice().iter().map(|&v| v as f32).collect()
1178 }
1179 }
1180 DType::I16 => {
1181 let t = proto_data.mask_coefficients.as_i16().expect("I16");
1182 let m = t.map()?;
1183 if let Some(q) = t.quantization() {
1184 use edgefirst_tensor::QuantMode;
1185 let (scale, zp) = match q.mode() {
1186 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1187 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1188 other => {
1189 return Err(Error::NotSupported(format!(
1190 "I16 mask_coefficients quantization mode {other:?} not supported"
1191 )));
1192 }
1193 };
1194 m.as_slice()
1195 .iter()
1196 .map(|&v| (v as f32 - zp) * scale)
1197 .collect()
1198 } else {
1199 m.as_slice().iter().map(|&v| v as f32).collect()
1200 }
1201 }
1202 other => {
1203 return Err(Error::InvalidShape(format!(
1204 "mask_coefficients dtype {other:?} not supported"
1205 )));
1206 }
1207 };
1208
1209 let (lx0, lx_range, ly0, ly_range) = match letterbox {
1211 Some([lx0, ly0, lx1, ly1]) => (lx0, lx1 - lx0, ly0, ly1 - ly0),
1212 None => (0.0_f32, 1.0_f32, 0.0_f32, 1.0_f32),
1213 };
1214
1215 match proto_data.protos.dtype() {
1218 DType::F32 => {
1219 let t = proto_data.protos.as_f32().expect("F32");
1220 let m = t.map()?;
1221 self.draw_proto_masks_inner(
1222 dst_slice,
1223 dst_w,
1224 dst_h,
1225 dst_rs,
1226 channels,
1227 detect,
1228 m.as_slice(),
1229 &coeff_f32,
1230 proto_h,
1231 proto_w,
1232 num_protos,
1233 opacity,
1234 (lx0, lx_range, ly0, ly_range),
1235 color_mode,
1236 0.0_f32,
1237 |p: &f32, _| *p,
1238 );
1239 }
1240 DType::F16 => {
1241 let t = proto_data.protos.as_f16().expect("F16");
1242 let m = t.map()?;
1243 self.draw_proto_masks_inner(
1244 dst_slice,
1245 dst_w,
1246 dst_h,
1247 dst_rs,
1248 channels,
1249 detect,
1250 m.as_slice(),
1251 &coeff_f32,
1252 proto_h,
1253 proto_w,
1254 num_protos,
1255 opacity,
1256 (lx0, lx_range, ly0, ly_range),
1257 color_mode,
1258 0.0_f32,
1259 |p: &half::f16, _| p.to_f32(),
1260 );
1261 }
1262 DType::I8 => {
1263 use edgefirst_tensor::QuantMode;
1264 let t = proto_data.protos.as_i8().expect("I8");
1265 let m = t.map()?;
1266 let quant = t.quantization().ok_or_else(|| {
1267 Error::InvalidShape("I8 protos require quantization metadata".into())
1268 })?;
1269 let (scale, zp) = match quant.mode() {
1270 QuantMode::PerTensor { scale, zero_point } => (scale, zero_point as f32),
1271 QuantMode::PerTensorSymmetric { scale } => (scale, 0.0),
1272 QuantMode::PerChannel { axis, .. }
1273 | QuantMode::PerChannelSymmetric { axis, .. } => {
1274 return Err(Error::NotSupported(format!(
1275 "per-channel quantization (axis={axis}) in draw_proto_masks \
1276 CPU path not yet supported"
1277 )));
1278 }
1279 };
1280 self.draw_proto_masks_inner(
1281 dst_slice,
1282 dst_w,
1283 dst_h,
1284 dst_rs,
1285 channels,
1286 detect,
1287 m.as_slice(),
1288 &coeff_f32,
1289 proto_h,
1290 proto_w,
1291 num_protos,
1292 opacity,
1293 (lx0, lx_range, ly0, ly_range),
1294 color_mode,
1295 scale,
1296 move |p: &i8, _| (*p as f32) - zp,
1297 );
1298 }
1299 other => {
1300 return Err(Error::InvalidShape(format!(
1301 "proto tensor dtype {other:?} not supported"
1302 )));
1303 }
1304 }
1305
1306 Ok(())
1307 }
1308
1309 #[allow(clippy::too_many_arguments)]
1310 fn draw_proto_masks_inner<P: Copy>(
1311 &self,
1312 dst_slice: &mut [u8],
1313 dst_w: usize,
1314 dst_h: usize,
1315 dst_rs: usize,
1316 channels: usize,
1317 detect: &[DetectBox],
1318 protos: &[P],
1319 coeff_all_f32: &[f32],
1320 proto_h: usize,
1321 proto_w: usize,
1322 num_protos: usize,
1323 opacity: f32,
1324 letterbox_xy: (f32, f32, f32, f32),
1325 color_mode: crate::ColorMode,
1326 acc_scale: f32,
1327 load_f32: impl Fn(&P, f32) -> f32 + Copy,
1328 ) {
1329 let (lx0, lx_range, ly0, ly_range) = letterbox_xy;
1330 let stride_y = proto_w * num_protos;
1331 for (idx, det) in detect.iter().enumerate() {
1332 let coeff = &coeff_all_f32[idx * num_protos..(idx + 1) * num_protos];
1333 let color_index = color_mode.index(idx, det.label);
1334 let color = self.colors[color_index % self.colors.len()];
1335 let alpha = if opacity == 1.0 {
1336 color[3] as u16
1337 } else {
1338 (color[3] as f32 * opacity).round() as u16
1339 };
1340
1341 let start_x = (dst_w as f32 * det.bbox.xmin).round() as usize;
1342 let start_y = (dst_h as f32 * det.bbox.ymin).round() as usize;
1343 let end_x = ((dst_w as f32 * det.bbox.xmax).round() as usize).min(dst_w);
1344 let end_y = ((dst_h as f32 * det.bbox.ymax).round() as usize).min(dst_h);
1345
1346 for y in start_y..end_y {
1347 for x in start_x..end_x {
1348 let px = (lx0 + (x as f32 / dst_w as f32) * lx_range) * proto_w as f32 - 0.5;
1349 let py = (ly0 + (y as f32 / dst_h as f32) * ly_range) * proto_h as f32 - 0.5;
1350
1351 let x0 = (px.floor() as isize).clamp(0, proto_w as isize - 1) as usize;
1355 let y0 = (py.floor() as isize).clamp(0, proto_h as isize - 1) as usize;
1356 let x1 = (x0 + 1).min(proto_w - 1);
1357 let y1 = (y0 + 1).min(proto_h - 1);
1358 let fx = px - px.floor();
1359 let fy = py - py.floor();
1360 let w00 = (1.0 - fx) * (1.0 - fy);
1361 let w10 = fx * (1.0 - fy);
1362 let w01 = (1.0 - fx) * fy;
1363 let w11 = fx * fy;
1364 let b00 = y0 * stride_y + x0 * num_protos;
1365 let b10 = y0 * stride_y + x1 * num_protos;
1366 let b01 = y1 * stride_y + x0 * num_protos;
1367 let b11 = y1 * stride_y + x1 * num_protos;
1368 let mut acc = 0.0_f32;
1369 for p in 0..num_protos {
1370 let v00 = load_f32(&protos[b00 + p], 0.0);
1371 let v10 = load_f32(&protos[b10 + p], 0.0);
1372 let v01 = load_f32(&protos[b01 + p], 0.0);
1373 let v11 = load_f32(&protos[b11 + p], 0.0);
1374 let val = w00 * v00 + w10 * v10 + w01 * v01 + w11 * v11;
1375 acc += coeff[p] * val;
1376 }
1377 let final_acc = if acc_scale == 0.0 {
1378 acc
1379 } else {
1380 acc_scale * acc
1381 };
1382 let mask = 1.0 / (1.0 + (-final_acc).exp());
1386 if mask < 0.5 {
1387 continue;
1388 }
1389 let dst_index = y * dst_rs + x * channels;
1390 for c in 0..3 {
1391 dst_slice[dst_index + c] = ((color[c] as u16 * alpha
1392 + dst_slice[dst_index + c] as u16 * (255 - alpha))
1393 / 255) as u8;
1394 }
1395 }
1396 }
1397 }
1398 }
1399}