ocr 0.1.0

A minimalist OCR library for Rust — from scratch, no external engine
Documentation
use crate::font::{self, FONT_HEIGHT, FONT_WIDTH};

type Template = [[bool; FONT_WIDTH]; FONT_HEIGHT];

#[derive(Debug, Clone)]
pub struct Recognizer {
    templates: Vec<(char, Template)>,
}

impl Default for Recognizer {
    fn default() -> Self {
        Self::new()
    }
}

impl Recognizer {
    pub fn new() -> Self {
        let mut templates = Vec::new();
        for &ch in font::supported_chars() {
            if ch == ' ' {
                continue;
            }
            if let Some(data) = font::glyph(ch) {
                templates.push((ch, font::glyph_to_pixels(&data)));
            }
        }
        Self { templates }
    }

    pub fn recognize(&self, pixels: &[Vec<bool>]) -> (char, f32) {
        let normalized = normalize(pixels);
        let mut best = ('?', 0.0_f32);
        for &(ch, ref tmpl) in &self.templates {
            let score = match_score(&normalized, tmpl);
            if score > best.1 {
                best = (ch, score);
            }
        }

        let (mut bc, mut bs) = best;
        if let Some(js) = template_score(&self.templates, &normalized, 'j')
            && bc == '3'
            && bs - js < 0.35
        {
            bc = 'j';
            bs = js;
        }
        if let Some(ks) = template_score(&self.templates, &normalized, 'k')
            && bc == 'x'
            && bs - ks < 0.35
        {
            bc = 'k';
            bs = ks;
        }
        if matches!(bc, '\'' | '`')
            && let Some(ts) = template_score(&self.templates, &normalized, 't')
            && ts >= bs - 0.45
        {
            bc = 't';
            bs = ts;
        }

        (bc, bs * 100.0)
    }
}

fn template_score(
    templates: &[(char, Template)],
    normalized: &Template,
    ch: char,
) -> Option<f32> {
    templates
        .iter()
        .find(|(c, _)| *c == ch)
        .map(|(_, t)| match_score(normalized, t))
}

fn normalize(pixels: &[Vec<bool>]) -> Template {
    let h0 = pixels.len();
    if h0 == 0 {
        return [[false; FONT_WIDTH]; FONT_HEIGHT];
    }
    let w0 = pixels[0].len();
    if w0 == 0 {
        return [[false; FONT_WIDTH]; FONT_HEIGHT];
    }

    // Segmentation often drops all-empty top rows of a glyph (e.g. 'c'); pad so height
    // matches the nominal 5×7 aspect before downsampling.
    let min_h = (w0 * FONT_HEIGHT).div_ceil(FONT_WIDTH);
    let padded: Vec<Vec<bool>> = if h0 < min_h {
        let pad = min_h - h0;
        let mut v = vec![vec![false; w0]; pad];
        v.extend_from_slice(pixels);
        v
    } else {
        pixels.to_vec()
    };
    let h = padded.len();
    let w = w0;

    let mut result = [[false; FONT_WIDTH]; FONT_HEIGHT];
    for ty in 0..FONT_HEIGHT {
        for tx in 0..FONT_WIDTH {
            let sy = ty * h / FONT_HEIGHT;
            let sx = tx * w / FONT_WIDTH;
            result[ty][tx] = padded[sy][sx];
        }
    }
    result
}

fn match_score(input: &Template, template: &Template) -> f32 {
    let mut matching = 0u32;
    let mut total = 0u32;
    let mut both_on = 0u32;
    let mut tmpl_on = 0u32;

    for y in 0..FONT_HEIGHT {
        for x in 0..FONT_WIDTH {
            let a = input[y][x];
            let b = template[y][x];
            if a == b {
                matching += 1;
            }
            total += 1;
            if a && b {
                both_on += 1;
            }
            if b {
                tmpl_on += 1;
            }
        }
    }

    let pixel_score = matching as f32 / total as f32;
    let coverage = if tmpl_on > 0 {
        both_on as f32 / tmpl_on as f32
    } else {
        0.0
    };

    0.4 * pixel_score + 0.6 * coverage
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::font;

    fn glyph_pixels(c: char) -> Vec<Vec<bool>> {
        let data = font::glyph(c).unwrap();
        let tmpl = font::glyph_to_pixels(&data);
        tmpl.iter().map(|row| row.to_vec()).collect()
    }

    #[test]
    fn recognize_perfect_template() {
        let rec = Recognizer::new();
        for &ch in &['A', 'B', '0', '5', 'Z', 'h', 'm'] {
            let pixels = glyph_pixels(ch);
            let (recognized, conf) = rec.recognize(&pixels);
            assert_eq!(recognized, ch, "failed to recognize '{}' (got '{}')", ch, recognized);
            assert!(conf > 90.0, "low confidence for '{}': {}", ch, conf);
        }
    }

    #[test]
    fn recognize_scaled_template() {
        let rec = Recognizer::new();
        let data = font::glyph('A').unwrap();
        let tmpl = font::glyph_to_pixels(&data);
        let scale = 4;
        let mut pixels = vec![vec![false; FONT_WIDTH * scale]; FONT_HEIGHT * scale];
        for y in 0..FONT_HEIGHT {
            for x in 0..FONT_WIDTH {
                if tmpl[y][x] {
                    for dy in 0..scale {
                        for dx in 0..scale {
                            pixels[y * scale + dy][x * scale + dx] = true;
                        }
                    }
                }
            }
        }
        let (ch, conf) = rec.recognize(&pixels);
        assert_eq!(ch, 'A');
        assert!(conf > 90.0);
    }

    #[test]
    fn recognize_scaled_variants() {
        let rec = Recognizer::new();
        for &scale in &[2, 3, 5, 8] {
            let data = font::glyph('A').unwrap();
            let tmpl = font::glyph_to_pixels(&data);
            let mut pixels = vec![vec![false; FONT_WIDTH * scale]; FONT_HEIGHT * scale];
            for y in 0..FONT_HEIGHT {
                for x in 0..FONT_WIDTH {
                    if tmpl[y][x] {
                        for dy in 0..scale {
                            for dx in 0..scale {
                                pixels[y * scale + dy][x * scale + dx] = true;
                            }
                        }
                    }
                }
            }
            let (ch, conf) = rec.recognize(&pixels);
            assert_eq!(ch, 'A', "scale={}, got '{}'", scale, ch);
            assert!(conf > 90.0, "scale={}, conf={}", scale, conf);
        }
    }

    #[test]
    fn recognize_scaled_c_at_8() {
        let rec = Recognizer::new();
        let scale = 8;
        let data = font::glyph('c').unwrap();
        let tmpl = font::glyph_to_pixels(&data);
        let mut pixels = vec![vec![false; FONT_WIDTH * scale]; FONT_HEIGHT * scale];
        for y in 0..FONT_HEIGHT {
            for x in 0..FONT_WIDTH {
                if tmpl[y][x] {
                    for dy in 0..scale {
                        for dx in 0..scale {
                            pixels[y * scale + dy][x * scale + dx] = true;
                        }
                    }
                }
            }
        }
        let (ch, conf) = rec.recognize(&pixels);
        assert_eq!(ch, 'c', "got '{}' conf={}", ch, conf);
        assert!(conf > 80.0, "conf={}", conf);
    }
}