|
| 1 | +//! Benchmark: ndarray SIMD OCR preprocessing vs tesseract. |
| 2 | +//! |
| 3 | +//! Loads raw grayscale page images, runs: |
| 4 | +//! 1. ndarray SIMD: otsu + binarize + density + skew |
| 5 | +//! 2. tesseract: full pipeline (preprocess + LSTM) |
| 6 | +//! |
| 7 | +//! Compares wall-clock time and output quality. |
| 8 | +
|
| 9 | +use ndarray::hpc::ocr_simd::*; |
| 10 | +use std::time::Instant; |
| 11 | + |
| 12 | +fn main() { |
| 13 | + eprintln!("═══════════════════════════════════════════════════════════"); |
| 14 | + eprintln!(" OCR Benchmark: ndarray SIMD vs tesseract"); |
| 15 | + eprintln!("═══════════════════════════════════════════════════════════\n"); |
| 16 | + |
| 17 | + let pages = vec![ |
| 18 | + "/tmp/ocr_bench/page-01.raw", |
| 19 | + "/tmp/ocr_bench/page-02.raw", |
| 20 | + "/tmp/ocr_bench/page-03.raw", |
| 21 | + ]; |
| 22 | + |
| 23 | + let png_pages = vec![ |
| 24 | + "/tmp/ocr_bench/page-01.png", |
| 25 | + "/tmp/ocr_bench/page-02.png", |
| 26 | + "/tmp/ocr_bench/page-03.png", |
| 27 | + ]; |
| 28 | + |
| 29 | + // ── ndarray SIMD preprocessing ──────────────────────────────────── |
| 30 | + eprintln!("=== ndarray SIMD preprocessing ===\n"); |
| 31 | + let mut simd_total = std::time::Duration::ZERO; |
| 32 | + |
| 33 | + for (i, path) in pages.iter().enumerate() { |
| 34 | + let data = match std::fs::read(path) { |
| 35 | + Ok(d) => d, |
| 36 | + Err(e) => { eprintln!(" skip {}: {}", path, e); continue; } |
| 37 | + }; |
| 38 | + if data.len() < 8 { continue; } |
| 39 | + |
| 40 | + let width = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize; |
| 41 | + let height = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize; |
| 42 | + let pixels = &data[8..]; |
| 43 | + |
| 44 | + eprintln!(" Page {}: {}×{} ({:.1}M pixels)", i + 1, width, height, |
| 45 | + (width * height) as f64 / 1_000_000.0); |
| 46 | + |
| 47 | + let img = GrayImage { data: pixels, width, height }; |
| 48 | + |
| 49 | + // Warm up |
| 50 | + let _ = otsu_threshold(&img); |
| 51 | + |
| 52 | + // Benchmark: full preprocessing pipeline |
| 53 | + let t0 = Instant::now(); |
| 54 | + let result = preprocess_page(&img); |
| 55 | + let elapsed = t0.elapsed(); |
| 56 | + simd_total += elapsed; |
| 57 | + |
| 58 | + let fg_count = foreground_count(&result.binary); |
| 59 | + eprintln!(" Otsu threshold: {}", result.threshold); |
| 60 | + eprintln!(" Foreground: {} pixels ({:.1}%)", fg_count, result.density * 100.0); |
| 61 | + eprintln!(" Skew angle: {:.2}°", result.skew_angle); |
| 62 | + eprintln!(" Is content: {}", result.is_content); |
| 63 | + eprintln!(" Time: {:.3}ms", elapsed.as_secs_f64() * 1000.0); |
| 64 | + |
| 65 | + // Also benchmark individual steps |
| 66 | + let t1 = Instant::now(); |
| 67 | + let threshold = otsu_threshold(&img); |
| 68 | + let otsu_time = t1.elapsed(); |
| 69 | + |
| 70 | + let t2 = Instant::now(); |
| 71 | + let binary = binarize(&img, threshold); |
| 72 | + let binarize_time = t2.elapsed(); |
| 73 | + |
| 74 | + let t3 = Instant::now(); |
| 75 | + let _ = foreground_density(&binary); |
| 76 | + let density_time = t3.elapsed(); |
| 77 | + |
| 78 | + let t4 = Instant::now(); |
| 79 | + let _ = estimate_skew(&binary); |
| 80 | + let skew_time = t4.elapsed(); |
| 81 | + |
| 82 | + eprintln!(" Breakdown:"); |
| 83 | + eprintln!(" Otsu: {:.3}ms", otsu_time.as_secs_f64() * 1000.0); |
| 84 | + eprintln!(" Binarize: {:.3}ms", binarize_time.as_secs_f64() * 1000.0); |
| 85 | + eprintln!(" Density: {:.3}ms", density_time.as_secs_f64() * 1000.0); |
| 86 | + eprintln!(" Skew: {:.3}ms", skew_time.as_secs_f64() * 1000.0); |
| 87 | + |
| 88 | + // Adaptive binarization benchmark |
| 89 | + let t5 = Instant::now(); |
| 90 | + let _ = adaptive_binarize(&img, 31, 10.0); |
| 91 | + let adaptive_time = t5.elapsed(); |
| 92 | + eprintln!(" Adaptive: {:.3}ms (window=31)", adaptive_time.as_secs_f64() * 1000.0); |
| 93 | + |
| 94 | + // Throughput |
| 95 | + let mpix = (width * height) as f64 / 1_000_000.0; |
| 96 | + let mpix_per_sec = mpix / elapsed.as_secs_f64(); |
| 97 | + eprintln!(" Throughput: {:.0} Mpix/s\n", mpix_per_sec); |
| 98 | + } |
| 99 | + |
| 100 | + // ── tesseract full pipeline ─────────────────────────────────────── |
| 101 | + eprintln!("=== tesseract (full pipeline: preprocess + LSTM) ===\n"); |
| 102 | + let mut tess_total = std::time::Duration::ZERO; |
| 103 | + |
| 104 | + for (i, path) in png_pages.iter().enumerate() { |
| 105 | + let t0 = Instant::now(); |
| 106 | + let output = std::process::Command::new("tesseract") |
| 107 | + .args([path.as_ref(), "stdout", "-l", "eng", "--psm", "1"]) |
| 108 | + .output(); |
| 109 | + let elapsed = t0.elapsed(); |
| 110 | + tess_total += elapsed; |
| 111 | + |
| 112 | + match output { |
| 113 | + Ok(o) if o.status.success() => { |
| 114 | + let text = String::from_utf8_lossy(&o.stdout); |
| 115 | + let words = text.split_whitespace().count(); |
| 116 | + eprintln!(" Page {}: {} words, {:.3}ms", |
| 117 | + i + 1, words, elapsed.as_secs_f64() * 1000.0); |
| 118 | + // Show first 100 chars |
| 119 | + let preview: String = text.chars().take(100).collect(); |
| 120 | + eprintln!(" Preview: {}", preview.replace('\n', " ")); |
| 121 | + } |
| 122 | + _ => { |
| 123 | + eprintln!(" Page {}: FAILED, {:.3}ms", i + 1, elapsed.as_secs_f64() * 1000.0); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + // ── Comparison ──────────────────────────────────────────────────── |
| 129 | + eprintln!("\n═══════════════════════════════════════════════════════════"); |
| 130 | + eprintln!(" COMPARISON (3 pages, 2481×3508 @ 300 DPI)"); |
| 131 | + eprintln!("═══════════════════════════════════════════════════════════"); |
| 132 | + eprintln!(" ndarray SIMD preprocess: {:.1}ms total", simd_total.as_secs_f64() * 1000.0); |
| 133 | + eprintln!(" tesseract full pipeline: {:.1}ms total", tess_total.as_secs_f64() * 1000.0); |
| 134 | + |
| 135 | + if tess_total.as_secs_f64() > 0.001 { |
| 136 | + let speedup = tess_total.as_secs_f64() / simd_total.as_secs_f64().max(0.001); |
| 137 | + eprintln!(" Speedup (preprocess): {:.0}x", speedup); |
| 138 | + } |
| 139 | + |
| 140 | + eprintln!("\n Note: SIMD does preprocessing only (binarize, skew, density)."); |
| 141 | + eprintln!(" tesseract does preprocessing + LSTM character recognition."); |
| 142 | + eprintln!(" Optimal: SIMD preprocess → pipe to tesseract LSTM only."); |
| 143 | + eprintln!("═══════════════════════════════════════════════════════════\n"); |
| 144 | +} |
0 commit comments