use image::RgbImage;
use ndarray::Array4;
pub(crate) const REC_HEIGHT: u32 = 48;
pub(crate) const REC_MAX_WIDTH: u32 = 320;
pub(crate) fn build_charset(dict: &str) -> Vec<String> {
let mut charset = vec![String::new()]; charset.extend(dict.lines().map(|l| l.to_string()));
charset.push(" ".to_string());
charset
}
pub(crate) fn preprocess_rec(crop: &RgbImage) -> Array4<f32> {
let scale = REC_HEIGHT as f32 / crop.height() as f32;
let w = ((crop.width() as f32 * scale).round() as u32).clamp(1, REC_MAX_WIDTH);
let resized =
image::imageops::resize(crop, w, REC_HEIGHT, image::imageops::FilterType::Triangle);
let mut input = Array4::<f32>::zeros((1, 3, REC_HEIGHT as usize, REC_MAX_WIDTH as usize));
for (x, y, pixel) in resized.enumerate_pixels() {
for c in 0..3 {
input[[0, c, y as usize, x as usize]] = (pixel.0[c] as f32 / 255.0 - 0.5) / 0.5;
}
}
input
}
pub(crate) fn ctc_decode(
probs: &[f32],
steps: usize,
classes: usize,
charset: &[String],
) -> (String, f32) {
let mut text = String::new();
let mut conf_sum = 0.0f32;
let mut kept = 0u32;
let mut prev_class = 0usize;
for t in 0..steps {
let row = &probs[t * classes..(t + 1) * classes];
let (best, &best_p) = row
.iter()
.enumerate()
.max_by(|a, b| a.1.total_cmp(b.1))
.expect("classes > 0");
if best != 0 && best != prev_class {
if let Some(ch) = charset.get(best) {
text.push_str(ch);
conf_sum += best_p;
kept += 1;
}
}
prev_class = best;
}
let confidence = if kept == 0 {
0.0
} else {
conf_sum / kept as f32
};
(text, confidence)
}
#[cfg(test)]
mod tests {
use super::*;
fn tiny_charset() -> Vec<String> {
build_charset("a\nb")
}
fn probs(steps: &[usize], classes: usize) -> Vec<f32> {
let mut out = vec![0.0f32; steps.len() * classes];
for (t, &c) in steps.iter().enumerate() {
out[t * classes + c] = 0.8;
}
out
}
#[test]
fn build_charset_adds_blank_and_space() {
let cs = tiny_charset();
assert_eq!(cs.len(), 4); assert_eq!(cs[1], "a");
assert_eq!(cs[3], " ");
}
#[test]
fn ctc_collapses_repeats_and_drops_blanks() {
let cs = tiny_charset();
let p = probs(&[1, 1, 0, 1, 2, 2], cs.len());
let (text, conf) = ctc_decode(&p, 6, cs.len(), &cs);
assert_eq!(text, "aab");
assert!((conf - 0.8).abs() < 1e-6);
}
#[test]
fn ctc_decodes_space_class() {
let cs = tiny_charset();
let p = probs(&[1, 3, 2], cs.len());
let (text, _) = ctc_decode(&p, 3, cs.len(), &cs);
assert_eq!(text, "a b");
}
#[test]
fn ctc_all_blank_is_empty_with_zero_confidence() {
let cs = tiny_charset();
let p = probs(&[0, 0, 0], cs.len());
let (text, conf) = ctc_decode(&p, 3, cs.len(), &cs);
assert_eq!(text, "");
assert_eq!(conf, 0.0);
}
#[test]
fn preprocess_rec_shape_and_padding() {
let crop = image::RgbImage::from_pixel(24, 24, image::Rgb([128, 128, 128]));
let t = preprocess_rec(&crop);
assert_eq!(t.shape(), &[1, 3, 48, 320]);
assert!((t[[0, 0, 0, 0]] - 0.003_921_57).abs() < 1e-4);
assert_eq!(t[[0, 0, 0, 319]], 0.0);
assert_eq!(t[[0, 2, 47, 100]], 0.0);
}
}