1use std::collections::BTreeMap;
20use std::path::PathBuf;
21
22use anyhow::{Context, Result};
23use monocr_onnx::MonOcr;
24
25fn char_cer(pred: &str, reference: &str) -> f64 {
33 let a: Vec<char> = pred.chars().collect();
34 let b: Vec<char> = reference.chars().collect();
35 if b.is_empty() {
36 return if a.is_empty() { 0.0 } else { 1.0 };
37 }
38
39 let mut prev: Vec<usize> = (0..=a.len()).collect();
40 let mut cur = vec![0usize; a.len() + 1];
41 for (j, bc) in b.iter().enumerate() {
42 cur[0] = j + 1;
43 for (i, ac) in a.iter().enumerate() {
44 let cost = usize::from(ac != bc);
45 cur[i + 1] = (cur[i] + 1).min(prev[i + 1] + 1).min(prev[i] + cost);
46 }
47 std::mem::swap(&mut prev, &mut cur);
48 }
49 prev[a.len()] as f64 / b.len() as f64
50}
51
52struct Row {
53 tiles: usize,
54 cer_tiled: f64,
55 cer_squeezed: f64,
56}
57
58#[tokio::main]
59async fn main() -> Result<()> {
60 let dir: PathBuf = std::env::args()
61 .nth(1)
62 .context("usage: tiling_ab <dir with line_*.png and labels.txt>")?
63 .into();
64
65 let labels_path = dir.join("labels.txt");
66 let labels_raw = std::fs::read_to_string(&labels_path)
67 .with_context(|| format!("cannot read {}", labels_path.display()))?;
68
69 let mut labels: Vec<(PathBuf, String)> = Vec::new();
70 for line in labels_raw.lines() {
71 let Some((name, text)) = line.split_once('\t') else {
72 continue;
73 };
74 labels.push((dir.join(name), text.to_string()));
75 }
76 if labels.is_empty() {
77 anyhow::bail!(
78 "{} contained no tab-separated entries",
79 labels_path.display()
80 );
81 }
82 eprintln!("{} labelled lines", labels.len());
83
84 assert_eq!(
88 char_cer("", "abc"),
89 1.0,
90 "empty prediction must score exactly 1.0"
91 );
92 assert_eq!(
93 char_cer("abc", "abc"),
94 0.0,
95 "identity must score exactly 0.0"
96 );
97 eprintln!("null baseline ok");
98
99 let mut tiled = MonOcr::builder().tile_wide_lines(true).build().await?;
102 let mut squeezed = MonOcr::builder().tile_wide_lines(false).build().await?;
103
104 let mut rows: Vec<Row> = Vec::new();
105 for (i, (path, truth)) in labels.iter().enumerate() {
106 let t = tiled.predict_single_line(path).await?;
107 let s = squeezed.predict_single_line(path).await?;
108
109 let img = image::open(path)?.to_luma8();
112 let (w, h) = img.dimensions();
113 let scaled = (w as f64 * (160.0 / h as f64)) as u32;
114 let tiles = ((scaled as f64) / 1024.0).ceil().max(1.0) as usize;
115
116 rows.push(Row {
117 tiles,
118 cer_tiled: char_cer(&t.text, truth),
119 cer_squeezed: char_cer(&s.text, truth),
120 });
121
122 if (i + 1) % 25 == 0 {
123 eprintln!(" {}/{}", i + 1, labels.len());
124 }
125 }
126
127 let mean =
128 |f: fn(&Row) -> f64, rs: &[Row]| -> f64 { rs.iter().map(f).sum::<f64>() / rs.len() as f64 };
129 let m_sq = mean(|r| r.cer_squeezed, &rows);
130 let m_ti = mean(|r| r.cer_tiled, &rows);
131
132 println!("\nn {}", rows.len());
133 println!("squeezed CER {m_sq:.4}");
134 println!("tiled CER {m_ti:.4}");
135 println!("ratio sq/tiled {:.2}x", m_sq / m_ti);
136 println!(
137 "tiled better on {}/{}",
138 rows.iter().filter(|r| r.cer_tiled < r.cer_squeezed).count(),
139 rows.len()
140 );
141
142 let mut bands: BTreeMap<usize, Vec<&Row>> = BTreeMap::new();
145 for r in &rows {
146 bands.entry(r.tiles).or_default().push(r);
147 }
148 println!("\n tiles n squeezed tiled ratio");
149 for (band, sub) in &bands {
150 if sub.len() < 3 {
151 continue;
152 }
153 let s_m = sub.iter().map(|r| r.cer_squeezed).sum::<f64>() / sub.len() as f64;
154 let t_m = sub.iter().map(|r| r.cer_tiled).sum::<f64>() / sub.len() as f64;
155 println!(
156 " {band:>5} {:>4} {s_m:>8.4} {t_m:>8.4} {:>6.1}x",
157 sub.len(),
158 s_m / t_m
159 );
160 }
161 Ok(())
162}