Skip to main content

ad_plugins_rs/
color_convert.rs

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