pub struct MonOcr { /* private fields */ }Expand description
Main OCR engine for text recognition
This struct encapsulates the OCR pipeline including:
- ONNX runtime session for model inference
- Character set for decoding predictions
- Line segmenter for page layout analysis
- Image preprocessing utilities
§Usage
Typically, you would create a MonOcr instance using the builder:
use monocr_onnx::MonOcr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ocr = MonOcr::builder().build().await?;
let text = ocr.read_image("document.png").await?;
println!("Recognized: {}", text);
Ok(())
}The instance must be mutable because internal state is modified during inference (e.g., the ONNX session).
Implementations§
Source§impl MonOcr
impl MonOcr
Sourcepub fn builder() -> MonOcrBuilder
pub fn builder() -> MonOcrBuilder
Create a builder for configuring MonOcr
This is the entry point for creating a customized OCR instance.
Use the builder methods to configure options, then call build().
§Returns
A new MonOcrBuilder instance
§Example
use monocr_onnx::MonOcr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ocr = MonOcr::builder()
.min_line_height(15)
.build()
.await?;
Ok(())
}Examples found in repository?
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 // Null baseline, asserted before any real number is produced. If the metric
85 // arithmetic is wrong every rate below is wrong in the same direction, and
86 // nothing else in this example would reveal it.
87 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 // Two sessions rather than rebuilding one per arm: the flag is fixed at build
100 // time, and reloading the graph 240 times would dominate the runtime.
101 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 // Recovered from the geometry: a tiled read reports the union of its
110 // tiles, so width over the window width is the tile count.
111 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 // The band table is the finding; the aggregate depends entirely on the width
143 // mix of whatever sample was handed in.
144 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}Sourcepub async fn read_image(
&mut self,
image_path: impl AsRef<Path>,
) -> Result<String>
pub async fn read_image( &mut self, image_path: impl AsRef<Path>, ) -> Result<String>
Read text from a single image
This method performs OCR on a single image file. The image is automatically segmented into lines, and each line is recognized using the ONNX model.
§Arguments
image_path- Path to the image file (PNG, JPG, BMP, etc.)
§Returns
Ok(String)- Recognized text with lines separated by newlinesErr(anyhow::Error)- If the image cannot be read or OCR fails
§Example
use monocr_onnx::MonOcr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ocr = MonOcr::builder().build().await?;
let text = ocr.read_image("document.png").await?;
println!("Recognized text:\n{}", text);
Ok(())
}Sourcepub async fn read_images(
&mut self,
image_paths: &[impl AsRef<Path>],
) -> Result<Vec<String>>
pub async fn read_images( &mut self, image_paths: &[impl AsRef<Path>], ) -> Result<Vec<String>>
Read text from multiple images
This method processes multiple images in sequence, returning a vector of recognized texts. Each image is segmented into lines and processed individually.
§Arguments
image_paths- A slice of paths to image files
§Returns
Ok(Vec<String>)- Vector of recognized texts, one per imageErr(anyhow::Error)- If any image cannot be processed
§Example
use monocr_onnx::MonOcr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ocr = MonOcr::builder().build().await?;
let paths = vec!["page1.png", "page2.png", "page3.png"];
let results = ocr.read_images(&paths).await?;
for (i, text) in results.iter().enumerate() {
println!("Page {}: {}", i + 1, text);
}
Ok(())
}Sourcepub async fn read_pdf(
&mut self,
pdf_path: impl AsRef<Path>,
) -> Result<Vec<String>>
pub async fn read_pdf( &mut self, pdf_path: impl AsRef<Path>, ) -> Result<Vec<String>>
Read text from a PDF file
This method converts a PDF document to images using pdftoppm and performs OCR on each page. Each page is processed as a separate image.
§Arguments
pdf_path- Path to the PDF file
§Returns
Ok(Vec<String>)- Vector of recognized texts, one per pageErr(anyhow::Error)- If PDF conversion fails or OCR fails
§Requirements
Requires pdftoppm from poppler-utils to be installed:
- Ubuntu/Debian:
sudo apt-get install poppler-utils - macOS:
brew install poppler
Sourcepub async fn predict_pdf(
&mut self,
pdf_path: impl AsRef<Path>,
) -> Result<Vec<Vec<LineResult>>>
pub async fn predict_pdf( &mut self, pdf_path: impl AsRef<Path>, ) -> Result<Vec<Vec<LineResult>>>
Predict text and geometry from a PDF file, page by page
Same conversion as read_pdf, but keeps the per-line
bounding boxes. Coordinates are in pixels of the 300 DPI render of the
page, not PDF points.
§Returns
Ok(Vec<Vec<LineResult>>)- One vector of line results per pageErr(anyhow::Error)- If PDF conversion fails or OCR fails
Sourcepub async fn read_image_with_accuracy(
&mut self,
image_path: impl AsRef<Path>,
ground_truth: &str,
) -> Result<OcrResult>
pub async fn read_image_with_accuracy( &mut self, image_path: impl AsRef<Path>, ground_truth: &str, ) -> Result<OcrResult>
Read image with accuracy measurement
This method performs OCR on an image and calculates accuracy by comparing the recognized text against ground truth using Levenshtein distance.
§Arguments
image_path- Path to the image fileground_truth- The expected/ground truth text to compare against
§Returns
Ok(OcrResult)- Contains recognized text and accuracy percentageErr(anyhow::Error)- If OCR fails
§Accuracy Calculation
Accuracy = (1 - CER) * 100, where CER is Character Error Rate calculated as Levenshtein distance / max(len(predicted), len(ground_truth))
Sourcepub async fn predict_page(
&mut self,
image_path: impl AsRef<Path>,
) -> Result<Vec<LineResult>>
pub async fn predict_page( &mut self, image_path: impl AsRef<Path>, ) -> Result<Vec<LineResult>>
Predict text from a full page image
This method segments the image into lines and recognizes each line using the ONNX model. Returns results with text and bounding boxes.
§Arguments
image_path- Path to the full page image
§Returns
Ok(Vec<LineResult>)- Vector of line results with text and bounding boxesErr(anyhow::Error)- If segmentation or OCR fails
§Process
- Segment the page into individual text lines using horizontal projection
- For each line:
- Tile it at whitespace columns if it is too wide for the model window
- Preprocess each tile for the model
- Run inference with the ONNX model
- Decode CTC output to text
- Return one result per line, with the text of its tiles concatenated
§Wide lines
A line wider than the model window is tiled by
crate::segmenter::tile_line, not squeezed.
Measured on this binding, 2026-08-22, over 201 rendered Mon lines by
examples/tiling_ab.rs. The answer depends on how wide the line is:
tiles squeezed tiled winner
2 0.0444 0.0635 squeezing, 0.7x
3 0.0317 0.0294 parity, 1.1x
4 0.1509 0.0364 tiling, 4.1x
6 0.8382 0.0229 tiling, 36.5x
8 0.9090 0.0387 tiling, 23.5xSo tiling is not a uniform win: it is a safety net. Up to 3 tiles the two are level, and from 4 up squeezing degrades without bound while tiling stays flat. Tiling is the default because that asymmetry is the whole argument — the downside is a fraction of a point on already-low rates, and the upside is not losing the line.
Char-level CER here; mon_OCR/eval/tiling-ab-2026-08-22.md scores the same
images by grapheme cluster and finds the same crossover. That report also
records that these numbers do not reproduce the older
squeezed-0.1434-against-tiled-0.0795 figures quoted elsewhere, whose
harness was never committed.
The measurement is one held-out font at one size, on rendered lines rather than photographed pages. If the pinned model moves, re-run the example rather than assuming any of this still holds.
The tiles of one line are joined with no separator, and their union is reported as that line’s bbox. Joining them with a newline is what produced “Mon E-boo” and “k library” as two readings of a single line.
Sourcepub async fn predict_single_line(
&mut self,
image_path: impl AsRef<Path>,
) -> Result<LineResult>
pub async fn predict_single_line( &mut self, image_path: impl AsRef<Path>, ) -> Result<LineResult>
Recognise an image that is already a single cropped line
Skips segmentation entirely. Use when the caller knows the input is one line — segmenting a line fragments it, because the projection profile has no gap to find and any faint row inside the glyphs becomes one. The crop is still tiled if it is wider than the model window, so a long line is not squeezed.
Deciding when an input is a single line belongs to the caller; the library does not guess.
§Returns
Ok(LineResult)- The text, with a bbox covering the whole source imageErr(anyhow::Error)- If the image cannot be read or inference fails
Examples found in repository?
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 // Null baseline, asserted before any real number is produced. If the metric
85 // arithmetic is wrong every rate below is wrong in the same direction, and
86 // nothing else in this example would reveal it.
87 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 // Two sessions rather than rebuilding one per arm: the flag is fixed at build
100 // time, and reloading the graph 240 times would dominate the runtime.
101 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 // Recovered from the geometry: a tiled read reports the union of its
110 // tiles, so width over the window width is the tile count.
111 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 // The band table is the finding; the aggregate depends entirely on the width
143 // mix of whatever sample was handed in.
144 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}Auto Trait Implementations§
impl !RefUnwindSafe for MonOcr
impl !UnwindSafe for MonOcr
impl Freeze for MonOcr
impl Send for MonOcr
impl Sync for MonOcr
impl Unpin for MonOcr
impl UnsafeUnpin for MonOcr
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more