rawshift_image/transforms/
bad_pixel.rs1use crate::core::image::{CfaPattern, RawImage};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum BadPixelCorrectionMode {
13 Median,
15 Average,
17}
18
19#[inline]
23fn cfa_color(x: u32, y: u32, pattern: CfaPattern) -> u8 {
24 match pattern {
25 CfaPattern::Rggb => match (x % 2, y % 2) {
26 (0, 0) => 0,
27 (1, 0) => 1,
28 (0, 1) => 3,
29 _ => 2,
30 },
31 CfaPattern::Grbg => match (x % 2, y % 2) {
32 (0, 0) => 1,
33 (1, 0) => 0,
34 (0, 1) => 2,
35 _ => 3,
36 },
37 CfaPattern::Bggr => match (x % 2, y % 2) {
38 (0, 0) => 2,
39 (1, 0) => 3,
40 (0, 1) => 1,
41 _ => 0,
42 },
43 CfaPattern::Gbrg => match (x % 2, y % 2) {
44 (0, 0) => 3,
45 (1, 0) => 2,
46 (0, 1) => 0,
47 _ => 1,
48 },
49 }
50}
51
52fn collect_same_color_neighbors(raw: &RawImage, cx: u32, cy: u32) -> Vec<u16> {
56 let center_color = cfa_color(cx, cy, raw.cfa_pattern());
57 let width = raw.width();
58 let height = raw.height();
59
60 let x_min = cx.saturating_sub(2);
61 let x_max = (cx + 2).min(width - 1);
62 let y_min = cy.saturating_sub(2);
63 let y_max = (cy + 2).min(height - 1);
64
65 let mut neighbors = Vec::with_capacity(12);
66 for ny in y_min..=y_max {
67 for nx in x_min..=x_max {
68 if nx == cx && ny == cy {
69 continue;
70 }
71 if cfa_color(nx, ny, raw.cfa_pattern()) == center_color {
72 let idx = (ny as usize) * (width as usize) + (nx as usize);
73 neighbors.push(raw.data[idx]);
74 }
75 }
76 }
77 neighbors
78}
79
80fn median(values: &mut [u16]) -> u16 {
84 if values.is_empty() {
85 return 0;
86 }
87 values.sort_unstable();
88 let mid = values.len() / 2;
89 if values.len().is_multiple_of(2) {
90 let a = values[mid - 1] as u32;
92 let b = values[mid] as u32;
93 ((a + b) / 2) as u16
94 } else {
95 values[mid]
96 }
97}
98
99fn average(values: &[u16]) -> u16 {
103 if values.is_empty() {
104 return 0;
105 }
106 let sum: u64 = values.iter().map(|&v| v as u64).sum();
107 (sum / values.len() as u64) as u16
108}
109
110pub fn detect_bad_pixels(raw: &RawImage, threshold_factor: f32) -> Vec<(u32, u32)> {
121 let width = raw.width();
122 let height = raw.height();
123 let mut bad = Vec::new();
124
125 for y in 0..height {
126 for x in 0..width {
127 let mut neighbors = collect_same_color_neighbors(raw, x, y);
128 if neighbors.is_empty() {
129 continue;
130 }
131 let med = median(&mut neighbors) as f32;
132 let pixel = raw.data[(y as usize) * (width as usize) + (x as usize)] as f32;
133 if med > 0.0 && (pixel - med).abs() > threshold_factor * med {
134 bad.push((x, y));
135 }
136 }
137 }
138
139 bad
140}
141
142pub fn correct_bad_pixels(raw: &mut RawImage, bad_pixels: &[(u32, u32)]) {
147 let replacements: Vec<(u32, u32, u16)> = bad_pixels
148 .iter()
149 .map(|&(x, y)| {
150 let mut neighbors = collect_same_color_neighbors(raw, x, y);
151 let replacement = median(&mut neighbors);
152 (x, y, replacement)
153 })
154 .collect();
155
156 for (x, y, value) in replacements {
157 raw.set_pixel(x, y, value);
158 }
159}
160
161pub fn apply_bad_pixel_correction(
167 raw: &mut RawImage,
168 mode: BadPixelCorrectionMode,
169 threshold_factor: f32,
170) {
171 let bad_pixels = detect_bad_pixels(raw, threshold_factor);
172
173 let replacements: Vec<(u32, u32, u16)> = bad_pixels
174 .iter()
175 .map(|&(x, y)| {
176 let neighbors = collect_same_color_neighbors(raw, x, y);
177 let replacement = match mode {
178 BadPixelCorrectionMode::Median => {
179 let mut n = neighbors;
180 median(&mut n)
181 }
182 BadPixelCorrectionMode::Average => average(&neighbors),
183 };
184 (x, y, replacement)
185 })
186 .collect();
187
188 for (x, y, value) in replacements {
189 raw.set_pixel(x, y, value);
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::core::image::{Rect, Size};
197
198 fn make_raw(width: u32, height: u32, fill: u16) -> RawImage {
200 let size = Size::new(width, height);
201 let active = Rect::from_coords(0, 0, width, height);
202 let mut img = RawImage::new(size, active, 14, CfaPattern::Rggb);
203 for v in img.data.iter_mut() {
204 *v = fill;
205 }
206 img
207 }
208
209 #[test]
210 fn test_no_bad_pixels_uniform() {
211 let raw = make_raw(10, 10, 1000);
213 let bad = detect_bad_pixels(&raw, 0.5);
214 assert!(bad.is_empty(), "expected no bad pixels, got {}", bad.len());
215 }
216
217 #[test]
218 fn test_single_hot_pixel_detected() {
219 let mut raw = make_raw(10, 10, 1000);
221 raw.set_pixel(5, 4, 10000); let bad = detect_bad_pixels(&raw, 0.5);
223 assert!(
224 bad.contains(&(5, 4)),
225 "hot pixel at (5,4) should be detected; found: {:?}",
226 bad
227 );
228 }
229
230 #[test]
231 fn test_correction_replaces_bad_pixel() {
232 let mut raw = make_raw(10, 10, 1000);
234 raw.set_pixel(5, 4, 10000);
235
236 let bad = detect_bad_pixels(&raw, 0.5);
237 assert!(bad.contains(&(5, 4)));
238 correct_bad_pixels(&mut raw, &bad);
239
240 let corrected = raw.get_pixel(5, 4).unwrap();
241 assert!(
243 corrected < 2000,
244 "corrected value {} should be near 1000",
245 corrected
246 );
247 }
248
249 #[test]
250 fn test_correct_bad_pixels_empty_list() {
251 let mut raw = make_raw(8, 8, 500);
253 correct_bad_pixels(&mut raw, &[]);
254 assert!(raw.data.iter().all(|&v| v == 500));
255 }
256
257 #[test]
258 fn test_detect_empty_image() {
259 let raw = make_raw(2, 2, 800);
261 let bad = detect_bad_pixels(&raw, 0.5);
262 let _ = bad;
265 }
266
267 #[test]
268 fn test_apply_bad_pixel_correction_average_mode() {
269 let mut raw = make_raw(10, 10, 1000);
270 raw.set_pixel(5, 4, 10000);
271 apply_bad_pixel_correction(&mut raw, BadPixelCorrectionMode::Average, 0.5);
272 let corrected = raw.get_pixel(5, 4).unwrap();
273 assert!(
274 corrected < 2000,
275 "average-corrected value {corrected} should be near 1000"
276 );
277 }
278
279 #[test]
280 fn test_cfa_color_rggb() {
281 assert_eq!(cfa_color(0, 0, CfaPattern::Rggb), 0); assert_eq!(cfa_color(1, 0, CfaPattern::Rggb), 1); assert_eq!(cfa_color(0, 1, CfaPattern::Rggb), 3); assert_eq!(cfa_color(1, 1, CfaPattern::Rggb), 2); }
286}