#![allow(dead_code)]
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SharpenMethod {
UnsharpMask,
Laplacian,
Adaptive,
}
impl fmt::Display for SharpenMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::UnsharpMask => "unsharp-mask",
Self::Laplacian => "laplacian",
Self::Adaptive => "adaptive",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone)]
pub struct SharpnessScaleConfig {
pub src_width: u32,
pub src_height: u32,
pub dst_width: u32,
pub dst_height: u32,
pub method: SharpenMethod,
pub strength: f64,
pub radius: u32,
pub threshold: f64,
}
impl SharpnessScaleConfig {
pub fn new(src_width: u32, src_height: u32, dst_width: u32, dst_height: u32) -> Self {
Self {
src_width,
src_height,
dst_width,
dst_height,
method: SharpenMethod::UnsharpMask,
strength: 0.5,
radius: 1,
threshold: 4.0,
}
}
pub fn with_method(mut self, method: SharpenMethod) -> Self {
self.method = method;
self
}
pub fn with_strength(mut self, strength: f64) -> Self {
self.strength = strength.clamp(0.0, 2.0);
self
}
pub fn with_radius(mut self, radius: u32) -> Self {
self.radius = radius.max(1);
self
}
pub fn with_threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold.max(0.0);
self
}
pub fn is_upscale(&self) -> bool {
self.dst_width > self.src_width || self.dst_height > self.src_height
}
}
#[allow(clippy::cast_precision_loss)]
pub fn sharpness_metric(data: &[u8], width: u32, height: u32) -> f64 {
if width < 2 || height < 2 {
return 0.0;
}
let mut sum = 0.0f64;
let mut count = 0u64;
for y in 0..(height - 1) {
for x in 0..(width - 1) {
let idx = (y * width + x) as usize;
let right = (y * width + x + 1) as usize;
let below = ((y + 1) * width + x) as usize;
if below < data.len() && right < data.len() {
let gx = (data[right] as f64 - data[idx] as f64).abs();
let gy = (data[below] as f64 - data[idx] as f64).abs();
sum += (gx * gx + gy * gy).sqrt();
count += 1;
}
}
}
if count == 0 {
0.0
} else {
sum / count as f64
}
}
#[allow(clippy::cast_precision_loss)]
pub fn unsharp_mask(data: &[u8], width: u32, height: u32, strength: f64, radius: u32) -> Vec<u8> {
let blurred = box_blur(data, width, height, radius);
let mut out = vec![0u8; data.len()];
for i in 0..data.len() {
let orig = data[i] as f64;
let blur = blurred[i] as f64;
let val = orig + strength * (orig - blur);
out[i] = val.round().clamp(0.0, 255.0) as u8;
}
out
}
#[allow(clippy::cast_precision_loss)]
pub fn box_blur(data: &[u8], width: u32, height: u32, radius: u32) -> Vec<u8> {
let mut out = vec![0u8; data.len()];
let r = radius as i32;
for y in 0..height as i32 {
for x in 0..width as i32 {
let mut sum = 0.0f64;
let mut count = 0u32;
for ky in -r..=r {
for kx in -r..=r {
let ny = y + ky;
let nx = x + kx;
if ny >= 0 && ny < height as i32 && nx >= 0 && nx < width as i32 {
sum += data[(ny as u32 * width + nx as u32) as usize] as f64;
count += 1;
}
}
}
out[(y as u32 * width + x as u32) as usize] = if count > 0 {
(sum / count as f64).round().clamp(0.0, 255.0) as u8
} else {
0
};
}
}
out
}
#[allow(clippy::cast_precision_loss)]
pub fn laplacian_sharpen(data: &[u8], width: u32, height: u32, strength: f64) -> Vec<u8> {
let mut out = vec![0u8; data.len()];
for y in 1..(height.saturating_sub(1)) {
for x in 1..(width.saturating_sub(1)) {
let idx = |xx: u32, yy: u32| data[(yy * width + xx) as usize] as f64;
let center = idx(x, y);
let laplacian =
-4.0 * center + idx(x - 1, y) + idx(x + 1, y) + idx(x, y - 1) + idx(x, y + 1);
let val = center - strength * laplacian;
out[(y * width + x) as usize] = val.round().clamp(0.0, 255.0) as u8;
}
}
for x in 0..width {
out[x as usize] = data[x as usize];
let last_row = ((height - 1) * width + x) as usize;
if last_row < data.len() {
out[last_row] = data[last_row];
}
}
for y in 0..height {
out[(y * width) as usize] = data[(y * width) as usize];
let last_col = (y * width + width - 1) as usize;
if last_col < data.len() {
out[last_col] = data[last_col];
}
}
out
}
#[allow(clippy::cast_precision_loss)]
pub fn adaptive_sharpen(
data: &[u8],
width: u32,
height: u32,
strength: f64,
threshold: f64,
) -> Vec<u8> {
let blurred = box_blur(data, width, height, 1);
let mut out = vec![0u8; data.len()];
for i in 0..data.len() {
let orig = data[i] as f64;
let blur = blurred[i] as f64;
let diff = (orig - blur).abs();
if diff > threshold {
let val = orig + strength * (orig - blur);
out[i] = val.round().clamp(0.0, 255.0) as u8;
} else {
out[i] = data[i];
}
}
out
}
pub fn sharpen(data: &[u8], width: u32, height: u32, config: &SharpnessScaleConfig) -> Vec<u8> {
match config.method {
SharpenMethod::UnsharpMask => {
unsharp_mask(data, width, height, config.strength, config.radius)
}
SharpenMethod::Laplacian => laplacian_sharpen(data, width, height, config.strength),
SharpenMethod::Adaptive => {
adaptive_sharpen(data, width, height, config.strength, config.threshold)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sharpen_method_display() {
assert_eq!(SharpenMethod::UnsharpMask.to_string(), "unsharp-mask");
assert_eq!(SharpenMethod::Laplacian.to_string(), "laplacian");
assert_eq!(SharpenMethod::Adaptive.to_string(), "adaptive");
}
#[test]
fn test_config_defaults() {
let cfg = SharpnessScaleConfig::new(1920, 1080, 3840, 2160);
assert_eq!(cfg.method, SharpenMethod::UnsharpMask);
assert!((cfg.strength - 0.5).abs() < f64::EPSILON);
assert_eq!(cfg.radius, 1);
}
#[test]
fn test_config_builder() {
let cfg = SharpnessScaleConfig::new(1920, 1080, 3840, 2160)
.with_method(SharpenMethod::Adaptive)
.with_strength(0.8)
.with_radius(2)
.with_threshold(10.0);
assert_eq!(cfg.method, SharpenMethod::Adaptive);
assert!((cfg.strength - 0.8).abs() < f64::EPSILON);
assert_eq!(cfg.radius, 2);
assert!((cfg.threshold - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_config_strength_clamped() {
let cfg = SharpnessScaleConfig::new(100, 100, 200, 200).with_strength(5.0);
assert!((cfg.strength - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_is_upscale() {
let up = SharpnessScaleConfig::new(100, 100, 200, 200);
assert!(up.is_upscale());
let down = SharpnessScaleConfig::new(200, 200, 100, 100);
assert!(!down.is_upscale());
}
#[test]
fn test_sharpness_metric_uniform() {
let data = vec![128u8; 16];
let metric = sharpness_metric(&data, 4, 4);
assert!((metric - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_sharpness_metric_edge() {
let mut data = vec![0u8; 16];
for y in 0..4u32 {
for x in 2..4u32 {
data[(y * 4 + x) as usize] = 255;
}
}
let metric = sharpness_metric(&data, 4, 4);
assert!(metric > 0.0);
}
#[test]
fn test_box_blur_uniform() {
let data = vec![100u8; 16];
let blurred = box_blur(&data, 4, 4, 1);
for &v in &blurred {
assert_eq!(v, 100);
}
}
#[test]
fn test_unsharp_mask_zero_strength() {
let data = vec![50u8; 16];
let result = unsharp_mask(&data, 4, 4, 0.0, 1);
assert_eq!(result, data);
}
#[test]
fn test_laplacian_uniform() {
let data = vec![128u8; 25]; let result = laplacian_sharpen(&data, 5, 5, 1.0);
for &v in &result {
assert_eq!(v, 128);
}
}
#[test]
fn test_adaptive_below_threshold() {
let data = vec![80u8; 16];
let result = adaptive_sharpen(&data, 4, 4, 1.0, 1.0);
assert_eq!(result, data);
}
#[test]
fn test_sharpen_dispatch_usm() {
let data = vec![128u8; 16];
let cfg = SharpnessScaleConfig::new(4, 4, 4, 4);
let result = sharpen(&data, 4, 4, &cfg);
assert_eq!(result.len(), 16);
}
#[test]
fn test_sharpen_dispatch_laplacian() {
let data = vec![128u8; 25];
let cfg = SharpnessScaleConfig::new(5, 5, 5, 5).with_method(SharpenMethod::Laplacian);
let result = sharpen(&data, 5, 5, &cfg);
assert_eq!(result.len(), 25);
}
#[test]
fn test_sharpen_dispatch_adaptive() {
let data = vec![128u8; 16];
let cfg = SharpnessScaleConfig::new(4, 4, 4, 4).with_method(SharpenMethod::Adaptive);
let result = sharpen(&data, 4, 4, &cfg);
assert_eq!(result.len(), 16);
}
}