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 offset_x = array.dims.first().map_or(0, |d| d.offset as i64);
277 let offset_y = array.dims.get(1).map_or(0, |d| d.offset as i64);
278 let binning_x = array.dims.first().map_or(1, |d| d.binning.max(1) as i64);
279 let binning_y = array.dims.get(1).map_or(1, |d| d.binning.max(1) as i64);
280
281 let mut out = array.clone();
282 self.apply_corrections(
283 &mut out.data,
284 self.width,
285 height,
286 offset_x,
287 offset_y,
288 binning_x,
289 binning_y,
290 );
291 ProcessResult::arrays(vec![Arc::new(out)])
292 }
293
294 fn plugin_type(&self) -> &str {
295 "NDPluginBadPixel"
296 }
297
298 fn register_params(
299 &mut self,
300 base: &mut asyn_rs::port::PortDriverBase,
301 ) -> asyn_rs::error::AsynResult<()> {
302 use asyn_rs::param::ParamType;
303 base.create_param("BAD_PIXEL_FILE_NAME", ParamType::Octet)?;
304 self.file_name_idx = base.find_param("BAD_PIXEL_FILE_NAME");
305 Ok(())
306 }
307
308 fn on_param_change(
309 &mut self,
310 reason: usize,
311 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
312 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
313 use ad_core_rs::plugin::runtime::ParamChangeValue;
314
315 if Some(reason) == self.file_name_idx {
316 if let ParamChangeValue::Octet(path) = ¶ms.value {
317 if !path.is_empty() {
318 match std::fs::read_to_string(path) {
319 Ok(json_str) => match Self::load_from_json(&json_str) {
320 Ok(pixels) => {
321 self.set_pixels(pixels);
322 tracing::info!(
323 "BadPixel: loaded {} pixels from {}",
324 self.pixels.len(),
325 path
326 );
327 }
328 Err(e) => {
329 tracing::warn!("BadPixel: failed to parse {}: {}", path, e);
330 }
331 },
332 Err(e) => {
333 tracing::warn!("BadPixel: failed to read {}: {}", path, e);
334 }
335 }
336 }
337 }
338 }
339
340 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use ad_core_rs::ndarray::{NDDataType, NDDimension};
348
349 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
350 let mut arr = NDArray::new(
351 vec![NDDimension::new(x), NDDimension::new(y)],
352 NDDataType::Float64,
353 );
354 if let NDDataBuffer::F64(ref mut v) = arr.data {
355 for iy in 0..y {
356 for ix in 0..x {
357 v[iy * x + ix] = fill(ix, iy);
358 }
359 }
360 }
361 arr
362 }
363
364 fn get_pixel(arr: &NDArray, x: usize, y: usize, width: usize) -> f64 {
365 arr.data.get_as_f64(y * width + x).unwrap()
366 }
367
368 fn set(x: i64, y: i64, value: f64) -> BadPixel {
369 BadPixel {
370 x,
371 y,
372 mode: BadPixelMode::Set { value },
373 }
374 }
375
376 #[test]
377 fn test_set_mode() {
378 let arr = make_2d_array(4, 4, |_, _| 100.0);
379 let pixels = vec![set(1, 1, 0.0), set(3, 2, 42.0)];
380
381 let mut proc = BadPixelProcessor::new(pixels);
382 let pool = NDArrayPool::new(1_000_000);
383 let result = proc.process_array(&arr, &pool);
384
385 assert_eq!(result.output_arrays.len(), 1);
386 let out = &result.output_arrays[0];
387 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
388 assert!((get_pixel(out, 3, 2, 4) - 42.0).abs() < 1e-10);
389 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
390 }
391
392 #[test]
393 fn test_replace_mode() {
394 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
395 let pixels = vec![BadPixel {
397 x: 2,
398 y: 2,
399 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
400 }];
401
402 let mut proc = BadPixelProcessor::new(pixels);
403 let pool = NDArrayPool::new(1_000_000);
404 let result = proc.process_array(&arr, &pool);
405
406 let out = &result.output_arrays[0];
407 assert!((get_pixel(out, 2, 2, 4) - 11.0).abs() < 1e-10);
409 }
410
411 #[test]
412 fn test_replace_skip_bad_neighbor() {
413 let arr = make_2d_array(4, 4, |_, _| 50.0);
414 let pixels = vec![
416 BadPixel {
417 x: 1,
418 y: 1,
419 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
420 },
421 set(2, 1, 0.0),
422 ];
423
424 let mut proc = BadPixelProcessor::new(pixels);
425 let pool = NDArrayPool::new(1_000_000);
426 let result = proc.process_array(&arr, &pool);
427
428 let out = &result.output_arrays[0];
429 assert!((get_pixel(out, 1, 1, 4) - 50.0).abs() < 1e-10);
431 assert!((get_pixel(out, 2, 1, 4) - 0.0).abs() < 1e-10);
433 }
434
435 #[test]
436 fn test_median_mode() {
437 let arr = make_2d_array(7, 7, |x, y| if x == 3 && y == 3 { 1000.0 } else { 10.0 });
439
440 let pixels = vec![BadPixel {
441 x: 3,
442 y: 3,
443 mode: BadPixelMode::Median {
444 half_x: 1,
445 half_y: 1,
446 },
447 }];
448
449 let mut proc = BadPixelProcessor::new(pixels);
450 let pool = NDArrayPool::new(1_000_000);
451 let result = proc.process_array(&arr, &pool);
452
453 let out = &result.output_arrays[0];
454 assert!((get_pixel(out, 3, 3, 7) - 10.0).abs() < 1e-10);
456 }
457
458 #[test]
459 fn test_median_half_extent_kernel_size() {
460 let arr = make_2d_array(9, 9, |x, y| {
464 let dx = x as i64 - 4;
465 let dy = y as i64 - 4;
466 if dx.abs() == 3 || dy.abs() == 3 {
468 100.0
469 } else {
470 10.0
471 }
472 });
473
474 let pixels = vec![BadPixel {
476 x: 4,
477 y: 4,
478 mode: BadPixelMode::Median {
479 half_x: 3,
480 half_y: 3,
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 let out = &result.output_arrays[0];
487 assert!((get_pixel(out, 4, 4, 9) - 55.0).abs() < 1e-10);
491
492 let pixels = vec![BadPixel {
496 x: 4,
497 y: 4,
498 mode: BadPixelMode::Median {
499 half_x: 1,
500 half_y: 1,
501 },
502 }];
503 let mut proc = BadPixelProcessor::new(pixels);
504 let result = proc.process_array(&arr, &pool);
505 let out = &result.output_arrays[0];
506 assert!((get_pixel(out, 4, 4, 9) - 10.0).abs() < 1e-10);
507 }
508
509 #[test]
510 fn test_median_skips_bad_neighbors() {
511 let arr = make_2d_array(7, 7, |_, _| 10.0);
512 let pixels = vec![
514 BadPixel {
515 x: 3,
516 y: 3,
517 mode: BadPixelMode::Median {
518 half_x: 1,
519 half_y: 1,
520 },
521 },
522 set(2, 3, 999.0),
523 ];
524
525 let mut proc = BadPixelProcessor::new(pixels);
526 let pool = NDArrayPool::new(1_000_000);
527 let result = proc.process_array(&arr, &pool);
528
529 let out = &result.output_arrays[0];
530 assert!((get_pixel(out, 3, 3, 7) - 10.0).abs() < 1e-10);
532 }
533
534 #[test]
535 fn test_boundary_pixel() {
536 let arr = make_2d_array(4, 4, |_, _| 20.0);
537 let pixels = vec![BadPixel {
538 x: 0,
539 y: 0,
540 mode: BadPixelMode::Median {
541 half_x: 1,
542 half_y: 1,
543 },
544 }];
545
546 let mut proc = BadPixelProcessor::new(pixels);
547 let pool = NDArrayPool::new(1_000_000);
548 let result = proc.process_array(&arr, &pool);
549
550 let out = &result.output_arrays[0];
551 assert!((get_pixel(out, 0, 0, 4) - 20.0).abs() < 1e-10);
553 }
554
555 #[test]
556 fn test_replace_out_of_bounds() {
557 let arr = make_2d_array(4, 4, |_, _| 50.0);
558 let pixels = vec![BadPixel {
560 x: 0,
561 y: 0,
562 mode: BadPixelMode::Replace { dx: -1, dy: 0 },
563 }];
564
565 let mut proc = BadPixelProcessor::new(pixels);
566 let pool = NDArrayPool::new(1_000_000);
567 let result = proc.process_array(&arr, &pool);
568
569 let out = &result.output_arrays[0];
570 assert!((get_pixel(out, 0, 0, 4) - 50.0).abs() < 1e-10);
571 }
572
573 #[test]
574 fn test_load_from_json_cpp_schema() {
575 let json = r#"{"Bad pixels": [
577 {"Pixel": [10, 20], "Set": 0},
578 {"Pixel": [5, 3], "Replace": [1, 0]},
579 {"Pixel": [7, 8], "Median": [3, 3]}
580 ]}"#;
581
582 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
583 assert_eq!(pixels.len(), 3);
584 assert_eq!(pixels[0].x, 10);
585 assert_eq!(pixels[0].y, 20);
586 assert_eq!(pixels[0].mode, BadPixelMode::Set { value: 0.0 });
587 assert_eq!(pixels[1].mode, BadPixelMode::Replace { dx: 1, dy: 0 });
588 assert_eq!(
589 pixels[2].mode,
590 BadPixelMode::Median {
591 half_x: 3,
592 half_y: 3
593 }
594 );
595 }
596
597 #[test]
598 fn test_load_from_json_no_key_defaults_to_set_zero() {
599 let json = r#"{"Bad pixels": [{"Pixel": [1, 2]}]}"#;
602 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
603 assert_eq!(pixels.len(), 1);
604 assert_eq!(pixels[0].mode, BadPixelMode::Set { value: 0.0 });
605 }
606
607 #[test]
608 fn test_no_bad_pixels_passthrough() {
609 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
610 let mut proc = BadPixelProcessor::new(vec![]);
611 let pool = NDArrayPool::new(1_000_000);
612 let result = proc.process_array(&arr, &pool);
613
614 assert_eq!(result.output_arrays.len(), 1);
615 for iy in 0..4 {
616 for ix in 0..4 {
617 let expected = (ix + iy * 4) as f64;
618 let actual = get_pixel(&result.output_arrays[0], ix, iy, 4);
619 assert!((actual - expected).abs() < 1e-10);
620 }
621 }
622 }
623
624 #[test]
625 fn test_bad_pixel_outside_image() {
626 let arr = make_2d_array(4, 4, |_, _| 10.0);
627 let pixels = vec![set(100, 100, 999.0)];
628
629 let mut proc = BadPixelProcessor::new(pixels);
630 let pool = NDArrayPool::new(1_000_000);
631 let result = proc.process_array(&arr, &pool);
632
633 let out = &result.output_arrays[0];
634 assert!((get_pixel(out, 0, 0, 4) - 10.0).abs() < 1e-10);
635 }
636
637 #[test]
638 fn test_u8_data() {
639 let mut arr = NDArray::new(
640 vec![NDDimension::new(4), NDDimension::new(4)],
641 NDDataType::UInt8,
642 );
643 if let NDDataBuffer::U8(ref mut v) = arr.data {
644 for val in v.iter_mut() {
645 *val = 100;
646 }
647 }
648
649 let pixels = vec![set(1, 1, 0.0)];
650
651 let mut proc = BadPixelProcessor::new(pixels);
652 let pool = NDArrayPool::new(1_000_000);
653 let result = proc.process_array(&arr, &pool);
654
655 let out = &result.output_arrays[0];
656 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
657 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
658 }
659
660 #[test]
661 fn test_set_pixels() {
662 let mut proc = BadPixelProcessor::new(vec![]);
663 assert!(proc.pixels().is_empty());
664
665 proc.set_pixels(vec![set(0, 0, 0.0)]);
666 assert_eq!(proc.pixels().len(), 1);
667 }
668}