Skip to main content

ad_plugins_rs/
color_convert.rs

1use std::sync::Arc;
2
3#[cfg(feature = "parallel")]
4use crate::par_util;
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7
8use ad_core_rs::color::{self, NDBayerPattern, NDColorMode};
9use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
10use ad_core_rs::ndarray_pool::NDArrayPool;
11use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
12
13/// Simple Bayer demosaic using bilinear interpolation.
14pub fn bayer_to_rgb1(src: &NDArray, pattern: NDBayerPattern) -> Option<NDArray> {
15    if src.dims.len() != 2 {
16        return None;
17    }
18    let w = src.dims[0].size;
19    let h = src.dims[1].size;
20
21    // Read dimension offsets to adjust the bayer phase when offset is odd
22    let offset_x = src.dims[0].offset;
23    let offset_y = src.dims[1].offset;
24
25    // Pre-compute source values into a flat f64 vec for efficient random access
26    let n = w * h;
27    let src_vals: Vec<f64> = (0..n)
28        .map(|i| src.data.get_as_f64(i).unwrap_or(0.0))
29        .collect();
30    let get_val = |x: usize, y: usize| -> f64 { src_vals[y * w + x] };
31
32    let mut r = vec![0.0f64; n];
33    let mut g = vec![0.0f64; n];
34    let mut b = vec![0.0f64; n];
35
36    // Determine which color each pixel position has, flipping phase for odd offsets
37    let (mut r_row_even, mut r_col_even) = match pattern {
38        NDBayerPattern::RGGB => (true, true),
39        NDBayerPattern::GBRG => (true, false),
40        NDBayerPattern::GRBG => (false, true),
41        NDBayerPattern::BGGR => (false, false),
42    };
43    if offset_x % 2 != 0 {
44        r_col_even = !r_col_even;
45    }
46    if offset_y % 2 != 0 {
47        r_row_even = !r_row_even;
48    }
49
50    // Helper to demosaic a single row into (r, g, b) slices.
51    //
52    // C interpolates only pixels not touching a border
53    // (NDPluginColorConvert.cpp:305): a pixel with x in 0/rowSize-1 or y in
54    // 0/numRows-1 keeps only its native Bayer channel, the other two stay 0.
55    // Interior pixels always have all 8 neighbours, so C's fixed divisors
56    // (/4 for the red/blue arms, /2 for green) apply directly.
57    let demosaic_row = |y: usize, r_row: &mut [f64], g_row: &mut [f64], b_row: &mut [f64]| {
58        let even_row = (y % 2 == 0) == r_row_even;
59        for x in 0..w {
60            let val = get_val(x, y);
61            let even_col = (x % 2 == 0) == r_col_even;
62            let interior = x > 0 && x + 1 < w && y > 0 && y + 1 < h;
63
64            match (even_row, even_col) {
65                (true, true) => {
66                    // Red pixel: green = orthogonal mean, blue = diagonal mean
67                    // (NDPluginColorConvert.cpp:308-309).
68                    r_row[x] = val;
69                    if interior {
70                        g_row[x] = (get_val(x - 1, y)
71                            + get_val(x + 1, y)
72                            + get_val(x, y - 1)
73                            + get_val(x, y + 1))
74                            / 4.0;
75                        b_row[x] = (get_val(x - 1, y - 1)
76                            + get_val(x + 1, y - 1)
77                            + get_val(x - 1, y + 1)
78                            + get_val(x + 1, y + 1))
79                            / 4.0;
80                    }
81                }
82                (true, false) | (false, true) => {
83                    // Green pixel.
84                    g_row[x] = val;
85                    if interior {
86                        if even_row {
87                            // Green next to red: red horizontal, blue vertical
88                            // (NDPluginColorConvert.cpp:313-314).
89                            r_row[x] = (get_val(x - 1, y) + get_val(x + 1, y)) / 2.0;
90                            b_row[x] = (get_val(x, y - 1) + get_val(x, y + 1)) / 2.0;
91                        } else {
92                            // Green next to blue: blue horizontal, red vertical
93                            // (NDPluginColorConvert.cpp:318-319).
94                            b_row[x] = (get_val(x - 1, y) + get_val(x + 1, y)) / 2.0;
95                            r_row[x] = (get_val(x, y - 1) + get_val(x, y + 1)) / 2.0;
96                        }
97                    }
98                }
99                (false, false) => {
100                    // Blue pixel: green = orthogonal mean, red = diagonal mean
101                    // (NDPluginColorConvert.cpp:308-309, blue branch).
102                    b_row[x] = val;
103                    if interior {
104                        g_row[x] = (get_val(x - 1, y)
105                            + get_val(x + 1, y)
106                            + get_val(x, y - 1)
107                            + get_val(x, y + 1))
108                            / 4.0;
109                        r_row[x] = (get_val(x - 1, y - 1)
110                            + get_val(x + 1, y - 1)
111                            + get_val(x - 1, y + 1)
112                            + get_val(x + 1, y + 1))
113                            / 4.0;
114                    }
115                }
116            }
117        }
118    };
119
120    #[cfg(feature = "parallel")]
121    let use_parallel = par_util::should_parallelize(n);
122    #[cfg(not(feature = "parallel"))]
123    let use_parallel = false;
124
125    if use_parallel {
126        #[cfg(feature = "parallel")]
127        {
128            // Split r, g, b into per-row mutable slices and process in parallel
129            let r_rows: Vec<&mut [f64]> = r.chunks_mut(w).collect();
130            let g_rows: Vec<&mut [f64]> = g.chunks_mut(w).collect();
131            let b_rows: Vec<&mut [f64]> = b.chunks_mut(w).collect();
132
133            par_util::thread_pool().install(|| {
134                r_rows
135                    .into_par_iter()
136                    .zip(g_rows.into_par_iter())
137                    .zip(b_rows.into_par_iter())
138                    .enumerate()
139                    .for_each(|(y, ((r_row, g_row), b_row))| {
140                        demosaic_row(y, r_row, g_row, b_row);
141                    });
142            });
143        }
144    } else {
145        for y in 0..h {
146            let row_start = y * w;
147            let row_end = row_start + w;
148            demosaic_row(
149                y,
150                &mut r[row_start..row_end],
151                &mut g[row_start..row_end],
152                &mut b[row_start..row_end],
153            );
154        }
155    }
156
157    // Build RGB1 interleaved output
158    let out_data = match src.data.data_type() {
159        NDDataType::UInt8 => {
160            let mut out = vec![0u8; n * 3];
161            for i in 0..n {
162                out[i * 3] = r[i].clamp(0.0, 255.0) as u8;
163                out[i * 3 + 1] = g[i].clamp(0.0, 255.0) as u8;
164                out[i * 3 + 2] = b[i].clamp(0.0, 255.0) as u8;
165            }
166            NDDataBuffer::U8(out)
167        }
168        NDDataType::UInt16 => {
169            let mut out = vec![0u16; n * 3];
170            for i in 0..n {
171                out[i * 3] = r[i].clamp(0.0, 65535.0) as u16;
172                out[i * 3 + 1] = g[i].clamp(0.0, 65535.0) as u16;
173                out[i * 3 + 2] = b[i].clamp(0.0, 65535.0) as u16;
174            }
175            NDDataBuffer::U16(out)
176        }
177        _ => return None,
178    };
179
180    let dims = vec![
181        NDDimension::new(3),
182        NDDimension::new(w),
183        NDDimension::new(h),
184    ];
185    let mut arr = NDArray::new(dims, src.data.data_type());
186    arr.data = out_data;
187    arr.unique_id = src.unique_id;
188    arr.timestamp = src.timestamp;
189    arr.attributes = src.attributes.clone();
190    Some(arr)
191}
192
193/// Rainbow false-color lookup table (`falseColor == 1`), 256 RGB entries.
194///
195/// Ported verbatim from ADCore `colorMaps.h` (RainbowColorR/G/B). C selects
196/// this table when the FalseColor parameter is 1
197/// (NDPluginColorConvert.cpp:62-66).
198const RAINBOW_COLOR_MAP: [[u8; 3]; 256] = [
199    [0, 0, 0],
200    [0, 0, 4],
201    [0, 0, 8],
202    [0, 0, 12],
203    [0, 0, 16],
204    [0, 0, 20],
205    [0, 0, 24],
206    [0, 0, 28],
207    [0, 0, 32],
208    [0, 0, 36],
209    [0, 0, 40],
210    [0, 0, 45],
211    [0, 0, 49],
212    [0, 0, 53],
213    [0, 0, 57],
214    [0, 0, 61],
215    [0, 0, 65],
216    [0, 0, 69],
217    [0, 0, 73],
218    [0, 0, 77],
219    [0, 0, 81],
220    [0, 0, 85],
221    [0, 0, 89],
222    [0, 0, 93],
223    [0, 0, 97],
224    [0, 0, 101],
225    [0, 0, 105],
226    [0, 0, 109],
227    [0, 0, 113],
228    [0, 0, 117],
229    [0, 0, 121],
230    [0, 0, 125],
231    [0, 0, 130],
232    [0, 0, 134],
233    [0, 0, 138],
234    [0, 0, 142],
235    [0, 0, 146],
236    [0, 0, 150],
237    [0, 0, 154],
238    [0, 0, 158],
239    [0, 0, 162],
240    [0, 0, 166],
241    [0, 0, 170],
242    [0, 0, 174],
243    [0, 0, 178],
244    [0, 0, 182],
245    [0, 0, 186],
246    [0, 0, 190],
247    [0, 0, 194],
248    [0, 0, 198],
249    [0, 0, 202],
250    [0, 0, 206],
251    [0, 0, 210],
252    [0, 0, 215],
253    [0, 0, 219],
254    [0, 0, 223],
255    [0, 0, 227],
256    [0, 0, 231],
257    [0, 0, 235],
258    [0, 0, 239],
259    [0, 0, 243],
260    [0, 0, 247],
261    [0, 0, 251],
262    [0, 0, 255],
263    [0, 4, 255],
264    [0, 8, 255],
265    [0, 12, 255],
266    [0, 16, 255],
267    [0, 20, 255],
268    [0, 24, 255],
269    [0, 28, 255],
270    [0, 32, 255],
271    [0, 36, 255],
272    [0, 40, 255],
273    [0, 44, 255],
274    [0, 48, 255],
275    [0, 52, 255],
276    [0, 56, 255],
277    [0, 60, 255],
278    [0, 64, 255],
279    [0, 68, 255],
280    [0, 72, 255],
281    [0, 76, 255],
282    [0, 80, 255],
283    [0, 84, 255],
284    [0, 88, 255],
285    [0, 92, 255],
286    [0, 96, 255],
287    [0, 100, 255],
288    [0, 104, 255],
289    [0, 108, 255],
290    [0, 112, 255],
291    [0, 116, 255],
292    [0, 120, 255],
293    [0, 124, 255],
294    [0, 128, 255],
295    [0, 131, 255],
296    [0, 135, 255],
297    [0, 139, 255],
298    [0, 143, 255],
299    [0, 147, 255],
300    [0, 151, 255],
301    [0, 155, 255],
302    [0, 159, 255],
303    [0, 163, 255],
304    [0, 167, 255],
305    [0, 171, 255],
306    [0, 175, 255],
307    [0, 179, 255],
308    [0, 183, 255],
309    [0, 187, 255],
310    [0, 191, 255],
311    [0, 195, 255],
312    [0, 199, 255],
313    [0, 203, 255],
314    [0, 207, 255],
315    [0, 211, 255],
316    [0, 215, 255],
317    [0, 219, 255],
318    [0, 223, 255],
319    [0, 227, 255],
320    [0, 231, 255],
321    [0, 235, 255],
322    [0, 239, 255],
323    [0, 243, 255],
324    [0, 247, 255],
325    [0, 251, 255],
326    [0, 255, 255],
327    [4, 255, 251],
328    [8, 255, 247],
329    [12, 255, 243],
330    [16, 255, 239],
331    [20, 255, 235],
332    [24, 255, 231],
333    [28, 255, 227],
334    [32, 255, 223],
335    [36, 255, 219],
336    [40, 255, 215],
337    [44, 255, 211],
338    [48, 255, 207],
339    [52, 255, 203],
340    [56, 255, 199],
341    [60, 255, 195],
342    [64, 255, 191],
343    [68, 255, 187],
344    [72, 255, 183],
345    [76, 255, 179],
346    [80, 255, 175],
347    [84, 255, 171],
348    [88, 255, 167],
349    [92, 255, 163],
350    [96, 255, 159],
351    [100, 255, 155],
352    [104, 255, 151],
353    [108, 255, 147],
354    [112, 255, 143],
355    [116, 255, 139],
356    [120, 255, 135],
357    [124, 255, 131],
358    [128, 255, 128],
359    [131, 255, 124],
360    [135, 255, 120],
361    [139, 255, 116],
362    [143, 255, 112],
363    [147, 255, 108],
364    [151, 255, 104],
365    [155, 255, 100],
366    [159, 255, 96],
367    [163, 255, 92],
368    [167, 255, 88],
369    [171, 255, 84],
370    [175, 255, 80],
371    [179, 255, 76],
372    [183, 255, 72],
373    [187, 255, 68],
374    [191, 255, 64],
375    [195, 255, 60],
376    [199, 255, 56],
377    [203, 255, 52],
378    [207, 255, 48],
379    [211, 255, 44],
380    [215, 255, 40],
381    [219, 255, 36],
382    [223, 255, 32],
383    [227, 255, 28],
384    [231, 255, 24],
385    [235, 255, 20],
386    [239, 255, 16],
387    [243, 255, 12],
388    [247, 255, 8],
389    [251, 255, 4],
390    [255, 255, 0],
391    [255, 251, 0],
392    [255, 247, 0],
393    [255, 243, 0],
394    [255, 239, 0],
395    [255, 235, 0],
396    [255, 231, 0],
397    [255, 227, 0],
398    [255, 223, 0],
399    [255, 219, 0],
400    [255, 215, 0],
401    [255, 211, 0],
402    [255, 207, 0],
403    [255, 203, 0],
404    [255, 199, 0],
405    [255, 195, 0],
406    [255, 191, 0],
407    [255, 187, 0],
408    [255, 183, 0],
409    [255, 179, 0],
410    [255, 175, 0],
411    [255, 171, 0],
412    [255, 167, 0],
413    [255, 163, 0],
414    [255, 159, 0],
415    [255, 155, 0],
416    [255, 151, 0],
417    [255, 147, 0],
418    [255, 143, 0],
419    [255, 139, 0],
420    [255, 135, 0],
421    [255, 131, 0],
422    [255, 128, 0],
423    [255, 124, 0],
424    [255, 120, 0],
425    [255, 116, 0],
426    [255, 112, 0],
427    [255, 108, 0],
428    [255, 104, 0],
429    [255, 100, 0],
430    [255, 96, 0],
431    [255, 92, 0],
432    [255, 88, 0],
433    [255, 84, 0],
434    [255, 80, 0],
435    [255, 76, 0],
436    [255, 72, 0],
437    [255, 68, 0],
438    [255, 64, 0],
439    [255, 60, 0],
440    [255, 56, 0],
441    [255, 52, 0],
442    [255, 48, 0],
443    [255, 44, 0],
444    [255, 40, 0],
445    [255, 36, 0],
446    [255, 32, 0],
447    [255, 28, 0],
448    [255, 24, 0],
449    [255, 20, 0],
450    [255, 16, 0],
451    [255, 12, 0],
452    [255, 8, 0],
453    [255, 4, 0],
454    [255, 255, 255],
455];
456
457/// Iron false-color lookup table (`falseColor == 2`), 256 RGB entries.
458///
459/// Ported verbatim from ADCore `colorMaps.h` (IronColorR/G/B). C selects this
460/// table when the FalseColor parameter is 2
461/// (NDPluginColorConvert.cpp:68-72).
462const IRON_COLOR_MAP: [[u8; 3]; 256] = [
463    [0, 0, 0],
464    [1, 0, 1],
465    [3, 1, 3],
466    [4, 1, 5],
467    [6, 2, 6],
468    [7, 2, 8],
469    [9, 3, 10],
470    [10, 3, 11],
471    [12, 4, 13],
472    [13, 4, 15],
473    [15, 5, 17],
474    [16, 5, 18],
475    [18, 6, 20],
476    [19, 6, 22],
477    [21, 7, 23],
478    [22, 7, 25],
479    [24, 8, 27],
480    [25, 8, 28],
481    [27, 9, 30],
482    [28, 9, 32],
483    [30, 10, 34],
484    [31, 10, 35],
485    [33, 11, 37],
486    [34, 11, 39],
487    [36, 12, 40],
488    [37, 12, 42],
489    [39, 13, 44],
490    [40, 13, 45],
491    [42, 14, 47],
492    [43, 14, 49],
493    [45, 15, 51],
494    [46, 15, 52],
495    [48, 16, 54],
496    [49, 16, 56],
497    [51, 17, 57],
498    [52, 17, 59],
499    [54, 18, 61],
500    [55, 18, 62],
501    [57, 19, 64],
502    [58, 19, 66],
503    [60, 20, 68],
504    [61, 20, 69],
505    [63, 21, 71],
506    [64, 21, 73],
507    [66, 22, 74],
508    [67, 22, 76],
509    [69, 23, 78],
510    [70, 23, 79],
511    [72, 24, 81],
512    [73, 24, 83],
513    [75, 25, 85],
514    [76, 25, 86],
515    [78, 26, 88],
516    [79, 26, 90],
517    [81, 27, 91],
518    [82, 27, 93],
519    [84, 28, 95],
520    [85, 28, 96],
521    [87, 29, 98],
522    [88, 29, 100],
523    [90, 30, 102],
524    [91, 30, 103],
525    [93, 31, 105],
526    [94, 31, 107],
527    [96, 32, 108],
528    [97, 32, 110],
529    [99, 33, 112],
530    [100, 33, 113],
531    [102, 34, 115],
532    [103, 34, 117],
533    [105, 35, 119],
534    [106, 35, 120],
535    [108, 36, 120],
536    [109, 36, 119],
537    [111, 37, 118],
538    [112, 37, 117],
539    [114, 38, 116],
540    [115, 38, 115],
541    [117, 39, 114],
542    [118, 39, 113],
543    [120, 40, 112],
544    [121, 40, 111],
545    [123, 41, 110],
546    [124, 41, 109],
547    [126, 42, 108],
548    [127, 42, 107],
549    [129, 43, 106],
550    [130, 43, 105],
551    [132, 44, 104],
552    [133, 44, 103],
553    [135, 45, 102],
554    [136, 45, 101],
555    [138, 46, 100],
556    [139, 46, 99],
557    [141, 47, 98],
558    [142, 47, 97],
559    [144, 48, 96],
560    [145, 48, 95],
561    [147, 49, 94],
562    [148, 49, 93],
563    [150, 50, 92],
564    [151, 50, 91],
565    [153, 51, 90],
566    [154, 51, 89],
567    [156, 52, 88],
568    [157, 52, 87],
569    [159, 53, 86],
570    [160, 53, 85],
571    [162, 54, 84],
572    [163, 54, 83],
573    [165, 55, 82],
574    [166, 55, 81],
575    [168, 56, 80],
576    [169, 56, 79],
577    [171, 57, 78],
578    [172, 57, 77],
579    [174, 58, 76],
580    [175, 58, 75],
581    [177, 59, 74],
582    [178, 59, 73],
583    [180, 60, 72],
584    [181, 60, 71],
585    [183, 61, 70],
586    [184, 61, 69],
587    [186, 62, 68],
588    [187, 62, 67],
589    [189, 63, 66],
590    [190, 63, 65],
591    [192, 64, 64],
592    [192, 65, 64],
593    [192, 67, 64],
594    [192, 68, 64],
595    [192, 70, 64],
596    [192, 71, 64],
597    [192, 73, 64],
598    [192, 74, 64],
599    [192, 76, 64],
600    [192, 77, 64],
601    [192, 79, 64],
602    [192, 80, 64],
603    [192, 82, 64],
604    [192, 83, 64],
605    [192, 85, 64],
606    [192, 86, 64],
607    [192, 88, 64],
608    [192, 89, 64],
609    [192, 91, 64],
610    [192, 92, 64],
611    [192, 94, 64],
612    [192, 95, 64],
613    [192, 97, 64],
614    [192, 98, 64],
615    [192, 100, 64],
616    [192, 101, 64],
617    [192, 103, 64],
618    [192, 104, 64],
619    [192, 106, 64],
620    [192, 107, 64],
621    [192, 109, 64],
622    [192, 110, 64],
623    [192, 112, 64],
624    [192, 113, 64],
625    [192, 115, 64],
626    [192, 116, 64],
627    [192, 118, 64],
628    [192, 119, 64],
629    [192, 121, 64],
630    [192, 122, 64],
631    [192, 124, 64],
632    [192, 125, 64],
633    [192, 127, 64],
634    [192, 128, 64],
635    [192, 130, 64],
636    [192, 131, 64],
637    [192, 133, 64],
638    [192, 134, 64],
639    [192, 136, 64],
640    [192, 137, 64],
641    [192, 139, 64],
642    [192, 140, 64],
643    [192, 142, 64],
644    [192, 143, 64],
645    [192, 145, 64],
646    [192, 146, 64],
647    [192, 148, 64],
648    [192, 149, 64],
649    [192, 151, 64],
650    [192, 152, 64],
651    [192, 154, 64],
652    [192, 155, 64],
653    [192, 157, 64],
654    [192, 158, 64],
655    [192, 160, 64],
656    [192, 161, 64],
657    [192, 163, 64],
658    [192, 164, 64],
659    [192, 166, 64],
660    [192, 167, 64],
661    [192, 169, 64],
662    [192, 170, 64],
663    [192, 172, 64],
664    [192, 173, 64],
665    [192, 175, 64],
666    [192, 176, 64],
667    [192, 178, 64],
668    [192, 179, 64],
669    [192, 181, 64],
670    [192, 182, 64],
671    [192, 184, 64],
672    [192, 185, 64],
673    [192, 187, 64],
674    [192, 188, 64],
675    [192, 190, 64],
676    [192, 191, 64],
677    [192, 193, 64],
678    [192, 194, 64],
679    [192, 196, 64],
680    [192, 197, 64],
681    [192, 199, 64],
682    [192, 200, 64],
683    [192, 202, 64],
684    [192, 203, 64],
685    [192, 205, 64],
686    [192, 206, 64],
687    [192, 208, 64],
688    [192, 209, 64],
689    [192, 211, 64],
690    [192, 212, 64],
691    [192, 214, 64],
692    [192, 215, 64],
693    [192, 217, 64],
694    [192, 218, 64],
695    [192, 220, 64],
696    [192, 221, 64],
697    [192, 223, 64],
698    [192, 224, 64],
699    [192, 226, 64],
700    [192, 227, 64],
701    [192, 229, 64],
702    [192, 230, 64],
703    [192, 232, 64],
704    [192, 233, 64],
705    [192, 235, 64],
706    [192, 236, 64],
707    [192, 238, 64],
708    [192, 239, 64],
709    [192, 241, 64],
710    [192, 242, 64],
711    [192, 244, 64],
712    [192, 245, 64],
713    [192, 247, 64],
714    [192, 248, 64],
715    [192, 250, 64],
716    [192, 251, 64],
717    [192, 253, 64],
718    [255, 255, 255],
719];
720
721/// Select the false-color LUT for a `falseColor` parameter value.
722///
723/// Matches NDPluginColorConvert.cpp:61-78: 1 => Rainbow, 2 => Iron, any other
724/// value => no false color (C resets `falseColor` to 0 in the `default` arm).
725fn false_color_lut(false_color: i32) -> Option<&'static [[u8; 3]; 256]> {
726    match false_color {
727        1 => Some(&RAINBOW_COLOR_MAP),
728        2 => Some(&IRON_COLOR_MAP),
729        _ => None,
730    }
731}
732
733/// Convert a mono UInt8 image to RGB1 using a false-color LUT.
734///
735/// Only supports 2D UInt8 arrays. Each pixel value indexes the selected LUT
736/// (Rainbow for `false_color == 1`, Iron for `2`) to produce a pseudo-color
737/// RGB1 output, matching C's `colorMapRGB[3*pixel]` application for the
738/// Mono->RGB1 case (NDPluginColorConvert.cpp:106-112). Returns `None` for
739/// unsupported input or a `false_color` value with no table.
740fn false_color_mono_to_rgb1(src: &NDArray, false_color: i32) -> Option<NDArray> {
741    if src.dims.len() != 2 || src.data.data_type() != NDDataType::UInt8 {
742        return None;
743    }
744    let lut = false_color_lut(false_color)?;
745
746    let w = src.dims[0].size;
747    let h = src.dims[1].size;
748    let n = w * h;
749
750    let src_slice = src.data.as_u8_slice();
751    let mut out = vec![0u8; n * 3];
752    for i in 0..n {
753        let val = src_slice[i] as usize;
754        let [r, g, b] = lut[val];
755        out[i * 3] = r;
756        out[i * 3 + 1] = g;
757        out[i * 3 + 2] = b;
758    }
759
760    let dims = vec![
761        NDDimension::new(3),
762        NDDimension::new(w),
763        NDDimension::new(h),
764    ];
765    let mut arr = NDArray::new(dims, NDDataType::UInt8);
766    arr.data = NDDataBuffer::U8(out);
767    arr.unique_id = src.unique_id;
768    arr.timestamp = src.timestamp;
769    arr.attributes = src.attributes.clone();
770    Some(arr)
771}
772
773/// Detect the color mode of an NDArray.
774///
775/// Checks the `ColorMode` NDAttribute first (required for YUV422/YUV411 which are
776/// 2D arrays indistinguishable from Mono, and YUV444/Bayer which share dimensions
777/// with RGB1/Mono). Falls back to dimension-based detection.
778fn detect_color_mode(array: &NDArray) -> NDColorMode {
779    if let Some(attr) = array.attributes.get("ColorMode") {
780        if let Some(v) = attr.value.as_i64() {
781            return NDColorMode::from_i32(v as i32);
782        }
783    }
784    match array.dims.len() {
785        0 | 1 => NDColorMode::Mono,
786        2 => NDColorMode::Mono,
787        3 => {
788            if array.dims[0].size == 3 {
789                NDColorMode::RGB1
790            } else if array.dims[1].size == 3 {
791                NDColorMode::RGB2
792            } else if array.dims[2].size == 3 {
793                NDColorMode::RGB3
794            } else {
795                NDColorMode::Mono
796            }
797        }
798        _ => NDColorMode::Mono,
799    }
800}
801
802/// Color convert plugin configuration.
803#[derive(Debug, Clone)]
804pub struct ColorConvertConfig {
805    pub target_mode: NDColorMode,
806    pub bayer_pattern: NDBayerPattern,
807    /// False color mode: 0=off, 1=Rainbow, 2=Iron. Nonzero is treated as enabled.
808    pub false_color: i32,
809}
810
811/// Pure color conversion processing logic.
812pub struct ColorConvertProcessor {
813    config: ColorConvertConfig,
814    color_mode_out_idx: Option<usize>,
815    false_color_idx: Option<usize>,
816}
817
818impl ColorConvertProcessor {
819    pub fn new(config: ColorConvertConfig) -> Self {
820        Self {
821            config,
822            color_mode_out_idx: None,
823            false_color_idx: None,
824        }
825    }
826}
827
828impl NDPluginProcess for ColorConvertProcessor {
829    fn register_params(
830        &mut self,
831        base: &mut asyn_rs::port::PortDriverBase,
832    ) -> asyn_rs::error::AsynResult<()> {
833        use asyn_rs::param::ParamType;
834        base.create_param("COLOR_MODE_OUT", ParamType::Int32)?;
835        base.create_param("FALSE_COLOR", ParamType::Int32)?;
836        self.color_mode_out_idx = base.find_param("COLOR_MODE_OUT");
837        self.false_color_idx = base.find_param("FALSE_COLOR");
838        Ok(())
839    }
840
841    fn on_param_change(
842        &mut self,
843        reason: usize,
844        params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
845    ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
846        if Some(reason) == self.color_mode_out_idx {
847            self.config.target_mode = NDColorMode::from_i32(params.value.as_i32());
848        } else if Some(reason) == self.false_color_idx {
849            self.config.false_color = params.value.as_i32();
850        }
851        ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
852    }
853
854    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
855        let src_mode = detect_color_mode(array);
856        let target = self.config.target_mode;
857
858        // Same mode - passthrough
859        if src_mode == target {
860            return ProcessResult::arrays(vec![Arc::new(array.clone())]);
861        }
862
863        // Step 1: Convert source to RGB1 intermediate
864        let rgb1 = match src_mode {
865            NDColorMode::RGB1 => Some(array.clone()),
866            NDColorMode::Mono => {
867                if self.config.false_color != 0 {
868                    false_color_mono_to_rgb1(array, self.config.false_color)
869                        .or_else(|| color::mono_to_rgb1(array).ok())
870                } else {
871                    color::mono_to_rgb1(array).ok()
872                }
873            }
874            NDColorMode::Bayer => bayer_to_rgb1(array, self.config.bayer_pattern),
875            NDColorMode::RGB2 | NDColorMode::RGB3 => {
876                color::convert_rgb_layout(array, src_mode, NDColorMode::RGB1).ok()
877            }
878            NDColorMode::YUV444 => color::yuv444_to_rgb1(array).ok(),
879            NDColorMode::YUV422 => color::yuv422_to_rgb1(array).ok(),
880            NDColorMode::YUV411 => color::yuv411_to_rgb1(array).ok(),
881        };
882
883        let rgb1 = match rgb1 {
884            Some(r) => r,
885            None => return ProcessResult::empty(),
886        };
887
888        // Step 2: Convert RGB1 intermediate to target
889        let result = match target {
890            NDColorMode::RGB1 => Some(rgb1),
891            NDColorMode::Mono => color::rgb1_to_mono(&rgb1).ok(),
892            NDColorMode::Bayer => None,
893            NDColorMode::RGB2 | NDColorMode::RGB3 => {
894                color::convert_rgb_layout(&rgb1, NDColorMode::RGB1, target).ok()
895            }
896            NDColorMode::YUV444 => color::rgb1_to_yuv444(&rgb1).ok(),
897            NDColorMode::YUV422 => color::rgb1_to_yuv422(&rgb1).ok(),
898            NDColorMode::YUV411 => color::rgb1_to_yuv411(&rgb1).ok(),
899        };
900
901        match result {
902            Some(mut out) => {
903                // C++: set ColorMode attribute on output array
904                let color_mode_val = match target {
905                    NDColorMode::Mono => 0i32,
906                    NDColorMode::Bayer => 1,
907                    NDColorMode::RGB1 => 2,
908                    NDColorMode::RGB2 => 3,
909                    NDColorMode::RGB3 => 4,
910                    NDColorMode::YUV444 => 5,
911                    NDColorMode::YUV422 => 6,
912                    NDColorMode::YUV411 => 7,
913                };
914                use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
915                out.attributes.add(NDAttribute::new_static(
916                    "ColorMode",
917                    "Color Mode",
918                    NDAttrSource::Driver,
919                    NDAttrValue::Int32(color_mode_val),
920                ));
921                ProcessResult::arrays(vec![Arc::new(out)])
922            }
923            None => ProcessResult::empty(),
924        }
925    }
926
927    fn plugin_type(&self) -> &str {
928        "NDPluginColorConvert"
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935
936    #[test]
937    fn test_bayer_to_rgb1_basic() {
938        // 4x4 RGGB bayer pattern
939        let mut arr = NDArray::new(
940            vec![NDDimension::new(4), NDDimension::new(4)],
941            NDDataType::UInt8,
942        );
943        if let NDDataBuffer::U8(ref mut v) = arr.data {
944            // Simple pattern: all pixels = 128
945            for i in 0..16 {
946                v[i] = 128;
947            }
948        }
949
950        let rgb = bayer_to_rgb1(&arr, NDBayerPattern::RGGB).unwrap();
951        assert_eq!(rgb.dims.len(), 3);
952        assert_eq!(rgb.dims[0].size, 3); // color
953        assert_eq!(rgb.dims[1].size, 4); // x
954        assert_eq!(rgb.dims[2].size, 4); // y
955    }
956
957    // RGB1 pixel (x,y) channel c lives at out[(y*w + x)*3 + c].
958    fn rgb1_pixel(arr: &NDArray, w: usize, x: usize, y: usize) -> [u8; 3] {
959        let i = (y * w + x) * 3;
960        if let NDDataBuffer::U8(ref v) = arr.data {
961            [v[i], v[i + 1], v[i + 2]]
962        } else {
963            panic!("expected UInt8 RGB1 output");
964        }
965    }
966
967    #[test]
968    fn test_adp4_bayer_border_keeps_native_channel_only() {
969        // 4x4 RGGB, all pixels = 100. C interpolates only interior pixels
970        // (NDPluginColorConvert.cpp:305); border pixels keep only their native
971        // Bayer channel, the other two stay 0. Previously the Rust border was
972        // interpolated from available neighbours (e.g. corner -> (100,100,100)).
973        let mut arr = NDArray::new(
974            vec![NDDimension::new(4), NDDimension::new(4)],
975            NDDataType::UInt8,
976        );
977        if let NDDataBuffer::U8(ref mut v) = arr.data {
978            v.iter_mut().for_each(|p| *p = 100);
979        }
980        let rgb = bayer_to_rgb1(&arr, NDBayerPattern::RGGB).unwrap();
981
982        // Border pixels: native channel only.
983        assert_eq!(rgb1_pixel(&rgb, 4, 0, 0), [100, 0, 0]); // corner, red
984        assert_eq!(rgb1_pixel(&rgb, 4, 1, 0), [0, 100, 0]); // top edge, green
985        assert_eq!(rgb1_pixel(&rgb, 4, 0, 1), [0, 100, 0]); // left edge, green
986        assert_eq!(rgb1_pixel(&rgb, 4, 3, 3), [0, 0, 100]); // corner, blue
987
988        // Interior pixels: fully interpolated (all neighbours = 100).
989        assert_eq!(rgb1_pixel(&rgb, 4, 1, 1), [100, 100, 100]); // blue
990        assert_eq!(rgb1_pixel(&rgb, 4, 1, 2), [100, 100, 100]); // green
991        assert_eq!(rgb1_pixel(&rgb, 4, 2, 2), [100, 100, 100]); // red
992    }
993
994    #[test]
995    fn test_adp4_bayer_interior_uses_fixed_quarter_divisor() {
996        // 3x3 RGGB: the only interior pixel is the blue centre (1,1). Its red is
997        // the diagonal mean /4 and green the orthogonal mean /4
998        // (NDPluginColorConvert.cpp:308-309). Distinct neighbour values pin the
999        // divisor: diagonals all 200 -> red 200; orthogonals 40,40,80,80 ->
1000        // green 60.
1001        let mut arr = NDArray::new(
1002            vec![NDDimension::new(3), NDDimension::new(3)],
1003            NDDataType::UInt8,
1004        );
1005        if let NDDataBuffer::U8(ref mut v) = arr.data {
1006            // row0: 200 40 200 / row1: 80 128 80 / row2: 200 40 200
1007            v.copy_from_slice(&[200, 40, 200, 80, 128, 80, 200, 40, 200]);
1008        }
1009        let rgb = bayer_to_rgb1(&arr, NDBayerPattern::RGGB).unwrap();
1010
1011        // Interior blue centre: red = (200*4)/4 = 200, green = (40+40+80+80)/4 = 60.
1012        assert_eq!(rgb1_pixel(&rgb, 3, 1, 1), [200, 60, 128]);
1013        // All eight surrounding pixels are border: native channel only.
1014        assert_eq!(rgb1_pixel(&rgb, 3, 0, 0), [200, 0, 0]); // red
1015        assert_eq!(rgb1_pixel(&rgb, 3, 1, 0), [0, 40, 0]); // green
1016    }
1017
1018    #[test]
1019    fn test_color_convert_processor_bayer() {
1020        let config = ColorConvertConfig {
1021            target_mode: NDColorMode::RGB1,
1022            bayer_pattern: NDBayerPattern::RGGB,
1023            false_color: 0,
1024        };
1025        let mut proc = ColorConvertProcessor::new(config);
1026        let pool = NDArrayPool::new(1_000_000);
1027
1028        let mut arr = NDArray::new(
1029            vec![NDDimension::new(4), NDDimension::new(4)],
1030            NDDataType::UInt8,
1031        );
1032        if let NDDataBuffer::U8(ref mut v) = arr.data {
1033            for i in 0..16 {
1034                v[i] = 128;
1035            }
1036        }
1037
1038        let result = proc.process_array(&arr, &pool);
1039        assert_eq!(result.output_arrays.len(), 1);
1040        assert_eq!(result.output_arrays[0].dims[0].size, 3); // RGB color dim
1041    }
1042
1043    #[test]
1044    fn test_false_color_conversion() {
1045        let config = ColorConvertConfig {
1046            target_mode: NDColorMode::RGB1,
1047            bayer_pattern: NDBayerPattern::RGGB,
1048            false_color: 1,
1049        };
1050        let mut proc = ColorConvertProcessor::new(config);
1051        let pool = NDArrayPool::new(1_000_000);
1052
1053        // Create a 4x4 mono UInt8 image with a gradient
1054        let mut arr = NDArray::new(
1055            vec![NDDimension::new(4), NDDimension::new(4)],
1056            NDDataType::UInt8,
1057        );
1058        if let NDDataBuffer::U8(ref mut v) = arr.data {
1059            for i in 0..16 {
1060                v[i] = (i * 17) as u8; // 0, 17, 34, ... 255
1061            }
1062        }
1063
1064        let result = proc.process_array(&arr, &pool);
1065        assert_eq!(result.output_arrays.len(), 1);
1066        let out = &result.output_arrays[0];
1067        assert_eq!(out.dims.len(), 3);
1068        assert_eq!(out.dims[0].size, 3); // color
1069        assert_eq!(out.dims[1].size, 4); // x
1070        assert_eq!(out.dims[2].size, 4); // y
1071
1072        // false_color=1 selects the Rainbow LUT (NDPluginColorConvert.cpp:62-66).
1073        // Pixel 0 (value=0) -> Rainbow[0]=(0,0,0); the old jet LUT gave B=127 here.
1074        if let NDDataBuffer::U8(ref v) = out.data {
1075            assert_eq!([v[0], v[1], v[2]], RAINBOW_COLOR_MAP[0]);
1076            assert_eq!([v[0], v[1], v[2]], [0, 0, 0]);
1077            // Last pixel (value=255) -> Rainbow[255]=(255,255,255).
1078            let last = 15 * 3;
1079            assert_eq!([v[last], v[last + 1], v[last + 2]], RAINBOW_COLOR_MAP[255]);
1080        } else {
1081            panic!("expected UInt8 output");
1082        }
1083    }
1084
1085    #[test]
1086    fn test_false_color_iron_table() {
1087        let config = ColorConvertConfig {
1088            target_mode: NDColorMode::RGB1,
1089            bayer_pattern: NDBayerPattern::RGGB,
1090            false_color: 2,
1091        };
1092        let mut proc = ColorConvertProcessor::new(config);
1093        let pool = NDArrayPool::new(1_000_000);
1094
1095        // 2x1 mono image with values 0 and 192 to probe distinct Iron entries.
1096        let mut arr = NDArray::new(
1097            vec![NDDimension::new(2), NDDimension::new(1)],
1098            NDDataType::UInt8,
1099        );
1100        if let NDDataBuffer::U8(ref mut v) = arr.data {
1101            v[0] = 0;
1102            v[1] = 192;
1103        }
1104
1105        let result = proc.process_array(&arr, &pool);
1106        let out = &result.output_arrays[0];
1107        if let NDDataBuffer::U8(ref v) = out.data {
1108            // false_color=2 selects Iron (NDPluginColorConvert.cpp:68-72).
1109            assert_eq!([v[0], v[1], v[2]], IRON_COLOR_MAP[0]);
1110            assert_eq!([v[3], v[4], v[5]], IRON_COLOR_MAP[192]);
1111            // Iron[192]=(192,160,64) differs from the Rainbow table at the same index.
1112            assert_eq!([v[3], v[4], v[5]], [192, 160, 64]);
1113            assert_ne!(IRON_COLOR_MAP[192], RAINBOW_COLOR_MAP[192]);
1114        } else {
1115            panic!("expected UInt8 output");
1116        }
1117    }
1118
1119    #[test]
1120    fn test_rgb1_to_rgb2_conversion() {
1121        let config = ColorConvertConfig {
1122            target_mode: NDColorMode::RGB2,
1123            bayer_pattern: NDBayerPattern::RGGB,
1124            false_color: 0,
1125        };
1126        let mut proc = ColorConvertProcessor::new(config);
1127        let pool = NDArrayPool::new(1_000_000);
1128
1129        // Create RGB1 image: dims [3, 4, 4]
1130        let mut arr = NDArray::new(
1131            vec![
1132                NDDimension::new(3),
1133                NDDimension::new(4),
1134                NDDimension::new(4),
1135            ],
1136            NDDataType::UInt8,
1137        );
1138        if let NDDataBuffer::U8(ref mut v) = arr.data {
1139            for i in 0..v.len() {
1140                v[i] = (i % 256) as u8;
1141            }
1142        }
1143
1144        let result = proc.process_array(&arr, &pool);
1145        assert_eq!(result.output_arrays.len(), 1);
1146        let out = &result.output_arrays[0];
1147        assert_eq!(out.dims.len(), 3);
1148        // RGB2 has color dim in position 1
1149        assert_eq!(out.dims[1].size, 3);
1150    }
1151
1152    #[test]
1153    fn test_rgb2_to_mono_conversion() {
1154        let config = ColorConvertConfig {
1155            target_mode: NDColorMode::Mono,
1156            bayer_pattern: NDBayerPattern::RGGB,
1157            false_color: 0,
1158        };
1159        let mut proc = ColorConvertProcessor::new(config);
1160        let pool = NDArrayPool::new(1_000_000);
1161
1162        // Create RGB2 image: dims [4, 3, 4]
1163        let mut arr = NDArray::new(
1164            vec![
1165                NDDimension::new(4),
1166                NDDimension::new(3),
1167                NDDimension::new(4),
1168            ],
1169            NDDataType::UInt8,
1170        );
1171        if let NDDataBuffer::U8(ref mut v) = arr.data {
1172            for i in 0..v.len() {
1173                v[i] = 128;
1174            }
1175        }
1176
1177        let result = proc.process_array(&arr, &pool);
1178        assert_eq!(result.output_arrays.len(), 1);
1179        let out = &result.output_arrays[0];
1180        // Mono output should be 2D
1181        assert_eq!(out.dims.len(), 2);
1182    }
1183
1184    #[test]
1185    fn test_detect_color_mode() {
1186        // 2D -> Mono
1187        let arr2d = NDArray::new(
1188            vec![NDDimension::new(4), NDDimension::new(4)],
1189            NDDataType::UInt8,
1190        );
1191        assert_eq!(detect_color_mode(&arr2d), NDColorMode::Mono);
1192
1193        // 3D with color dim first -> RGB1
1194        let arr_rgb1 = NDArray::new(
1195            vec![
1196                NDDimension::new(3),
1197                NDDimension::new(4),
1198                NDDimension::new(4),
1199            ],
1200            NDDataType::UInt8,
1201        );
1202        assert_eq!(detect_color_mode(&arr_rgb1), NDColorMode::RGB1);
1203
1204        // 3D with color dim second -> RGB2
1205        let arr_rgb2 = NDArray::new(
1206            vec![
1207                NDDimension::new(4),
1208                NDDimension::new(3),
1209                NDDimension::new(4),
1210            ],
1211            NDDataType::UInt8,
1212        );
1213        assert_eq!(detect_color_mode(&arr_rgb2), NDColorMode::RGB2);
1214
1215        // 3D with color dim last -> RGB3
1216        let arr_rgb3 = NDArray::new(
1217            vec![
1218                NDDimension::new(4),
1219                NDDimension::new(4),
1220                NDDimension::new(3),
1221            ],
1222            NDDataType::UInt8,
1223        );
1224        assert_eq!(detect_color_mode(&arr_rgb3), NDColorMode::RGB3);
1225    }
1226
1227    #[test]
1228    fn test_same_mode_passthrough() {
1229        let config = ColorConvertConfig {
1230            target_mode: NDColorMode::Mono,
1231            bayer_pattern: NDBayerPattern::RGGB,
1232            false_color: 0,
1233        };
1234        let mut proc = ColorConvertProcessor::new(config);
1235        let pool = NDArrayPool::new(1_000_000);
1236
1237        // 2D mono input with Mono target -> passthrough
1238        let mut arr = NDArray::new(
1239            vec![NDDimension::new(4), NDDimension::new(4)],
1240            NDDataType::UInt8,
1241        );
1242        arr.unique_id = 42;
1243        if let NDDataBuffer::U8(ref mut v) = arr.data {
1244            for i in 0..16 {
1245                v[i] = i as u8;
1246            }
1247        }
1248
1249        let result = proc.process_array(&arr, &pool);
1250        assert_eq!(result.output_arrays.len(), 1);
1251        assert_eq!(result.output_arrays[0].unique_id, 42);
1252        assert_eq!(result.output_arrays[0].dims.len(), 2);
1253    }
1254
1255    fn set_color_mode_attr(arr: &mut NDArray, mode: NDColorMode) {
1256        use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
1257        arr.attributes.add(NDAttribute::new_static(
1258            "ColorMode",
1259            String::new(),
1260            NDAttrSource::Driver,
1261            NDAttrValue::Int32(mode as i32),
1262        ));
1263    }
1264
1265    #[test]
1266    fn test_bayer_to_mono_via_rgb1() {
1267        let config = ColorConvertConfig {
1268            target_mode: NDColorMode::Mono,
1269            bayer_pattern: NDBayerPattern::RGGB,
1270            false_color: 0,
1271        };
1272        let mut proc = ColorConvertProcessor::new(config);
1273        let pool = NDArrayPool::new(1_000_000);
1274
1275        let mut arr = NDArray::new(
1276            vec![NDDimension::new(4), NDDimension::new(4)],
1277            NDDataType::UInt8,
1278        );
1279        set_color_mode_attr(&mut arr, NDColorMode::Bayer);
1280        if let NDDataBuffer::U8(ref mut v) = arr.data {
1281            for i in 0..16 {
1282                v[i] = 128;
1283            }
1284        }
1285
1286        let result = proc.process_array(&arr, &pool);
1287        assert_eq!(result.output_arrays.len(), 1);
1288        assert_eq!(result.output_arrays[0].dims.len(), 2);
1289    }
1290
1291    #[test]
1292    fn test_rgb1_to_yuv444_conversion() {
1293        let config = ColorConvertConfig {
1294            target_mode: NDColorMode::YUV444,
1295            bayer_pattern: NDBayerPattern::RGGB,
1296            false_color: 0,
1297        };
1298        let mut proc = ColorConvertProcessor::new(config);
1299        let pool = NDArrayPool::new(1_000_000);
1300
1301        let mut arr = NDArray::new(
1302            vec![
1303                NDDimension::new(3),
1304                NDDimension::new(4),
1305                NDDimension::new(4),
1306            ],
1307            NDDataType::UInt8,
1308        );
1309        if let NDDataBuffer::U8(ref mut v) = arr.data {
1310            for i in 0..v.len() {
1311                v[i] = (i % 256) as u8;
1312            }
1313        }
1314
1315        let result = proc.process_array(&arr, &pool);
1316        assert_eq!(result.output_arrays.len(), 1);
1317        let out = &result.output_arrays[0];
1318        assert_eq!(out.dims.len(), 3);
1319        assert_eq!(out.dims[0].size, 3);
1320    }
1321
1322    #[test]
1323    fn test_yuv422_to_rgb1_conversion() {
1324        let config = ColorConvertConfig {
1325            target_mode: NDColorMode::RGB1,
1326            bayer_pattern: NDBayerPattern::RGGB,
1327            false_color: 0,
1328        };
1329        let mut proc = ColorConvertProcessor::new(config);
1330        let pool = NDArrayPool::new(1_000_000);
1331
1332        // packed_x=8 means 4 pixels wide, 2 rows
1333        let mut arr = NDArray::new(
1334            vec![NDDimension::new(8), NDDimension::new(2)],
1335            NDDataType::UInt8,
1336        );
1337        set_color_mode_attr(&mut arr, NDColorMode::YUV422);
1338        if let NDDataBuffer::U8(ref mut v) = arr.data {
1339            // UYVY pattern: U Y0 V Y1
1340            let uyvy: [u8; 16] = [
1341                128, 100, 128, 150, 128, 200, 128, 50, 128, 128, 128, 128, 128, 64, 128, 192,
1342            ];
1343            v[..16].copy_from_slice(&uyvy);
1344        }
1345
1346        let result = proc.process_array(&arr, &pool);
1347        assert_eq!(result.output_arrays.len(), 1);
1348        let out = &result.output_arrays[0];
1349        assert_eq!(out.dims[0].size, 3);
1350        assert_eq!(out.dims[1].size, 4);
1351        assert_eq!(out.dims[2].size, 2);
1352    }
1353
1354    #[test]
1355    fn test_mono_to_yuv422_conversion() {
1356        let config = ColorConvertConfig {
1357            target_mode: NDColorMode::YUV422,
1358            bayer_pattern: NDBayerPattern::RGGB,
1359            false_color: 0,
1360        };
1361        let mut proc = ColorConvertProcessor::new(config);
1362        let pool = NDArrayPool::new(1_000_000);
1363
1364        let mut arr = NDArray::new(
1365            vec![NDDimension::new(4), NDDimension::new(2)],
1366            NDDataType::UInt8,
1367        );
1368        if let NDDataBuffer::U8(ref mut v) = arr.data {
1369            for i in 0..8 {
1370                v[i] = (i * 30) as u8;
1371            }
1372        }
1373
1374        let result = proc.process_array(&arr, &pool);
1375        assert_eq!(result.output_arrays.len(), 1);
1376        let out = &result.output_arrays[0];
1377        assert_eq!(out.dims.len(), 2);
1378        assert_eq!(out.dims[0].size, 8); // packed_x = 4*2
1379    }
1380
1381    #[test]
1382    fn test_yuv444_to_mono_conversion() {
1383        let config = ColorConvertConfig {
1384            target_mode: NDColorMode::Mono,
1385            bayer_pattern: NDBayerPattern::RGGB,
1386            false_color: 0,
1387        };
1388        let mut proc = ColorConvertProcessor::new(config);
1389        let pool = NDArrayPool::new(1_000_000);
1390
1391        let mut arr = NDArray::new(
1392            vec![
1393                NDDimension::new(3),
1394                NDDimension::new(4),
1395                NDDimension::new(4),
1396            ],
1397            NDDataType::UInt8,
1398        );
1399        set_color_mode_attr(&mut arr, NDColorMode::YUV444);
1400        if let NDDataBuffer::U8(ref mut v) = arr.data {
1401            for i in 0..v.len() {
1402                v[i] = 128;
1403            }
1404        }
1405
1406        let result = proc.process_array(&arr, &pool);
1407        assert_eq!(result.output_arrays.len(), 1);
1408        let out = &result.output_arrays[0];
1409        assert_eq!(out.dims.len(), 2);
1410        assert_eq!(out.dims[0].size, 4);
1411        assert_eq!(out.dims[1].size, 4);
1412    }
1413
1414    #[test]
1415    fn test_detect_color_mode_with_attribute() {
1416        let mut arr = NDArray::new(
1417            vec![NDDimension::new(8), NDDimension::new(2)],
1418            NDDataType::UInt8,
1419        );
1420        assert_eq!(detect_color_mode(&arr), NDColorMode::Mono);
1421
1422        set_color_mode_attr(&mut arr, NDColorMode::YUV422);
1423        assert_eq!(detect_color_mode(&arr), NDColorMode::YUV422);
1424    }
1425
1426    #[test]
1427    fn test_false_color_table_endpoints() {
1428        // Both ADCore tables start at black and end at white (colorMaps.h).
1429        // The previous jet generator gave (0,0,127) at index 0 — the divergence
1430        // this fix closes.
1431        assert_eq!(RAINBOW_COLOR_MAP[0], [0, 0, 0]);
1432        assert_eq!(RAINBOW_COLOR_MAP[255], [255, 255, 255]);
1433        assert_eq!(IRON_COLOR_MAP[0], [0, 0, 0]);
1434        assert_eq!(IRON_COLOR_MAP[255], [255, 255, 255]);
1435
1436        // Mid-table sample pins the exact ported bytes (RainbowColor at 128,
1437        // IronColor at 128 per colorMaps.h).
1438        assert_eq!(RAINBOW_COLOR_MAP[128], [4, 255, 251]);
1439        assert_eq!(IRON_COLOR_MAP[128], [192, 64, 64]);
1440
1441        // falseColor selector matches NDPluginColorConvert.cpp:61-78.
1442        assert_eq!(false_color_lut(1), Some(&RAINBOW_COLOR_MAP));
1443        assert_eq!(false_color_lut(2), Some(&IRON_COLOR_MAP));
1444        assert_eq!(false_color_lut(0), None);
1445        assert_eq!(false_color_lut(3), None);
1446    }
1447}