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];
}
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);
}
}