use crate::core::image::OcrImage;
use crate::utils::{OcrError, Result, SimdImageOps};
use image::{imageops, DynamicImage, GrayImage, Luma};
pub struct ImageEnhancer;
impl ImageEnhancer {
pub fn enhance_contrast(img: &OcrImage, factor: f32) -> Result<OcrImage> {
let gray = img.data.to_luma8();
let pixels: Vec<u8> = gray.pixels().map(|p| p[0]).collect();
let adjusted = SimdImageOps::contrast_adjust(&pixels, factor);
let mut result = GrayImage::new(gray.width(), gray.height());
for (i, pixel) in adjusted.iter().enumerate() {
let x = (i as u32) % gray.width();
let y = (i as u32) / gray.width();
result.put_pixel(x, y, Luma([*pixel]));
}
Ok(OcrImage::new(DynamicImage::ImageLuma8(result), img.dpi))
}
pub fn reduce_noise(img: &OcrImage) -> Result<OcrImage> {
let blurred = imageops::blur(&img.data, 0.5);
Ok(OcrImage::new(DynamicImage::ImageRgba8(blurred), img.dpi))
}
pub fn sharpen(img: &OcrImage) -> Result<OcrImage> {
let mut sharpened = img.data.clone();
imageops::unsharpen(&mut sharpened, 1.0, 1);
Ok(OcrImage::new(sharpened, img.dpi))
}
pub fn deskew(img: &OcrImage) -> Result<OcrImage> {
let gray = img.to_grayscale();
let binary = gray.threshold(200);
let mut best_angle = 0.0;
let mut max_variance = 0.0;
let range = 20;
for i in -range..=range {
let angle_deg = i as f32 * 0.1;
let angle_rad = angle_deg.to_radians();
let rotated = binary.rotate(angle_rad)?;
let variance = Self::calculate_projection_variance(&rotated);
if variance > max_variance {
max_variance = variance;
best_angle = angle_rad;
}
}
if best_angle.abs() > 0.001 {
img.rotate(best_angle).map_err(OcrError::from)
} else {
Ok(img.clone())
}
}
pub fn estimate_dpi(img: &OcrImage) -> u32 {
let gray = img.data.to_luma8();
let (width, height) = gray.dimensions();
if width < 50 || height < 50 {
return img.dpi.max(72);
}
let mid_y = height / 2;
let mut run_lengths = Vec::new();
let mut current_run = 0u32;
let mut in_dark = gray.get_pixel(0, mid_y)[0] < 128;
for x in 0..width {
let dark = gray.get_pixel(x, mid_y)[0] < 128;
if dark == in_dark {
current_run += 1;
} else {
if in_dark && current_run > 0 && current_run < width / 4 {
run_lengths.push(current_run);
}
current_run = 1;
in_dark = dark;
}
}
if run_lengths.len() < 3 {
return img.dpi.max(72);
}
run_lengths.sort_unstable();
let median_stroke = run_lengths[run_lengths.len() / 2];
let estimated = (median_stroke as f32 * 72.0) as u32;
estimated.clamp(72, 1200).max(img.dpi)
}
pub fn detect_orientation(img: &OcrImage) -> u32 {
let gray = img.data.to_luma8();
let (width, height) = gray.dimensions();
if width < 50 || height < 50 {
return 0;
}
let binary = Self::simple_threshold(&gray);
let score_0 = Self::orientation_score(&binary, width, height);
let score_90 = Self::orientation_score(&Self::rotate_90_cw(&binary), height, width);
let score_180 = Self::orientation_score(&Self::rotate_180(&binary), width, height);
let score_270 = Self::orientation_score(&Self::rotate_90_ccw(&binary), height, width);
let scores = [
(0u32, score_0),
(90, score_90),
(180, score_180),
(270, score_270),
];
let best = scores
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap();
best.0
}
pub fn correct_orientation(img: &OcrImage) -> Result<OcrImage> {
let orientation = Self::detect_orientation(img);
match orientation {
0 => Ok(img.clone()),
90 => img
.rotate(std::f32::consts::FRAC_PI_2)
.map_err(OcrError::from),
180 => img.rotate(std::f32::consts::PI).map_err(OcrError::from),
270 => img
.rotate(-std::f32::consts::FRAC_PI_2)
.map_err(OcrError::from),
_ => Ok(img.clone()),
}
}
pub fn remove_borders(img: &OcrImage) -> Result<OcrImage> {
let gray = img.data.to_luma8();
let (width, height) = gray.dimensions();
if width < 20 || height < 20 {
return Ok(img.clone());
}
let threshold = 200u8;
let margin = 5u32;
let mut left = margin;
let mut right = width - margin - 1;
let mut top = margin;
let mut bottom = height - margin - 1;
for x in margin..(width - margin) {
let mut has_content = false;
for y in margin..(height - margin) {
if gray.get_pixel(x, y)[0] < threshold {
has_content = true;
break;
}
}
if has_content {
left = x.saturating_sub(margin);
break;
}
}
for x in (margin..(width - margin)).rev() {
let mut has_content = false;
for y in margin..(height - margin) {
if gray.get_pixel(x, y)[0] < threshold {
has_content = true;
break;
}
}
if has_content {
right = (x + margin).min(width);
break;
}
}
for y in margin..(height - margin) {
let mut has_content = false;
for x in margin..(width - margin) {
if gray.get_pixel(x, y)[0] < threshold {
has_content = true;
break;
}
}
if has_content {
top = y.saturating_sub(margin);
break;
}
}
for y in (margin..(height - margin)).rev() {
let mut has_content = false;
for x in margin..(width - margin) {
if gray.get_pixel(x, y)[0] < threshold {
has_content = true;
break;
}
}
if has_content {
bottom = (y + margin).min(height);
break;
}
}
let crop_width = right.saturating_sub(left);
let crop_height = bottom.saturating_sub(top);
if crop_width < 20 || crop_height < 20 || crop_width >= width || crop_height >= height {
return Ok(img.clone());
}
img.crop(left, top, crop_width, crop_height)
.map_err(OcrError::from)
}
pub fn remove_speckle(img: &OcrImage, max_speckle_area: u32) -> Result<OcrImage> {
let gray = img.data.to_luma8();
let (width, height) = gray.dimensions();
if width == 0 || height == 0 {
return Ok(img.clone());
}
let total_pixels = (width * height) as usize;
let mut labels = vec![0u32; total_pixels];
let mut next_label = 1u32;
let mut areas = vec![0u32; total_pixels + 1];
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
if gray.get_pixel(x, y)[0] >= 128 {
continue;
}
let up = if y > 0 {
labels[((y - 1) * width + x) as usize]
} else {
0
};
let left_label = if x > 0 {
labels[(y * width + x - 1) as usize]
} else {
0
};
if up == 0 && left_label == 0 {
labels[idx] = next_label;
next_label += 1;
} else if up > 0 && left_label == 0 {
labels[idx] = up;
} else if left_label > 0 && up == 0 {
labels[idx] = left_label;
} else {
labels[idx] = up.min(left_label);
}
}
}
for &label in &labels {
if label > 0 && (label as usize) < areas.len() {
areas[label as usize] += 1;
}
}
let mut result = gray.clone();
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize;
let label = labels[idx];
if label > 0
&& (label as usize) < areas.len()
&& areas[label as usize] < max_speckle_area
{
result.put_pixel(x, y, Luma([255u8]));
}
}
}
Ok(OcrImage::new(DynamicImage::ImageLuma8(result), img.dpi))
}
fn simple_threshold(gray: &GrayImage) -> GrayImage {
let (width, height) = gray.dimensions();
let mut histogram = [0u32; 256];
for pixel in gray.pixels() {
histogram[pixel[0] as usize] += 1;
}
let total = (width * height) as u32;
let mut sum = 0u32;
for i in 0..256 {
sum += histogram[i] * (i as u32);
}
let mut sum_b = 0u32;
let mut w_b = 0u32;
let mut max_variance = 0f64;
let mut threshold = 128u8;
for i in 0..256 {
w_b += histogram[i];
if w_b == 0 {
continue;
}
let w_f = total - w_b;
if w_f == 0 {
break;
}
sum_b += (i as u32) * histogram[i];
let m_b = sum_b as f64 / w_b as f64;
let m_f = (sum - sum_b) as f64 / w_f as f64;
let v = (w_b as f64) * (w_f as f64) * (m_b - m_f) * (m_b - m_f);
if v > max_variance {
max_variance = v;
threshold = i as u8;
}
}
let mut result = GrayImage::new(width, height);
for (x, y, pixel) in gray.enumerate_pixels() {
let value = if pixel[0] > threshold { 255u8 } else { 0u8 };
result.put_pixel(x, y, Luma([value]));
}
result
}
fn orientation_score(binary: &GrayImage, width: u32, height: u32) -> f64 {
let mut row_sums = Vec::with_capacity(height as usize);
for y in 0..height {
let mut sum = 0u64;
for x in 0..width {
if binary.get_pixel(x, y)[0] < 128 {
sum += 1;
}
}
row_sums.push(sum as f64);
}
if row_sums.is_empty() {
return 0.0;
}
let mean = row_sums.iter().sum::<f64>() / row_sums.len() as f64;
let variance = row_sums
.iter()
.map(|s| (s - mean) * (s - mean))
.sum::<f64>()
/ row_sums.len() as f64;
variance
}
fn rotate_90_cw(img: &GrayImage) -> GrayImage {
let (width, height) = img.dimensions();
let mut result = GrayImage::new(height, width);
for y in 0..height {
for x in 0..width {
let src_pixel = img.get_pixel(x, y);
result.put_pixel(height - 1 - y, x, *src_pixel);
}
}
result
}
fn rotate_90_ccw(img: &GrayImage) -> GrayImage {
let (width, height) = img.dimensions();
let mut result = GrayImage::new(height, width);
for y in 0..height {
for x in 0..width {
let src_pixel = img.get_pixel(x, y);
result.put_pixel(y, width - 1 - x, *src_pixel);
}
}
result
}
fn rotate_180(img: &GrayImage) -> GrayImage {
let (width, height) = img.dimensions();
let mut result = GrayImage::new(width, height);
for y in 0..height {
for x in 0..width {
let src_pixel = img.get_pixel(x, y);
result.put_pixel(width - 1 - x, height - 1 - y, *src_pixel);
}
}
result
}
fn calculate_projection_variance(img: &OcrImage) -> f64 {
let (width, height) = img.dimensions();
let mut row_sums = Vec::with_capacity(height as usize);
if let Some(buf) = img.data.as_luma8() {
for y in 0..height {
let mut sum = 0u64;
for x in 0..width {
sum += buf.get_pixel(x, y)[0] as u64;
}
row_sums.push(sum as f64);
}
} else {
return 0.0;
}
if row_sums.is_empty() {
return 0.0;
}
let mean = row_sums.iter().sum::<f64>() / row_sums.len() as f64;
let variance = row_sums
.iter()
.map(|s| {
let diff = s - mean;
diff * diff
})
.sum::<f64>()
/ row_sums.len() as f64;
variance
}
}