use std::collections::BTreeMap;
use std::path::PathBuf;
use anyhow::{Context, Result};
use monocr_onnx::MonOcr;
fn char_cer(pred: &str, reference: &str) -> f64 {
let a: Vec<char> = pred.chars().collect();
let b: Vec<char> = reference.chars().collect();
if b.is_empty() {
return if a.is_empty() { 0.0 } else { 1.0 };
}
let mut prev: Vec<usize> = (0..=a.len()).collect();
let mut cur = vec![0usize; a.len() + 1];
for (j, bc) in b.iter().enumerate() {
cur[0] = j + 1;
for (i, ac) in a.iter().enumerate() {
let cost = usize::from(ac != bc);
cur[i + 1] = (cur[i] + 1).min(prev[i + 1] + 1).min(prev[i] + cost);
}
std::mem::swap(&mut prev, &mut cur);
}
prev[a.len()] as f64 / b.len() as f64
}
struct Row {
tiles: usize,
cer_tiled: f64,
cer_squeezed: f64,
}
#[tokio::main]
async fn main() -> Result<()> {
let dir: PathBuf = std::env::args()
.nth(1)
.context("usage: tiling_ab <dir with line_*.png and labels.txt>")?
.into();
let labels_path = dir.join("labels.txt");
let labels_raw = std::fs::read_to_string(&labels_path)
.with_context(|| format!("cannot read {}", labels_path.display()))?;
let mut labels: Vec<(PathBuf, String)> = Vec::new();
for line in labels_raw.lines() {
let Some((name, text)) = line.split_once('\t') else {
continue;
};
labels.push((dir.join(name), text.to_string()));
}
if labels.is_empty() {
anyhow::bail!(
"{} contained no tab-separated entries",
labels_path.display()
);
}
eprintln!("{} labelled lines", labels.len());
assert_eq!(
char_cer("", "abc"),
1.0,
"empty prediction must score exactly 1.0"
);
assert_eq!(
char_cer("abc", "abc"),
0.0,
"identity must score exactly 0.0"
);
eprintln!("null baseline ok");
let mut tiled = MonOcr::builder().tile_wide_lines(true).build().await?;
let mut squeezed = MonOcr::builder().tile_wide_lines(false).build().await?;
let mut rows: Vec<Row> = Vec::new();
for (i, (path, truth)) in labels.iter().enumerate() {
let t = tiled.predict_single_line(path).await?;
let s = squeezed.predict_single_line(path).await?;
let img = image::open(path)?.to_luma8();
let (w, h) = img.dimensions();
let scaled = (w as f64 * (160.0 / h as f64)) as u32;
let tiles = ((scaled as f64) / 1024.0).ceil().max(1.0) as usize;
rows.push(Row {
tiles,
cer_tiled: char_cer(&t.text, truth),
cer_squeezed: char_cer(&s.text, truth),
});
if (i + 1) % 25 == 0 {
eprintln!(" {}/{}", i + 1, labels.len());
}
}
let mean =
|f: fn(&Row) -> f64, rs: &[Row]| -> f64 { rs.iter().map(f).sum::<f64>() / rs.len() as f64 };
let m_sq = mean(|r| r.cer_squeezed, &rows);
let m_ti = mean(|r| r.cer_tiled, &rows);
println!("\nn {}", rows.len());
println!("squeezed CER {m_sq:.4}");
println!("tiled CER {m_ti:.4}");
println!("ratio sq/tiled {:.2}x", m_sq / m_ti);
println!(
"tiled better on {}/{}",
rows.iter().filter(|r| r.cer_tiled < r.cer_squeezed).count(),
rows.len()
);
let mut bands: BTreeMap<usize, Vec<&Row>> = BTreeMap::new();
for r in &rows {
bands.entry(r.tiles).or_default().push(r);
}
println!("\n tiles n squeezed tiled ratio");
for (band, sub) in &bands {
if sub.len() < 3 {
continue;
}
let s_m = sub.iter().map(|r| r.cer_squeezed).sum::<f64>() / sub.len() as f64;
let t_m = sub.iter().map(|r| r.cer_tiled).sum::<f64>() / sub.len() as f64;
println!(
" {band:>5} {:>4} {s_m:>8.4} {t_m:>8.4} {:>6.1}x",
sub.len(),
s_m / t_m
);
}
Ok(())
}