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