1use std::collections::HashSet;
21use std::sync::Arc;
22
23use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
24use ad_core_rs::ndarray_pool::NDArrayPool;
25use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
26use parking_lot::Mutex;
27use serde::Deserialize;
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum BadPixelMode {
32 Set { value: f64 },
34 Replace { dx: i32, dy: i32 },
36 Median { half_x: i64, half_y: i64 },
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub struct BadPixel {
45 pub x: i64,
46 pub y: i64,
47 pub mode: BadPixelMode,
48}
49
50#[derive(Debug, Deserialize)]
54struct BadPixelJson {
55 #[serde(rename = "Pixel")]
56 pixel: [i64; 2],
57 #[serde(rename = "Set", default)]
58 set: Option<f64>,
59 #[serde(rename = "Replace", default)]
60 replace: Option<[i64; 2]>,
61 #[serde(rename = "Median", default)]
62 median: Option<[i64; 2]>,
63}
64
65#[derive(Debug, Deserialize)]
67struct BadPixelFileJson {
68 #[serde(rename = "Bad pixels")]
69 bad_pixels: Vec<BadPixelJson>,
70}
71
72struct BadPixelList {
75 pixels: Vec<BadPixel>,
76 bad_set: HashSet<(i64, i64)>,
79}
80
81impl BadPixelList {
82 fn new(pixels: Vec<BadPixel>) -> Self {
83 let bad_set: HashSet<(i64, i64)> = pixels.iter().map(|p| (p.x, p.y)).collect();
84 Self { pixels, bad_set }
85 }
86
87 fn is_bad(&self, x: i64, y: i64) -> bool {
89 self.bad_set.contains(&(x, y))
90 }
91}
92
93pub struct BadPixelProcessor {
95 list: Mutex<Arc<BadPixelList>>,
101 file_name_idx: Option<usize>,
102}
103
104impl BadPixelProcessor {
105 pub fn new(pixels: Vec<BadPixel>) -> Self {
107 Self {
108 list: Mutex::new(Arc::new(BadPixelList::new(pixels))),
109 file_name_idx: None,
110 }
111 }
112
113 pub fn load_from_json(json_str: &str) -> Result<Vec<BadPixel>, serde_json::Error> {
120 let file: BadPixelFileJson = serde_json::from_str(json_str)?;
121 Ok(file
122 .bad_pixels
123 .into_iter()
124 .map(|e| {
125 let mut mode = BadPixelMode::Set { value: 0.0 };
128 if let Some(m) = e.median {
129 mode = BadPixelMode::Median {
130 half_x: m[0],
131 half_y: m[1],
132 };
133 }
134 if let Some(v) = e.set {
135 mode = BadPixelMode::Set { value: v };
136 }
137 if let Some(r) = e.replace {
138 mode = BadPixelMode::Replace {
139 dx: r[0] as i32,
140 dy: r[1] as i32,
141 };
142 }
143 BadPixel {
144 x: e.pixel[0],
145 y: e.pixel[1],
146 mode,
147 }
148 })
149 .collect())
150 }
151
152 pub fn set_pixels(&self, pixels: Vec<BadPixel>) {
154 *self.list.lock() = Arc::new(BadPixelList::new(pixels));
155 }
156
157 pub fn pixels(&self) -> Vec<BadPixel> {
159 self.list.lock().pixels.clone()
160 }
161
162 #[allow(clippy::too_many_arguments)]
171 fn apply_corrections(
172 list: &BadPixelList,
173 data: &mut NDDataBuffer,
174 width: usize,
175 height: usize,
176 offset_x: i64,
177 offset_y: i64,
178 binning_x: i64,
179 binning_y: i64,
180 ) {
181 let scale_x = binning_x.max(1);
182 let scale_y = binning_y.max(1);
183
184 let pixel_offset = |sx: i64, sy: i64| -> Option<usize> {
194 let x = (sx - offset_x).div_euclid(binning_x.max(1));
195 let y = (sy - offset_y).div_euclid(binning_y.max(1));
196 if x >= 0 && y >= 0 && x < width as i64 && y < height as i64 {
197 Some(y as usize * width + x as usize)
198 } else {
199 None
200 }
201 };
202
203 let mut corrections: Vec<(usize, f64)> = Vec::with_capacity(list.pixels.len());
205
206 for bp in &list.pixels {
207 let Some(offset) = pixel_offset(bp.x, bp.y) else {
208 continue;
209 };
210
211 let value = match &bp.mode {
212 BadPixelMode::Set { value } => *value,
213
214 BadPixelMode::Replace { dx, dy } => {
215 let nx = bp.x + (*dx as i64) * scale_x;
217 let ny = bp.y + (*dy as i64) * scale_y;
218 if list.is_bad(nx, ny) {
220 continue;
221 }
222 let Some(replace_offset) = pixel_offset(nx, ny) else {
223 continue;
224 };
225 match data.get_as_f64(replace_offset) {
226 Some(v) => v,
227 None => continue,
228 }
229 }
230
231 BadPixelMode::Median { half_x, half_y } => {
232 let mut neighbors = Vec::new();
234 for i in -*half_y..=*half_y {
235 let cy = bp.y + i * scale_y;
236 for j in -*half_x..=*half_x {
237 if i == 0 && j == 0 {
238 continue; }
240 let cx = bp.x + j * scale_x;
241 if list.is_bad(cx, cy) {
243 continue;
244 }
245 let Some(idx) = pixel_offset(cx, cy) else {
246 continue;
247 };
248 if let Some(v) = data.get_as_f64(idx) {
249 neighbors.push(v);
250 }
251 }
252 }
253
254 if neighbors.is_empty() {
255 continue; }
257
258 neighbors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
259 let mid = neighbors.len() / 2;
260 if neighbors.len() % 2 == 0 {
261 (neighbors[mid - 1] + neighbors[mid]) / 2.0
262 } else {
263 neighbors[mid]
264 }
265 }
266 };
267
268 corrections.push((offset, value));
269 }
270
271 for (idx, value) in corrections {
273 data.set_from_f64(idx, value);
274 }
275 }
276}
277
278impl NDPluginProcess for BadPixelProcessor {
279 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
280 let info = array.info();
281 let width = info.x_size;
282 let height = info.y_size;
283
284 let list = Arc::clone(&self.list.lock());
285 if list.pixels.is_empty() {
286 return ProcessResult::arrays(vec![Arc::new(array.clone())]);
288 }
289
290 let [x_dim, y_dim, _] = info.user_dims();
296 let offset_x = array.dims.get(x_dim).map_or(0, |d| d.offset as i64);
297 let binning_x = array.dims.get(x_dim).map_or(1, |d| d.binning.max(1) as i64);
298 let (offset_y, binning_y) = if array.dims.len() > 1 {
299 let d = &array.dims[y_dim];
300 (d.offset as i64, d.binning.max(1) as i64)
301 } else {
302 (0, 1)
303 };
304
305 let mut out = array.clone();
306 Self::apply_corrections(
307 &list,
308 &mut out.data,
309 width,
310 height,
311 offset_x,
312 offset_y,
313 binning_x,
314 binning_y,
315 );
316 ProcessResult::arrays(vec![Arc::new(out)])
317 }
318
319 fn plugin_type(&self) -> &str {
320 "NDPluginBadPixel"
321 }
322
323 fn register_params(
324 &mut self,
325 base: &mut asyn_rs::port::PortDriverBase,
326 ) -> asyn_rs::error::AsynResult<()> {
327 use asyn_rs::param::ParamType;
328 base.create_param("BAD_PIXEL_FILE_NAME", ParamType::Octet)?;
329 self.file_name_idx = base.find_param("BAD_PIXEL_FILE_NAME");
330 Ok(())
331 }
332
333 fn on_param_change(
334 &self,
335 reason: usize,
336 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
337 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
338 use ad_core_rs::plugin::runtime::ParamChangeValue;
339
340 if Some(reason) == self.file_name_idx {
341 if let ParamChangeValue::Octet(path) = ¶ms.value {
342 if !path.is_empty() {
343 match std::fs::read_to_string(path) {
344 Ok(json_str) => match Self::load_from_json(&json_str) {
345 Ok(pixels) => {
346 let n = pixels.len();
347 self.set_pixels(pixels);
348 tracing::info!("BadPixel: loaded {} pixels from {}", n, path);
349 }
350 Err(e) => {
351 tracing::warn!("BadPixel: failed to parse {}: {}", path, e);
352 }
353 },
354 Err(e) => {
355 tracing::warn!("BadPixel: failed to read {}: {}", path, e);
356 }
357 }
358 }
359 }
360 }
361
362 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use ad_core_rs::ndarray::{NDDataType, NDDimension};
370
371 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
372 let mut arr = NDArray::new(
373 vec![NDDimension::new(x), NDDimension::new(y)],
374 NDDataType::Float64,
375 );
376 if let NDDataBuffer::F64(ref mut v) = arr.data {
377 for iy in 0..y {
378 for ix in 0..x {
379 v[iy * x + ix] = fill(ix, iy);
380 }
381 }
382 }
383 arr
384 }
385
386 fn get_pixel(arr: &NDArray, x: usize, y: usize, width: usize) -> f64 {
387 arr.data.get_as_f64(y * width + x).unwrap()
388 }
389
390 fn set(x: i64, y: i64, value: f64) -> BadPixel {
391 BadPixel {
392 x,
393 y,
394 mode: BadPixelMode::Set { value },
395 }
396 }
397
398 #[test]
399 fn test_set_mode() {
400 let arr = make_2d_array(4, 4, |_, _| 100.0);
401 let pixels = vec![set(1, 1, 0.0), set(3, 2, 42.0)];
402
403 let proc = BadPixelProcessor::new(pixels);
404 let pool = NDArrayPool::new(1_000_000);
405 let result = proc.process_array(&arr, &pool);
406
407 assert_eq!(result.output_arrays.len(), 1);
408 let out = &result.output_arrays[0];
409 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
410 assert!((get_pixel(out, 3, 2, 4) - 42.0).abs() < 1e-10);
411 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
412 }
413
414 #[test]
415 fn test_r9_67_readout_offset_comes_from_the_user_dims_axis() {
416 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
423 use ad_core_rs::color::NDColorMode;
424
425 let mut arr = NDArray::new(
428 vec![
429 NDDimension::new(3),
430 NDDimension::new(4),
431 NDDimension::new(2),
432 ],
433 NDDataType::Float64,
434 );
435 arr.dims[1].offset = 2;
436 arr.attributes.add(NDAttribute::new_static(
437 "ColorMode",
438 "",
439 NDAttrSource::Driver,
440 NDAttrValue::Int32(NDColorMode::RGB1 as i32),
441 ));
442 if let NDDataBuffer::F64(ref mut v) = arr.data {
443 v.iter_mut().for_each(|x| *x = 100.0);
444 }
445
446 let proc = BadPixelProcessor::new(vec![set(3, 0, 7.0)]);
451 let pool = NDArrayPool::new(1_000_000);
452 let result = proc.process_array(&arr, &pool);
453 let out = &result.output_arrays[0];
454
455 assert_eq!(out.data.get_as_f64(1), Some(7.0), "corrected element 1");
456 assert_eq!(
457 out.data.get_as_f64(3),
458 Some(100.0),
459 "element 3 must be untouched — that is where the physical-index bug wrote"
460 );
461 }
462
463 #[test]
464 fn test_replace_mode() {
465 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
466 let pixels = vec![BadPixel {
468 x: 2,
469 y: 2,
470 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
471 }];
472
473 let proc = BadPixelProcessor::new(pixels);
474 let pool = NDArrayPool::new(1_000_000);
475 let result = proc.process_array(&arr, &pool);
476
477 let out = &result.output_arrays[0];
478 assert!((get_pixel(out, 2, 2, 4) - 11.0).abs() < 1e-10);
480 }
481
482 #[test]
483 fn test_replace_skip_bad_neighbor() {
484 let arr = make_2d_array(4, 4, |_, _| 50.0);
485 let pixels = vec![
487 BadPixel {
488 x: 1,
489 y: 1,
490 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
491 },
492 set(2, 1, 0.0),
493 ];
494
495 let proc = BadPixelProcessor::new(pixels);
496 let pool = NDArrayPool::new(1_000_000);
497 let result = proc.process_array(&arr, &pool);
498
499 let out = &result.output_arrays[0];
500 assert!((get_pixel(out, 1, 1, 4) - 50.0).abs() < 1e-10);
502 assert!((get_pixel(out, 2, 1, 4) - 0.0).abs() < 1e-10);
504 }
505
506 #[test]
507 fn test_median_mode() {
508 let arr = make_2d_array(7, 7, |x, y| if x == 3 && y == 3 { 1000.0 } else { 10.0 });
510
511 let pixels = vec![BadPixel {
512 x: 3,
513 y: 3,
514 mode: BadPixelMode::Median {
515 half_x: 1,
516 half_y: 1,
517 },
518 }];
519
520 let proc = BadPixelProcessor::new(pixels);
521 let pool = NDArrayPool::new(1_000_000);
522 let result = proc.process_array(&arr, &pool);
523
524 let out = &result.output_arrays[0];
525 assert!((get_pixel(out, 3, 3, 7) - 10.0).abs() < 1e-10);
527 }
528
529 #[test]
530 fn test_median_half_extent_kernel_size() {
531 let arr = make_2d_array(9, 9, |x, y| {
535 let dx = x as i64 - 4;
536 let dy = y as i64 - 4;
537 if dx.abs() == 3 || dy.abs() == 3 {
539 100.0
540 } else {
541 10.0
542 }
543 });
544
545 let pixels = vec![BadPixel {
547 x: 4,
548 y: 4,
549 mode: BadPixelMode::Median {
550 half_x: 3,
551 half_y: 3,
552 },
553 }];
554 let proc = BadPixelProcessor::new(pixels);
555 let pool = NDArrayPool::new(1_000_000);
556 let result = proc.process_array(&arr, &pool);
557 let out = &result.output_arrays[0];
558 assert!((get_pixel(out, 4, 4, 9) - 55.0).abs() < 1e-10);
562
563 let pixels = vec![BadPixel {
567 x: 4,
568 y: 4,
569 mode: BadPixelMode::Median {
570 half_x: 1,
571 half_y: 1,
572 },
573 }];
574 let proc = BadPixelProcessor::new(pixels);
575 let result = proc.process_array(&arr, &pool);
576 let out = &result.output_arrays[0];
577 assert!((get_pixel(out, 4, 4, 9) - 10.0).abs() < 1e-10);
578 }
579
580 #[test]
581 fn test_median_skips_bad_neighbors() {
582 let arr = make_2d_array(7, 7, |_, _| 10.0);
583 let pixels = vec![
585 BadPixel {
586 x: 3,
587 y: 3,
588 mode: BadPixelMode::Median {
589 half_x: 1,
590 half_y: 1,
591 },
592 },
593 set(2, 3, 999.0),
594 ];
595
596 let proc = BadPixelProcessor::new(pixels);
597 let pool = NDArrayPool::new(1_000_000);
598 let result = proc.process_array(&arr, &pool);
599
600 let out = &result.output_arrays[0];
601 assert!((get_pixel(out, 3, 3, 7) - 10.0).abs() < 1e-10);
603 }
604
605 #[test]
606 fn test_boundary_pixel() {
607 let arr = make_2d_array(4, 4, |_, _| 20.0);
608 let pixels = vec![BadPixel {
609 x: 0,
610 y: 0,
611 mode: BadPixelMode::Median {
612 half_x: 1,
613 half_y: 1,
614 },
615 }];
616
617 let proc = BadPixelProcessor::new(pixels);
618 let pool = NDArrayPool::new(1_000_000);
619 let result = proc.process_array(&arr, &pool);
620
621 let out = &result.output_arrays[0];
622 assert!((get_pixel(out, 0, 0, 4) - 20.0).abs() < 1e-10);
624 }
625
626 #[test]
627 fn test_replace_out_of_bounds() {
628 let arr = make_2d_array(4, 4, |_, _| 50.0);
629 let pixels = vec![BadPixel {
631 x: 0,
632 y: 0,
633 mode: BadPixelMode::Replace { dx: -1, dy: 0 },
634 }];
635
636 let proc = BadPixelProcessor::new(pixels);
637 let pool = NDArrayPool::new(1_000_000);
638 let result = proc.process_array(&arr, &pool);
639
640 let out = &result.output_arrays[0];
641 assert!((get_pixel(out, 0, 0, 4) - 50.0).abs() < 1e-10);
642 }
643
644 #[test]
645 fn test_load_from_json_cpp_schema() {
646 let json = r#"{"Bad pixels": [
648 {"Pixel": [10, 20], "Set": 0},
649 {"Pixel": [5, 3], "Replace": [1, 0]},
650 {"Pixel": [7, 8], "Median": [3, 3]}
651 ]}"#;
652
653 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
654 assert_eq!(pixels.len(), 3);
655 assert_eq!(pixels[0].x, 10);
656 assert_eq!(pixels[0].y, 20);
657 assert_eq!(pixels[0].mode, BadPixelMode::Set { value: 0.0 });
658 assert_eq!(pixels[1].mode, BadPixelMode::Replace { dx: 1, dy: 0 });
659 assert_eq!(
660 pixels[2].mode,
661 BadPixelMode::Median {
662 half_x: 3,
663 half_y: 3
664 }
665 );
666 }
667
668 #[test]
669 fn test_load_from_json_no_key_defaults_to_set_zero() {
670 let json = r#"{"Bad pixels": [{"Pixel": [1, 2]}]}"#;
673 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
674 assert_eq!(pixels.len(), 1);
675 assert_eq!(pixels[0].mode, BadPixelMode::Set { value: 0.0 });
676 }
677
678 #[test]
679 fn test_no_bad_pixels_passthrough() {
680 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
681 let proc = BadPixelProcessor::new(vec![]);
682 let pool = NDArrayPool::new(1_000_000);
683 let result = proc.process_array(&arr, &pool);
684
685 assert_eq!(result.output_arrays.len(), 1);
686 for iy in 0..4 {
687 for ix in 0..4 {
688 let expected = (ix + iy * 4) as f64;
689 let actual = get_pixel(&result.output_arrays[0], ix, iy, 4);
690 assert!((actual - expected).abs() < 1e-10);
691 }
692 }
693 }
694
695 #[test]
696 fn test_bad_pixel_outside_image() {
697 let arr = make_2d_array(4, 4, |_, _| 10.0);
698 let pixels = vec![set(100, 100, 999.0)];
699
700 let proc = BadPixelProcessor::new(pixels);
701 let pool = NDArrayPool::new(1_000_000);
702 let result = proc.process_array(&arr, &pool);
703
704 let out = &result.output_arrays[0];
705 assert!((get_pixel(out, 0, 0, 4) - 10.0).abs() < 1e-10);
706 }
707
708 #[test]
709 fn test_u8_data() {
710 let mut arr = NDArray::new(
711 vec![NDDimension::new(4), NDDimension::new(4)],
712 NDDataType::UInt8,
713 );
714 if let NDDataBuffer::U8(ref mut v) = arr.data {
715 for val in v.iter_mut() {
716 *val = 100;
717 }
718 }
719
720 let pixels = vec![set(1, 1, 0.0)];
721
722 let proc = BadPixelProcessor::new(pixels);
723 let pool = NDArrayPool::new(1_000_000);
724 let result = proc.process_array(&arr, &pool);
725
726 let out = &result.output_arrays[0];
727 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
728 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_set_pixels() {
733 let proc = BadPixelProcessor::new(vec![]);
734 assert!(proc.pixels().is_empty());
735
736 proc.set_pixels(vec![set(0, 0, 0.0)]);
737 assert_eq!(proc.pixels().len(), 1);
738 }
739}