pub struct MonOcrBuilder { /* private fields */ }Expand description
Builder for configuring and creating MonOcr instances
The builder pattern allows flexible configuration of OCR settings before creating an instance. All settings have sensible defaults.
§Configuration Options
model_path: Custom path to the ONNX model file (default: download from HuggingFace)charset: Custom character set for OCR (default: built-in Mon charset)min_line_height: Minimum height for line segmentation (default: 10 pixels)smooth_window: Window size for smoothing projection profile (default: 3)density_threshold_ratio: Gap threshold as a fraction of mean row density (default: 0.05)
§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)
.smooth_window(5)
.build()
.await?;
let text = ocr.read_image("document.png").await?;
println!("{text}");
Ok(())
}Implementations§
Source§impl MonOcrBuilder
impl MonOcrBuilder
Sourcepub fn model_path(self, path: impl AsRef<Path>) -> Self
pub fn model_path(self, path: impl AsRef<Path>) -> Self
Set the path to the ONNX model file
By default, the model is downloaded from HuggingFace if not found in cache. Use this method to specify a custom model file location.
§Arguments
path- Path to the ONNX model file
§Returns
The builder with the model path set
§Example
use monocr_onnx::MonOcr;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ocr = MonOcr::builder()
.model_path("./models/monocr.onnx")
.build()
.await?;
Ok(())
}Sourcepub fn charset(self, charset: impl Into<String>) -> Self
pub fn charset(self, charset: impl Into<String>) -> Self
Set the charset string directly
The charset defines all characters that the OCR model can recognize. It should be a string containing all valid characters in order.
§Arguments
charset- A string containing the character set
§Returns
The builder with the charset set
§Note
The charset must match the one used during model training. The default charset is built-in and suitable for Mon text.
Sourcepub fn min_line_height(self, height: u32) -> Self
pub fn min_line_height(self, height: u32) -> Self
Set the minimum line height for segmentation
During line segmentation, any detected region shorter than this value will be ignored. This helps filter out noise and small artifacts.
§Arguments
height- Minimum line height in pixels (default: 10)
§Returns
The builder with the minimum line height set
§Recommendation
Increase this value for noisy documents or decrease for documents with small font sizes.
Sourcepub fn smooth_window(self, window: u32) -> Self
pub fn smooth_window(self, window: u32) -> Self
Set the smoothing window for projection profile
The smoothing window is used when computing the horizontal projection profile for line detection. A larger window produces smoother results but may merge close lines.
§Arguments
window- Window size for smoothing (default: 3, use 1 for no smoothing)
§Returns
The builder with the smooth window set
Sourcepub fn tile_wide_lines(self, tile: bool) -> Self
pub fn tile_wide_lines(self, tile: bool) -> Self
Set the gap threshold for line segmentation
A row counts as a gap between lines when its ink density falls below
ratio times the mean density of the page’s non-empty rows. Lower it to
split lines that are being merged; raise it to stop faint texture between
lines from cutting one line in two.
§Arguments
ratio- Fraction of mean row density, greater than 0 (default: 0.05)
§Why this is exposed
The right value is a property of the input class, not a constant waiting
to be settled. mon_OCR/docs/LIMITATIONS.md:304-334 measured the
ordering reversing between a book page and a photographed poster: a
six-line slide returned 3 lines at the low ratio and all 6 at 0.50, and
the response to the ratio is explicitly non-monotone. So a caller that
knows what it is reading can do better than any single default, and every
port of this pipeline picked a different number.
§Errors
build fails if ratio is not finite or not positive. At
0 every row clears the threshold and the page comes back as one band,
which is a wrong result rather than a degraded one.
Squeeze wide lines into the window instead of tiling them.
Tiling is the default and should stay the default. This exists so the two
strategies can be measured against each other on the same pipeline, which
mon_OCR/docs/ROADMAP.md item 4.5.6 requires before either is trusted,
and which was impossible while the squeeze arm was unreachable.
The measurement in mon_OCR/eval/tiling-ab-2026-08-22.md found the answer
is width-dependent: squeezing is mildly better up to 3 tiles and 3.7x to
24x worse from 4 tiles up, where it drives CER above 0.9. Tiling is the
safe default because its downside is bounded and squeezing’s is not.
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}pub fn density_threshold_ratio(self, ratio: f32) -> Self
Sourcepub async fn build(self) -> Result<MonOcr>
pub async fn build(self) -> Result<MonOcr>
Build the MonOcr instance
This method initializes the ONNX runtime session and prepares the OCR engine for use. It may download the model if not cached.
§Returns
Ok(MonOcr)- Ready-to-use OCR instanceErr(anyhow::Error)- If model loading fails
§Async
This function is async because model initialization may involve downloading the model file from the network.
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}Trait Implementations§
Source§impl Default for MonOcrBuilder
impl Default for MonOcrBuilder
Source§fn default() -> Self
fn default() -> Self
Create a MonOcrBuilder with default settings
Default values:
- model_path: None (will download from HuggingFace)
- charset: None (uses the charset published with the pinned model, falling back to the built-in Mon charset)
- min_line_height: 10 pixels
- smooth_window: 3
- density_threshold_ratio: 0.05
Auto Trait Implementations§
impl Freeze for MonOcrBuilder
impl RefUnwindSafe for MonOcrBuilder
impl Send for MonOcrBuilder
impl Sync for MonOcrBuilder
impl Unpin for MonOcrBuilder
impl UnsafeUnpin for MonOcrBuilder
impl UnwindSafe for MonOcrBuilder
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 moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<R, P> ReadPrimitive<R> for P
impl<R, P> ReadPrimitive<R> for P
Source§fn read_from_little_endian(read: &mut R) -> Result<Self, Error>
fn read_from_little_endian(read: &mut R) -> Result<Self, Error>
ReadEndian::read_from_little_endian().