use crate::core::image::{CfaPattern, RawImage};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BadPixelCorrectionMode {
Median,
Average,
}
#[inline]
fn cfa_color(x: u32, y: u32, pattern: CfaPattern) -> u8 {
match pattern {
CfaPattern::Rggb => match (x % 2, y % 2) {
(0, 0) => 0,
(1, 0) => 1,
(0, 1) => 3,
_ => 2,
},
CfaPattern::Grbg => match (x % 2, y % 2) {
(0, 0) => 1,
(1, 0) => 0,
(0, 1) => 2,
_ => 3,
},
CfaPattern::Bggr => match (x % 2, y % 2) {
(0, 0) => 2,
(1, 0) => 3,
(0, 1) => 1,
_ => 0,
},
CfaPattern::Gbrg => match (x % 2, y % 2) {
(0, 0) => 3,
(1, 0) => 2,
(0, 1) => 0,
_ => 1,
},
}
}
fn collect_same_color_neighbors(raw: &RawImage, cx: u32, cy: u32) -> Vec<u16> {
let center_color = cfa_color(cx, cy, raw.cfa_pattern());
let width = raw.width();
let height = raw.height();
let x_min = cx.saturating_sub(2);
let x_max = (cx + 2).min(width - 1);
let y_min = cy.saturating_sub(2);
let y_max = (cy + 2).min(height - 1);
let mut neighbors = Vec::with_capacity(12);
for ny in y_min..=y_max {
for nx in x_min..=x_max {
if nx == cx && ny == cy {
continue;
}
if cfa_color(nx, ny, raw.cfa_pattern()) == center_color {
let idx = (ny as usize) * (width as usize) + (nx as usize);
neighbors.push(raw.data[idx]);
}
}
}
neighbors
}
fn median(values: &mut [u16]) -> u16 {
if values.is_empty() {
return 0;
}
values.sort_unstable();
let mid = values.len() / 2;
if values.len().is_multiple_of(2) {
let a = values[mid - 1] as u32;
let b = values[mid] as u32;
((a + b) / 2) as u16
} else {
values[mid]
}
}
fn average(values: &[u16]) -> u16 {
if values.is_empty() {
return 0;
}
let sum: u64 = values.iter().map(|&v| v as u64).sum();
(sum / values.len() as u64) as u16
}
pub fn detect_bad_pixels(raw: &RawImage, threshold_factor: f32) -> Vec<(u32, u32)> {
let width = raw.width();
let height = raw.height();
let mut bad = Vec::new();
for y in 0..height {
for x in 0..width {
let mut neighbors = collect_same_color_neighbors(raw, x, y);
if neighbors.is_empty() {
continue;
}
let med = median(&mut neighbors) as f32;
let pixel = raw.data[(y as usize) * (width as usize) + (x as usize)] as f32;
if med > 0.0 && (pixel - med).abs() > threshold_factor * med {
bad.push((x, y));
}
}
}
bad
}
pub fn correct_bad_pixels(raw: &mut RawImage, bad_pixels: &[(u32, u32)]) {
let replacements: Vec<(u32, u32, u16)> = bad_pixels
.iter()
.map(|&(x, y)| {
let mut neighbors = collect_same_color_neighbors(raw, x, y);
let replacement = median(&mut neighbors);
(x, y, replacement)
})
.collect();
for (x, y, value) in replacements {
raw.set_pixel(x, y, value);
}
}
pub fn apply_bad_pixel_correction(
raw: &mut RawImage,
mode: BadPixelCorrectionMode,
threshold_factor: f32,
) {
let bad_pixels = detect_bad_pixels(raw, threshold_factor);
let replacements: Vec<(u32, u32, u16)> = bad_pixels
.iter()
.map(|&(x, y)| {
let neighbors = collect_same_color_neighbors(raw, x, y);
let replacement = match mode {
BadPixelCorrectionMode::Median => {
let mut n = neighbors;
median(&mut n)
}
BadPixelCorrectionMode::Average => average(&neighbors),
};
(x, y, replacement)
})
.collect();
for (x, y, value) in replacements {
raw.set_pixel(x, y, value);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::image::{Rect, Size};
fn make_raw(width: u32, height: u32, fill: u16) -> RawImage {
let size = Size::new(width, height);
let active = Rect::from_coords(0, 0, width, height);
let mut img = RawImage::new(size, active, 14, CfaPattern::Rggb);
for v in img.data.iter_mut() {
*v = fill;
}
img
}
#[test]
fn test_no_bad_pixels_uniform() {
let raw = make_raw(10, 10, 1000);
let bad = detect_bad_pixels(&raw, 0.5);
assert!(bad.is_empty(), "expected no bad pixels, got {}", bad.len());
}
#[test]
fn test_single_hot_pixel_detected() {
let mut raw = make_raw(10, 10, 1000);
raw.set_pixel(5, 4, 10000); let bad = detect_bad_pixels(&raw, 0.5);
assert!(
bad.contains(&(5, 4)),
"hot pixel at (5,4) should be detected; found: {:?}",
bad
);
}
#[test]
fn test_correction_replaces_bad_pixel() {
let mut raw = make_raw(10, 10, 1000);
raw.set_pixel(5, 4, 10000);
let bad = detect_bad_pixels(&raw, 0.5);
assert!(bad.contains(&(5, 4)));
correct_bad_pixels(&mut raw, &bad);
let corrected = raw.get_pixel(5, 4).unwrap();
assert!(
corrected < 2000,
"corrected value {} should be near 1000",
corrected
);
}
#[test]
fn test_correct_bad_pixels_empty_list() {
let mut raw = make_raw(8, 8, 500);
correct_bad_pixels(&mut raw, &[]);
assert!(raw.data.iter().all(|&v| v == 500));
}
#[test]
fn test_detect_empty_image() {
let raw = make_raw(2, 2, 800);
let bad = detect_bad_pixels(&raw, 0.5);
let _ = bad;
}
#[test]
fn test_apply_bad_pixel_correction_average_mode() {
let mut raw = make_raw(10, 10, 1000);
raw.set_pixel(5, 4, 10000);
apply_bad_pixel_correction(&mut raw, BadPixelCorrectionMode::Average, 0.5);
let corrected = raw.get_pixel(5, 4).unwrap();
assert!(
corrected < 2000,
"average-corrected value {corrected} should be near 1000"
);
}
#[test]
fn test_cfa_color_rggb() {
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); }
}