#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#[derive(Debug, Clone, PartialEq)]
pub enum SharpnessMode {
Unsharp {
sigma: f32,
amount: f32,
threshold: u8,
},
HighFreqBoost {
strength: f32,
},
CAS {
sharpness: f32,
},
Adaptive,
}
fn build_gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
let radius = ((3.0 * sigma).ceil() as usize).max(1);
let size = 2 * radius + 1;
let mut kernel = Vec::with_capacity(size);
let mut sum = 0.0f32;
for i in 0..size {
let x = i as f32 - radius as f32;
let w = (-x * x / (2.0 * sigma * sigma)).exp();
kernel.push(w);
sum += w;
}
if sum > 1e-10 {
for w in &mut kernel {
*w /= sum;
}
}
kernel
}
fn gaussian_blur_x(src: &[f32], width: usize, height: usize, kernel: &[f32]) -> Vec<f32> {
let radius = kernel.len() / 2;
let mut dst = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let mut acc = 0.0f32;
let mut w_sum = 0.0f32;
for (k, &w) in kernel.iter().enumerate() {
let sx = x as i64 + k as i64 - radius as i64;
let clamped = sx.clamp(0, width as i64 - 1) as usize;
acc += src[y * width + clamped] * w;
w_sum += w;
}
dst[y * width + x] = if w_sum > 1e-10 { acc / w_sum } else { 0.0 };
}
}
dst
}
fn gaussian_blur_y(src: &[f32], width: usize, height: usize, kernel: &[f32]) -> Vec<f32> {
let radius = kernel.len() / 2;
let mut dst = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let mut acc = 0.0f32;
let mut w_sum = 0.0f32;
for (k, &w) in kernel.iter().enumerate() {
let sy = y as i64 + k as i64 - radius as i64;
let clamped = sy.clamp(0, height as i64 - 1) as usize;
acc += src[clamped * width + x] * w;
w_sum += w;
}
dst[y * width + x] = if w_sum > 1e-10 { acc / w_sum } else { 0.0 };
}
}
dst
}
#[must_use]
pub fn gaussian_blur_1d(src: &[f32], size: usize, sigma: f32) -> Vec<f32> {
if size == 0 || src.is_empty() {
return src.to_vec();
}
let height = src.len() / size;
if height == 0 {
return src.to_vec();
}
let kernel = build_gaussian_kernel_1d(sigma);
let h_pass = gaussian_blur_x(src, size, height, &kernel);
gaussian_blur_y(&h_pass, size, height, &kernel)
}
#[must_use]
pub fn local_laplacian(data: &[f32], x: usize, y: usize, stride: usize) -> f32 {
if stride == 0 || data.is_empty() {
return 0.0;
}
let height = data.len() / stride;
let get = |px: i64, py: i64| -> f32 {
let cx = px.clamp(0, stride as i64 - 1) as usize;
let cy = py.clamp(0, height as i64 - 1) as usize;
data[cy * stride + cx]
};
let xi = x as i64;
let yi = y as i64;
let centre = get(xi, yi);
let left = get(xi - 1, yi);
let right = get(xi + 1, yi);
let up = get(xi, yi - 1);
let down = get(xi, yi + 1);
left + right + up + down - 4.0 * centre
}
#[derive(Debug, Clone)]
pub struct UnsharpMask {
pub sigma: f32,
pub amount: f32,
pub threshold: u8,
}
impl UnsharpMask {
#[must_use]
pub fn new(sigma: f32, amount: f32, threshold: u8) -> Self {
Self {
sigma,
amount,
threshold,
}
}
#[must_use]
pub fn apply(&self, src: &[f32], width: usize) -> Vec<f32> {
if src.is_empty() || width == 0 {
return src.to_vec();
}
let blurred = gaussian_blur_1d(src, width, self.sigma);
let threshold_f = self.threshold as f32 / 255.0;
src.iter()
.zip(blurred.iter())
.map(|(&s, &b)| {
let diff = s - b;
if diff.abs() > threshold_f {
s + self.amount * diff
} else {
s
}
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct CasSharpener {
pub sharpness: f32,
}
impl CasSharpener {
#[must_use]
pub fn new(sharpness: f32) -> Self {
Self {
sharpness: sharpness.clamp(0.0, 1.0),
}
}
#[must_use]
pub fn apply(&self, src: &[f32], width: usize) -> Vec<f32> {
if src.is_empty() || width == 0 {
return src.to_vec();
}
let height = src.len() / width;
if height < 3 {
return src.to_vec();
}
let mut dst = src.to_vec();
for y in 1..height - 1 {
for x in 1..width - 1 {
let p = src[y * width + x];
let neighbours = [
src[(y - 1) * width + (x - 1)],
src[(y - 1) * width + x],
src[(y - 1) * width + (x + 1)],
src[y * width + (x - 1)],
src[y * width + (x + 1)],
src[(y + 1) * width + (x - 1)],
src[(y + 1) * width + x],
src[(y + 1) * width + (x + 1)],
];
let min_g = neighbours.iter().cloned().fold(p, f32::min);
let max_g = neighbours.iter().cloned().fold(p, f32::max);
let denom = (1.0 - max_g).max(1e-6);
let sharpness_factor = (min_g / denom).max(0.0).sqrt();
let weight = sharpness_factor * (-0.125 * self.sharpness - 0.125);
let denom_cas = 1.0 + 4.0 * weight;
let output = if denom_cas.abs() > 1e-8 {
(p + 4.0 * weight * p) / denom_cas
} else {
p
};
dst[y * width + x] = output;
}
}
dst
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveSharpener {
pub base_strength: f32,
pub blur_sigma: f32,
}
impl AdaptiveSharpener {
#[must_use]
pub fn new(base_strength: f32, blur_sigma: f32) -> Self {
Self {
base_strength,
blur_sigma,
}
}
#[must_use]
pub fn apply(&self, src: &[f32], width: usize) -> Vec<f32> {
if src.is_empty() || width == 0 {
return src.to_vec();
}
let height = src.len() / width;
if height == 0 {
return src.to_vec();
}
let laplacians: Vec<f32> = (0..height)
.flat_map(|y| (0..width).map(move |x| local_laplacian(src, x, y, width).abs()))
.collect();
let max_lap = laplacians.iter().cloned().fold(0.0_f32, f32::max).max(1e-8);
let blurred = gaussian_blur_1d(src, width, self.blur_sigma);
src.iter()
.enumerate()
.map(|(idx, &s)| {
let lap_norm = laplacians[idx] / max_lap; let local_strength = self.base_strength * (1.0 - lap_norm).max(0.0);
let diff = s - blurred[idx];
s + local_strength * diff
})
.collect()
}
}
#[must_use]
pub fn sharpen(src: &[f32], width: usize, mode: &SharpnessMode) -> Vec<f32> {
match mode {
SharpnessMode::Unsharp {
sigma,
amount,
threshold,
} => UnsharpMask::new(*sigma, *amount, *threshold).apply(src, width),
SharpnessMode::HighFreqBoost { strength } => {
if src.is_empty() || width == 0 {
return src.to_vec();
}
let blurred = gaussian_blur_1d(src, width, 1.0);
src.iter()
.zip(blurred.iter())
.map(|(&s, &b)| s + strength * (s - b))
.collect()
}
SharpnessMode::CAS { sharpness } => CasSharpener::new(*sharpness).apply(src, width),
SharpnessMode::Adaptive => AdaptiveSharpener::new(1.5, 1.0).apply(src, width),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gaussian_blur_1d_uniform_image_unchanged() {
let src = vec![0.5f32; 64];
let blurred = gaussian_blur_1d(&src, 8, 1.0);
for &v in &blurred {
assert!((v - 0.5).abs() < 0.001, "uniform image blurred to {v}");
}
}
#[test]
fn gaussian_blur_1d_reduces_peak() {
let mut src = vec![0.0f32; 81];
src[40] = 1.0; let blurred = gaussian_blur_1d(&src, 9, 1.5);
assert!(blurred[40] < 1.0, "blur should reduce peak");
assert!(blurred[40] > 0.0, "peak should not vanish entirely");
}
#[test]
fn gaussian_blur_1d_empty() {
let result = gaussian_blur_1d(&[], 8, 1.0);
assert!(result.is_empty());
}
#[test]
fn gaussian_blur_1d_zero_width() {
let src = vec![0.5f32; 8];
let result = gaussian_blur_1d(&src, 0, 1.0);
assert_eq!(result.len(), src.len()); }
#[test]
fn local_laplacian_uniform_image_zero() {
let src = vec![0.5f32; 25]; for y in 1..4 {
for x in 1..4 {
let lap = local_laplacian(&src, x, y, 5);
assert!(lap.abs() < 1e-5, "uniform Laplacian at ({x},{y}) = {lap}");
}
}
}
#[test]
fn local_laplacian_spike_positive() {
let mut src = vec![0.0f32; 25]; src[12] = 1.0; let lap = local_laplacian(&src, 2, 2, 5);
assert!((lap - (-4.0)).abs() < 1e-4, "spike Laplacian = {lap}");
}
#[test]
fn local_laplacian_border_clamping() {
let src = vec![0.0f32; 25];
let v = local_laplacian(&src, 0, 0, 5);
assert!(v.is_finite());
let v2 = local_laplacian(&src, 4, 4, 5);
assert!(v2.is_finite());
}
#[test]
fn unsharp_mask_uniform_unchanged() {
let src = vec![0.5f32; 64];
let um = UnsharpMask::new(1.0, 1.0, 0);
let out = um.apply(&src, 8);
for &v in &out {
assert!((v - 0.5).abs() < 0.001);
}
}
#[test]
fn unsharp_mask_sharpens_edge() {
let src: Vec<f32> = (0..8).map(|i| if i < 4 { 0.0 } else { 1.0 }).collect();
let um = UnsharpMask::new(1.0, 2.0, 0);
let out = um.apply(&src, 8);
assert!(
out[3] <= src[3],
"edge pixel should be pushed down or unchanged"
);
assert!(
out[4] >= src[4],
"edge pixel should be pushed up or unchanged"
);
}
#[test]
fn unsharp_mask_threshold_suppresses_noise() {
let src: Vec<f32> = (0..64)
.map(|i| 0.5 + if i % 2 == 0 { 0.001 } else { -0.001 })
.collect();
let um = UnsharpMask::new(1.0, 5.0, 10); let out = um.apply(&src, 8);
for (&s, &o) in src.iter().zip(out.iter()) {
assert!(
(s - o).abs() < 0.01,
"noise above threshold after suppression"
);
}
}
#[test]
fn unsharp_mask_empty_returns_empty() {
let um = UnsharpMask::new(1.0, 1.0, 0);
let out = um.apply(&[], 8);
assert!(out.is_empty());
}
#[test]
fn cas_uniform_image_nearly_unchanged() {
let src = vec![0.5f32; 64];
let cas = CasSharpener::new(0.5);
let out = cas.apply(&src, 8);
for &v in &out {
assert!((v - 0.5).abs() < 0.05, "CAS changed uniform pixel to {v}");
}
}
#[test]
fn cas_sharpness_clamped_to_unit() {
let cas = CasSharpener::new(5.0);
assert!(cas.sharpness <= 1.0);
let cas2 = CasSharpener::new(-1.0);
assert!(cas2.sharpness >= 0.0);
}
#[test]
fn cas_returns_same_size() {
let src = vec![0.5f32; 64];
let cas = CasSharpener::new(0.5);
let out = cas.apply(&src, 8);
assert_eq!(out.len(), 64);
}
#[test]
fn cas_empty_returns_empty() {
let cas = CasSharpener::new(0.5);
let out = cas.apply(&[], 8);
assert!(out.is_empty());
}
#[test]
fn cas_output_finite() {
let src: Vec<f32> = (0..64).map(|i| (i as f32 / 63.0).min(1.0)).collect();
let cas = CasSharpener::new(0.8);
let out = cas.apply(&src, 8);
for &v in &out {
assert!(v.is_finite(), "CAS produced non-finite value");
}
}
#[test]
fn adaptive_uniform_unchanged() {
let src = vec![0.5f32; 64];
let ad = AdaptiveSharpener::new(1.0, 1.0);
let out = ad.apply(&src, 8);
for &v in &out {
assert!((v - 0.5).abs() < 0.001, "adaptive changed uniform to {v}");
}
}
#[test]
fn adaptive_sharpens_smooth_region() {
let src: Vec<f32> = (0..64).map(|i| i as f32 / 63.0).collect();
let ad = AdaptiveSharpener::new(2.0, 0.5);
let out = ad.apply(&src, 8);
let total_diff: f32 = src
.iter()
.zip(out.iter())
.map(|(&s, &o)| (s - o).abs())
.sum();
assert!(total_diff > 0.0, "adaptive produced no change on gradient");
}
#[test]
fn adaptive_output_finite() {
let src: Vec<f32> = (0..64).map(|i| i as f32 / 63.0).collect();
let ad = AdaptiveSharpener::new(1.5, 1.0);
let out = ad.apply(&src, 8);
for &v in &out {
assert!(v.is_finite());
}
}
#[test]
fn sharpen_dispatch_unsharp() {
let src = vec![0.5f32; 64];
let mode = SharpnessMode::Unsharp {
sigma: 1.0,
amount: 1.0,
threshold: 0,
};
let out = sharpen(&src, 8, &mode);
assert_eq!(out.len(), 64);
}
#[test]
fn sharpen_dispatch_high_freq_boost() {
let src = vec![0.5f32; 64];
let mode = SharpnessMode::HighFreqBoost { strength: 1.0 };
let out = sharpen(&src, 8, &mode);
assert_eq!(out.len(), 64);
}
#[test]
fn sharpen_dispatch_cas() {
let src = vec![0.5f32; 64];
let mode = SharpnessMode::CAS { sharpness: 0.5 };
let out = sharpen(&src, 8, &mode);
assert_eq!(out.len(), 64);
}
#[test]
fn sharpen_dispatch_adaptive() {
let src = vec![0.5f32; 64];
let out = sharpen(&src, 8, &SharpnessMode::Adaptive);
assert_eq!(out.len(), 64);
}
#[test]
fn sharpen_high_freq_boost_increases_contrast() {
let mut src = vec![0.5f32; 64];
for i in 0..8 {
src[i] = if i < 4 { 0.2 } else { 0.8 };
}
let mode = SharpnessMode::HighFreqBoost { strength: 1.5 };
let out = sharpen(&src, 8, &mode);
let max_diff = src
.iter()
.zip(out.iter())
.map(|(&s, &o)| (s - o).abs())
.fold(0.0_f32, f32::max);
assert!(max_diff > 0.0, "high-freq boost produced no change");
}
}