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(&self, data: &mut NDDataBuffer, width: usize, height: usize) {
93 let mut corrections: Vec<(usize, f64)> = Vec::with_capacity(self.pixels.len());
99
100 for bp in &self.pixels {
101 if bp.x >= width || bp.y >= height {
102 continue;
103 }
104
105 let value = match &bp.mode {
106 BadPixelMode::Set { value } => *value,
107
108 BadPixelMode::Replace { dx, dy } => {
109 let nx = bp.x as i64 + *dx as i64;
110 let ny = bp.y as i64 + *dy as i64;
111
112 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
113 continue; }
115
116 let nx = nx as usize;
117 let ny = ny as usize;
118
119 if self.is_bad(nx, ny) {
121 continue;
122 }
123
124 let idx = ny * width + nx;
125 match data.get_as_f64(idx) {
126 Some(v) => v,
127 None => continue,
128 }
129 }
130
131 BadPixelMode::Median { kernel_x, kernel_y } => {
132 let half_x = (*kernel_x / 2) as i64;
133 let half_y = (*kernel_y / 2) as i64;
134 let cx = bp.x as i64;
135 let cy = bp.y as i64;
136
137 let mut neighbors = Vec::new();
138 for ky in (cy - half_y)..=(cy + half_y) {
139 for kx in (cx - half_x)..=(cx + half_x) {
140 if kx < 0 || kx >= width as i64 || ky < 0 || ky >= height as i64 {
141 continue;
142 }
143 let kxu = kx as usize;
144 let kyu = ky as usize;
145 if kxu == bp.x && kyu == bp.y {
147 continue;
148 }
149 if self.is_bad(kxu, kyu) {
150 continue;
151 }
152 let idx = kyu * width + kxu;
153 if let Some(v) = data.get_as_f64(idx) {
154 neighbors.push(v);
155 }
156 }
157 }
158
159 if neighbors.is_empty() {
160 continue; }
162
163 neighbors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
164 let mid = neighbors.len() / 2;
165 if neighbors.len() % 2 == 0 {
166 (neighbors[mid - 1] + neighbors[mid]) / 2.0
167 } else {
168 neighbors[mid]
169 }
170 }
171 };
172
173 let idx = bp.y * width + bp.x;
174 corrections.push((idx, value));
175 }
176
177 for (idx, value) in corrections {
179 data.set_from_f64(idx, value);
180 }
181 }
182}
183
184impl NDPluginProcess for BadPixelProcessor {
185 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
186 let info = array.info();
187 self.width = info.x_size;
188 let height = info.y_size;
189
190 if self.pixels.is_empty() {
191 return ProcessResult::arrays(vec![Arc::new(array.clone())]);
193 }
194
195 let mut out = array.clone();
196 self.apply_corrections(&mut out.data, self.width, height);
197 ProcessResult::arrays(vec![Arc::new(out)])
198 }
199
200 fn plugin_type(&self) -> &str {
201 "NDPluginBadPixel"
202 }
203
204 fn register_params(
205 &mut self,
206 base: &mut asyn_rs::port::PortDriverBase,
207 ) -> asyn_rs::error::AsynResult<()> {
208 use asyn_rs::param::ParamType;
209 base.create_param("BAD_PIXEL_FILE_NAME", ParamType::Octet)?;
210 self.file_name_idx = base.find_param("BAD_PIXEL_FILE_NAME");
211 Ok(())
212 }
213
214 fn on_param_change(
215 &mut self,
216 reason: usize,
217 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
218 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
219 use ad_core_rs::plugin::runtime::ParamChangeValue;
220
221 if Some(reason) == self.file_name_idx {
222 if let ParamChangeValue::Octet(path) = ¶ms.value {
223 if !path.is_empty() {
224 match std::fs::read_to_string(path) {
225 Ok(json_str) => match Self::load_from_json(&json_str) {
226 Ok(pixels) => {
227 self.set_pixels(pixels);
228 tracing::info!(
229 "BadPixel: loaded {} pixels from {}",
230 self.pixels.len(),
231 path
232 );
233 }
234 Err(e) => {
235 tracing::warn!("BadPixel: failed to parse {}: {}", path, e);
236 }
237 },
238 Err(e) => {
239 tracing::warn!("BadPixel: failed to read {}: {}", path, e);
240 }
241 }
242 }
243 }
244 }
245
246 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use ad_core_rs::ndarray::{NDDataType, NDDimension};
254
255 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
256 let mut arr = NDArray::new(
257 vec![NDDimension::new(x), NDDimension::new(y)],
258 NDDataType::Float64,
259 );
260 if let NDDataBuffer::F64(ref mut v) = arr.data {
261 for iy in 0..y {
262 for ix in 0..x {
263 v[iy * x + ix] = fill(ix, iy);
264 }
265 }
266 }
267 arr
268 }
269
270 fn get_pixel(arr: &NDArray, x: usize, y: usize, width: usize) -> f64 {
271 arr.data.get_as_f64(y * width + x).unwrap()
272 }
273
274 #[test]
275 fn test_set_mode() {
276 let arr = make_2d_array(4, 4, |_, _| 100.0);
277 let pixels = vec![
278 BadPixel {
279 x: 1,
280 y: 1,
281 mode: BadPixelMode::Set { value: 0.0 },
282 },
283 BadPixel {
284 x: 3,
285 y: 2,
286 mode: BadPixelMode::Set { value: 42.0 },
287 },
288 ];
289
290 let mut proc = BadPixelProcessor::new(pixels);
291 let pool = NDArrayPool::new(1_000_000);
292 let result = proc.process_array(&arr, &pool);
293
294 assert_eq!(result.output_arrays.len(), 1);
295 let out = &result.output_arrays[0];
296 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
297 assert!((get_pixel(out, 3, 2, 4) - 42.0).abs() < 1e-10);
298 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
300 }
301
302 #[test]
303 fn test_replace_mode() {
304 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
305 let pixels = vec![BadPixel {
307 x: 2,
308 y: 2,
309 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
310 }];
311
312 let mut proc = BadPixelProcessor::new(pixels);
313 let pool = NDArrayPool::new(1_000_000);
314 let result = proc.process_array(&arr, &pool);
315
316 let out = &result.output_arrays[0];
317 assert!((get_pixel(out, 2, 2, 4) - 11.0).abs() < 1e-10);
319 }
320
321 #[test]
322 fn test_replace_skip_bad_neighbor() {
323 let arr = make_2d_array(4, 4, |_, _| 50.0);
324 let pixels = vec![
326 BadPixel {
327 x: 1,
328 y: 1,
329 mode: BadPixelMode::Replace { dx: 1, dy: 0 },
330 },
331 BadPixel {
332 x: 2,
333 y: 1,
334 mode: BadPixelMode::Set { value: 0.0 },
335 },
336 ];
337
338 let mut proc = BadPixelProcessor::new(pixels);
339 let pool = NDArrayPool::new(1_000_000);
340 let result = proc.process_array(&arr, &pool);
341
342 let out = &result.output_arrays[0];
343 assert!((get_pixel(out, 1, 1, 4) - 50.0).abs() < 1e-10);
345 assert!((get_pixel(out, 2, 1, 4) - 0.0).abs() < 1e-10);
347 }
348
349 #[test]
350 fn test_median_mode() {
351 let arr = make_2d_array(5, 5, |x, y| if x == 2 && y == 2 { 1000.0 } else { 10.0 });
353
354 let pixels = vec![BadPixel {
355 x: 2,
356 y: 2,
357 mode: BadPixelMode::Median {
358 kernel_x: 3,
359 kernel_y: 3,
360 },
361 }];
362
363 let mut proc = BadPixelProcessor::new(pixels);
364 let pool = NDArrayPool::new(1_000_000);
365 let result = proc.process_array(&arr, &pool);
366
367 let out = &result.output_arrays[0];
368 assert!((get_pixel(out, 2, 2, 5) - 10.0).abs() < 1e-10);
370 }
371
372 #[test]
373 fn test_median_skips_bad_neighbors() {
374 let arr = make_2d_array(5, 5, |_, _| 10.0);
375 let pixels = vec![
377 BadPixel {
378 x: 2,
379 y: 2,
380 mode: BadPixelMode::Median {
381 kernel_x: 3,
382 kernel_y: 3,
383 },
384 },
385 BadPixel {
386 x: 1,
387 y: 2,
388 mode: BadPixelMode::Set { value: 999.0 },
389 },
390 ];
391
392 let mut proc = BadPixelProcessor::new(pixels);
393 let pool = NDArrayPool::new(1_000_000);
394 let result = proc.process_array(&arr, &pool);
395
396 let out = &result.output_arrays[0];
397 assert!((get_pixel(out, 2, 2, 5) - 10.0).abs() < 1e-10);
399 }
400
401 #[test]
402 fn test_boundary_pixel() {
403 let arr = make_2d_array(4, 4, |_, _| 20.0);
404 let pixels = vec![BadPixel {
406 x: 0,
407 y: 0,
408 mode: BadPixelMode::Median {
409 kernel_x: 3,
410 kernel_y: 3,
411 },
412 }];
413
414 let mut proc = BadPixelProcessor::new(pixels);
415 let pool = NDArrayPool::new(1_000_000);
416 let result = proc.process_array(&arr, &pool);
417
418 let out = &result.output_arrays[0];
419 assert!((get_pixel(out, 0, 0, 4) - 20.0).abs() < 1e-10);
421 }
422
423 #[test]
424 fn test_replace_out_of_bounds() {
425 let arr = make_2d_array(4, 4, |_, _| 50.0);
426 let pixels = vec![BadPixel {
428 x: 0,
429 y: 0,
430 mode: BadPixelMode::Replace { dx: -1, dy: 0 },
431 }];
432
433 let mut proc = BadPixelProcessor::new(pixels);
434 let pool = NDArrayPool::new(1_000_000);
435 let result = proc.process_array(&arr, &pool);
436
437 let out = &result.output_arrays[0];
438 assert!((get_pixel(out, 0, 0, 4) - 50.0).abs() < 1e-10);
440 }
441
442 #[test]
443 fn test_load_from_json() {
444 let json = r#"{"bad_pixels": [
445 {"x": 10, "y": 20, "mode": "set", "value": 0},
446 {"x": 5, "y": 3, "mode": "replace", "dx": 1, "dy": 0},
447 {"x": 7, "y": 8, "mode": "median", "kernel_x": 3, "kernel_y": 3}
448 ]}"#;
449
450 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
451 assert_eq!(pixels.len(), 3);
452 assert_eq!(pixels[0].x, 10);
453 assert_eq!(pixels[0].y, 20);
454 match &pixels[0].mode {
455 BadPixelMode::Set { value } => assert!((value - 0.0).abs() < 1e-10),
456 _ => panic!("expected Set mode"),
457 }
458 match &pixels[1].mode {
459 BadPixelMode::Replace { dx, dy } => {
460 assert_eq!(*dx, 1);
461 assert_eq!(*dy, 0);
462 }
463 _ => panic!("expected Replace mode"),
464 }
465 match &pixels[2].mode {
466 BadPixelMode::Median { kernel_x, kernel_y } => {
467 assert_eq!(*kernel_x, 3);
468 assert_eq!(*kernel_y, 3);
469 }
470 _ => panic!("expected Median mode"),
471 }
472 }
473
474 #[test]
475 fn test_no_bad_pixels_passthrough() {
476 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
477 let mut proc = BadPixelProcessor::new(vec![]);
478 let pool = NDArrayPool::new(1_000_000);
479 let result = proc.process_array(&arr, &pool);
480
481 assert_eq!(result.output_arrays.len(), 1);
482 for iy in 0..4 {
484 for ix in 0..4 {
485 let expected = (ix + iy * 4) as f64;
486 let actual = get_pixel(&result.output_arrays[0], ix, iy, 4);
487 assert!((actual - expected).abs() < 1e-10);
488 }
489 }
490 }
491
492 #[test]
493 fn test_bad_pixel_outside_image() {
494 let arr = make_2d_array(4, 4, |_, _| 10.0);
495 let pixels = vec![BadPixel {
496 x: 100,
497 y: 100,
498 mode: BadPixelMode::Set { value: 999.0 },
499 }];
500
501 let mut proc = BadPixelProcessor::new(pixels);
502 let pool = NDArrayPool::new(1_000_000);
503 let result = proc.process_array(&arr, &pool);
504
505 let out = &result.output_arrays[0];
507 assert!((get_pixel(out, 0, 0, 4) - 10.0).abs() < 1e-10);
508 }
509
510 #[test]
511 fn test_u8_data() {
512 let mut arr = NDArray::new(
513 vec![NDDimension::new(4), NDDimension::new(4)],
514 NDDataType::UInt8,
515 );
516 if let NDDataBuffer::U8(ref mut v) = arr.data {
517 for val in v.iter_mut() {
518 *val = 100;
519 }
520 }
521
522 let pixels = vec![BadPixel {
523 x: 1,
524 y: 1,
525 mode: BadPixelMode::Set { value: 0.0 },
526 }];
527
528 let mut proc = BadPixelProcessor::new(pixels);
529 let pool = NDArrayPool::new(1_000_000);
530 let result = proc.process_array(&arr, &pool);
531
532 let out = &result.output_arrays[0];
533 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
534 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
535 }
536
537 #[test]
538 fn test_set_pixels() {
539 let mut proc = BadPixelProcessor::new(vec![]);
540 assert!(proc.pixels().is_empty());
541
542 let new_pixels = vec![BadPixel {
543 x: 0,
544 y: 0,
545 mode: BadPixelMode::Set { value: 0.0 },
546 }];
547 proc.set_pixels(new_pixels);
548 assert_eq!(proc.pixels().len(), 1);
549 }
550}