use super::engine::OcrImage;
pub fn otsu_threshold(image: &OcrImage) -> u8 {
let mut histogram = [0u32; 256];
for &pixel in &image.data {
histogram[pixel as usize] += 1;
}
let total = image.data.len() as f64;
if total == 0.0 {
return 128;
}
let mut sum_total: f64 = 0.0;
for (i, &count) in histogram.iter().enumerate() {
sum_total += i as f64 * count as f64;
}
let mut sum_bg: f64 = 0.0;
let mut weight_bg: f64 = 0.0;
let mut best_threshold = 0u8;
let mut max_variance: f64 = 0.0;
for t in 0..255u8 {
let count = histogram[t as usize];
weight_bg += count as f64;
if weight_bg == 0.0 {
continue;
}
let weight_fg = total - weight_bg;
if weight_fg == 0.0 {
break;
}
sum_bg += t as f64 * count as f64;
let mean_bg = sum_bg / weight_bg;
let mean_fg = (sum_total - sum_bg) / weight_fg;
let between_variance = weight_bg * weight_fg * (mean_bg - mean_fg).powi(2);
if between_variance >= max_variance {
max_variance = between_variance;
best_threshold = t;
}
}
best_threshold
}
pub fn binarize(image: &OcrImage) -> OcrImage {
let threshold = otsu_threshold(image);
let data = image
.data
.iter()
.map(|&p| if p > threshold { 255 } else { 0 })
.collect();
OcrImage {
data,
width: image.width,
height: image.height,
}
}
pub fn enhance_contrast(image: &OcrImage) -> OcrImage {
if image.data.is_empty() {
return image.clone();
}
let min_val = *image.data.iter().min().unwrap_or(&0);
let max_val = *image.data.iter().max().unwrap_or(&255);
let range = max_val as f64 - min_val as f64;
if range < 1.0 {
return image.clone();
}
let data = image
.data
.iter()
.map(|&p| ((p as f64 - min_val as f64) / range * 255.0).round() as u8)
.collect();
OcrImage {
data,
width: image.width,
height: image.height,
}
}
pub fn rotate_90_cw(image: &OcrImage) -> OcrImage {
let w = image.width as usize;
let h = image.height as usize;
let mut data = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let new_x = h - 1 - y;
let new_y = x;
data[new_y * h + new_x] = image.data[y * w + x];
}
}
OcrImage {
data,
width: image.height,
height: image.width,
}
}
pub fn rotate_180(image: &OcrImage) -> OcrImage {
let data: Vec<u8> = image.data.iter().rev().copied().collect();
OcrImage {
data,
width: image.width,
height: image.height,
}
}
pub fn rotate_270_cw(image: &OcrImage) -> OcrImage {
let w = image.width as usize;
let h = image.height as usize;
let mut data = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let new_x = y;
let new_y = w - 1 - x;
data[new_y * h + new_x] = image.data[y * w + x];
}
}
OcrImage {
data,
width: image.height,
height: image.width,
}
}
pub fn preprocess_for_ocr(image: &OcrImage) -> OcrImage {
let enhanced = enhance_contrast(image);
binarize(&enhanced)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_image(data: &[u8], width: u32, height: u32) -> OcrImage {
OcrImage {
data: data.to_vec(),
width,
height,
}
}
#[test]
fn otsu_bimodal_image() {
let mut data = vec![0u8; 50];
data.extend_from_slice(&vec![255u8; 50]);
let img = make_image(&data, 10, 10);
let t = otsu_threshold(&img);
assert!(t > 0 && t < 255, "Otsu should find split, got {t}");
}
#[test]
fn otsu_all_same_value() {
let img = make_image(&[128; 100], 10, 10);
let t = otsu_threshold(&img);
assert!(t <= 255);
}
#[test]
fn otsu_empty_image() {
let img = make_image(&[], 0, 0);
let t = otsu_threshold(&img);
assert_eq!(t, 128, "Empty image should return default threshold");
}
#[test]
fn otsu_text_like_distribution() {
let mut data = vec![240u8; 80]; data.extend_from_slice(&[20u8; 20]); let img = make_image(&data, 10, 10);
let t = otsu_threshold(&img);
assert!(
t > 20 && t < 240,
"Threshold should split text from background, got {t}"
);
}
#[test]
fn binarize_produces_only_black_and_white() {
let data: Vec<u8> = (0..100).collect();
let img = make_image(&data, 10, 10);
let result = binarize(&img);
for &pixel in &result.data {
assert!(
pixel == 0 || pixel == 255,
"Binarize should produce only 0 or 255, got {pixel}"
);
}
}
#[test]
fn binarize_preserves_dimensions() {
let img = make_image(&[128; 200], 20, 10);
let result = binarize(&img);
assert_eq!(result.width, 20);
assert_eq!(result.height, 10);
assert_eq!(result.data.len(), 200);
}
#[test]
fn enhance_contrast_stretches_range() {
let data: Vec<u8> = (100..150).collect();
let img = make_image(&data, 5, 10);
let result = enhance_contrast(&img);
assert_eq!(
*result.data.iter().min().unwrap(),
0,
"Min should stretch to 0"
);
assert_eq!(
*result.data.iter().max().unwrap(),
255,
"Max should stretch to 255"
);
}
#[test]
fn enhance_contrast_full_range_is_noop() {
let mut data = vec![0u8];
data.push(255);
data.extend_from_slice(&[128; 8]);
let img = make_image(&data, 10, 1);
let result = enhance_contrast(&img);
assert_eq!(result.data[0], 0);
assert_eq!(result.data[1], 255);
}
#[test]
fn enhance_contrast_uniform_returns_clone() {
let img = make_image(&[128; 100], 10, 10);
let result = enhance_contrast(&img);
assert_eq!(result.data, img.data);
}
#[test]
fn enhance_contrast_empty_returns_clone() {
let img = make_image(&[], 0, 0);
let result = enhance_contrast(&img);
assert!(result.data.is_empty());
}
#[test]
fn rotate_90_cw_swaps_dimensions() {
let img = make_image(&[1, 2, 3, 4, 5, 6], 3, 2);
let rotated = rotate_90_cw(&img);
assert_eq!(rotated.width, 2);
assert_eq!(rotated.height, 3);
assert_eq!(rotated.data.len(), 6);
}
#[test]
fn rotate_90_cw_pixel_mapping() {
let img = make_image(&[1, 2, 3, 4], 2, 2);
let rotated = rotate_90_cw(&img);
assert_eq!(rotated.data, vec![3, 1, 4, 2]);
}
#[test]
fn rotate_180_reverses_data() {
let img = make_image(&[1, 2, 3, 4], 2, 2);
let rotated = rotate_180(&img);
assert_eq!(rotated.data, vec![4, 3, 2, 1]);
assert_eq!(rotated.width, 2);
assert_eq!(rotated.height, 2);
}
#[test]
fn rotate_270_cw_is_inverse_of_90() {
let img = make_image(&[1, 2, 3, 4, 5, 6], 3, 2);
let r90 = rotate_90_cw(&img);
let r90_then_270 = rotate_270_cw(&r90);
assert_eq!(r90_then_270.width, img.width);
assert_eq!(r90_then_270.height, img.height);
assert_eq!(r90_then_270.data, img.data);
}
#[test]
fn preprocess_pipeline_produces_binary_output() {
let data: Vec<u8> = (0..100).collect();
let img = make_image(&data, 10, 10);
let result = preprocess_for_ocr(&img);
for &pixel in &result.data {
assert!(pixel == 0 || pixel == 255);
}
}
#[test]
fn preprocess_pipeline_preserves_dimensions() {
let img = make_image(&[128; 300], 30, 10);
let result = preprocess_for_ocr(&img);
assert_eq!(result.width, 30);
assert_eq!(result.height, 10);
assert_eq!(result.data.len(), 300);
}
}