Skip to main content

edgefirst_image/
cpu.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    Crop, Error, Flip, FunctionTimer, ImageProcessorTrait, Rect, Result, Rotation, TensorImage,
6    TensorImageDst, TensorImageRef, GREY, NV12, NV16, PLANAR_RGB, PLANAR_RGBA, RGB, RGBA, YUYV,
7};
8#[cfg(feature = "decoder")]
9use edgefirst_decoder::{DetectBox, ProtoData, Segmentation};
10use edgefirst_tensor::{TensorMapTrait, TensorTrait};
11use four_char_code::FourCharCode;
12use ndarray::{ArrayView3, ArrayViewMut3, Axis};
13use rayon::iter::{
14    IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
15};
16use std::ops::Shr;
17
18/// CPUConverter implements the ImageProcessor trait using the fallback CPU
19/// implementation for image processing.
20#[derive(Debug, Clone)]
21pub struct CPUProcessor {
22    resizer: fast_image_resize::Resizer,
23    options: fast_image_resize::ResizeOptions,
24    #[cfg(feature = "decoder")]
25    colors: [[u8; 4]; 20],
26}
27
28unsafe impl Send for CPUProcessor {}
29unsafe impl Sync for CPUProcessor {}
30
31#[inline(always)]
32fn limit_to_full(l: u8) -> u8 {
33    (((l as u16 - 16) * 255 + (240 - 16) / 2) / (240 - 16)) as u8
34}
35
36#[inline(always)]
37fn full_to_limit(l: u8) -> u8 {
38    ((l as u16 * (240 - 16) + 255 / 2) / 255 + 16) as u8
39}
40
41impl Default for CPUProcessor {
42    fn default() -> Self {
43        Self::new_bilinear()
44    }
45}
46
47impl CPUProcessor {
48    /// Creates a new CPUConverter with bilinear resizing.
49    pub fn new() -> Self {
50        Self::new_bilinear()
51    }
52
53    /// Creates a new CPUConverter with bilinear resizing.
54    fn new_bilinear() -> Self {
55        let resizer = fast_image_resize::Resizer::new();
56        let options = fast_image_resize::ResizeOptions::new()
57            .resize_alg(fast_image_resize::ResizeAlg::Convolution(
58                fast_image_resize::FilterType::Bilinear,
59            ))
60            .use_alpha(false);
61
62        log::debug!("CPUConverter created");
63        Self {
64            resizer,
65            options,
66            #[cfg(feature = "decoder")]
67            colors: crate::DEFAULT_COLORS_U8,
68        }
69    }
70
71    /// Creates a new CPUConverter with nearest neighbor resizing.
72    pub fn new_nearest() -> Self {
73        let resizer = fast_image_resize::Resizer::new();
74        let options = fast_image_resize::ResizeOptions::new()
75            .resize_alg(fast_image_resize::ResizeAlg::Nearest)
76            .use_alpha(false);
77        log::debug!("CPUConverter created");
78        Self {
79            resizer,
80            options,
81            #[cfg(feature = "decoder")]
82            colors: crate::DEFAULT_COLORS_U8,
83        }
84    }
85
86    pub(crate) fn flip_rotate_ndarray(
87        src_map: &[u8],
88        dst_map: &mut [u8],
89        dst: &TensorImage,
90        rotation: Rotation,
91        flip: Flip,
92    ) -> Result<(), crate::Error> {
93        let mut dst_view =
94            ArrayViewMut3::from_shape((dst.height(), dst.width(), dst.channels()), dst_map)?;
95        let mut src_view = match rotation {
96            Rotation::None | Rotation::Rotate180 => {
97                ArrayView3::from_shape((dst.height(), dst.width(), dst.channels()), src_map)?
98            }
99            Rotation::Clockwise90 | Rotation::CounterClockwise90 => {
100                ArrayView3::from_shape((dst.width(), dst.height(), dst.channels()), src_map)?
101            }
102        };
103
104        match flip {
105            Flip::None => {}
106            Flip::Vertical => {
107                src_view.invert_axis(Axis(0));
108            }
109            Flip::Horizontal => {
110                src_view.invert_axis(Axis(1));
111            }
112        }
113
114        match rotation {
115            Rotation::None => {}
116            Rotation::Clockwise90 => {
117                src_view.swap_axes(0, 1);
118                src_view.invert_axis(Axis(1));
119            }
120            Rotation::Rotate180 => {
121                src_view.invert_axis(Axis(0));
122                src_view.invert_axis(Axis(1));
123            }
124            Rotation::CounterClockwise90 => {
125                src_view.swap_axes(0, 1);
126                src_view.invert_axis(Axis(0));
127            }
128        }
129
130        dst_view.assign(&src_view);
131
132        Ok(())
133    }
134
135    fn convert_nv12_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
136        assert_eq!(src.fourcc(), NV12);
137        assert_eq!(dst.fourcc(), RGB);
138        let map = src.tensor.map()?;
139        let y_stride = src.width() as u32;
140        let uv_stride = src.width() as u32;
141        let slices = map.as_slice().split_at(y_stride as usize * src.height());
142
143        let src = yuv::YuvBiPlanarImage {
144            y_plane: slices.0,
145            y_stride,
146            uv_plane: slices.1,
147            uv_stride,
148            width: src.width() as u32,
149            height: src.height() as u32,
150        };
151
152        Ok(yuv::yuv_nv12_to_rgb(
153            &src,
154            dst.tensor.map()?.as_mut_slice(),
155            dst.row_stride() as u32,
156            yuv::YuvRange::Limited,
157            yuv::YuvStandardMatrix::Bt709,
158            yuv::YuvConversionMode::Balanced,
159        )?)
160    }
161
162    fn convert_nv12_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
163        assert_eq!(src.fourcc(), NV12);
164        assert_eq!(dst.fourcc(), RGBA);
165        let map = src.tensor.map()?;
166        let y_stride = src.width() as u32;
167        let uv_stride = src.width() as u32;
168        let slices = map.as_slice().split_at(y_stride as usize * src.height());
169
170        let src = yuv::YuvBiPlanarImage {
171            y_plane: slices.0,
172            y_stride,
173            uv_plane: slices.1,
174            uv_stride,
175            width: src.width() as u32,
176            height: src.height() as u32,
177        };
178
179        Ok(yuv::yuv_nv12_to_rgba(
180            &src,
181            dst.tensor.map()?.as_mut_slice(),
182            dst.row_stride() as u32,
183            yuv::YuvRange::Limited,
184            yuv::YuvStandardMatrix::Bt709,
185            yuv::YuvConversionMode::Balanced,
186        )?)
187    }
188
189    fn convert_nv12_to_grey(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
190        assert_eq!(src.fourcc(), NV12);
191        assert_eq!(dst.fourcc(), GREY);
192        let src_map = src.tensor.map()?;
193        let mut dst_map = dst.tensor.map()?;
194        let y_stride = src.width() as u32;
195        let y_slice = src_map
196            .as_slice()
197            .split_at(y_stride as usize * src.height())
198            .0;
199        let src_chunks = y_slice.as_chunks::<8>();
200        let dst_chunks = dst_map.as_chunks_mut::<8>();
201        for (s, d) in src_chunks.0.iter().zip(dst_chunks.0) {
202            s.iter().zip(d).for_each(|(s, d)| *d = limit_to_full(*s));
203        }
204
205        for (s, d) in src_chunks.1.iter().zip(dst_chunks.1) {
206            *d = limit_to_full(*s);
207        }
208
209        Ok(())
210    }
211
212    fn convert_yuyv_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
213        assert_eq!(src.fourcc(), YUYV);
214        assert_eq!(dst.fourcc(), RGB);
215        let src = yuv::YuvPackedImage::<u8> {
216            yuy: &src.tensor.map()?,
217            yuy_stride: src.row_stride() as u32, // we assume packed yuyv
218            width: src.width() as u32,
219            height: src.height() as u32,
220        };
221
222        Ok(yuv::yuyv422_to_rgb(
223            &src,
224            dst.tensor.map()?.as_mut_slice(),
225            dst.width() as u32 * 3,
226            yuv::YuvRange::Limited,
227            yuv::YuvStandardMatrix::Bt709,
228        )?)
229    }
230
231    fn convert_yuyv_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
232        assert_eq!(src.fourcc(), YUYV);
233        assert_eq!(dst.fourcc(), RGBA);
234        let src = yuv::YuvPackedImage::<u8> {
235            yuy: &src.tensor.map()?,
236            yuy_stride: src.row_stride() as u32, // we assume packed yuyv
237            width: src.width() as u32,
238            height: src.height() as u32,
239        };
240
241        Ok(yuv::yuyv422_to_rgba(
242            &src,
243            dst.tensor.map()?.as_mut_slice(),
244            dst.row_stride() as u32,
245            yuv::YuvRange::Limited,
246            yuv::YuvStandardMatrix::Bt709,
247        )?)
248    }
249
250    fn convert_yuyv_to_8bps(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
251        assert_eq!(src.fourcc(), YUYV);
252        assert_eq!(dst.fourcc(), PLANAR_RGB);
253        let mut tmp = TensorImage::new(src.width(), src.height(), RGB, None)?;
254        Self::convert_yuyv_to_rgb(src, &mut tmp)?;
255        Self::convert_rgb_to_8bps(&tmp, dst)
256    }
257
258    fn convert_yuyv_to_prgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
259        assert_eq!(src.fourcc(), YUYV);
260        assert_eq!(dst.fourcc(), PLANAR_RGBA);
261        let mut tmp = TensorImage::new(src.width(), src.height(), RGB, None)?;
262        Self::convert_yuyv_to_rgb(src, &mut tmp)?;
263        Self::convert_rgb_to_prgba(&tmp, dst)
264    }
265
266    fn convert_yuyv_to_grey(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
267        assert_eq!(src.fourcc(), YUYV);
268        assert_eq!(dst.fourcc(), GREY);
269        let src_map = src.tensor.map()?;
270        let mut dst_map = dst.tensor.map()?;
271        let src_chunks = src_map.as_chunks::<16>();
272        let dst_chunks = dst_map.as_chunks_mut::<8>();
273        for (s, d) in src_chunks.0.iter().zip(dst_chunks.0) {
274            s.iter()
275                .step_by(2)
276                .zip(d)
277                .for_each(|(s, d)| *d = limit_to_full(*s));
278        }
279
280        for (s, d) in src_chunks.1.iter().step_by(2).zip(dst_chunks.1) {
281            *d = limit_to_full(*s);
282        }
283
284        Ok(())
285    }
286
287    fn convert_yuyv_to_nv16(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
288        assert_eq!(src.fourcc(), YUYV);
289        assert_eq!(dst.fourcc(), NV16);
290        let src_map = src.tensor.map()?;
291        let mut dst_map = dst.tensor.map()?;
292
293        let src_chunks = src_map.as_chunks::<2>().0;
294        let (y_plane, uv_plane) = dst_map.split_at_mut(dst.row_stride() * dst.height());
295
296        for ((s, y), uv) in src_chunks.iter().zip(y_plane).zip(uv_plane) {
297            *y = s[0];
298            *uv = s[1];
299        }
300        Ok(())
301    }
302
303    fn convert_grey_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
304        assert_eq!(src.fourcc(), GREY);
305        assert_eq!(dst.fourcc(), RGB);
306        let src = yuv::YuvGrayImage::<u8> {
307            y_plane: &src.tensor.map()?,
308            y_stride: src.row_stride() as u32, // we assume packed Y
309            width: src.width() as u32,
310            height: src.height() as u32,
311        };
312        Ok(yuv::yuv400_to_rgb(
313            &src,
314            dst.tensor.map()?.as_mut_slice(),
315            dst.row_stride() as u32,
316            yuv::YuvRange::Full,
317            yuv::YuvStandardMatrix::Bt709,
318        )?)
319    }
320
321    fn convert_grey_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
322        assert_eq!(src.fourcc(), GREY);
323        assert_eq!(dst.fourcc(), RGBA);
324        let src = yuv::YuvGrayImage::<u8> {
325            y_plane: &src.tensor.map()?,
326            y_stride: src.row_stride() as u32,
327            width: src.width() as u32,
328            height: src.height() as u32,
329        };
330        Ok(yuv::yuv400_to_rgba(
331            &src,
332            dst.tensor.map()?.as_mut_slice(),
333            dst.row_stride() as u32,
334            yuv::YuvRange::Full,
335            yuv::YuvStandardMatrix::Bt709,
336        )?)
337    }
338
339    fn convert_grey_to_8bps(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
340        assert_eq!(src.fourcc(), GREY);
341        assert_eq!(dst.fourcc(), PLANAR_RGB);
342
343        let src = src.tensor().map()?;
344        let src = src.as_slice();
345
346        let mut dst_map = dst.tensor().map()?;
347        let dst_ = dst_map.as_mut_slice();
348
349        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
350        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
351
352        rayon::scope(|s| {
353            s.spawn(|_| dst0.copy_from_slice(src));
354            s.spawn(|_| dst1.copy_from_slice(src));
355            s.spawn(|_| dst2.copy_from_slice(src));
356        });
357        Ok(())
358    }
359
360    fn convert_grey_to_prgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
361        assert_eq!(src.fourcc(), GREY);
362        assert_eq!(dst.fourcc(), PLANAR_RGBA);
363
364        let src = src.tensor().map()?;
365        let src = src.as_slice();
366
367        let mut dst_map = dst.tensor().map()?;
368        let dst_ = dst_map.as_mut_slice();
369
370        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
371        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
372        let (dst2, dst3) = dst2.split_at_mut(dst.width() * dst.height());
373        rayon::scope(|s| {
374            s.spawn(|_| dst0.copy_from_slice(src));
375            s.spawn(|_| dst1.copy_from_slice(src));
376            s.spawn(|_| dst2.copy_from_slice(src));
377            s.spawn(|_| dst3.fill(255));
378        });
379        Ok(())
380    }
381
382    fn convert_grey_to_yuyv(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
383        assert_eq!(src.fourcc(), GREY);
384        assert_eq!(dst.fourcc(), YUYV);
385
386        let src = src.tensor().map()?;
387        let src = src.as_slice();
388
389        let mut dst = dst.tensor().map()?;
390        let dst = dst.as_mut_slice();
391        for (s, d) in src
392            .as_chunks::<2>()
393            .0
394            .iter()
395            .zip(dst.as_chunks_mut::<4>().0.iter_mut())
396        {
397            d[0] = full_to_limit(s[0]);
398            d[1] = 128;
399
400            d[2] = full_to_limit(s[1]);
401            d[3] = 128;
402        }
403        Ok(())
404    }
405
406    fn convert_grey_to_nv16(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
407        assert_eq!(src.fourcc(), GREY);
408        assert_eq!(dst.fourcc(), NV16);
409
410        let src = src.tensor().map()?;
411        let src = src.as_slice();
412
413        let mut dst = dst.tensor().map()?;
414        let dst = dst.as_mut_slice();
415
416        for (s, d) in src.iter().zip(dst[0..src.len()].iter_mut()) {
417            *d = full_to_limit(*s);
418        }
419        dst[src.len()..].fill(128);
420
421        Ok(())
422    }
423
424    fn convert_rgba_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
425        assert_eq!(src.fourcc(), RGBA);
426        assert_eq!(dst.fourcc(), RGB);
427
428        Ok(yuv::rgba_to_rgb(
429            src.tensor.map()?.as_slice(),
430            (src.width() * src.channels()) as u32,
431            dst.tensor.map()?.as_mut_slice(),
432            (dst.width() * dst.channels()) as u32,
433            src.width() as u32,
434            src.height() as u32,
435        )?)
436    }
437
438    fn convert_rgba_to_grey(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
439        assert_eq!(src.fourcc(), RGBA);
440        assert_eq!(dst.fourcc(), GREY);
441
442        let mut dst = yuv::YuvGrayImageMut::<u8> {
443            y_plane: yuv::BufferStoreMut::Borrowed(&mut dst.tensor.map()?),
444            y_stride: dst.row_stride() as u32,
445            width: dst.width() as u32,
446            height: dst.height() as u32,
447        };
448        Ok(yuv::rgba_to_yuv400(
449            &mut dst,
450            src.tensor.map()?.as_slice(),
451            src.row_stride() as u32,
452            yuv::YuvRange::Full,
453            yuv::YuvStandardMatrix::Bt709,
454        )?)
455    }
456
457    fn convert_rgba_to_8bps(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
458        assert_eq!(src.fourcc(), RGBA);
459        assert_eq!(dst.fourcc(), PLANAR_RGB);
460
461        let src = src.tensor().map()?;
462        let src = src.as_slice();
463        let src = src.as_chunks::<4>().0;
464
465        let mut dst_map = dst.tensor().map()?;
466        let dst_ = dst_map.as_mut_slice();
467
468        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
469        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
470
471        src.par_iter()
472            .zip_eq(dst0)
473            .zip_eq(dst1)
474            .zip_eq(dst2)
475            .for_each(|(((s, d0), d1), d2)| {
476                *d0 = s[0];
477                *d1 = s[1];
478                *d2 = s[2];
479            });
480        Ok(())
481    }
482
483    fn convert_rgba_to_prgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
484        assert_eq!(src.fourcc(), RGBA);
485        assert_eq!(dst.fourcc(), PLANAR_RGBA);
486
487        let src = src.tensor().map()?;
488        let src = src.as_slice();
489        let src = src.as_chunks::<4>().0;
490
491        let mut dst_map = dst.tensor().map()?;
492        let dst_ = dst_map.as_mut_slice();
493
494        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
495        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
496        let (dst2, dst3) = dst2.split_at_mut(dst.width() * dst.height());
497
498        src.par_iter()
499            .zip_eq(dst0)
500            .zip_eq(dst1)
501            .zip_eq(dst2)
502            .zip_eq(dst3)
503            .for_each(|((((s, d0), d1), d2), d3)| {
504                *d0 = s[0];
505                *d1 = s[1];
506                *d2 = s[2];
507                *d3 = s[3];
508            });
509        Ok(())
510    }
511
512    fn convert_rgba_to_yuyv(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
513        assert_eq!(src.fourcc(), RGBA);
514        assert_eq!(dst.fourcc(), YUYV);
515
516        let src = src.tensor().map()?;
517        let src = src.as_slice();
518
519        let mut dst = dst.tensor().map()?;
520        let dst = dst.as_mut_slice();
521
522        // compute quantized Bt.709 limited range RGB to YUV matrix
523        const KR: f64 = 0.2126f64;
524        const KB: f64 = 0.0722f64;
525        const KG: f64 = 1.0 - KR - KB;
526        const BIAS: i32 = 20;
527
528        const Y_R: i32 = (KR * (219 << BIAS) as f64 / 255.0).round() as i32;
529        const Y_G: i32 = (KG * (219 << BIAS) as f64 / 255.0).round() as i32;
530        const Y_B: i32 = (KB * (219 << BIAS) as f64 / 255.0).round() as i32;
531
532        const U_R: i32 = (-KR / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
533        const U_G: i32 = (-KG / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
534        const U_B: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
535
536        const V_R: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
537        const V_G: i32 = (-KG / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
538        const V_B: i32 = (-KB / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
539        const ROUND: i32 = 1 << (BIAS - 1);
540        const ROUND2: i32 = 1 << BIAS;
541        let process_rgba_to_yuyv = |s: &[u8; 8], d: &mut [u8; 4]| {
542            let [r0, g0, b0, _, r1, g1, b1, _] = *s;
543            let r0 = r0 as i32;
544            let g0 = g0 as i32;
545            let b0 = b0 as i32;
546            let r1 = r1 as i32;
547            let g1 = g1 as i32;
548            let b1 = b1 as i32;
549            d[0] = ((Y_R * r0 + Y_G * g0 + Y_B * b0 + ROUND).shr(BIAS) + 16) as u8;
550            d[1] = ((U_R * r0 + U_G * g0 + U_B * b0 + U_R * r1 + U_G * g1 + U_B * b1 + ROUND2)
551                .shr(BIAS + 1)
552                + 128) as u8;
553            d[2] = ((Y_R * r1 + Y_G * g1 + Y_B * b1 + ROUND).shr(BIAS) + 16) as u8;
554            d[3] = ((V_R * r0 + V_G * g0 + V_B * b0 + V_R * r1 + V_G * g1 + V_B * b1 + ROUND2)
555                .shr(BIAS + 1)
556                + 128) as u8;
557        };
558
559        let src = src.as_chunks::<{ 8 * 32 }>();
560        let dst = dst.as_chunks_mut::<{ 4 * 32 }>();
561
562        for (s, d) in src.0.iter().zip(dst.0.iter_mut()) {
563            let s = s.as_chunks::<8>().0;
564            let d = d.as_chunks_mut::<4>().0;
565            for (s, d) in s.iter().zip(d.iter_mut()) {
566                process_rgba_to_yuyv(s, d);
567            }
568        }
569
570        let s = src.1.as_chunks::<8>().0;
571        let d = dst.1.as_chunks_mut::<4>().0;
572        for (s, d) in s.iter().zip(d.iter_mut()) {
573            process_rgba_to_yuyv(s, d);
574        }
575
576        Ok(())
577    }
578
579    fn convert_rgba_to_nv16(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
580        assert_eq!(src.fourcc(), RGBA);
581        assert_eq!(dst.fourcc(), NV16);
582
583        let mut dst_map = dst.tensor().map()?;
584
585        let (y_plane, uv_plane) = dst_map.split_at_mut(dst.width() * dst.height());
586        let mut bi_planar_image = yuv::YuvBiPlanarImageMut::<u8> {
587            y_plane: yuv::BufferStoreMut::Borrowed(y_plane),
588            y_stride: dst.width() as u32,
589            uv_plane: yuv::BufferStoreMut::Borrowed(uv_plane),
590            uv_stride: dst.width() as u32,
591            width: dst.width() as u32,
592            height: dst.height() as u32,
593        };
594
595        Ok(yuv::rgba_to_yuv_nv16(
596            &mut bi_planar_image,
597            src.tensor.map()?.as_slice(),
598            src.row_stride() as u32,
599            yuv::YuvRange::Limited,
600            yuv::YuvStandardMatrix::Bt709,
601            yuv::YuvConversionMode::Balanced,
602        )?)
603    }
604
605    fn convert_rgb_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
606        assert_eq!(src.fourcc(), RGB);
607        assert_eq!(dst.fourcc(), RGBA);
608
609        Ok(yuv::rgb_to_rgba(
610            src.tensor.map()?.as_slice(),
611            (src.width() * src.channels()) as u32,
612            dst.tensor.map()?.as_mut_slice(),
613            (dst.width() * dst.channels()) as u32,
614            src.width() as u32,
615            src.height() as u32,
616        )?)
617    }
618
619    fn convert_rgb_to_grey(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
620        assert_eq!(src.fourcc(), RGB);
621        assert_eq!(dst.fourcc(), GREY);
622
623        let mut dst = yuv::YuvGrayImageMut::<u8> {
624            y_plane: yuv::BufferStoreMut::Borrowed(&mut dst.tensor.map()?),
625            y_stride: dst.row_stride() as u32,
626            width: dst.width() as u32,
627            height: dst.height() as u32,
628        };
629        Ok(yuv::rgb_to_yuv400(
630            &mut dst,
631            src.tensor.map()?.as_slice(),
632            src.row_stride() as u32,
633            yuv::YuvRange::Full,
634            yuv::YuvStandardMatrix::Bt709,
635        )?)
636    }
637
638    fn convert_rgb_to_8bps(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
639        assert_eq!(src.fourcc(), RGB);
640        assert_eq!(dst.fourcc(), PLANAR_RGB);
641
642        let src = src.tensor().map()?;
643        let src = src.as_slice();
644        let src = src.as_chunks::<3>().0;
645
646        let mut dst_map = dst.tensor().map()?;
647        let dst_ = dst_map.as_mut_slice();
648
649        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
650        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
651
652        src.par_iter()
653            .zip_eq(dst0)
654            .zip_eq(dst1)
655            .zip_eq(dst2)
656            .for_each(|(((s, d0), d1), d2)| {
657                *d0 = s[0];
658                *d1 = s[1];
659                *d2 = s[2];
660            });
661        Ok(())
662    }
663
664    fn convert_rgb_to_prgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
665        assert_eq!(src.fourcc(), RGB);
666        assert_eq!(dst.fourcc(), PLANAR_RGBA);
667
668        let src = src.tensor().map()?;
669        let src = src.as_slice();
670        let src = src.as_chunks::<3>().0;
671
672        let mut dst_map = dst.tensor().map()?;
673        let dst_ = dst_map.as_mut_slice();
674
675        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
676        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
677        let (dst2, dst3) = dst2.split_at_mut(dst.width() * dst.height());
678
679        rayon::scope(|s| {
680            s.spawn(|_| {
681                src.par_iter()
682                    .zip_eq(dst0)
683                    .zip_eq(dst1)
684                    .zip_eq(dst2)
685                    .for_each(|(((s, d0), d1), d2)| {
686                        *d0 = s[0];
687                        *d1 = s[1];
688                        *d2 = s[2];
689                    })
690            });
691            s.spawn(|_| dst3.fill(255));
692        });
693        Ok(())
694    }
695
696    fn convert_rgb_to_yuyv(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
697        assert_eq!(src.fourcc(), RGB);
698        assert_eq!(dst.fourcc(), YUYV);
699
700        let src = src.tensor().map()?;
701        let src = src.as_slice();
702
703        let mut dst = dst.tensor().map()?;
704        let dst = dst.as_mut_slice();
705
706        // compute quantized Bt.709 limited range RGB to YUV matrix
707        const BIAS: i32 = 20;
708        const KR: f64 = 0.2126f64;
709        const KB: f64 = 0.0722f64;
710        const KG: f64 = 1.0 - KR - KB;
711        const Y_R: i32 = (KR * (219 << BIAS) as f64 / 255.0).round() as i32;
712        const Y_G: i32 = (KG * (219 << BIAS) as f64 / 255.0).round() as i32;
713        const Y_B: i32 = (KB * (219 << BIAS) as f64 / 255.0).round() as i32;
714
715        const U_R: i32 = (-KR / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
716        const U_G: i32 = (-KG / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
717        const U_B: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
718
719        const V_R: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
720        const V_G: i32 = (-KG / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
721        const V_B: i32 = (-KB / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
722        const ROUND: i32 = 1 << (BIAS - 1);
723        const ROUND2: i32 = 1 << BIAS;
724        let process_rgb_to_yuyv = |s: &[u8; 6], d: &mut [u8; 4]| {
725            let [r0, g0, b0, r1, g1, b1] = *s;
726            let r0 = r0 as i32;
727            let g0 = g0 as i32;
728            let b0 = b0 as i32;
729            let r1 = r1 as i32;
730            let g1 = g1 as i32;
731            let b1 = b1 as i32;
732            d[0] = ((Y_R * r0 + Y_G * g0 + Y_B * b0 + ROUND).shr(BIAS) + 16) as u8;
733            d[1] = ((U_R * r0 + U_G * g0 + U_B * b0 + U_R * r1 + U_G * g1 + U_B * b1 + ROUND2)
734                .shr(BIAS + 1)
735                + 128) as u8;
736            d[2] = ((Y_R * r1 + Y_G * g1 + Y_B * b1 + ROUND).shr(BIAS) + 16) as u8;
737            d[3] = ((V_R * r0 + V_G * g0 + V_B * b0 + V_R * r1 + V_G * g1 + V_B * b1 + ROUND2)
738                .shr(BIAS + 1)
739                + 128) as u8;
740        };
741
742        let src = src.as_chunks::<{ 6 * 32 }>();
743        let dst = dst.as_chunks_mut::<{ 4 * 32 }>();
744        for (s, d) in src.0.iter().zip(dst.0.iter_mut()) {
745            let s = s.as_chunks::<6>().0;
746            let d = d.as_chunks_mut::<4>().0;
747            for (s, d) in s.iter().zip(d.iter_mut()) {
748                process_rgb_to_yuyv(s, d);
749            }
750        }
751
752        let s = src.1.as_chunks::<6>().0;
753        let d = dst.1.as_chunks_mut::<4>().0;
754        for (s, d) in s.iter().zip(d.iter_mut()) {
755            process_rgb_to_yuyv(s, d);
756        }
757
758        Ok(())
759    }
760
761    fn convert_rgb_to_nv16(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
762        assert_eq!(src.fourcc(), RGB);
763        assert_eq!(dst.fourcc(), NV16);
764
765        let mut dst_map = dst.tensor().map()?;
766
767        let (y_plane, uv_plane) = dst_map.split_at_mut(dst.width() * dst.height());
768        let mut bi_planar_image = yuv::YuvBiPlanarImageMut::<u8> {
769            y_plane: yuv::BufferStoreMut::Borrowed(y_plane),
770            y_stride: dst.width() as u32,
771            uv_plane: yuv::BufferStoreMut::Borrowed(uv_plane),
772            uv_stride: dst.width() as u32,
773            width: dst.width() as u32,
774            height: dst.height() as u32,
775        };
776
777        Ok(yuv::rgb_to_yuv_nv16(
778            &mut bi_planar_image,
779            src.tensor.map()?.as_slice(),
780            src.row_stride() as u32,
781            yuv::YuvRange::Limited,
782            yuv::YuvStandardMatrix::Bt709,
783            yuv::YuvConversionMode::Balanced,
784        )?)
785    }
786
787    fn copy_image(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
788        assert_eq!(src.fourcc(), dst.fourcc());
789        dst.tensor().map()?.copy_from_slice(&src.tensor().map()?);
790        Ok(())
791    }
792
793    fn convert_nv16_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
794        assert_eq!(src.fourcc(), NV16);
795        assert_eq!(dst.fourcc(), RGB);
796        let map = src.tensor.map()?;
797        let y_stride = src.width() as u32;
798        let uv_stride = src.width() as u32;
799        let slices = map.as_slice().split_at(y_stride as usize * src.height());
800
801        let src = yuv::YuvBiPlanarImage {
802            y_plane: slices.0,
803            y_stride,
804            uv_plane: slices.1,
805            uv_stride,
806            width: src.width() as u32,
807            height: src.height() as u32,
808        };
809
810        Ok(yuv::yuv_nv16_to_rgb(
811            &src,
812            dst.tensor.map()?.as_mut_slice(),
813            dst.row_stride() as u32,
814            yuv::YuvRange::Limited,
815            yuv::YuvStandardMatrix::Bt709,
816            yuv::YuvConversionMode::Balanced,
817        )?)
818    }
819
820    fn convert_nv16_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
821        assert_eq!(src.fourcc(), NV16);
822        assert_eq!(dst.fourcc(), RGBA);
823        let map = src.tensor.map()?;
824        let y_stride = src.width() as u32;
825        let uv_stride = src.width() as u32;
826        let slices = map.as_slice().split_at(y_stride as usize * src.height());
827
828        let src = yuv::YuvBiPlanarImage {
829            y_plane: slices.0,
830            y_stride,
831            uv_plane: slices.1,
832            uv_stride,
833            width: src.width() as u32,
834            height: src.height() as u32,
835        };
836
837        Ok(yuv::yuv_nv16_to_rgba(
838            &src,
839            dst.tensor.map()?.as_mut_slice(),
840            dst.row_stride() as u32,
841            yuv::YuvRange::Limited,
842            yuv::YuvStandardMatrix::Bt709,
843            yuv::YuvConversionMode::Balanced,
844        )?)
845    }
846
847    fn convert_8bps_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
848        assert_eq!(src.fourcc(), PLANAR_RGB);
849        assert_eq!(dst.fourcc(), RGB);
850
851        let src_map = src.tensor().map()?;
852        let src_ = src_map.as_slice();
853
854        let (src0, src1) = src_.split_at(src.width() * src.height());
855        let (src1, src2) = src1.split_at(src.width() * src.height());
856
857        let mut dst_map = dst.tensor().map()?;
858        let dst_ = dst_map.as_mut_slice();
859
860        src0.par_iter()
861            .zip_eq(src1)
862            .zip_eq(src2)
863            .zip_eq(dst_.as_chunks_mut::<3>().0.par_iter_mut())
864            .for_each(|(((s0, s1), s2), d)| {
865                d[0] = *s0;
866                d[1] = *s1;
867                d[2] = *s2;
868            });
869        Ok(())
870    }
871
872    fn convert_8bps_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
873        assert_eq!(src.fourcc(), PLANAR_RGB);
874        assert_eq!(dst.fourcc(), RGBA);
875
876        let src_map = src.tensor().map()?;
877        let src_ = src_map.as_slice();
878
879        let (src0, src1) = src_.split_at(src.width() * src.height());
880        let (src1, src2) = src1.split_at(src.width() * src.height());
881
882        let mut dst_map = dst.tensor().map()?;
883        let dst_ = dst_map.as_mut_slice();
884
885        src0.par_iter()
886            .zip_eq(src1)
887            .zip_eq(src2)
888            .zip_eq(dst_.as_chunks_mut::<4>().0.par_iter_mut())
889            .for_each(|(((s0, s1), s2), d)| {
890                d[0] = *s0;
891                d[1] = *s1;
892                d[2] = *s2;
893                d[3] = 255;
894            });
895        Ok(())
896    }
897
898    fn convert_prgba_to_rgb(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
899        assert_eq!(src.fourcc(), PLANAR_RGBA);
900        assert_eq!(dst.fourcc(), RGB);
901
902        let src_map = src.tensor().map()?;
903        let src_ = src_map.as_slice();
904
905        let (src0, src1) = src_.split_at(src.width() * src.height());
906        let (src1, src2) = src1.split_at(src.width() * src.height());
907        let (src2, _src3) = src2.split_at(src.width() * src.height());
908
909        let mut dst_map = dst.tensor().map()?;
910        let dst_ = dst_map.as_mut_slice();
911
912        src0.par_iter()
913            .zip_eq(src1)
914            .zip_eq(src2)
915            .zip_eq(dst_.as_chunks_mut::<3>().0.par_iter_mut())
916            .for_each(|(((s0, s1), s2), d)| {
917                d[0] = *s0;
918                d[1] = *s1;
919                d[2] = *s2;
920            });
921        Ok(())
922    }
923
924    fn convert_prgba_to_rgba(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
925        assert_eq!(src.fourcc(), PLANAR_RGBA);
926        assert_eq!(dst.fourcc(), RGBA);
927
928        let src_map = src.tensor().map()?;
929        let src_ = src_map.as_slice();
930
931        let (src0, src1) = src_.split_at(src.width() * src.height());
932        let (src1, src2) = src1.split_at(src.width() * src.height());
933        let (src2, src3) = src2.split_at(src.width() * src.height());
934
935        let mut dst_map = dst.tensor().map()?;
936        let dst_ = dst_map.as_mut_slice();
937
938        src0.par_iter()
939            .zip_eq(src1)
940            .zip_eq(src2)
941            .zip_eq(src3)
942            .zip_eq(dst_.as_chunks_mut::<4>().0.par_iter_mut())
943            .for_each(|((((s0, s1), s2), s3), d)| {
944                d[0] = *s0;
945                d[1] = *s1;
946                d[2] = *s2;
947                d[3] = *s3;
948            });
949        Ok(())
950    }
951
952    pub(crate) fn support_conversion(src: FourCharCode, dst: FourCharCode) -> bool {
953        matches!(
954            (src, dst),
955            (NV12, RGB)
956                | (NV12, RGBA)
957                | (NV12, GREY)
958                | (NV16, RGB)
959                | (NV16, RGBA)
960                | (YUYV, RGB)
961                | (YUYV, RGBA)
962                | (YUYV, GREY)
963                | (YUYV, YUYV)
964                | (YUYV, PLANAR_RGB)
965                | (YUYV, PLANAR_RGBA)
966                | (YUYV, NV16)
967                | (RGBA, RGB)
968                | (RGBA, RGBA)
969                | (RGBA, GREY)
970                | (RGBA, YUYV)
971                | (RGBA, PLANAR_RGB)
972                | (RGBA, PLANAR_RGBA)
973                | (RGBA, NV16)
974                | (RGB, RGB)
975                | (RGB, RGBA)
976                | (RGB, GREY)
977                | (RGB, YUYV)
978                | (RGB, PLANAR_RGB)
979                | (RGB, PLANAR_RGBA)
980                | (RGB, NV16)
981                | (GREY, RGB)
982                | (GREY, RGBA)
983                | (GREY, GREY)
984                | (GREY, YUYV)
985                | (GREY, PLANAR_RGB)
986                | (GREY, PLANAR_RGBA)
987                | (GREY, NV16)
988        )
989    }
990
991    pub(crate) fn convert_format(src: &TensorImage, dst: &mut TensorImage) -> Result<()> {
992        // shapes should be equal
993        let _timer = FunctionTimer::new(format!(
994            "ImageProcessor::convert_format {} to {}",
995            src.fourcc().display(),
996            dst.fourcc().display()
997        ));
998        assert_eq!(src.height(), dst.height());
999        assert_eq!(src.width(), dst.width());
1000
1001        match (src.fourcc(), dst.fourcc()) {
1002            (NV12, RGB) => Self::convert_nv12_to_rgb(src, dst),
1003            (NV12, RGBA) => Self::convert_nv12_to_rgba(src, dst),
1004            (NV12, GREY) => Self::convert_nv12_to_grey(src, dst),
1005            (YUYV, RGB) => Self::convert_yuyv_to_rgb(src, dst),
1006            (YUYV, RGBA) => Self::convert_yuyv_to_rgba(src, dst),
1007            (YUYV, GREY) => Self::convert_yuyv_to_grey(src, dst),
1008            (YUYV, YUYV) => Self::copy_image(src, dst),
1009            (YUYV, PLANAR_RGB) => Self::convert_yuyv_to_8bps(src, dst),
1010            (YUYV, PLANAR_RGBA) => Self::convert_yuyv_to_prgba(src, dst),
1011            (YUYV, NV16) => Self::convert_yuyv_to_nv16(src, dst),
1012            (RGBA, RGB) => Self::convert_rgba_to_rgb(src, dst),
1013            (RGBA, RGBA) => Self::copy_image(src, dst),
1014            (RGBA, GREY) => Self::convert_rgba_to_grey(src, dst),
1015            (RGBA, YUYV) => Self::convert_rgba_to_yuyv(src, dst),
1016            (RGBA, PLANAR_RGB) => Self::convert_rgba_to_8bps(src, dst),
1017            (RGBA, PLANAR_RGBA) => Self::convert_rgba_to_prgba(src, dst),
1018            (RGBA, NV16) => Self::convert_rgba_to_nv16(src, dst),
1019            (RGB, RGB) => Self::copy_image(src, dst),
1020            (RGB, RGBA) => Self::convert_rgb_to_rgba(src, dst),
1021            (RGB, GREY) => Self::convert_rgb_to_grey(src, dst),
1022            (RGB, YUYV) => Self::convert_rgb_to_yuyv(src, dst),
1023            (RGB, PLANAR_RGB) => Self::convert_rgb_to_8bps(src, dst),
1024            (RGB, PLANAR_RGBA) => Self::convert_rgb_to_prgba(src, dst),
1025            (RGB, NV16) => Self::convert_rgb_to_nv16(src, dst),
1026            (GREY, RGB) => Self::convert_grey_to_rgb(src, dst),
1027            (GREY, RGBA) => Self::convert_grey_to_rgba(src, dst),
1028            (GREY, GREY) => Self::copy_image(src, dst),
1029            (GREY, YUYV) => Self::convert_grey_to_yuyv(src, dst),
1030            (GREY, PLANAR_RGB) => Self::convert_grey_to_8bps(src, dst),
1031            (GREY, PLANAR_RGBA) => Self::convert_grey_to_prgba(src, dst),
1032            (GREY, NV16) => Self::convert_grey_to_nv16(src, dst),
1033
1034            // the following converts are added for use in testing
1035            (NV16, RGB) => Self::convert_nv16_to_rgb(src, dst),
1036            (NV16, RGBA) => Self::convert_nv16_to_rgba(src, dst),
1037            (PLANAR_RGB, RGB) => Self::convert_8bps_to_rgb(src, dst),
1038            (PLANAR_RGB, RGBA) => Self::convert_8bps_to_rgba(src, dst),
1039            (PLANAR_RGBA, RGB) => Self::convert_prgba_to_rgb(src, dst),
1040            (PLANAR_RGBA, RGBA) => Self::convert_prgba_to_rgba(src, dst),
1041            (s, d) => Err(Error::NotSupported(format!(
1042                "Conversion from {} to {}",
1043                s.display(),
1044                d.display()
1045            ))),
1046        }
1047    }
1048
1049    /// Generic RGB to PLANAR_RGB conversion that works with any TensorImageDst.
1050    fn convert_rgb_to_planar_rgb_generic<D: TensorImageDst>(
1051        src: &TensorImage,
1052        dst: &mut D,
1053    ) -> Result<()> {
1054        assert_eq!(src.fourcc(), RGB);
1055        assert_eq!(dst.fourcc(), PLANAR_RGB);
1056
1057        let src = src.tensor().map()?;
1058        let src = src.as_slice();
1059        let src = src.as_chunks::<3>().0;
1060
1061        let mut dst_map = dst.tensor_mut().map()?;
1062        let dst_ = dst_map.as_mut_slice();
1063
1064        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
1065        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
1066
1067        src.par_iter()
1068            .zip_eq(dst0)
1069            .zip_eq(dst1)
1070            .zip_eq(dst2)
1071            .for_each(|(((s, d0), d1), d2)| {
1072                *d0 = s[0];
1073                *d1 = s[1];
1074                *d2 = s[2];
1075            });
1076        Ok(())
1077    }
1078
1079    /// Generic RGBA to PLANAR_RGB conversion that works with any
1080    /// TensorImageDst.
1081    fn convert_rgba_to_planar_rgb_generic<D: TensorImageDst>(
1082        src: &TensorImage,
1083        dst: &mut D,
1084    ) -> Result<()> {
1085        assert_eq!(src.fourcc(), RGBA);
1086        assert_eq!(dst.fourcc(), PLANAR_RGB);
1087
1088        let src = src.tensor().map()?;
1089        let src = src.as_slice();
1090        let src = src.as_chunks::<4>().0;
1091
1092        let mut dst_map = dst.tensor_mut().map()?;
1093        let dst_ = dst_map.as_mut_slice();
1094
1095        let (dst0, dst1) = dst_.split_at_mut(dst.width() * dst.height());
1096        let (dst1, dst2) = dst1.split_at_mut(dst.width() * dst.height());
1097
1098        src.par_iter()
1099            .zip_eq(dst0)
1100            .zip_eq(dst1)
1101            .zip_eq(dst2)
1102            .for_each(|(((s, d0), d1), d2)| {
1103                *d0 = s[0];
1104                *d1 = s[1];
1105                *d2 = s[2];
1106            });
1107        Ok(())
1108    }
1109
1110    /// Generic copy for same-format images that works with any TensorImageDst.
1111    fn copy_image_generic<D: TensorImageDst>(src: &TensorImage, dst: &mut D) -> Result<()> {
1112        assert_eq!(src.fourcc(), dst.fourcc());
1113        dst.tensor_mut()
1114            .map()?
1115            .copy_from_slice(&src.tensor().map()?);
1116        Ok(())
1117    }
1118
1119    /// Format conversion that writes to a generic TensorImageDst.
1120    /// Supports common zero-copy preprocessing cases.
1121    pub(crate) fn convert_format_generic<D: TensorImageDst>(
1122        src: &TensorImage,
1123        dst: &mut D,
1124    ) -> Result<()> {
1125        let _timer = FunctionTimer::new(format!(
1126            "ImageProcessor::convert_format_generic {} to {}",
1127            src.fourcc().display(),
1128            dst.fourcc().display()
1129        ));
1130        assert_eq!(src.height(), dst.height());
1131        assert_eq!(src.width(), dst.width());
1132
1133        match (src.fourcc(), dst.fourcc()) {
1134            (RGB, PLANAR_RGB) => Self::convert_rgb_to_planar_rgb_generic(src, dst),
1135            (RGBA, PLANAR_RGB) => Self::convert_rgba_to_planar_rgb_generic(src, dst),
1136            (f1, f2) if f1 == f2 => Self::copy_image_generic(src, dst),
1137            (s, d) => Err(Error::NotSupported(format!(
1138                "Generic conversion from {} to {} not supported",
1139                s.display(),
1140                d.display()
1141            ))),
1142        }
1143    }
1144
1145    /// The src and dest img should be in RGB/RGBA/grey format for correct
1146    /// output. If the format is not 1, 3, or 4 bits per pixel, and error will
1147    /// be returned. The src and dest img must have the same fourcc,
1148    /// otherwise the function will panic.
1149    fn resize_flip_rotate(
1150        &mut self,
1151        src: &TensorImage,
1152        dst: &mut TensorImage,
1153        rotation: Rotation,
1154        flip: Flip,
1155        crop: Crop,
1156    ) -> Result<()> {
1157        let _timer = FunctionTimer::new(format!(
1158            "ImageProcessor::resize_flip_rotate {}x{} to {}x{} {}",
1159            src.width(),
1160            src.height(),
1161            dst.width(),
1162            dst.height(),
1163            dst.fourcc().display()
1164        ));
1165        assert_eq!(src.fourcc(), dst.fourcc());
1166
1167        let src_type = match src.channels() {
1168            1 => fast_image_resize::PixelType::U8,
1169            3 => fast_image_resize::PixelType::U8x3,
1170            4 => fast_image_resize::PixelType::U8x4,
1171            _ => {
1172                return Err(Error::NotImplemented(
1173                    "Unsupported source image format".to_string(),
1174                ));
1175            }
1176        };
1177
1178        let mut src_map = src.tensor().map()?;
1179
1180        let mut dst_map = dst.tensor().map()?;
1181
1182        let options = if let Some(crop) = crop.src_rect {
1183            self.options.crop(
1184                crop.left as f64,
1185                crop.top as f64,
1186                crop.width as f64,
1187                crop.height as f64,
1188            )
1189        } else {
1190            self.options
1191        };
1192
1193        let mut dst_rect = crop.dst_rect.unwrap_or_else(|| Rect {
1194            left: 0,
1195            top: 0,
1196            width: dst.width(),
1197            height: dst.height(),
1198        });
1199
1200        // adjust crop box for rotation/flip
1201        Self::adjust_dest_rect_for_rotate_flip(&mut dst_rect, dst, rotation, flip);
1202
1203        let needs_resize = src.width() != dst.width()
1204            || src.height() != dst.height()
1205            || crop.src_rect.is_some_and(|crop| {
1206                crop != Rect {
1207                    left: 0,
1208                    top: 0,
1209                    width: src.width(),
1210                    height: src.height(),
1211                }
1212            })
1213            || crop.dst_rect.is_some_and(|crop| {
1214                crop != Rect {
1215                    left: 0,
1216                    top: 0,
1217                    width: dst.width(),
1218                    height: dst.height(),
1219                }
1220            });
1221
1222        if needs_resize {
1223            let src_view = fast_image_resize::images::Image::from_slice_u8(
1224                src.width() as u32,
1225                src.height() as u32,
1226                &mut src_map,
1227                src_type,
1228            )?;
1229
1230            match (rotation, flip) {
1231                (Rotation::None, Flip::None) => {
1232                    let mut dst_view = fast_image_resize::images::Image::from_slice_u8(
1233                        dst.width() as u32,
1234                        dst.height() as u32,
1235                        &mut dst_map,
1236                        src_type,
1237                    )?;
1238
1239                    let mut dst_view = fast_image_resize::images::CroppedImageMut::new(
1240                        &mut dst_view,
1241                        dst_rect.left as u32,
1242                        dst_rect.top as u32,
1243                        dst_rect.width as u32,
1244                        dst_rect.height as u32,
1245                    )?;
1246
1247                    self.resizer.resize(&src_view, &mut dst_view, &options)?;
1248                }
1249                (Rotation::Clockwise90, _) | (Rotation::CounterClockwise90, _) => {
1250                    let mut tmp = vec![0; dst.row_stride() * dst.height()];
1251                    let mut tmp_view = fast_image_resize::images::Image::from_slice_u8(
1252                        dst.height() as u32,
1253                        dst.width() as u32,
1254                        &mut tmp,
1255                        src_type,
1256                    )?;
1257
1258                    let mut tmp_view = fast_image_resize::images::CroppedImageMut::new(
1259                        &mut tmp_view,
1260                        dst_rect.left as u32,
1261                        dst_rect.top as u32,
1262                        dst_rect.width as u32,
1263                        dst_rect.height as u32,
1264                    )?;
1265
1266                    self.resizer.resize(&src_view, &mut tmp_view, &options)?;
1267                    Self::flip_rotate_ndarray(&tmp, &mut dst_map, dst, rotation, flip)?;
1268                }
1269                (Rotation::None, _) | (Rotation::Rotate180, _) => {
1270                    let mut tmp = vec![0; dst.row_stride() * dst.height()];
1271                    let mut tmp_view = fast_image_resize::images::Image::from_slice_u8(
1272                        dst.width() as u32,
1273                        dst.height() as u32,
1274                        &mut tmp,
1275                        src_type,
1276                    )?;
1277
1278                    let mut tmp_view = fast_image_resize::images::CroppedImageMut::new(
1279                        &mut tmp_view,
1280                        dst_rect.left as u32,
1281                        dst_rect.top as u32,
1282                        dst_rect.width as u32,
1283                        dst_rect.height as u32,
1284                    )?;
1285
1286                    self.resizer.resize(&src_view, &mut tmp_view, &options)?;
1287                    Self::flip_rotate_ndarray(&tmp, &mut dst_map, dst, rotation, flip)?;
1288                }
1289            }
1290        } else {
1291            Self::flip_rotate_ndarray(&src_map, &mut dst_map, dst, rotation, flip)?;
1292        }
1293        Ok(())
1294    }
1295
1296    fn adjust_dest_rect_for_rotate_flip(
1297        crop: &mut Rect,
1298        dst: &TensorImage,
1299        rot: Rotation,
1300        flip: Flip,
1301    ) {
1302        match rot {
1303            Rotation::None => {}
1304            Rotation::Clockwise90 => {
1305                *crop = Rect {
1306                    left: crop.top,
1307                    top: dst.width() - crop.left - crop.width,
1308                    width: crop.height,
1309                    height: crop.width,
1310                }
1311            }
1312            Rotation::Rotate180 => {
1313                *crop = Rect {
1314                    left: dst.width() - crop.left - crop.width,
1315                    top: dst.height() - crop.top - crop.height,
1316                    width: crop.width,
1317                    height: crop.height,
1318                }
1319            }
1320            Rotation::CounterClockwise90 => {
1321                *crop = Rect {
1322                    left: dst.height() - crop.top - crop.height,
1323                    top: crop.left,
1324                    width: crop.height,
1325                    height: crop.width,
1326                }
1327            }
1328        }
1329
1330        match flip {
1331            Flip::None => {}
1332            Flip::Vertical => crop.top = dst.height() - crop.top - crop.height,
1333            Flip::Horizontal => crop.left = dst.width() - crop.left - crop.width,
1334        }
1335    }
1336
1337    /// Fills the area outside a crop rectangle with the specified color.
1338    pub fn fill_image_outside_crop(dst: &mut TensorImage, rgba: [u8; 4], crop: Rect) -> Result<()> {
1339        let dst_fourcc = dst.fourcc();
1340        let mut dst_map = dst.tensor().map()?;
1341        let dst = (dst_map.as_mut_slice(), dst.width(), dst.height());
1342        match dst_fourcc {
1343            RGBA => Self::fill_image_outside_crop_(dst, rgba, crop),
1344            RGB => Self::fill_image_outside_crop_(dst, Self::rgba_to_rgb(rgba), crop),
1345            GREY => Self::fill_image_outside_crop_(dst, Self::rgba_to_grey(rgba), crop),
1346            YUYV => Self::fill_image_outside_crop_(
1347                (dst.0, dst.1 / 2, dst.2),
1348                Self::rgba_to_yuyv(rgba),
1349                Rect::new(crop.left / 2, crop.top, crop.width.div_ceil(2), crop.height),
1350            ),
1351            PLANAR_RGB => Self::fill_image_outside_crop_planar(dst, Self::rgba_to_rgb(rgba), crop),
1352            PLANAR_RGBA => Self::fill_image_outside_crop_planar(dst, rgba, crop),
1353            NV16 => {
1354                let yuyv = Self::rgba_to_yuyv(rgba);
1355                Self::fill_image_outside_crop_yuv_semiplanar(dst, yuyv[0], [yuyv[1], yuyv[3]], crop)
1356            }
1357            _ => Err(Error::Internal(format!(
1358                "Found unexpected destination {}",
1359                dst_fourcc.display()
1360            ))),
1361        }
1362    }
1363
1364    /// Generic fill for TensorImageDst types.
1365    pub(crate) fn fill_image_outside_crop_generic<D: TensorImageDst>(
1366        dst: &mut D,
1367        rgba: [u8; 4],
1368        crop: Rect,
1369    ) -> Result<()> {
1370        let dst_fourcc = dst.fourcc();
1371        let dst_width = dst.width();
1372        let dst_height = dst.height();
1373        let mut dst_map = dst.tensor_mut().map()?;
1374        let dst = (dst_map.as_mut_slice(), dst_width, dst_height);
1375        match dst_fourcc {
1376            RGBA => Self::fill_image_outside_crop_(dst, rgba, crop),
1377            RGB => Self::fill_image_outside_crop_(dst, Self::rgba_to_rgb(rgba), crop),
1378            GREY => Self::fill_image_outside_crop_(dst, Self::rgba_to_grey(rgba), crop),
1379            YUYV => Self::fill_image_outside_crop_(
1380                (dst.0, dst.1 / 2, dst.2),
1381                Self::rgba_to_yuyv(rgba),
1382                Rect::new(crop.left / 2, crop.top, crop.width.div_ceil(2), crop.height),
1383            ),
1384            PLANAR_RGB => Self::fill_image_outside_crop_planar(dst, Self::rgba_to_rgb(rgba), crop),
1385            PLANAR_RGBA => Self::fill_image_outside_crop_planar(dst, rgba, crop),
1386            NV16 => {
1387                let yuyv = Self::rgba_to_yuyv(rgba);
1388                Self::fill_image_outside_crop_yuv_semiplanar(dst, yuyv[0], [yuyv[1], yuyv[3]], crop)
1389            }
1390            _ => Err(Error::Internal(format!(
1391                "Found unexpected destination {}",
1392                dst_fourcc.display()
1393            ))),
1394        }
1395    }
1396
1397    fn fill_image_outside_crop_<const N: usize>(
1398        (dst, dst_width, _dst_height): (&mut [u8], usize, usize),
1399        pix: [u8; N],
1400        crop: Rect,
1401    ) -> Result<()> {
1402        use rayon::{
1403            iter::{IntoParallelRefMutIterator, ParallelIterator},
1404            prelude::ParallelSliceMut,
1405        };
1406
1407        let s = dst.as_chunks_mut::<N>().0;
1408        // calculate the top/bottom
1409        let top_offset = (0, (crop.top * dst_width + crop.left));
1410        let bottom_offset = (
1411            ((crop.top + crop.height) * dst_width + crop.left).min(s.len()),
1412            s.len(),
1413        );
1414
1415        s[top_offset.0..top_offset.1]
1416            .par_iter_mut()
1417            .for_each(|x| *x = pix);
1418
1419        s[bottom_offset.0..bottom_offset.1]
1420            .par_iter_mut()
1421            .for_each(|x| *x = pix);
1422
1423        if dst_width == crop.width {
1424            return Ok(());
1425        }
1426
1427        // the middle part has a stride as well
1428        let middle_stride = dst_width - crop.width;
1429        let middle_offset = (
1430            (crop.top * dst_width + crop.left + crop.width),
1431            ((crop.top + crop.height) * dst_width + crop.left + crop.width).min(s.len()),
1432        );
1433
1434        s[middle_offset.0..middle_offset.1]
1435            .par_chunks_exact_mut(dst_width)
1436            .for_each(|row| {
1437                for p in &mut row[0..middle_stride] {
1438                    *p = pix;
1439                }
1440            });
1441
1442        Ok(())
1443    }
1444
1445    fn fill_image_outside_crop_planar<const N: usize>(
1446        (dst, dst_width, dst_height): (&mut [u8], usize, usize),
1447        pix: [u8; N],
1448        crop: Rect,
1449    ) -> Result<()> {
1450        use rayon::{
1451            iter::{IntoParallelRefMutIterator, ParallelIterator},
1452            prelude::ParallelSliceMut,
1453        };
1454
1455        // map.as_mut_slice().splitn_mut(n, pred)
1456        let s_rem = dst;
1457
1458        s_rem
1459            .par_chunks_exact_mut(dst_height * dst_width)
1460            .zip(pix)
1461            .for_each(|(s, p)| {
1462                let top_offset = (0, (crop.top * dst_width + crop.left));
1463                let bottom_offset = (
1464                    ((crop.top + crop.height) * dst_width + crop.left).min(s.len()),
1465                    s.len(),
1466                );
1467
1468                s[top_offset.0..top_offset.1]
1469                    .par_iter_mut()
1470                    .for_each(|x| *x = p);
1471
1472                s[bottom_offset.0..bottom_offset.1]
1473                    .par_iter_mut()
1474                    .for_each(|x| *x = p);
1475
1476                if dst_width == crop.width {
1477                    return;
1478                }
1479
1480                // the middle part has a stride as well
1481                let middle_stride = dst_width - crop.width;
1482                let middle_offset = (
1483                    (crop.top * dst_width + crop.left + crop.width),
1484                    ((crop.top + crop.height) * dst_width + crop.left + crop.width).min(s.len()),
1485                );
1486
1487                s[middle_offset.0..middle_offset.1]
1488                    .par_chunks_exact_mut(dst_width)
1489                    .for_each(|row| {
1490                        for x in &mut row[0..middle_stride] {
1491                            *x = p;
1492                        }
1493                    });
1494            });
1495        Ok(())
1496    }
1497
1498    fn fill_image_outside_crop_yuv_semiplanar(
1499        (dst, dst_width, dst_height): (&mut [u8], usize, usize),
1500        y: u8,
1501        uv: [u8; 2],
1502        mut crop: Rect,
1503    ) -> Result<()> {
1504        let (y_plane, uv_plane) = dst.split_at_mut(dst_width * dst_height);
1505        Self::fill_image_outside_crop_::<1>((y_plane, dst_width, dst_height), [y], crop)?;
1506        crop.left /= 2;
1507        crop.width /= 2;
1508        Self::fill_image_outside_crop_::<2>((uv_plane, dst_width / 2, dst_height), uv, crop)?;
1509        Ok(())
1510    }
1511
1512    fn rgba_to_rgb(rgba: [u8; 4]) -> [u8; 3] {
1513        let [r, g, b, _] = rgba;
1514        [r, g, b]
1515    }
1516
1517    fn rgba_to_grey(rgba: [u8; 4]) -> [u8; 1] {
1518        const BIAS: i32 = 20;
1519        const KR: f64 = 0.2126f64;
1520        const KB: f64 = 0.0722f64;
1521        const KG: f64 = 1.0 - KR - KB;
1522        const Y_R: i32 = (KR * (255 << BIAS) as f64 / 255.0).round() as i32;
1523        const Y_G: i32 = (KG * (255 << BIAS) as f64 / 255.0).round() as i32;
1524        const Y_B: i32 = (KB * (255 << BIAS) as f64 / 255.0).round() as i32;
1525
1526        const ROUND: i32 = 1 << (BIAS - 1);
1527
1528        let [r, g, b, _] = rgba;
1529        let y = ((Y_R * r as i32 + Y_G * g as i32 + Y_B * b as i32 + ROUND) >> BIAS) as u8;
1530        [y]
1531    }
1532
1533    fn rgba_to_yuyv(rgba: [u8; 4]) -> [u8; 4] {
1534        const KR: f64 = 0.2126f64;
1535        const KB: f64 = 0.0722f64;
1536        const KG: f64 = 1.0 - KR - KB;
1537        const BIAS: i32 = 20;
1538
1539        const Y_R: i32 = (KR * (219 << BIAS) as f64 / 255.0).round() as i32;
1540        const Y_G: i32 = (KG * (219 << BIAS) as f64 / 255.0).round() as i32;
1541        const Y_B: i32 = (KB * (219 << BIAS) as f64 / 255.0).round() as i32;
1542
1543        const U_R: i32 = (-KR / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
1544        const U_G: i32 = (-KG / (KR + KG) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
1545        const U_B: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
1546
1547        const V_R: i32 = (0.5_f64 * (224 << BIAS) as f64 / 255.0).ceil() as i32;
1548        const V_G: i32 = (-KG / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
1549        const V_B: i32 = (-KB / (KG + KB) / 2.0 * (224 << BIAS) as f64 / 255.0).round() as i32;
1550        const ROUND: i32 = 1 << (BIAS - 1);
1551
1552        let [r, g, b, _] = rgba;
1553        let r = r as i32;
1554        let g = g as i32;
1555        let b = b as i32;
1556        let y = (((Y_R * r + Y_G * g + Y_B * b + ROUND) >> BIAS) + 16) as u8;
1557        let u = (((U_R * r + U_G * g + U_B * b + ROUND) >> BIAS) + 128) as u8;
1558        let v = (((V_R * r + V_G * g + V_B * b + ROUND) >> BIAS) + 128) as u8;
1559
1560        [y, u, y, v]
1561    }
1562
1563    #[cfg(feature = "decoder")]
1564    fn render_modelpack_segmentation(
1565        &mut self,
1566        dst: &TensorImage,
1567        dst_slice: &mut [u8],
1568        segmentation: &Segmentation,
1569    ) -> Result<()> {
1570        use ndarray_stats::QuantileExt;
1571
1572        let seg = &segmentation.segmentation;
1573        let [seg_height, seg_width, seg_classes] = *seg.shape() else {
1574            unreachable!("Array3 did not have [usize; 3] as shape");
1575        };
1576        let start_y = (dst.height() as f32 * segmentation.ymin).round();
1577        let end_y = (dst.height() as f32 * segmentation.ymax).round();
1578        let start_x = (dst.width() as f32 * segmentation.xmin).round();
1579        let end_x = (dst.width() as f32 * segmentation.xmax).round();
1580
1581        let scale_x = (seg_width as f32 - 1.0) / ((end_x - start_x) - 1.0);
1582        let scale_y = (seg_height as f32 - 1.0) / ((end_y - start_y) - 1.0);
1583
1584        let start_x_u = (start_x as usize).min(dst.width());
1585        let start_y_u = (start_y as usize).min(dst.height());
1586        let end_x_u = (end_x as usize).min(dst.width());
1587        let end_y_u = (end_y as usize).min(dst.height());
1588
1589        let argmax = seg.map_axis(Axis(2), |r| r.argmax().unwrap());
1590        let get_value_at_nearest = |x: f32, y: f32| -> usize {
1591            let x = x.round() as usize;
1592            let y = y.round() as usize;
1593            argmax
1594                .get([y.min(seg_height - 1), x.min(seg_width - 1)])
1595                .copied()
1596                .unwrap_or(0)
1597        };
1598
1599        for y in start_y_u..end_y_u {
1600            for x in start_x_u..end_x_u {
1601                let seg_x = (x as f32 - start_x) * scale_x;
1602                let seg_y = (y as f32 - start_y) * scale_y;
1603                let label = get_value_at_nearest(seg_x, seg_y);
1604
1605                if label == seg_classes - 1 {
1606                    continue;
1607                }
1608
1609                let color = self.colors[label % self.colors.len()];
1610
1611                let alpha = color[3] as u16;
1612
1613                let dst_index = (y * dst.row_stride()) + (x * dst.channels());
1614                for c in 0..3 {
1615                    dst_slice[dst_index + c] = ((color[c] as u16 * alpha
1616                        + dst_slice[dst_index + c] as u16 * (255 - alpha))
1617                        / 255) as u8;
1618                }
1619            }
1620        }
1621
1622        Ok(())
1623    }
1624
1625    #[cfg(feature = "decoder")]
1626    fn render_yolo_segmentation(
1627        &mut self,
1628        dst: &TensorImage,
1629        dst_slice: &mut [u8],
1630        segmentation: &Segmentation,
1631        class: usize,
1632    ) -> Result<()> {
1633        let seg = &segmentation.segmentation;
1634        let [seg_height, seg_width, classes] = *seg.shape() else {
1635            unreachable!("Array3 did not have [usize;3] as shape");
1636        };
1637        debug_assert_eq!(classes, 1);
1638
1639        let start_y = (dst.height() as f32 * segmentation.ymin).round();
1640        let end_y = (dst.height() as f32 * segmentation.ymax).round();
1641        let start_x = (dst.width() as f32 * segmentation.xmin).round();
1642        let end_x = (dst.width() as f32 * segmentation.xmax).round();
1643
1644        let scale_x = (seg_width as f32 - 1.0) / ((end_x - start_x) - 1.0);
1645        let scale_y = (seg_height as f32 - 1.0) / ((end_y - start_y) - 1.0);
1646
1647        let start_x_u = (start_x as usize).min(dst.width());
1648        let start_y_u = (start_y as usize).min(dst.height());
1649        let end_x_u = (end_x as usize).min(dst.width());
1650        let end_y_u = (end_y as usize).min(dst.height());
1651
1652        for y in start_y_u..end_y_u {
1653            for x in start_x_u..end_x_u {
1654                let seg_x = ((x as f32 - start_x) * scale_x) as usize;
1655                let seg_y = ((y as f32 - start_y) * scale_y) as usize;
1656                let val = *seg.get([seg_y, seg_x, 0]).unwrap_or(&0);
1657
1658                if val < 127 {
1659                    continue;
1660                }
1661
1662                let color = self.colors[class % self.colors.len()];
1663
1664                let alpha = color[3] as u16;
1665
1666                let dst_index = (y * dst.row_stride()) + (x * dst.channels());
1667                for c in 0..3 {
1668                    dst_slice[dst_index + c] = ((color[c] as u16 * alpha
1669                        + dst_slice[dst_index + c] as u16 * (255 - alpha))
1670                        / 255) as u8;
1671                }
1672            }
1673        }
1674
1675        Ok(())
1676    }
1677
1678    #[cfg(feature = "decoder")]
1679    fn render_box(
1680        &mut self,
1681        dst: &TensorImage,
1682        dst_slice: &mut [u8],
1683        detect: &[DetectBox],
1684    ) -> Result<()> {
1685        const LINE_THICKNESS: usize = 3;
1686        for d in detect {
1687            use edgefirst_decoder::BoundingBox;
1688
1689            let label = d.label;
1690            let [r, g, b, _] = self.colors[label % self.colors.len()];
1691            let bbox = d.bbox.to_canonical();
1692            let bbox = BoundingBox {
1693                xmin: bbox.xmin.clamp(0.0, 1.0),
1694                ymin: bbox.ymin.clamp(0.0, 1.0),
1695                xmax: bbox.xmax.clamp(0.0, 1.0),
1696                ymax: bbox.ymax.clamp(0.0, 1.0),
1697            };
1698            let inner = [
1699                ((dst.width() - 1) as f32 * bbox.xmin - 0.5).round() as usize,
1700                ((dst.height() - 1) as f32 * bbox.ymin - 0.5).round() as usize,
1701                ((dst.width() - 1) as f32 * bbox.xmax + 0.5).round() as usize,
1702                ((dst.height() - 1) as f32 * bbox.ymax + 0.5).round() as usize,
1703            ];
1704
1705            let outer = [
1706                inner[0].saturating_sub(LINE_THICKNESS),
1707                inner[1].saturating_sub(LINE_THICKNESS),
1708                (inner[2] + LINE_THICKNESS).min(dst.width()),
1709                (inner[3] + LINE_THICKNESS).min(dst.height()),
1710            ];
1711
1712            // top line
1713            for y in outer[1] + 1..=inner[1] {
1714                for x in outer[0] + 1..outer[2] {
1715                    let index = (y * dst.row_stride()) + (x * dst.channels());
1716                    dst_slice[index..(index + 3)].copy_from_slice(&[r, g, b]);
1717                }
1718            }
1719
1720            // left and right lines
1721            for y in inner[1]..inner[3] {
1722                for x in outer[0] + 1..=inner[0] {
1723                    let index = (y * dst.row_stride()) + (x * dst.channels());
1724                    dst_slice[index..(index + 3)].copy_from_slice(&[r, g, b]);
1725                }
1726
1727                for x in inner[2]..outer[2] {
1728                    let index = (y * dst.row_stride()) + (x * dst.channels());
1729                    dst_slice[index..(index + 3)].copy_from_slice(&[r, g, b]);
1730                }
1731            }
1732
1733            // bottom line
1734            for y in inner[3]..outer[3] {
1735                for x in outer[0] + 1..outer[2] {
1736                    let index = (y * dst.row_stride()) + (x * dst.channels());
1737                    dst_slice[index..(index + 3)].copy_from_slice(&[r, g, b]);
1738                }
1739            }
1740        }
1741        Ok(())
1742    }
1743}
1744
1745impl ImageProcessorTrait for CPUProcessor {
1746    fn convert(
1747        &mut self,
1748        src: &TensorImage,
1749        dst: &mut TensorImage,
1750        rotation: Rotation,
1751        flip: Flip,
1752        crop: Crop,
1753    ) -> Result<()> {
1754        crop.check_crop(src, dst)?;
1755        // supported destinations and srcs:
1756        let intermediate = match (src.fourcc(), dst.fourcc()) {
1757            (NV12, RGB) => RGB,
1758            (NV12, RGBA) => RGBA,
1759            (NV12, GREY) => GREY,
1760            (NV12, YUYV) => RGBA, // RGBA intermediary for YUYV dest resize/convert/rotation/flip
1761            (NV12, NV16) => RGBA, // RGBA intermediary for YUYV dest resize/convert/rotation/flip
1762            (NV12, PLANAR_RGB) => RGB,
1763            (NV12, PLANAR_RGBA) => RGBA,
1764            (YUYV, RGB) => RGB,
1765            (YUYV, RGBA) => RGBA,
1766            (YUYV, GREY) => GREY,
1767            (YUYV, YUYV) => RGBA, // RGBA intermediary for YUYV dest resize/convert/rotation/flip
1768            (YUYV, PLANAR_RGB) => RGB,
1769            (YUYV, PLANAR_RGBA) => RGBA,
1770            (YUYV, NV16) => RGBA,
1771            (RGBA, RGB) => RGBA,
1772            (RGBA, RGBA) => RGBA,
1773            (RGBA, GREY) => GREY,
1774            (RGBA, YUYV) => RGBA, // RGBA intermediary for YUYV dest resize/convert/rotation/flip
1775            (RGBA, PLANAR_RGB) => RGBA,
1776            (RGBA, PLANAR_RGBA) => RGBA,
1777            (RGBA, NV16) => RGBA,
1778            (RGB, RGB) => RGB,
1779            (RGB, RGBA) => RGB,
1780            (RGB, GREY) => GREY,
1781            (RGB, YUYV) => RGB, // RGB intermediary for YUYV dest resize/convert/rotation/flip
1782            (RGB, PLANAR_RGB) => RGB,
1783            (RGB, PLANAR_RGBA) => RGB,
1784            (RGB, NV16) => RGB,
1785            (GREY, RGB) => RGB,
1786            (GREY, RGBA) => RGBA,
1787            (GREY, GREY) => GREY,
1788            (GREY, YUYV) => GREY,
1789            (GREY, PLANAR_RGB) => GREY,
1790            (GREY, PLANAR_RGBA) => GREY,
1791            (GREY, NV16) => GREY,
1792            (s, d) => {
1793                return Err(Error::NotSupported(format!(
1794                    "Conversion from {} to {}",
1795                    s.display(),
1796                    d.display()
1797                )));
1798            }
1799        };
1800
1801        // let crop = crop.src_rect;
1802
1803        let need_resize_flip_rotation = rotation != Rotation::None
1804            || flip != Flip::None
1805            || src.width() != dst.width()
1806            || src.height() != dst.height()
1807            || crop.src_rect.is_some_and(|crop| {
1808                crop != Rect {
1809                    left: 0,
1810                    top: 0,
1811                    width: src.width(),
1812                    height: src.height(),
1813                }
1814            })
1815            || crop.dst_rect.is_some_and(|crop| {
1816                crop != Rect {
1817                    left: 0,
1818                    top: 0,
1819                    width: dst.width(),
1820                    height: dst.height(),
1821                }
1822            });
1823
1824        // check if a direct conversion can be done
1825        if !need_resize_flip_rotation && Self::support_conversion(src.fourcc(), dst.fourcc()) {
1826            return Self::convert_format(src, dst);
1827        };
1828
1829        // any extra checks
1830        if dst.fourcc() == YUYV && !dst.width().is_multiple_of(2) {
1831            return Err(Error::NotSupported(format!(
1832                "{} destination must have width divisible by 2",
1833                dst.fourcc().display(),
1834            )));
1835        }
1836
1837        // create tmp buffer
1838        let mut tmp_buffer;
1839        let tmp;
1840        if intermediate != src.fourcc() {
1841            tmp_buffer = TensorImage::new(
1842                src.width(),
1843                src.height(),
1844                intermediate,
1845                Some(edgefirst_tensor::TensorMemory::Mem),
1846            )?;
1847
1848            Self::convert_format(src, &mut tmp_buffer)?;
1849            tmp = &tmp_buffer;
1850        } else {
1851            tmp = src;
1852        }
1853
1854        // format must be RGB/RGBA/GREY
1855        matches!(tmp.fourcc(), RGB | RGBA | GREY);
1856        if tmp.fourcc() == dst.fourcc() {
1857            self.resize_flip_rotate(tmp, dst, rotation, flip, crop)?;
1858        } else if !need_resize_flip_rotation {
1859            Self::convert_format(tmp, dst)?;
1860        } else {
1861            let mut tmp2 = TensorImage::new(
1862                dst.width(),
1863                dst.height(),
1864                tmp.fourcc(),
1865                Some(edgefirst_tensor::TensorMemory::Mem),
1866            )?;
1867            if crop.dst_rect.is_some_and(|crop| {
1868                crop != Rect {
1869                    left: 0,
1870                    top: 0,
1871                    width: dst.width(),
1872                    height: dst.height(),
1873                }
1874            }) && crop.dst_color.is_none()
1875            {
1876                // convert the dst into tmp2 when there is a dst crop
1877                // TODO: this could be optimized by changing convert_format to take a
1878                // destination crop?
1879
1880                Self::convert_format(dst, &mut tmp2)?;
1881            }
1882            self.resize_flip_rotate(tmp, &mut tmp2, rotation, flip, crop)?;
1883            Self::convert_format(&tmp2, dst)?;
1884        }
1885        if let (Some(dst_rect), Some(dst_color)) = (crop.dst_rect, crop.dst_color) {
1886            let full_rect = Rect {
1887                left: 0,
1888                top: 0,
1889                width: dst.width(),
1890                height: dst.height(),
1891            };
1892            if dst_rect != full_rect {
1893                Self::fill_image_outside_crop(dst, dst_color, dst_rect)?;
1894            }
1895        }
1896
1897        Ok(())
1898    }
1899
1900    fn convert_ref(
1901        &mut self,
1902        src: &TensorImage,
1903        dst: &mut TensorImageRef<'_>,
1904        rotation: Rotation,
1905        flip: Flip,
1906        crop: Crop,
1907    ) -> Result<()> {
1908        crop.check_crop_ref(src, dst)?;
1909
1910        // Determine intermediate format needed for conversion
1911        let intermediate = match (src.fourcc(), dst.fourcc()) {
1912            (NV12, RGB) => RGB,
1913            (NV12, RGBA) => RGBA,
1914            (NV12, GREY) => GREY,
1915            (NV12, PLANAR_RGB) => RGB,
1916            (NV12, PLANAR_RGBA) => RGBA,
1917            (YUYV, RGB) => RGB,
1918            (YUYV, RGBA) => RGBA,
1919            (YUYV, GREY) => GREY,
1920            (YUYV, PLANAR_RGB) => RGB,
1921            (YUYV, PLANAR_RGBA) => RGBA,
1922            (RGBA, RGB) => RGBA,
1923            (RGBA, RGBA) => RGBA,
1924            (RGBA, GREY) => GREY,
1925            (RGBA, PLANAR_RGB) => RGBA,
1926            (RGBA, PLANAR_RGBA) => RGBA,
1927            (RGB, RGB) => RGB,
1928            (RGB, RGBA) => RGB,
1929            (RGB, GREY) => GREY,
1930            (RGB, PLANAR_RGB) => RGB,
1931            (RGB, PLANAR_RGBA) => RGB,
1932            (GREY, RGB) => RGB,
1933            (GREY, RGBA) => RGBA,
1934            (GREY, GREY) => GREY,
1935            (GREY, PLANAR_RGB) => GREY,
1936            (GREY, PLANAR_RGBA) => GREY,
1937            (s, d) => {
1938                return Err(Error::NotSupported(format!(
1939                    "Conversion from {} to {}",
1940                    s.display(),
1941                    d.display()
1942                )));
1943            }
1944        };
1945
1946        let need_resize_flip_rotation = rotation != Rotation::None
1947            || flip != Flip::None
1948            || src.width() != dst.width()
1949            || src.height() != dst.height()
1950            || crop.src_rect.is_some_and(|crop| {
1951                crop != Rect {
1952                    left: 0,
1953                    top: 0,
1954                    width: src.width(),
1955                    height: src.height(),
1956                }
1957            })
1958            || crop.dst_rect.is_some_and(|crop| {
1959                crop != Rect {
1960                    left: 0,
1961                    top: 0,
1962                    width: dst.width(),
1963                    height: dst.height(),
1964                }
1965            });
1966
1967        // Simple case: no resize/flip/rotation needed
1968        if !need_resize_flip_rotation {
1969            // Try direct generic conversion (zero-copy path)
1970            if let Ok(()) = Self::convert_format_generic(src, dst) {
1971                return Ok(());
1972            }
1973        }
1974
1975        // Complex case: need intermediate buffers
1976        // First, convert source to intermediate format if needed
1977        let mut tmp_buffer;
1978        let tmp: &TensorImage;
1979        if intermediate != src.fourcc() {
1980            tmp_buffer = TensorImage::new(
1981                src.width(),
1982                src.height(),
1983                intermediate,
1984                Some(edgefirst_tensor::TensorMemory::Mem),
1985            )?;
1986            Self::convert_format(src, &mut tmp_buffer)?;
1987            tmp = &tmp_buffer;
1988        } else {
1989            tmp = src;
1990        }
1991
1992        // Process resize/flip/rotation if needed
1993        if need_resize_flip_rotation {
1994            // Create intermediate buffer for resize output
1995            let mut tmp2 = TensorImage::new(
1996                dst.width(),
1997                dst.height(),
1998                tmp.fourcc(),
1999                Some(edgefirst_tensor::TensorMemory::Mem),
2000            )?;
2001            self.resize_flip_rotate(tmp, &mut tmp2, rotation, flip, crop)?;
2002
2003            // Final conversion to destination (zero-copy into dst)
2004            Self::convert_format_generic(&tmp2, dst)?;
2005        } else {
2006            // Direct conversion (already checked above, but handle edge cases)
2007            Self::convert_format_generic(tmp, dst)?;
2008        }
2009
2010        // Handle destination crop fill if needed
2011        if let (Some(dst_rect), Some(dst_color)) = (crop.dst_rect, crop.dst_color) {
2012            let full_rect = Rect {
2013                left: 0,
2014                top: 0,
2015                width: dst.width(),
2016                height: dst.height(),
2017            };
2018            if dst_rect != full_rect {
2019                Self::fill_image_outside_crop_generic(dst, dst_color, dst_rect)?;
2020            }
2021        }
2022
2023        Ok(())
2024    }
2025
2026    #[cfg(feature = "decoder")]
2027    fn render_to_image(
2028        &mut self,
2029        dst: &mut TensorImage,
2030        detect: &[DetectBox],
2031        segmentation: &[Segmentation],
2032    ) -> Result<()> {
2033        if !matches!(dst.fourcc(), RGBA | RGB) {
2034            return Err(crate::Error::NotSupported(
2035                "CPU image rendering only supports RGBA or RGB images".to_string(),
2036            ));
2037        }
2038
2039        let _timer = FunctionTimer::new("CPUProcessor::render_to_image");
2040
2041        let mut map = dst.tensor.map()?;
2042        let dst_slice = map.as_mut_slice();
2043
2044        self.render_box(dst, dst_slice, detect)?;
2045
2046        if segmentation.is_empty() {
2047            return Ok(());
2048        }
2049
2050        let is_modelpack = segmentation[0].segmentation.shape()[2] > 1;
2051
2052        if is_modelpack {
2053            self.render_modelpack_segmentation(dst, dst_slice, &segmentation[0])?;
2054        } else {
2055            for (seg, detect) in segmentation.iter().zip(detect) {
2056                self.render_yolo_segmentation(dst, dst_slice, seg, detect.label)?;
2057            }
2058        }
2059
2060        Ok(())
2061    }
2062
2063    #[cfg(feature = "decoder")]
2064    fn render_from_protos(
2065        &mut self,
2066        dst: &mut TensorImage,
2067        detect: &[DetectBox],
2068        proto_data: &ProtoData,
2069    ) -> Result<()> {
2070        if !matches!(dst.fourcc(), RGBA | RGB) {
2071            return Err(crate::Error::NotSupported(
2072                "CPU image rendering only supports RGBA or RGB images".to_string(),
2073            ));
2074        }
2075
2076        let _timer = FunctionTimer::new("CPUProcessor::render_from_protos");
2077
2078        let mut map = dst.tensor.map()?;
2079        let dst_slice = map.as_mut_slice();
2080
2081        self.render_box(dst, dst_slice, detect)?;
2082
2083        if detect.is_empty() || proto_data.mask_coefficients.is_empty() {
2084            return Ok(());
2085        }
2086
2087        let protos_cow = proto_data.protos.as_f32();
2088        let protos = protos_cow.as_ref();
2089        let proto_h = protos.shape()[0];
2090        let proto_w = protos.shape()[1];
2091        let num_protos = protos.shape()[2];
2092        let dst_w = dst.width();
2093        let dst_h = dst.height();
2094        let row_stride = dst.row_stride();
2095        let channels = dst.channels();
2096
2097        for (det, coeff) in detect.iter().zip(proto_data.mask_coefficients.iter()) {
2098            let color = self.colors[det.label % self.colors.len()];
2099            let alpha = color[3] as u16;
2100
2101            // Pixel bounds of the detection in dst image space
2102            let start_x = (dst_w as f32 * det.bbox.xmin).round() as usize;
2103            let start_y = (dst_h as f32 * det.bbox.ymin).round() as usize;
2104            let end_x = ((dst_w as f32 * det.bbox.xmax).round() as usize).min(dst_w);
2105            let end_y = ((dst_h as f32 * det.bbox.ymax).round() as usize).min(dst_h);
2106
2107            for y in start_y..end_y {
2108                for x in start_x..end_x {
2109                    // Map pixel (x, y) to proto space
2110                    let px = (x as f32 / dst_w as f32) * proto_w as f32 - 0.5;
2111                    let py = (y as f32 / dst_h as f32) * proto_h as f32 - 0.5;
2112
2113                    // Bilinear interpolation + dot product
2114                    let acc = bilinear_dot(protos, coeff, num_protos, px, py, proto_w, proto_h);
2115
2116                    // Sigmoid threshold
2117                    let mask = 1.0 / (1.0 + (-acc).exp());
2118                    if mask < 0.5 {
2119                        continue;
2120                    }
2121
2122                    // Alpha blend
2123                    let dst_index = y * row_stride + x * channels;
2124                    for c in 0..3 {
2125                        dst_slice[dst_index + c] = ((color[c] as u16 * alpha
2126                            + dst_slice[dst_index + c] as u16 * (255 - alpha))
2127                            / 255) as u8;
2128                    }
2129                }
2130            }
2131        }
2132
2133        Ok(())
2134    }
2135
2136    #[cfg(feature = "decoder")]
2137    fn render_masks_from_protos(
2138        &mut self,
2139        detect: &[crate::DetectBox],
2140        proto_data: crate::ProtoData,
2141        output_width: usize,
2142        output_height: usize,
2143    ) -> Result<Vec<crate::MaskResult>> {
2144        use crate::FunctionTimer;
2145
2146        let _timer = FunctionTimer::new("CPUProcessor::render_masks_from_protos");
2147
2148        if detect.is_empty() || proto_data.mask_coefficients.is_empty() {
2149            return Ok(Vec::new());
2150        }
2151
2152        let protos_cow = proto_data.protos.as_f32();
2153        let protos = protos_cow.as_ref();
2154        let proto_h = protos.shape()[0];
2155        let proto_w = protos.shape()[1];
2156        let num_protos = protos.shape()[2];
2157
2158        let mut results = Vec::with_capacity(detect.len());
2159
2160        for (det, coeff) in detect.iter().zip(proto_data.mask_coefficients.iter()) {
2161            let start_x = (output_width as f32 * det.bbox.xmin).round() as usize;
2162            let start_y = (output_height as f32 * det.bbox.ymin).round() as usize;
2163            let end_x = ((output_width as f32 * det.bbox.xmax).round() as usize).min(output_width);
2164            let end_y =
2165                ((output_height as f32 * det.bbox.ymax).round() as usize).min(output_height);
2166            let bbox_w = end_x.saturating_sub(start_x).max(1);
2167            let bbox_h = end_y.saturating_sub(start_y).max(1);
2168
2169            let mut pixels = vec![0u8; bbox_w * bbox_h];
2170
2171            for row in 0..bbox_h {
2172                let y = start_y + row;
2173                for col in 0..bbox_w {
2174                    let x = start_x + col;
2175                    let px = (x as f32 / output_width as f32) * proto_w as f32 - 0.5;
2176                    let py = (y as f32 / output_height as f32) * proto_h as f32 - 0.5;
2177                    let acc = bilinear_dot(protos, coeff, num_protos, px, py, proto_w, proto_h);
2178                    let mask = 1.0 / (1.0 + (-acc).exp());
2179                    pixels[row * bbox_w + col] = (mask * 255.0).round() as u8;
2180                }
2181            }
2182
2183            results.push(crate::MaskResult {
2184                x: start_x,
2185                y: start_y,
2186                w: bbox_w,
2187                h: bbox_h,
2188                pixels,
2189            });
2190        }
2191
2192        Ok(results)
2193    }
2194
2195    #[cfg(feature = "decoder")]
2196    fn set_class_colors(&mut self, colors: &[[u8; 4]]) -> Result<()> {
2197        for (c, new_c) in self.colors.iter_mut().zip(colors.iter()) {
2198            *c = *new_c;
2199        }
2200        Ok(())
2201    }
2202}
2203
2204/// Bilinear interpolation of proto values at `(px, py)` combined with dot
2205/// product against `coeff`. Returns the scalar accumulator before sigmoid.
2206///
2207/// Samples the four nearest proto texels, weights by bilinear coefficients,
2208/// and simultaneously computes the dot product with the mask coefficients.
2209#[cfg(feature = "decoder")]
2210#[inline]
2211fn bilinear_dot(
2212    protos: &ndarray::Array3<f32>,
2213    coeff: &[f32],
2214    num_protos: usize,
2215    px: f32,
2216    py: f32,
2217    proto_w: usize,
2218    proto_h: usize,
2219) -> f32 {
2220    let x0 = (px.floor() as isize).clamp(0, proto_w as isize - 1) as usize;
2221    let y0 = (py.floor() as isize).clamp(0, proto_h as isize - 1) as usize;
2222    let x1 = (x0 + 1).min(proto_w - 1);
2223    let y1 = (y0 + 1).min(proto_h - 1);
2224
2225    let fx = px - px.floor();
2226    let fy = py - py.floor();
2227
2228    let w00 = (1.0 - fx) * (1.0 - fy);
2229    let w10 = fx * (1.0 - fy);
2230    let w01 = (1.0 - fx) * fy;
2231    let w11 = fx * fy;
2232
2233    let mut acc = 0.0f32;
2234    for p in 0..num_protos {
2235        let val = w00 * protos[[y0, x0, p]]
2236            + w10 * protos[[y0, x1, p]]
2237            + w01 * protos[[y1, x0, p]]
2238            + w11 * protos[[y1, x1, p]];
2239        acc += coeff[p] * val;
2240    }
2241    acc
2242}
2243
2244#[cfg(test)]
2245#[cfg_attr(coverage_nightly, coverage(off))]
2246mod cpu_tests {
2247
2248    use super::*;
2249    use crate::{CPUProcessor, Rotation, TensorImageRef, RGBA};
2250    use edgefirst_tensor::{Tensor, TensorMapTrait, TensorMemory};
2251    use image::buffer::ConvertBuffer;
2252
2253    macro_rules! function {
2254        () => {{
2255            fn f() {}
2256            fn type_name_of<T>(_: T) -> &'static str {
2257                std::any::type_name::<T>()
2258            }
2259            let name = type_name_of(f);
2260
2261            // Find and cut the rest of the path
2262            match &name[..name.len() - 3].rfind(':') {
2263                Some(pos) => &name[pos + 1..name.len() - 3],
2264                None => &name[..name.len() - 3],
2265            }
2266        }};
2267    }
2268
2269    fn compare_images_convert_to_grey(
2270        img1: &TensorImage,
2271        img2: &TensorImage,
2272        threshold: f64,
2273        name: &str,
2274    ) {
2275        assert_eq!(img1.height(), img2.height(), "Heights differ");
2276        assert_eq!(img1.width(), img2.width(), "Widths differ");
2277
2278        let mut img_rgb1 = TensorImage::new(img1.width(), img1.height(), RGBA, None).unwrap();
2279        let mut img_rgb2 = TensorImage::new(img1.width(), img1.height(), RGBA, None).unwrap();
2280        CPUProcessor::convert_format(img1, &mut img_rgb1).unwrap();
2281        CPUProcessor::convert_format(img2, &mut img_rgb2).unwrap();
2282
2283        let image1 = image::RgbaImage::from_vec(
2284            img_rgb1.width() as u32,
2285            img_rgb1.height() as u32,
2286            img_rgb1.tensor().map().unwrap().to_vec(),
2287        )
2288        .unwrap();
2289
2290        let image2 = image::RgbaImage::from_vec(
2291            img_rgb2.width() as u32,
2292            img_rgb2.height() as u32,
2293            img_rgb2.tensor().map().unwrap().to_vec(),
2294        )
2295        .unwrap();
2296
2297        let similarity = image_compare::gray_similarity_structure(
2298            &image_compare::Algorithm::RootMeanSquared,
2299            &image1.convert(),
2300            &image2.convert(),
2301        )
2302        .expect("Image Comparison failed");
2303        if similarity.score < threshold {
2304            // image1.save(format!("{name}_1.png"));
2305            // image2.save(format!("{name}_2.png"));
2306            similarity
2307                .image
2308                .to_color_map()
2309                .save(format!("{name}.png"))
2310                .unwrap();
2311            panic!(
2312                "{name}: converted image and target image have similarity score too low: {} < {}",
2313                similarity.score, threshold
2314            )
2315        }
2316    }
2317
2318    fn compare_images_convert_to_rgb(
2319        img1: &TensorImage,
2320        img2: &TensorImage,
2321        threshold: f64,
2322        name: &str,
2323    ) {
2324        assert_eq!(img1.height(), img2.height(), "Heights differ");
2325        assert_eq!(img1.width(), img2.width(), "Widths differ");
2326
2327        let mut img_rgb1 = TensorImage::new(img1.width(), img1.height(), RGB, None).unwrap();
2328        let mut img_rgb2 = TensorImage::new(img1.width(), img1.height(), RGB, None).unwrap();
2329        CPUProcessor::convert_format(img1, &mut img_rgb1).unwrap();
2330        CPUProcessor::convert_format(img2, &mut img_rgb2).unwrap();
2331
2332        let image1 = image::RgbImage::from_vec(
2333            img_rgb1.width() as u32,
2334            img_rgb1.height() as u32,
2335            img_rgb1.tensor().map().unwrap().to_vec(),
2336        )
2337        .unwrap();
2338
2339        let image2 = image::RgbImage::from_vec(
2340            img_rgb2.width() as u32,
2341            img_rgb2.height() as u32,
2342            img_rgb2.tensor().map().unwrap().to_vec(),
2343        )
2344        .unwrap();
2345
2346        let similarity = image_compare::rgb_similarity_structure(
2347            &image_compare::Algorithm::RootMeanSquared,
2348            &image1,
2349            &image2,
2350        )
2351        .expect("Image Comparison failed");
2352        if similarity.score < threshold {
2353            // image1.save(format!("{name}_1.png"));
2354            // image2.save(format!("{name}_2.png"));
2355            similarity
2356                .image
2357                .to_color_map()
2358                .save(format!("{name}.png"))
2359                .unwrap();
2360            panic!(
2361                "{name}: converted image and target image have similarity score too low: {} < {}",
2362                similarity.score, threshold
2363            )
2364        }
2365    }
2366
2367    fn load_bytes_to_tensor(
2368        width: usize,
2369        height: usize,
2370        fourcc: FourCharCode,
2371        memory: Option<TensorMemory>,
2372        bytes: &[u8],
2373    ) -> Result<TensorImage, Error> {
2374        log::debug!("Current function is {}", function!());
2375        let src = TensorImage::new(width, height, fourcc, memory)?;
2376        src.tensor().map()?.as_mut_slice()[0..bytes.len()].copy_from_slice(bytes);
2377        Ok(src)
2378    }
2379
2380    macro_rules! generate_conversion_tests {
2381        (
2382        $src_fmt:ident,  $src_file:expr, $dst_fmt:ident, $dst_file:expr
2383    ) => {{
2384            // Load source
2385            let src = load_bytes_to_tensor(
2386                1280,
2387                720,
2388                $src_fmt,
2389                None,
2390                include_bytes!(concat!("../../../testdata/", $src_file)),
2391            )?;
2392
2393            // Load destination reference
2394            let dst = load_bytes_to_tensor(
2395                1280,
2396                720,
2397                $dst_fmt,
2398                None,
2399                include_bytes!(concat!("../../../testdata/", $dst_file)),
2400            )?;
2401
2402            let mut converter = CPUProcessor::default();
2403
2404            let mut converted = TensorImage::new(src.width(), src.height(), dst.fourcc(), None)?;
2405
2406            converter.convert(
2407                &src,
2408                &mut converted,
2409                Rotation::None,
2410                Flip::None,
2411                Crop::default(),
2412            )?;
2413
2414            compare_images_convert_to_rgb(&dst, &converted, 0.99, function!());
2415
2416            Ok(())
2417        }};
2418    }
2419
2420    macro_rules! generate_conversion_tests_greyscale {
2421        (
2422        $src_fmt:ident,  $src_file:expr, $dst_fmt:ident, $dst_file:expr
2423    ) => {{
2424            // Load source
2425            let src = load_bytes_to_tensor(
2426                1280,
2427                720,
2428                $src_fmt,
2429                None,
2430                include_bytes!(concat!("../../../testdata/", $src_file)),
2431            )?;
2432
2433            // Load destination reference
2434            let dst = load_bytes_to_tensor(
2435                1280,
2436                720,
2437                $dst_fmt,
2438                None,
2439                include_bytes!(concat!("../../../testdata/", $dst_file)),
2440            )?;
2441
2442            let mut converter = CPUProcessor::default();
2443
2444            let mut converted = TensorImage::new(src.width(), src.height(), dst.fourcc(), None)?;
2445
2446            converter.convert(
2447                &src,
2448                &mut converted,
2449                Rotation::None,
2450                Flip::None,
2451                Crop::default(),
2452            )?;
2453
2454            compare_images_convert_to_grey(&dst, &converted, 0.985, function!());
2455
2456            Ok(())
2457        }};
2458    }
2459
2460    // let mut dsts = [yuyv, rgb, rgba, grey, nv16, planar_rgb, planar_rgba];
2461
2462    #[test]
2463    fn test_cpu_yuyv_to_yuyv() -> Result<()> {
2464        generate_conversion_tests!(YUYV, "camera720p.yuyv", YUYV, "camera720p.yuyv")
2465    }
2466
2467    #[test]
2468    fn test_cpu_yuyv_to_rgb() -> Result<()> {
2469        generate_conversion_tests!(YUYV, "camera720p.yuyv", RGB, "camera720p.rgb")
2470    }
2471
2472    #[test]
2473    fn test_cpu_yuyv_to_rgba() -> Result<()> {
2474        generate_conversion_tests!(YUYV, "camera720p.yuyv", RGBA, "camera720p.rgba")
2475    }
2476
2477    #[test]
2478    fn test_cpu_yuyv_to_grey() -> Result<()> {
2479        generate_conversion_tests!(YUYV, "camera720p.yuyv", GREY, "camera720p.y800")
2480    }
2481
2482    #[test]
2483    fn test_cpu_yuyv_to_nv16() -> Result<()> {
2484        generate_conversion_tests!(YUYV, "camera720p.yuyv", NV16, "camera720p.nv16")
2485    }
2486
2487    #[test]
2488    fn test_cpu_yuyv_to_planar_rgb() -> Result<()> {
2489        generate_conversion_tests!(YUYV, "camera720p.yuyv", PLANAR_RGB, "camera720p.8bps")
2490    }
2491
2492    #[test]
2493    fn test_cpu_yuyv_to_planar_rgba() -> Result<()> {
2494        generate_conversion_tests!(YUYV, "camera720p.yuyv", PLANAR_RGBA, "camera720p.8bpa")
2495    }
2496
2497    #[test]
2498    fn test_cpu_rgb_to_yuyv() -> Result<()> {
2499        generate_conversion_tests!(RGB, "camera720p.rgb", YUYV, "camera720p.yuyv")
2500    }
2501
2502    #[test]
2503    fn test_cpu_rgb_to_rgb() -> Result<()> {
2504        generate_conversion_tests!(RGB, "camera720p.rgb", RGB, "camera720p.rgb")
2505    }
2506
2507    #[test]
2508    fn test_cpu_rgb_to_rgba() -> Result<()> {
2509        generate_conversion_tests!(RGB, "camera720p.rgb", RGBA, "camera720p.rgba")
2510    }
2511
2512    #[test]
2513    fn test_cpu_rgb_to_grey() -> Result<()> {
2514        generate_conversion_tests!(RGB, "camera720p.rgb", GREY, "camera720p.y800")
2515    }
2516
2517    #[test]
2518    fn test_cpu_rgb_to_nv16() -> Result<()> {
2519        generate_conversion_tests!(RGB, "camera720p.rgb", NV16, "camera720p.nv16")
2520    }
2521
2522    #[test]
2523    fn test_cpu_rgb_to_planar_rgb() -> Result<()> {
2524        generate_conversion_tests!(RGB, "camera720p.rgb", PLANAR_RGB, "camera720p.8bps")
2525    }
2526
2527    #[test]
2528    fn test_cpu_rgb_to_planar_rgba() -> Result<()> {
2529        generate_conversion_tests!(RGB, "camera720p.rgb", PLANAR_RGBA, "camera720p.8bpa")
2530    }
2531
2532    #[test]
2533    fn test_cpu_rgba_to_yuyv() -> Result<()> {
2534        generate_conversion_tests!(RGBA, "camera720p.rgba", YUYV, "camera720p.yuyv")
2535    }
2536
2537    #[test]
2538    fn test_cpu_rgba_to_rgb() -> Result<()> {
2539        generate_conversion_tests!(RGBA, "camera720p.rgba", RGB, "camera720p.rgb")
2540    }
2541
2542    #[test]
2543    fn test_cpu_rgba_to_rgba() -> Result<()> {
2544        generate_conversion_tests!(RGBA, "camera720p.rgba", RGBA, "camera720p.rgba")
2545    }
2546
2547    #[test]
2548    fn test_cpu_rgba_to_grey() -> Result<()> {
2549        generate_conversion_tests!(RGBA, "camera720p.rgba", GREY, "camera720p.y800")
2550    }
2551
2552    #[test]
2553    fn test_cpu_rgba_to_nv16() -> Result<()> {
2554        generate_conversion_tests!(RGBA, "camera720p.rgba", NV16, "camera720p.nv16")
2555    }
2556
2557    #[test]
2558    fn test_cpu_rgba_to_planar_rgb() -> Result<()> {
2559        generate_conversion_tests!(RGBA, "camera720p.rgba", PLANAR_RGB, "camera720p.8bps")
2560    }
2561
2562    #[test]
2563    fn test_cpu_rgba_to_planar_rgba() -> Result<()> {
2564        generate_conversion_tests!(RGBA, "camera720p.rgba", PLANAR_RGBA, "camera720p.8bpa")
2565    }
2566
2567    #[test]
2568    fn test_cpu_nv12_to_rgb() -> Result<()> {
2569        generate_conversion_tests!(NV12, "camera720p.nv12", RGB, "camera720p.rgb")
2570    }
2571
2572    #[test]
2573    fn test_cpu_nv12_to_yuyv() -> Result<()> {
2574        generate_conversion_tests!(NV12, "camera720p.nv12", YUYV, "camera720p.yuyv")
2575    }
2576
2577    #[test]
2578    fn test_cpu_nv12_to_rgba() -> Result<()> {
2579        generate_conversion_tests!(NV12, "camera720p.nv12", RGBA, "camera720p.rgba")
2580    }
2581
2582    #[test]
2583    fn test_cpu_nv12_to_grey() -> Result<()> {
2584        generate_conversion_tests!(NV12, "camera720p.nv12", GREY, "camera720p.y800")
2585    }
2586
2587    #[test]
2588    fn test_cpu_nv12_to_nv16() -> Result<()> {
2589        generate_conversion_tests!(NV12, "camera720p.nv12", NV16, "camera720p.nv16")
2590    }
2591
2592    #[test]
2593    fn test_cpu_nv12_to_planar_rgb() -> Result<()> {
2594        generate_conversion_tests!(NV12, "camera720p.nv12", PLANAR_RGB, "camera720p.8bps")
2595    }
2596
2597    #[test]
2598    fn test_cpu_nv12_to_planar_rgba() -> Result<()> {
2599        generate_conversion_tests!(NV12, "camera720p.nv12", PLANAR_RGBA, "camera720p.8bpa")
2600    }
2601
2602    #[test]
2603    fn test_cpu_grey_to_yuyv() -> Result<()> {
2604        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", YUYV, "camera720p.yuyv")
2605    }
2606
2607    #[test]
2608    fn test_cpu_grey_to_rgb() -> Result<()> {
2609        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", RGB, "camera720p.rgb")
2610    }
2611
2612    #[test]
2613    fn test_cpu_grey_to_rgba() -> Result<()> {
2614        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", RGBA, "camera720p.rgba")
2615    }
2616
2617    #[test]
2618    fn test_cpu_grey_to_grey() -> Result<()> {
2619        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", GREY, "camera720p.y800")
2620    }
2621
2622    #[test]
2623    fn test_cpu_grey_to_nv16() -> Result<()> {
2624        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", NV16, "camera720p.nv16")
2625    }
2626
2627    #[test]
2628    fn test_cpu_grey_to_planar_rgb() -> Result<()> {
2629        generate_conversion_tests_greyscale!(GREY, "camera720p.y800", PLANAR_RGB, "camera720p.8bps")
2630    }
2631
2632    #[test]
2633    fn test_cpu_grey_to_planar_rgba() -> Result<()> {
2634        generate_conversion_tests_greyscale!(
2635            GREY,
2636            "camera720p.y800",
2637            PLANAR_RGBA,
2638            "camera720p.8bpa"
2639        )
2640    }
2641
2642    #[test]
2643    fn test_cpu_nearest() -> Result<()> {
2644        // Load source
2645        let src = load_bytes_to_tensor(2, 1, RGB, None, &[0, 0, 0, 255, 255, 255])?;
2646
2647        let mut converter = CPUProcessor::new_nearest();
2648
2649        let mut converted = TensorImage::new(4, 1, RGB, None)?;
2650
2651        converter.convert(
2652            &src,
2653            &mut converted,
2654            Rotation::None,
2655            Flip::None,
2656            Crop::default(),
2657        )?;
2658
2659        assert_eq!(
2660            &converted.tensor().map()?.as_slice(),
2661            &[0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255]
2662        );
2663
2664        Ok(())
2665    }
2666
2667    #[test]
2668    fn test_cpu_rotate_cw() -> Result<()> {
2669        // Load source
2670        let src = load_bytes_to_tensor(
2671            2,
2672            2,
2673            RGBA,
2674            None,
2675            &[0, 0, 0, 255, 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255],
2676        )?;
2677
2678        let mut converter = CPUProcessor::default();
2679
2680        let mut converted = TensorImage::new(4, 4, RGBA, None)?;
2681
2682        converter.convert(
2683            &src,
2684            &mut converted,
2685            Rotation::Clockwise90,
2686            Flip::None,
2687            Crop::default(),
2688        )?;
2689
2690        assert_eq!(&converted.tensor().map()?.as_slice()[0..4], &[2, 2, 2, 255]);
2691        assert_eq!(
2692            &converted.tensor().map()?.as_slice()[12..16],
2693            &[0, 0, 0, 255]
2694        );
2695        assert_eq!(
2696            &converted.tensor().map()?.as_slice()[48..52],
2697            &[3, 3, 3, 255]
2698        );
2699
2700        assert_eq!(
2701            &converted.tensor().map()?.as_slice()[60..64],
2702            &[1, 1, 1, 255]
2703        );
2704
2705        Ok(())
2706    }
2707
2708    #[test]
2709    fn test_cpu_rotate_ccw() -> Result<()> {
2710        // Load source
2711        let src = load_bytes_to_tensor(
2712            2,
2713            2,
2714            RGBA,
2715            None,
2716            &[0, 0, 0, 255, 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255],
2717        )?;
2718
2719        let mut converter = CPUProcessor::default();
2720
2721        let mut converted = TensorImage::new(4, 4, RGBA, None)?;
2722
2723        converter.convert(
2724            &src,
2725            &mut converted,
2726            Rotation::CounterClockwise90,
2727            Flip::None,
2728            Crop::default(),
2729        )?;
2730
2731        assert_eq!(&converted.tensor().map()?.as_slice()[0..4], &[1, 1, 1, 255]);
2732        assert_eq!(
2733            &converted.tensor().map()?.as_slice()[12..16],
2734            &[3, 3, 3, 255]
2735        );
2736        assert_eq!(
2737            &converted.tensor().map()?.as_slice()[48..52],
2738            &[0, 0, 0, 255]
2739        );
2740
2741        assert_eq!(
2742            &converted.tensor().map()?.as_slice()[60..64],
2743            &[2, 2, 2, 255]
2744        );
2745
2746        Ok(())
2747    }
2748
2749    #[test]
2750    fn test_cpu_rotate_180() -> Result<()> {
2751        // Load source
2752        let src = load_bytes_to_tensor(
2753            2,
2754            2,
2755            RGBA,
2756            None,
2757            &[0, 0, 0, 255, 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255],
2758        )?;
2759
2760        let mut converter = CPUProcessor::default();
2761
2762        let mut converted = TensorImage::new(4, 4, RGBA, None)?;
2763
2764        converter.convert(
2765            &src,
2766            &mut converted,
2767            Rotation::Rotate180,
2768            Flip::None,
2769            Crop::default(),
2770        )?;
2771
2772        assert_eq!(&converted.tensor().map()?.as_slice()[0..4], &[3, 3, 3, 255]);
2773        assert_eq!(
2774            &converted.tensor().map()?.as_slice()[12..16],
2775            &[2, 2, 2, 255]
2776        );
2777        assert_eq!(
2778            &converted.tensor().map()?.as_slice()[48..52],
2779            &[1, 1, 1, 255]
2780        );
2781
2782        assert_eq!(
2783            &converted.tensor().map()?.as_slice()[60..64],
2784            &[0, 0, 0, 255]
2785        );
2786
2787        Ok(())
2788    }
2789
2790    #[test]
2791    fn test_cpu_flip_v() -> Result<()> {
2792        // Load source
2793        let src = load_bytes_to_tensor(
2794            2,
2795            2,
2796            RGBA,
2797            None,
2798            &[0, 0, 0, 255, 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255],
2799        )?;
2800
2801        let mut converter = CPUProcessor::default();
2802
2803        let mut converted = TensorImage::new(4, 4, RGBA, None)?;
2804
2805        converter.convert(
2806            &src,
2807            &mut converted,
2808            Rotation::None,
2809            Flip::Vertical,
2810            Crop::default(),
2811        )?;
2812
2813        assert_eq!(&converted.tensor().map()?.as_slice()[0..4], &[2, 2, 2, 255]);
2814        assert_eq!(
2815            &converted.tensor().map()?.as_slice()[12..16],
2816            &[3, 3, 3, 255]
2817        );
2818        assert_eq!(
2819            &converted.tensor().map()?.as_slice()[48..52],
2820            &[0, 0, 0, 255]
2821        );
2822
2823        assert_eq!(
2824            &converted.tensor().map()?.as_slice()[60..64],
2825            &[1, 1, 1, 255]
2826        );
2827
2828        Ok(())
2829    }
2830
2831    #[test]
2832    fn test_cpu_flip_h() -> Result<()> {
2833        // Load source
2834        let src = load_bytes_to_tensor(
2835            2,
2836            2,
2837            RGBA,
2838            None,
2839            &[0, 0, 0, 255, 1, 1, 1, 255, 2, 2, 2, 255, 3, 3, 3, 255],
2840        )?;
2841
2842        let mut converter = CPUProcessor::default();
2843
2844        let mut converted = TensorImage::new(4, 4, RGBA, None)?;
2845
2846        converter.convert(
2847            &src,
2848            &mut converted,
2849            Rotation::None,
2850            Flip::Horizontal,
2851            Crop::default(),
2852        )?;
2853
2854        assert_eq!(&converted.tensor().map()?.as_slice()[0..4], &[1, 1, 1, 255]);
2855        assert_eq!(
2856            &converted.tensor().map()?.as_slice()[12..16],
2857            &[0, 0, 0, 255]
2858        );
2859        assert_eq!(
2860            &converted.tensor().map()?.as_slice()[48..52],
2861            &[3, 3, 3, 255]
2862        );
2863
2864        assert_eq!(
2865            &converted.tensor().map()?.as_slice()[60..64],
2866            &[2, 2, 2, 255]
2867        );
2868
2869        Ok(())
2870    }
2871
2872    #[test]
2873    fn test_cpu_src_crop() -> Result<()> {
2874        // Load source
2875        let src = load_bytes_to_tensor(2, 2, GREY, None, &[10, 20, 30, 40])?;
2876
2877        let mut converter = CPUProcessor::default();
2878
2879        let mut converted = TensorImage::new(2, 2, RGBA, None)?;
2880
2881        converter.convert(
2882            &src,
2883            &mut converted,
2884            Rotation::None,
2885            Flip::None,
2886            Crop::new().with_src_rect(Some(Rect::new(0, 0, 1, 2))),
2887        )?;
2888
2889        assert_eq!(
2890            converted.tensor().map()?.as_slice(),
2891            &[10, 10, 10, 255, 13, 13, 13, 255, 30, 30, 30, 255, 33, 33, 33, 255]
2892        );
2893        Ok(())
2894    }
2895
2896    #[test]
2897    fn test_cpu_dst_crop() -> Result<()> {
2898        // Load source
2899        let src = load_bytes_to_tensor(2, 2, GREY, None, &[2, 4, 6, 8])?;
2900
2901        let mut converter = CPUProcessor::default();
2902
2903        let mut converted =
2904            load_bytes_to_tensor(2, 2, YUYV, None, &[200, 128, 200, 128, 200, 128, 200, 128])?;
2905
2906        converter.convert(
2907            &src,
2908            &mut converted,
2909            Rotation::None,
2910            Flip::None,
2911            Crop::new().with_dst_rect(Some(Rect::new(0, 0, 2, 1))),
2912        )?;
2913
2914        assert_eq!(
2915            converted.tensor().map()?.as_slice(),
2916            &[20, 128, 21, 128, 200, 128, 200, 128]
2917        );
2918        Ok(())
2919    }
2920
2921    #[test]
2922    fn test_cpu_fill_rgba() -> Result<()> {
2923        // Load source
2924        let src = load_bytes_to_tensor(1, 1, RGBA, None, &[3, 3, 3, 255])?;
2925
2926        let mut converter = CPUProcessor::default();
2927
2928        let mut converted = TensorImage::new(2, 2, RGBA, None)?;
2929
2930        converter.convert(
2931            &src,
2932            &mut converted,
2933            Rotation::None,
2934            Flip::None,
2935            Crop {
2936                src_rect: None,
2937                dst_rect: Some(Rect {
2938                    left: 1,
2939                    top: 1,
2940                    width: 1,
2941                    height: 1,
2942                }),
2943                dst_color: Some([255, 0, 0, 255]),
2944            },
2945        )?;
2946
2947        assert_eq!(
2948            converted.tensor().map()?.as_slice(),
2949            &[255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 3, 3, 3, 255]
2950        );
2951        Ok(())
2952    }
2953
2954    #[test]
2955    fn test_cpu_fill_yuyv() -> Result<()> {
2956        // Load source
2957        let src = load_bytes_to_tensor(2, 1, RGBA, None, &[3, 3, 3, 255, 3, 3, 3, 255])?;
2958
2959        let mut converter = CPUProcessor::default();
2960
2961        let mut converted = TensorImage::new(2, 3, YUYV, None)?;
2962
2963        converter.convert(
2964            &src,
2965            &mut converted,
2966            Rotation::None,
2967            Flip::None,
2968            Crop {
2969                src_rect: None,
2970                dst_rect: Some(Rect {
2971                    left: 0,
2972                    top: 1,
2973                    width: 2,
2974                    height: 1,
2975                }),
2976                dst_color: Some([255, 0, 0, 255]),
2977            },
2978        )?;
2979
2980        assert_eq!(
2981            converted.tensor().map()?.as_slice(),
2982            &[63, 102, 63, 240, 19, 128, 19, 128, 63, 102, 63, 240]
2983        );
2984        Ok(())
2985    }
2986
2987    #[test]
2988    fn test_cpu_fill_grey() -> Result<()> {
2989        // Load source
2990        let src = load_bytes_to_tensor(2, 1, RGBA, None, &[3, 3, 3, 255, 3, 3, 3, 255])?;
2991
2992        let mut converter = CPUProcessor::default();
2993
2994        let mut converted = TensorImage::new(2, 3, GREY, None)?;
2995
2996        converter.convert(
2997            &src,
2998            &mut converted,
2999            Rotation::None,
3000            Flip::None,
3001            Crop {
3002                src_rect: None,
3003                dst_rect: Some(Rect {
3004                    left: 0,
3005                    top: 1,
3006                    width: 2,
3007                    height: 1,
3008                }),
3009                dst_color: Some([200, 200, 200, 255]),
3010            },
3011        )?;
3012
3013        assert_eq!(
3014            converted.tensor().map()?.as_slice(),
3015            &[200, 200, 3, 3, 200, 200]
3016        );
3017        Ok(())
3018    }
3019
3020    #[test]
3021    #[cfg(feature = "decoder")]
3022    fn test_segmentation() {
3023        use edgefirst_decoder::Segmentation;
3024        use ndarray::Array3;
3025
3026        let mut image = TensorImage::load(
3027            include_bytes!("../../../testdata/giraffe.jpg"),
3028            Some(RGBA),
3029            None,
3030        )
3031        .unwrap();
3032
3033        let mut segmentation = Array3::from_shape_vec(
3034            (2, 160, 160),
3035            include_bytes!("../../../testdata/modelpack_seg_2x160x160.bin").to_vec(),
3036        )
3037        .unwrap();
3038        segmentation.swap_axes(0, 1);
3039        segmentation.swap_axes(1, 2);
3040        let segmentation = segmentation.as_standard_layout().to_owned();
3041
3042        let seg = Segmentation {
3043            segmentation,
3044            xmin: 0.0,
3045            ymin: 0.0,
3046            xmax: 1.0,
3047            ymax: 1.0,
3048        };
3049
3050        let mut renderer = CPUProcessor::new();
3051        renderer.render_to_image(&mut image, &[], &[seg]).unwrap();
3052
3053        image.save_jpeg("test_segmentation.jpg", 80).unwrap();
3054    }
3055
3056    #[test]
3057    #[cfg(feature = "decoder")]
3058    fn test_segmentation_yolo() {
3059        use edgefirst_decoder::Segmentation;
3060        use ndarray::Array3;
3061
3062        let mut image = TensorImage::load(
3063            include_bytes!("../../../testdata/giraffe.jpg"),
3064            Some(RGBA),
3065            None,
3066        )
3067        .unwrap();
3068
3069        let segmentation = Array3::from_shape_vec(
3070            (76, 55, 1),
3071            include_bytes!("../../../testdata/yolov8_seg_crop_76x55.bin").to_vec(),
3072        )
3073        .unwrap();
3074
3075        let detect = DetectBox {
3076            bbox: [0.59375, 0.25, 0.9375, 0.725].into(),
3077            score: 0.99,
3078            label: 1,
3079        };
3080
3081        let seg = Segmentation {
3082            segmentation,
3083            xmin: 0.59375,
3084            ymin: 0.25,
3085            xmax: 0.9375,
3086            ymax: 0.725,
3087        };
3088
3089        let mut renderer = CPUProcessor::new();
3090        renderer
3091            .set_class_colors(&[[255, 255, 0, 233], [128, 128, 255, 100]])
3092            .unwrap();
3093        assert_eq!(renderer.colors[1], [128, 128, 255, 100]);
3094        renderer
3095            .render_to_image(&mut image, &[detect], &[seg])
3096            .unwrap();
3097        let expected = TensorImage::load(
3098            include_bytes!("../../../testdata/output_render_cpu.jpg"),
3099            Some(RGBA),
3100            None,
3101        )
3102        .unwrap();
3103        compare_images_convert_to_rgb(&image, &expected, 0.99, function!());
3104    }
3105
3106    // =========================================================================
3107    // Generic Conversion Tests (TensorImageRef support)
3108    // =========================================================================
3109
3110    #[test]
3111    fn test_convert_rgb_to_planar_rgb_generic() {
3112        // Create RGB source image
3113        let mut src = TensorImage::new(4, 4, RGB, None).unwrap();
3114        {
3115            let mut map = src.tensor_mut().map().unwrap();
3116            let data = map.as_mut_slice();
3117            // Fill with pattern: pixel 0 = [10, 20, 30], pixel 1 = [40, 50, 60], etc.
3118            for i in 0..16 {
3119                data[i * 3] = (i * 10) as u8;
3120                data[i * 3 + 1] = (i * 10 + 1) as u8;
3121                data[i * 3 + 2] = (i * 10 + 2) as u8;
3122            }
3123        }
3124
3125        // Create planar RGB destination using TensorImageRef
3126        let mut tensor = Tensor::<u8>::new(&[3, 4, 4], None, None).unwrap();
3127        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3128
3129        CPUProcessor::convert_format_generic(&src, &mut dst).unwrap();
3130
3131        // Verify the conversion - check first few pixels of each plane
3132        let map = dst.tensor().map().unwrap();
3133        let data = map.as_slice();
3134
3135        // R plane starts at 0, G at 16, B at 32
3136        assert_eq!(data[0], 0); // R of pixel 0
3137        assert_eq!(data[16], 1); // G of pixel 0
3138        assert_eq!(data[32], 2); // B of pixel 0
3139
3140        assert_eq!(data[1], 10); // R of pixel 1
3141        assert_eq!(data[17], 11); // G of pixel 1
3142        assert_eq!(data[33], 12); // B of pixel 1
3143    }
3144
3145    #[test]
3146    fn test_convert_rgba_to_planar_rgb_generic() {
3147        // Create RGBA source image
3148        let mut src = TensorImage::new(4, 4, RGBA, None).unwrap();
3149        {
3150            let mut map = src.tensor_mut().map().unwrap();
3151            let data = map.as_mut_slice();
3152            // Fill with pattern
3153            for i in 0..16 {
3154                data[i * 4] = (i * 10) as u8; // R
3155                data[i * 4 + 1] = (i * 10 + 1) as u8; // G
3156                data[i * 4 + 2] = (i * 10 + 2) as u8; // B
3157                data[i * 4 + 3] = 255; // A (ignored)
3158            }
3159        }
3160
3161        // Create planar RGB destination
3162        let mut tensor = Tensor::<u8>::new(&[3, 4, 4], None, None).unwrap();
3163        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3164
3165        CPUProcessor::convert_format_generic(&src, &mut dst).unwrap();
3166
3167        // Verify the conversion
3168        let map = dst.tensor().map().unwrap();
3169        let data = map.as_slice();
3170
3171        assert_eq!(data[0], 0); // R of pixel 0
3172        assert_eq!(data[16], 1); // G of pixel 0
3173        assert_eq!(data[32], 2); // B of pixel 0
3174    }
3175
3176    #[test]
3177    fn test_copy_image_generic_same_format() {
3178        // Create source image with data
3179        let mut src = TensorImage::new(4, 4, RGB, None).unwrap();
3180        {
3181            let mut map = src.tensor_mut().map().unwrap();
3182            let data = map.as_mut_slice();
3183            for (i, byte) in data.iter_mut().enumerate() {
3184                *byte = (i % 256) as u8;
3185            }
3186        }
3187
3188        // Create destination tensor
3189        let mut tensor = Tensor::<u8>::new(&[4, 4, 3], None, None).unwrap();
3190        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, RGB).unwrap();
3191
3192        CPUProcessor::convert_format_generic(&src, &mut dst).unwrap();
3193
3194        // Verify data was copied
3195        let src_map = src.tensor().map().unwrap();
3196        let dst_map = dst.tensor().map().unwrap();
3197        assert_eq!(src_map.as_slice(), dst_map.as_slice());
3198    }
3199
3200    #[test]
3201    fn test_convert_format_generic_unsupported() {
3202        // Try unsupported conversion (NV12 to PLANAR_RGB)
3203        let src = TensorImage::new(8, 8, NV12, None).unwrap();
3204        let mut tensor = Tensor::<u8>::new(&[3, 8, 8], None, None).unwrap();
3205        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3206
3207        let result = CPUProcessor::convert_format_generic(&src, &mut dst);
3208        assert!(result.is_err());
3209        assert!(matches!(result, Err(Error::NotSupported(_))));
3210    }
3211
3212    #[test]
3213    fn test_fill_image_outside_crop_generic_rgba() {
3214        let mut tensor = Tensor::<u8>::new(&[4, 4, 4], None, None).unwrap();
3215        // Initialize to zeros
3216        tensor.map().unwrap().as_mut_slice().fill(0);
3217
3218        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, RGBA).unwrap();
3219
3220        // Fill outside a 2x2 crop in the center with red
3221        let crop = Rect::new(1, 1, 2, 2);
3222        CPUProcessor::fill_image_outside_crop_generic(&mut dst, [255, 0, 0, 255], crop).unwrap();
3223
3224        let map = dst.tensor().map().unwrap();
3225        let data = map.as_slice();
3226
3227        // Top-left corner should be filled (red)
3228        assert_eq!(&data[0..4], &[255, 0, 0, 255]);
3229
3230        // Center pixel (1,1) should still be zero (inside crop)
3231        // row=1, col=1, width=4, bytes_per_pixel=4 -> offset = (1*4 + 1) * 4 = 20
3232        let center_offset = 20;
3233        assert_eq!(&data[center_offset..center_offset + 4], &[0, 0, 0, 0]);
3234    }
3235
3236    #[test]
3237    fn test_fill_image_outside_crop_generic_rgb() {
3238        let mut tensor = Tensor::<u8>::new(&[4, 4, 3], None, None).unwrap();
3239        tensor.map().unwrap().as_mut_slice().fill(0);
3240
3241        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, RGB).unwrap();
3242
3243        let crop = Rect::new(1, 1, 2, 2);
3244        CPUProcessor::fill_image_outside_crop_generic(&mut dst, [0, 255, 0, 255], crop).unwrap();
3245
3246        let map = dst.tensor().map().unwrap();
3247        let data = map.as_slice();
3248
3249        // Top-left corner should be green
3250        assert_eq!(&data[0..3], &[0, 255, 0]);
3251
3252        // Center pixel (1,1): row=1, col=1, width=4, bytes=3 -> offset = (1*4 + 1) * 3
3253        // = 15
3254        let center_offset = 15;
3255        assert_eq!(&data[center_offset..center_offset + 3], &[0, 0, 0]);
3256    }
3257
3258    #[test]
3259    fn test_fill_image_outside_crop_generic_planar_rgb() {
3260        let mut tensor = Tensor::<u8>::new(&[3, 4, 4], None, None).unwrap();
3261        tensor.map().unwrap().as_mut_slice().fill(0);
3262
3263        let mut dst = TensorImageRef::from_borrowed_tensor(&mut tensor, PLANAR_RGB).unwrap();
3264
3265        let crop = Rect::new(1, 1, 2, 2);
3266        CPUProcessor::fill_image_outside_crop_generic(&mut dst, [128, 64, 32, 255], crop).unwrap();
3267
3268        let map = dst.tensor().map().unwrap();
3269        let data = map.as_slice();
3270
3271        // For planar: R plane is [0..16], G plane is [16..32], B plane is [32..48]
3272        // Top-left pixel (0,0) should have R=128, G=64, B=32
3273        assert_eq!(data[0], 128); // R plane, pixel 0
3274        assert_eq!(data[16], 64); // G plane, pixel 0
3275        assert_eq!(data[32], 32); // B plane, pixel 0
3276
3277        // Center pixel (1,1): row=1, col=1, width=4 -> index = 1*4 + 1 = 5
3278        let center_idx = 5;
3279        assert_eq!(data[center_idx], 0); // R
3280        assert_eq!(data[16 + center_idx], 0); // G
3281        assert_eq!(data[32 + center_idx], 0); // B
3282    }
3283}