1use std::collections::HashSet;
10use std::sync::Arc;
11
12use ad_core::ndarray::{NDArray, NDDataBuffer};
13use ad_core::ndarray_pool::NDArrayPool;
14use ad_core::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}
55
56impl BadPixelProcessor {
57 pub fn new(pixels: Vec<BadPixel>) -> Self {
59 let bad_set: HashSet<(usize, usize)> = pixels.iter().map(|p| (p.x, p.y)).collect();
60 Self {
61 pixels,
62 bad_set,
63 width: 0,
64 }
65 }
66
67 pub fn load_from_json(json_str: &str) -> Result<Vec<BadPixel>, serde_json::Error> {
69 let list: BadPixelList = serde_json::from_str(json_str)?;
70 Ok(list.bad_pixels)
71 }
72
73 pub fn set_pixels(&mut self, pixels: Vec<BadPixel>) {
75 self.bad_set = pixels.iter().map(|p| (p.x, p.y)).collect();
76 self.pixels = pixels;
77 }
78
79 pub fn pixels(&self) -> &[BadPixel] {
81 &self.pixels
82 }
83
84 fn is_bad(&self, x: usize, y: usize) -> bool {
86 self.bad_set.contains(&(x, y))
87 }
88
89 fn apply_corrections(&self, data: &mut NDDataBuffer, width: usize, height: usize) {
91 let mut corrections: Vec<(usize, f64)> = Vec::with_capacity(self.pixels.len());
97
98 for bp in &self.pixels {
99 if bp.x >= width || bp.y >= height {
100 continue;
101 }
102
103 let value = match &bp.mode {
104 BadPixelMode::Set { value } => *value,
105
106 BadPixelMode::Replace { dx, dy } => {
107 let nx = bp.x as i64 + *dx as i64;
108 let ny = bp.y as i64 + *dy as i64;
109
110 if nx < 0 || nx >= width as i64 || ny < 0 || ny >= height as i64 {
111 continue; }
113
114 let nx = nx as usize;
115 let ny = ny as usize;
116
117 if self.is_bad(nx, ny) {
119 continue;
120 }
121
122 let idx = ny * width + nx;
123 match data.get_as_f64(idx) {
124 Some(v) => v,
125 None => continue,
126 }
127 }
128
129 BadPixelMode::Median { kernel_x, kernel_y } => {
130 let half_x = (*kernel_x / 2) as i64;
131 let half_y = (*kernel_y / 2) as i64;
132 let cx = bp.x as i64;
133 let cy = bp.y as i64;
134
135 let mut neighbors = Vec::new();
136 for ky in (cy - half_y)..=(cy + half_y) {
137 for kx in (cx - half_x)..=(cx + half_x) {
138 if kx < 0 || kx >= width as i64 || ky < 0 || ky >= height as i64 {
139 continue;
140 }
141 let kxu = kx as usize;
142 let kyu = ky as usize;
143 if kxu == bp.x && kyu == bp.y {
145 continue;
146 }
147 if self.is_bad(kxu, kyu) {
148 continue;
149 }
150 let idx = kyu * width + kxu;
151 if let Some(v) = data.get_as_f64(idx) {
152 neighbors.push(v);
153 }
154 }
155 }
156
157 if neighbors.is_empty() {
158 continue; }
160
161 neighbors.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
162 let mid = neighbors.len() / 2;
163 if neighbors.len() % 2 == 0 {
164 (neighbors[mid - 1] + neighbors[mid]) / 2.0
165 } else {
166 neighbors[mid]
167 }
168 }
169 };
170
171 let idx = bp.y * width + bp.x;
172 corrections.push((idx, value));
173 }
174
175 for (idx, value) in corrections {
177 data.set_from_f64(idx, value);
178 }
179 }
180}
181
182impl NDPluginProcess for BadPixelProcessor {
183 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
184 let info = array.info();
185 self.width = info.x_size;
186 let height = info.y_size;
187
188 if self.pixels.is_empty() {
189 return ProcessResult::arrays(vec![Arc::new(array.clone())]);
191 }
192
193 let mut out = array.clone();
194 self.apply_corrections(&mut out.data, self.width, height);
195 ProcessResult::arrays(vec![Arc::new(out)])
196 }
197
198 fn plugin_type(&self) -> &str {
199 "NDPluginBadPixel"
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use ad_core::ndarray::{NDDataType, NDDimension};
207
208 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
209 let mut arr = NDArray::new(
210 vec![NDDimension::new(x), NDDimension::new(y)],
211 NDDataType::Float64,
212 );
213 if let NDDataBuffer::F64(ref mut v) = arr.data {
214 for iy in 0..y {
215 for ix in 0..x {
216 v[iy * x + ix] = fill(ix, iy);
217 }
218 }
219 }
220 arr
221 }
222
223 fn get_pixel(arr: &NDArray, x: usize, y: usize, width: usize) -> f64 {
224 arr.data.get_as_f64(y * width + x).unwrap()
225 }
226
227 #[test]
228 fn test_set_mode() {
229 let arr = make_2d_array(4, 4, |_, _| 100.0);
230 let pixels = vec![
231 BadPixel { x: 1, y: 1, mode: BadPixelMode::Set { value: 0.0 } },
232 BadPixel { x: 3, y: 2, mode: BadPixelMode::Set { value: 42.0 } },
233 ];
234
235 let mut proc = BadPixelProcessor::new(pixels);
236 let pool = NDArrayPool::new(1_000_000);
237 let result = proc.process_array(&arr, &pool);
238
239 assert_eq!(result.output_arrays.len(), 1);
240 let out = &result.output_arrays[0];
241 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
242 assert!((get_pixel(out, 3, 2, 4) - 42.0).abs() < 1e-10);
243 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
245 }
246
247 #[test]
248 fn test_replace_mode() {
249 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
250 let pixels = vec![
252 BadPixel { x: 2, y: 2, mode: BadPixelMode::Replace { dx: 1, dy: 0 } },
253 ];
254
255 let mut proc = BadPixelProcessor::new(pixels);
256 let pool = NDArrayPool::new(1_000_000);
257 let result = proc.process_array(&arr, &pool);
258
259 let out = &result.output_arrays[0];
260 assert!((get_pixel(out, 2, 2, 4) - 11.0).abs() < 1e-10);
262 }
263
264 #[test]
265 fn test_replace_skip_bad_neighbor() {
266 let arr = make_2d_array(4, 4, |_, _| 50.0);
267 let pixels = vec![
269 BadPixel { x: 1, y: 1, mode: BadPixelMode::Replace { dx: 1, dy: 0 } },
270 BadPixel { x: 2, y: 1, mode: BadPixelMode::Set { value: 0.0 } },
271 ];
272
273 let mut proc = BadPixelProcessor::new(pixels);
274 let pool = NDArrayPool::new(1_000_000);
275 let result = proc.process_array(&arr, &pool);
276
277 let out = &result.output_arrays[0];
278 assert!((get_pixel(out, 1, 1, 4) - 50.0).abs() < 1e-10);
280 assert!((get_pixel(out, 2, 1, 4) - 0.0).abs() < 1e-10);
282 }
283
284 #[test]
285 fn test_median_mode() {
286 let arr = make_2d_array(5, 5, |x, y| {
288 if x == 2 && y == 2 { 1000.0 } else { 10.0 }
289 });
290
291 let pixels = vec![
292 BadPixel { x: 2, y: 2, mode: BadPixelMode::Median { kernel_x: 3, kernel_y: 3 } },
293 ];
294
295 let mut proc = BadPixelProcessor::new(pixels);
296 let pool = NDArrayPool::new(1_000_000);
297 let result = proc.process_array(&arr, &pool);
298
299 let out = &result.output_arrays[0];
300 assert!((get_pixel(out, 2, 2, 5) - 10.0).abs() < 1e-10);
302 }
303
304 #[test]
305 fn test_median_skips_bad_neighbors() {
306 let arr = make_2d_array(5, 5, |_, _| 10.0);
307 let pixels = vec![
309 BadPixel { x: 2, y: 2, mode: BadPixelMode::Median { kernel_x: 3, kernel_y: 3 } },
310 BadPixel { x: 1, y: 2, mode: BadPixelMode::Set { value: 999.0 } },
311 ];
312
313 let mut proc = BadPixelProcessor::new(pixels);
314 let pool = NDArrayPool::new(1_000_000);
315 let result = proc.process_array(&arr, &pool);
316
317 let out = &result.output_arrays[0];
318 assert!((get_pixel(out, 2, 2, 5) - 10.0).abs() < 1e-10);
320 }
321
322 #[test]
323 fn test_boundary_pixel() {
324 let arr = make_2d_array(4, 4, |_, _| 20.0);
325 let pixels = vec![
327 BadPixel { x: 0, y: 0, mode: BadPixelMode::Median { kernel_x: 3, kernel_y: 3 } },
328 ];
329
330 let mut proc = BadPixelProcessor::new(pixels);
331 let pool = NDArrayPool::new(1_000_000);
332 let result = proc.process_array(&arr, &pool);
333
334 let out = &result.output_arrays[0];
335 assert!((get_pixel(out, 0, 0, 4) - 20.0).abs() < 1e-10);
337 }
338
339 #[test]
340 fn test_replace_out_of_bounds() {
341 let arr = make_2d_array(4, 4, |_, _| 50.0);
342 let pixels = vec![
344 BadPixel { x: 0, y: 0, mode: BadPixelMode::Replace { dx: -1, dy: 0 } },
345 ];
346
347 let mut proc = BadPixelProcessor::new(pixels);
348 let pool = NDArrayPool::new(1_000_000);
349 let result = proc.process_array(&arr, &pool);
350
351 let out = &result.output_arrays[0];
352 assert!((get_pixel(out, 0, 0, 4) - 50.0).abs() < 1e-10);
354 }
355
356 #[test]
357 fn test_load_from_json() {
358 let json = r#"{"bad_pixels": [
359 {"x": 10, "y": 20, "mode": "set", "value": 0},
360 {"x": 5, "y": 3, "mode": "replace", "dx": 1, "dy": 0},
361 {"x": 7, "y": 8, "mode": "median", "kernel_x": 3, "kernel_y": 3}
362 ]}"#;
363
364 let pixels = BadPixelProcessor::load_from_json(json).unwrap();
365 assert_eq!(pixels.len(), 3);
366 assert_eq!(pixels[0].x, 10);
367 assert_eq!(pixels[0].y, 20);
368 match &pixels[0].mode {
369 BadPixelMode::Set { value } => assert!((value - 0.0).abs() < 1e-10),
370 _ => panic!("expected Set mode"),
371 }
372 match &pixels[1].mode {
373 BadPixelMode::Replace { dx, dy } => {
374 assert_eq!(*dx, 1);
375 assert_eq!(*dy, 0);
376 }
377 _ => panic!("expected Replace mode"),
378 }
379 match &pixels[2].mode {
380 BadPixelMode::Median { kernel_x, kernel_y } => {
381 assert_eq!(*kernel_x, 3);
382 assert_eq!(*kernel_y, 3);
383 }
384 _ => panic!("expected Median mode"),
385 }
386 }
387
388 #[test]
389 fn test_no_bad_pixels_passthrough() {
390 let arr = make_2d_array(4, 4, |x, y| (x + y * 4) as f64);
391 let mut proc = BadPixelProcessor::new(vec![]);
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 for iy in 0..4 {
398 for ix in 0..4 {
399 let expected = (ix + iy * 4) as f64;
400 let actual = get_pixel(&result.output_arrays[0], ix, iy, 4);
401 assert!((actual - expected).abs() < 1e-10);
402 }
403 }
404 }
405
406 #[test]
407 fn test_bad_pixel_outside_image() {
408 let arr = make_2d_array(4, 4, |_, _| 10.0);
409 let pixels = vec![
410 BadPixel { x: 100, y: 100, mode: BadPixelMode::Set { value: 999.0 } },
411 ];
412
413 let mut proc = BadPixelProcessor::new(pixels);
414 let pool = NDArrayPool::new(1_000_000);
415 let result = proc.process_array(&arr, &pool);
416
417 let out = &result.output_arrays[0];
419 assert!((get_pixel(out, 0, 0, 4) - 10.0).abs() < 1e-10);
420 }
421
422 #[test]
423 fn test_u8_data() {
424 let mut arr = NDArray::new(
425 vec![NDDimension::new(4), NDDimension::new(4)],
426 NDDataType::UInt8,
427 );
428 if let NDDataBuffer::U8(ref mut v) = arr.data {
429 for val in v.iter_mut() {
430 *val = 100;
431 }
432 }
433
434 let pixels = vec![
435 BadPixel { x: 1, y: 1, mode: BadPixelMode::Set { value: 0.0 } },
436 ];
437
438 let mut proc = BadPixelProcessor::new(pixels);
439 let pool = NDArrayPool::new(1_000_000);
440 let result = proc.process_array(&arr, &pool);
441
442 let out = &result.output_arrays[0];
443 assert!((get_pixel(out, 1, 1, 4) - 0.0).abs() < 1e-10);
444 assert!((get_pixel(out, 0, 0, 4) - 100.0).abs() < 1e-10);
445 }
446
447 #[test]
448 fn test_set_pixels() {
449 let mut proc = BadPixelProcessor::new(vec![]);
450 assert!(proc.pixels().is_empty());
451
452 let new_pixels = vec![
453 BadPixel { x: 0, y: 0, mode: BadPixelMode::Set { value: 0.0 } },
454 ];
455 proc.set_pixels(new_pixels);
456 assert_eq!(proc.pixels().len(), 1);
457 }
458}