1use std::collections::HashSet;
10use std::sync::Arc;
11
12use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
13use ad_core_rs::ndarray_pool::NDArrayPool;
14use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
15use serde::Deserialize;
16
17#[derive(Debug, Clone, Deserialize)]
19#[serde(tag = "mode")]
20pub enum BadPixelMode {
21 #[serde(rename = "set")]
23 Set { value: f64 },
24 #[serde(rename = "replace")]
26 Replace { dx: i32, dy: i32 },
27 #[serde(rename = "median")]
29 Median { kernel_x: usize, kernel_y: usize },
30}
31
32#[derive(Debug, Clone, Deserialize)]
34pub struct BadPixel {
35 pub x: usize,
36 pub y: usize,
37 #[serde(flatten)]
38 pub mode: BadPixelMode,
39}
40
41#[derive(Debug, Clone, Deserialize)]
43pub struct BadPixelList {
44 pub bad_pixels: Vec<BadPixel>,
45}
46
47pub struct BadPixelProcessor {
49 pixels: Vec<BadPixel>,
50 bad_set: HashSet<(usize, usize)>,
52 width: usize,
54 file_name_idx: Option<usize>,
55}
56
57impl BadPixelProcessor {
58 pub fn new(pixels: Vec<BadPixel>) -> Self {
60 let bad_set: HashSet<(usize, usize)> = pixels.iter().map(|p| (p.x, p.y)).collect();
61 Self {
62 pixels,
63 bad_set,
64 width: 0,
65 file_name_idx: None,
66 }
67 }
68
69 pub fn load_from_json(json_str: &str) -> Result<Vec<BadPixel>, serde_json::Error> {
71 let list: BadPixelList = serde_json::from_str(json_str)?;
72 Ok(list.bad_pixels)
73 }
74
75 pub fn set_pixels(&mut self, pixels: Vec<BadPixel>) {
77 self.bad_set = pixels.iter().map(|p| (p.x, p.y)).collect();
78 self.pixels = pixels;
79 }
80
81 pub fn pixels(&self) -> &[BadPixel] {
83 &self.pixels
84 }
85
86 fn is_bad(&self, x: usize, y: usize) -> bool {
88 self.bad_set.contains(&(x, y))
89 }
90
91 fn apply_corrections(
95 &self,
96 data: &mut NDDataBuffer,
97 width: usize,
98 height: usize,
99 offset_x: i64,
100 offset_y: i64,
101 binning_x: i64,
102 binning_y: i64,
103 ) {
104 let mut corrections: Vec<(usize, f64)> = Vec::with_capacity(self.pixels.len());
110
111 for bp in &self.pixels {
112 let adj_x = (bp.x as i64 - offset_x) / binning_x;
114 let adj_y = (bp.y as i64 - offset_y) / binning_y;
115 if adj_x < 0 || adj_y < 0 {
116 continue;
117 }
118 let adj_x = adj_x as usize;
119 let adj_y = adj_y as usize;
120 if adj_x >= width || adj_y >= height {
121 continue;
122 }
123
124 let value = match &bp.mode {
125 BadPixelMode::Set { value } => *value,
126
127 BadPixelMode::Replace { dx, dy } => {
128 let nx = adj_x as i64 + *dx as i64;
129 let ny = adj_y as i64 + *dy as i64;
130
131 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
132 continue; }
134
135 let nx = nx as usize;
136 let ny = ny as usize;
137
138 if self.is_bad(nx, ny) {
140 continue;
141 }
142
143 let idx = ny * width + nx;
144 match data.get_as_f64(idx) {
145 Some(v) => v,
146 None => continue,
147 }
148 }
149
150 BadPixelMode::Median { kernel_x, kernel_y } => {
151 let half_x = (*kernel_x / 2) as i64;
152 let half_y = (*kernel_y / 2) as i64;
153 let cx = adj_x as i64;
154 let cy = adj_y as i64;
155
156 let mut neighbors = Vec::new();
157 for ky in (cy - half_y)..=(cy + half_y) {
158 for kx in (cx - half_x)..=(cx + half_x) {
159 if kx < 0 || kx >= width as i64 || ky < 0 || ky >= height as i64 {
160 continue;
161 }
162 let kxu = kx as usize;
163 let kyu = ky as usize;
164 if kxu == adj_x && kyu == adj_y {
166 continue;
167 }
168 if self.is_bad(kxu, kyu) {
169 continue;
170 }
171 let idx = kyu * width + kxu;
172 if let Some(v) = data.get_as_f64(idx) {
173 neighbors.push(v);
174 }
175 }
176 }
177
178 if neighbors.is_empty() {
179 continue; }
181
182 neighbors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
183 let mid = neighbors.len() / 2;
184 if neighbors.len() % 2 == 0 {
185 (neighbors[mid - 1] + neighbors[mid]) / 2.0
186 } else {
187 neighbors[mid]
188 }
189 }
190 };
191
192 let idx = adj_y * width + adj_x;
193 corrections.push((idx, value));
194 }
195
196 for (idx, value) in corrections {
198 data.set_from_f64(idx, value);
199 }
200 }
201}
202
203impl NDPluginProcess for BadPixelProcessor {
204 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
205 let info = array.info();
206 self.width = info.x_size;
207 let height = info.y_size;
208
209 if self.pixels.is_empty() {
210 return ProcessResult::arrays(vec![Arc::new(array.clone())]);
212 }
213
214 let offset_x = array.dims.first().map_or(0, |d| d.offset as i64);
215 let offset_y = array.dims.get(1).map_or(0, |d| d.offset as i64);
216 let binning_x = array.dims.first().map_or(1, |d| d.binning.max(1) as i64);
217 let binning_y = array.dims.get(1).map_or(1, |d| d.binning.max(1) as i64);
218
219 let mut out = array.clone();
220 self.apply_corrections(
221 &mut out.data,
222 self.width,
223 height,
224 offset_x,
225 offset_y,
226 binning_x,
227 binning_y,
228 );
229 ProcessResult::arrays(vec![Arc::new(out)])
230 }
231
232 fn plugin_type(&self) -> &str {
233 "NDPluginBadPixel"
234 }
235
236 fn register_params(
237 &mut self,
238 base: &mut asyn_rs::port::PortDriverBase,
239 ) -> asyn_rs::error::AsynResult<()> {
240 use asyn_rs::param::ParamType;
241 base.create_param("BAD_PIXEL_FILE_NAME", ParamType::Octet)?;
242 self.file_name_idx = base.find_param("BAD_PIXEL_FILE_NAME");
243 Ok(())
244 }
245
246 fn on_param_change(
247 &mut self,
248 reason: usize,
249 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
250 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
251 use ad_core_rs::plugin::runtime::ParamChangeValue;
252
253 if Some(reason) == self.file_name_idx {
254 if let ParamChangeValue::Octet(path) = ¶ms.value {
255 if !path.is_empty() {
256 match std::fs::read_to_string(path) {
257 Ok(json_str) => match Self::load_from_json(&json_str) {
258 Ok(pixels) => {
259 self.set_pixels(pixels);
260 tracing::info!(
261 "BadPixel: loaded {} pixels from {}",
262 self.pixels.len(),
263 path
264 );
265 }
266 Err(e) => {
267 tracing::warn!("BadPixel: failed to parse {}: {}", path, e);
268 }
269 },
270 Err(e) => {
271 tracing::warn!("BadPixel: failed to read {}: {}", path, e);
272 }
273 }
274 }
275 }
276 }
277
278 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use ad_core_rs::ndarray::{NDDataType, NDDimension};
286
287 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
288 let mut arr = NDArray::new(
289 vec![NDDimension::new(x), NDDimension::new(y)],
290 NDDataType::Float64,
291 );
292 if let NDDataBuffer::F64(ref mut v) = arr.data {
293 for iy in 0..y {
294 for ix in 0..x {
295 v[iy * x + ix] = fill(ix, iy);
296 }
297 }
298 }
299 arr
300 }
301
302 fn get_pixel(arr: &NDArray, x: usize, y: usize, width: usize) -> f64 {
303 arr.data.get_as_f64(y * width + x).unwrap()
304 }
305
306 #[test]
307 fn test_set_mode() {
308 let arr = make_2d_array(4, 4, |_, _| 100.0);
309 let pixels = vec![
310 BadPixel {
311 x: 1,
312 y: 1,
313 mode: BadPixelMode::Set { value: 0.0 },
314 },
315 BadPixel {
316 x: 3,
317 y: 2,
318 mode: BadPixelMode::Set { value: 42.0 },
319 },
320 ];
321
322 let mut proc = BadPixelProcessor::new(pixels);
323 let pool = NDArrayPool::new(1_000_000);
324 let result = proc.process_array(&arr, &pool);
325
326 assert_eq!(result.output_arrays.len(), 1);
327 let out = &result.output_arrays[0];
328 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
329 assert!((get_pixel(out, 3, 2, 4) - 42.0).abs() < 1e-10);
330 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
332 }
333
334 #[test]
335 fn test_replace_mode() {
336 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
337 let pixels = vec![BadPixel {
339 x: 2,
340 y: 2,
341 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
342 }];
343
344 let mut proc = BadPixelProcessor::new(pixels);
345 let pool = NDArrayPool::new(1_000_000);
346 let result = proc.process_array(&arr, &pool);
347
348 let out = &result.output_arrays[0];
349 assert!((get_pixel(out, 2, 2, 4) - 11.0).abs() < 1e-10);
351 }
352
353 #[test]
354 fn test_replace_skip_bad_neighbor() {
355 let arr = make_2d_array(4, 4, |_, _| 50.0);
356 let pixels = vec![
358 BadPixel {
359 x: 1,
360 y: 1,
361 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
362 },
363 BadPixel {
364 x: 2,
365 y: 1,
366 mode: BadPixelMode::Set { value: 0.0 },
367 },
368 ];
369
370 let mut proc = BadPixelProcessor::new(pixels);
371 let pool = NDArrayPool::new(1_000_000);
372 let result = proc.process_array(&arr, &pool);
373
374 let out = &result.output_arrays[0];
375 assert!((get_pixel(out, 1, 1, 4) - 50.0).abs() < 1e-10);
377 assert!((get_pixel(out, 2, 1, 4) - 0.0).abs() < 1e-10);
379 }
380
381 #[test]
382 fn test_median_mode() {
383 let arr = make_2d_array(5, 5, |x, y| if x == 2 && y == 2 { 1000.0 } else { 10.0 });
385
386 let pixels = vec![BadPixel {
387 x: 2,
388 y: 2,
389 mode: BadPixelMode::Median {
390 kernel_x: 3,
391 kernel_y: 3,
392 },
393 }];
394
395 let mut proc = BadPixelProcessor::new(pixels);
396 let pool = NDArrayPool::new(1_000_000);
397 let result = proc.process_array(&arr, &pool);
398
399 let out = &result.output_arrays[0];
400 assert!((get_pixel(out, 2, 2, 5) - 10.0).abs() < 1e-10);
402 }
403
404 #[test]
405 fn test_median_skips_bad_neighbors() {
406 let arr = make_2d_array(5, 5, |_, _| 10.0);
407 let pixels = vec![
409 BadPixel {
410 x: 2,
411 y: 2,
412 mode: BadPixelMode::Median {
413 kernel_x: 3,
414 kernel_y: 3,
415 },
416 },
417 BadPixel {
418 x: 1,
419 y: 2,
420 mode: BadPixelMode::Set { value: 999.0 },
421 },
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, 2, 2, 5) - 10.0).abs() < 1e-10);
431 }
432
433 #[test]
434 fn test_boundary_pixel() {
435 let arr = make_2d_array(4, 4, |_, _| 20.0);
436 let pixels = vec![BadPixel {
438 x: 0,
439 y: 0,
440 mode: BadPixelMode::Median {
441 kernel_x: 3,
442 kernel_y: 3,
443 },
444 }];
445
446 let mut proc = BadPixelProcessor::new(pixels);
447 let pool = NDArrayPool::new(1_000_000);
448 let result = proc.process_array(&arr, &pool);
449
450 let out = &result.output_arrays[0];
451 assert!((get_pixel(out, 0, 0, 4) - 20.0).abs() < 1e-10);
453 }
454
455 #[test]
456 fn test_replace_out_of_bounds() {
457 let arr = make_2d_array(4, 4, |_, _| 50.0);
458 let pixels = vec![BadPixel {
460 x: 0,
461 y: 0,
462 mode: BadPixelMode::Replace { dx: -1, dy: 0 },
463 }];
464
465 let mut proc = BadPixelProcessor::new(pixels);
466 let pool = NDArrayPool::new(1_000_000);
467 let result = proc.process_array(&arr, &pool);
468
469 let out = &result.output_arrays[0];
470 assert!((get_pixel(out, 0, 0, 4) - 50.0).abs() < 1e-10);
472 }
473
474 #[test]
475 fn test_load_from_json() {
476 let json = r#"{"bad_pixels": [
477 {"x": 10, "y": 20, "mode": "set", "value": 0},
478 {"x": 5, "y": 3, "mode": "replace", "dx": 1, "dy": 0},
479 {"x": 7, "y": 8, "mode": "median", "kernel_x": 3, "kernel_y": 3}
480 ]}"#;
481
482 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
483 assert_eq!(pixels.len(), 3);
484 assert_eq!(pixels[0].x, 10);
485 assert_eq!(pixels[0].y, 20);
486 match &pixels[0].mode {
487 BadPixelMode::Set { value } => assert!((value - 0.0).abs() < 1e-10),
488 _ => panic!("expected Set mode"),
489 }
490 match &pixels[1].mode {
491 BadPixelMode::Replace { dx, dy } => {
492 assert_eq!(*dx, 1);
493 assert_eq!(*dy, 0);
494 }
495 _ => panic!("expected Replace mode"),
496 }
497 match &pixels[2].mode {
498 BadPixelMode::Median { kernel_x, kernel_y } => {
499 assert_eq!(*kernel_x, 3);
500 assert_eq!(*kernel_y, 3);
501 }
502 _ => panic!("expected Median mode"),
503 }
504 }
505
506 #[test]
507 fn test_no_bad_pixels_passthrough() {
508 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
509 let mut proc = BadPixelProcessor::new(vec![]);
510 let pool = NDArrayPool::new(1_000_000);
511 let result = proc.process_array(&arr, &pool);
512
513 assert_eq!(result.output_arrays.len(), 1);
514 for iy in 0..4 {
516 for ix in 0..4 {
517 let expected = (ix + iy * 4) as f64;
518 let actual = get_pixel(&result.output_arrays[0], ix, iy, 4);
519 assert!((actual - expected).abs() < 1e-10);
520 }
521 }
522 }
523
524 #[test]
525 fn test_bad_pixel_outside_image() {
526 let arr = make_2d_array(4, 4, |_, _| 10.0);
527 let pixels = vec![BadPixel {
528 x: 100,
529 y: 100,
530 mode: BadPixelMode::Set { value: 999.0 },
531 }];
532
533 let mut proc = BadPixelProcessor::new(pixels);
534 let pool = NDArrayPool::new(1_000_000);
535 let result = proc.process_array(&arr, &pool);
536
537 let out = &result.output_arrays[0];
539 assert!((get_pixel(out, 0, 0, 4) - 10.0).abs() < 1e-10);
540 }
541
542 #[test]
543 fn test_u8_data() {
544 let mut arr = NDArray::new(
545 vec![NDDimension::new(4), NDDimension::new(4)],
546 NDDataType::UInt8,
547 );
548 if let NDDataBuffer::U8(ref mut v) = arr.data {
549 for val in v.iter_mut() {
550 *val = 100;
551 }
552 }
553
554 let pixels = vec![BadPixel {
555 x: 1,
556 y: 1,
557 mode: BadPixelMode::Set { value: 0.0 },
558 }];
559
560 let mut proc = BadPixelProcessor::new(pixels);
561 let pool = NDArrayPool::new(1_000_000);
562 let result = proc.process_array(&arr, &pool);
563
564 let out = &result.output_arrays[0];
565 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
566 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
567 }
568
569 #[test]
570 fn test_set_pixels() {
571 let mut proc = BadPixelProcessor::new(vec![]);
572 assert!(proc.pixels().is_empty());
573
574 let new_pixels = vec![BadPixel {
575 x: 0,
576 y: 0,
577 mode: BadPixelMode::Set { value: 0.0 },
578 }];
579 proc.set_pixels(new_pixels);
580 assert_eq!(proc.pixels().len(), 1);
581 }
582}