Skip to main content

MonOcrBuilder

Struct MonOcrBuilder 

Source
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

Source

pub fn new() -> Self

Create a new builder with default settings

This is equivalent to calling MonOcrBuilder::default().

§Returns

A new MonOcrBuilder instance with default configuration

§Example
use monocr_onnx::MonOcrBuilder;

let builder = MonOcrBuilder::new();
Source

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(())
}
Source

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.

Source

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.

Source

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

Source

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?
examples/tiling_ab.rs (line 101)
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}
Source

pub fn density_threshold_ratio(self, ratio: f32) -> Self

Source

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 instance
  • Err(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?
examples/tiling_ab.rs (line 101)
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

Source§

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<R, P> ReadPrimitive<R> for P
where R: Read + ReadEndian<P>, P: Default,

Source§

fn read_from_little_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_little_endian().
Source§

fn read_from_big_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_big_endian().
Source§

fn read_from_native_endian(read: &mut R) -> Result<Self, Error>

Read this value from the supplied reader. Same as ReadEndian::read_from_native_endian().
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more