Self-hosted document scanner and media processing tools. - Rust Axum gateway + worker pool with NATS JetStream - Next.js 16 frontend with shadcn/ui - Scanner pipeline: edge detection, warp, binarization, OCR - Image tools: compress, resize, convert - PDF tools: merge, split, compress - CI/CD with Docker multi-stage build Co-Authored-By: Kilo <kilo@kilo.ai>
134 lines
4.3 KiB
Rust
134 lines
4.3 KiB
Rust
use image::{GrayImage, Luma};
|
|
use imageproc::filter::gaussian_blur_f32;
|
|
|
|
/// Apply final sharpening and contrast optimization.
|
|
pub fn enhance_final(img: &GrayImage) -> GrayImage {
|
|
let sharpened = unsharp_mask(img, 1.0, 1.0);
|
|
adjust_contrast(&sharpened, 1.2)
|
|
}
|
|
|
|
/// Unsharp mask: add high-frequency detail back to the image.
|
|
/// result = img + amount * (img - blurred)
|
|
pub fn unsharp_mask(img: &GrayImage, sigma: f64, amount: f64) -> GrayImage {
|
|
let (w, h) = (img.width(), img.height());
|
|
let blurred = gaussian_blur_f32(img, sigma as f32);
|
|
|
|
let mut output = GrayImage::new(w, h);
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let orig = img.get_pixel(x, y)[0] as f64;
|
|
let blur = blurred.get_pixel(x, y)[0] as f64;
|
|
let mask = orig - blur;
|
|
let result = (orig + amount * mask).clamp(0.0, 255.0) as u8;
|
|
output.put_pixel(x, y, Luma([result]));
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Adjust contrast by scaling pixel values around the mean.
|
|
pub fn adjust_contrast(img: &GrayImage, factor: f64) -> GrayImage {
|
|
let (w, h) = (img.width(), img.height());
|
|
let mean = mean_value(img);
|
|
|
|
let mut output = GrayImage::new(w, h);
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let pixel = img.get_pixel(x, y)[0] as f64;
|
|
let adjusted = ((pixel - mean) * factor + mean).clamp(0.0, 255.0) as u8;
|
|
output.put_pixel(x, y, Luma([adjusted]));
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Remove salt-and-pepper noise using a median-like filter.
|
|
#[allow(dead_code)]
|
|
pub fn remove_noise(img: &GrayImage, threshold: u8) -> GrayImage {
|
|
let (w, h) = (img.width(), img.height());
|
|
let mut output = GrayImage::new(w, h);
|
|
|
|
for y in 1..h - 1 {
|
|
for x in 1..w - 1 {
|
|
let center = img.get_pixel(x, y)[0];
|
|
// Check if pixel is significantly different from neighbors
|
|
let mut neighbors = Vec::new();
|
|
for dy in -1i32..=1 {
|
|
for dx in -1i32..=1 {
|
|
if dx == 0 && dy == 0 {
|
|
continue;
|
|
}
|
|
neighbors.push(
|
|
img.get_pixel((x as i32 + dx) as u32, (y as i32 + dy) as u32)[0],
|
|
);
|
|
}
|
|
}
|
|
let min = *neighbors.iter().min().unwrap_or(&0);
|
|
let max = *neighbors.iter().max().unwrap_or(&255);
|
|
|
|
if (center as i16 - min as i16).abs() > threshold as i16
|
|
|| (center as i16 - max as i16).abs() > threshold as i16
|
|
{
|
|
// Replace with median
|
|
neighbors.sort();
|
|
output.put_pixel(x, y, Luma([neighbors[neighbors.len() / 2]]));
|
|
} else {
|
|
output.put_pixel(x, y, Luma([center]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy edges
|
|
for x in 0..w {
|
|
output.put_pixel(x, 0, *img.get_pixel(x, 0));
|
|
output.put_pixel(x, h - 1, *img.get_pixel(x, h - 1));
|
|
}
|
|
for y in 0..h {
|
|
output.put_pixel(0, y, *img.get_pixel(0, y));
|
|
output.put_pixel(w - 1, y, *img.get_pixel(w - 1, y));
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
/// Compute mean pixel value.
|
|
fn mean_value(img: &GrayImage) -> f64 {
|
|
let sum: u64 = img.iter().map(|&p| p as u64).sum();
|
|
let count = img.width() as u64 * img.height() as u64;
|
|
if count > 0 {
|
|
sum as f64 / count as f64
|
|
} else {
|
|
128.0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_unsharp_mask_no_change() {
|
|
// Uniform image should remain unchanged
|
|
let img = GrayImage::from_pixel(50, 50, Luma([128]));
|
|
let result = unsharp_mask(&img, 1.0, 0.0);
|
|
assert_eq!(result.get_pixel(25, 25)[0], 128);
|
|
}
|
|
|
|
#[test]
|
|
fn test_contrast_increase() {
|
|
let mut img = GrayImage::new(10, 10);
|
|
img.put_pixel(0, 0, Luma([100]));
|
|
img.put_pixel(1, 0, Luma([200]));
|
|
let result = adjust_contrast(&img, 2.0);
|
|
// With factor > 1, contrast increases
|
|
let diff_orig = (200 - 100) as f64;
|
|
let diff_result = (result.get_pixel(1, 0)[0] as f64) - (result.get_pixel(0, 0)[0] as f64);
|
|
// The difference after contrast adjustment should be greater than original
|
|
assert!(
|
|
diff_result.abs() > diff_orig.abs() * 0.5,
|
|
"diff_orig={}, diff_result={}",
|
|
diff_orig,
|
|
diff_result
|
|
);
|
|
}
|
|
} |