1use ort::session::Session;
2use ort::value::Tensor;
3use ort::{inputs, session::builder::SessionBuilder};
4use std::collections::HashMap;
5
6use crate::{base_net::BaseNet, ocr_error::OcrError, ocr_result::TextLine, ocr_utils::OcrUtils};
7
8#[derive(Debug, Clone, Copy)]
13pub(crate) struct CtcSelection {
14 pub timestep: usize,
15 pub score: f32,
16}
17
18#[derive(Debug, Clone)]
22pub(crate) struct WordRange {
23 pub text: String,
24 pub start_ts: usize,
25 pub end_ts: usize,
26 pub score: f32,
27}
28
29pub(crate) const CRNN_DST_HEIGHT: u32 = 48;
30const MEAN_VALUES: [f32; 3] = [127.5, 127.5, 127.5];
31const NORM_VALUES: [f32; 3] = [1.0 / 127.5, 1.0 / 127.5, 1.0 / 127.5];
32
33#[derive(Debug)]
34pub struct CrnnNet {
35 session: Option<Session>,
36 keys: Vec<String>,
37 input_names: Vec<String>,
38}
39
40impl BaseNet for CrnnNet {
41 fn new() -> Self {
42 Self {
43 session: None,
44 keys: Vec::new(),
45 input_names: Vec::new(),
46 }
47 }
48
49 fn set_input_names(&mut self, input_names: Vec<String>) {
50 self.input_names = input_names;
51 }
52
53 fn set_session(&mut self, session: Option<Session>) {
54 self.session = session;
55 }
56}
57
58impl CrnnNet {
59 pub fn init_model(
60 &mut self,
61 path: &str,
62 num_thread: usize,
63 builder_fn: Option<fn(SessionBuilder) -> Result<SessionBuilder, ort::Error>>,
64 ) -> Result<(), OcrError> {
65 BaseNet::init_model(self, path, num_thread, builder_fn)?;
66
67 self.keys = self.get_keys()?;
68
69 Ok(())
70 }
71
72 pub fn init_model_dict_file(
73 &mut self,
74 path: &str,
75 num_thread: usize,
76 builder_fn: Option<fn(SessionBuilder) -> Result<SessionBuilder, ort::Error>>,
77 dict_file_path: &str,
78 ) -> Result<(), OcrError> {
79 BaseNet::init_model(self, path, num_thread, builder_fn)?;
80
81 self.read_keys_from_file(dict_file_path)?;
82
83 Ok(())
84 }
85
86 pub fn init_model_from_memory(
87 &mut self,
88 model_bytes: &[u8],
89 num_thread: usize,
90 builder_fn: Option<fn(SessionBuilder) -> Result<SessionBuilder, ort::Error>>,
91 ) -> Result<(), OcrError> {
92 BaseNet::init_model_from_memory(self, model_bytes, num_thread, builder_fn)?;
93
94 self.keys = self.get_keys()?;
95
96 Ok(())
97 }
98
99 fn get_keys(&mut self) -> Result<Vec<String>, OcrError> {
100 let model_charater_list = self
106 .session
107 .as_ref()
108 .expect("crnn_net session not initialized")
109 .metadata()
110 .expect("crnn_net metadata not initialized")
111 .custom("character")
112 .expect("crnn_net character meta not found")
113 .expect("crnn_net character meta is None");
114
115 let mut keys = Vec::with_capacity((model_charater_list.len() as f32 / 3.9) as usize);
116
117 keys.push("#".to_string());
118
119 keys.extend(model_charater_list.split('\n').map(|s: &str| s.to_string()));
120
121 keys.push(" ".to_string());
122
123 Ok(keys)
124 }
125
126 fn read_keys_from_file(&mut self, path: &str) -> Result<(), OcrError> {
127 let content = std::fs::read_to_string(path)?;
128 let mut keys = Vec::new();
129
130 keys.push("#".to_string());
132
133 let mut lines: Vec<&str> = content.split('\n').collect();
141 if lines.last().map_or(false, |l| l.trim_end_matches('\r').is_empty()) {
142 lines.pop();
143 }
144 keys.extend(lines.iter().map(|s| s.trim_end_matches('\r').to_string()));
145
146 keys.push(" ".to_string());
148
149 self.keys = keys;
150 Ok(())
151 }
152
153 pub fn get_text_lines(
154 &mut self,
155 part_imgs: &[image::RgbImage],
156 angle_rollback_records: &HashMap<usize, image::RgbImage>,
157 angle_rollback_threshold: f32,
158 ) -> Result<Vec<TextLine>, OcrError> {
159 let lines_with_words = self.get_text_lines_with_word_ranges(
160 part_imgs,
161 angle_rollback_records,
162 angle_rollback_threshold,
163 )?;
164 Ok(lines_with_words.into_iter().map(|(line, _, _, _, _)| line).collect())
165 }
166
167 pub(crate) fn get_text_lines_with_word_ranges(
176 &mut self,
177 part_imgs: &[image::RgbImage],
178 angle_rollback_records: &HashMap<usize, image::RgbImage>,
179 angle_rollback_threshold: f32,
180 ) -> Result<Vec<(TextLine, Vec<WordRange>, (u32, u32), usize, usize)>, OcrError> {
181 let mut out = Vec::with_capacity(part_imgs.len());
182
183 let base_wh_ratio = 320.0 / CRNN_DST_HEIGHT as f32;
185 let max_wh_ratio = part_imgs
186 .iter()
187 .map(|img| img.width() as f32 / img.height().max(1) as f32)
188 .fold(base_wh_ratio, f32::max);
189
190 for (index, img) in part_imgs.iter().enumerate() {
191 let mut entry = self.recognize_one(img, max_wh_ratio)?;
192 if entry.0.text_score.is_nan() || entry.0.text_score < angle_rollback_threshold {
195 if let Some(rollback_img) = angle_rollback_records.get(&index) {
196 entry = self.recognize_one(rollback_img, max_wh_ratio)?;
197 }
198 }
199 out.push(entry);
200 }
201 Ok(out)
202 }
203
204 fn recognize_one(
209 &mut self,
210 img_src: &image::RgbImage,
211 max_wh_ratio: f32,
212 ) -> Result<(TextLine, Vec<WordRange>, (u32, u32), usize, usize), OcrError> {
213 let crop_size = (img_src.width(), img_src.height());
214 let (line, selection, target_w, t) =
215 self.get_text_line_with_wh_ratio(img_src, max_wh_ratio)?;
216 let words = selection_to_word_ranges(&line.text, &selection);
217 Ok((line, words, crop_size, target_w, t))
218 }
219
220 fn get_text_line_with_wh_ratio(
231 &mut self,
232 img_src: &image::RgbImage,
233 max_wh_ratio: f32,
234 ) -> Result<(TextLine, Vec<CtcSelection>, usize, usize), OcrError> {
235 let Some(session) = &mut self.session else {
236 return Err(OcrError::SessionNotInitialized);
237 };
238
239 let scale = CRNN_DST_HEIGHT as f32 / img_src.height() as f32;
240 let resized_w = (img_src.width() as f32 * scale).ceil() as u32;
241
242 let src_resize = image::imageops::resize(
243 img_src,
244 resized_w,
245 CRNN_DST_HEIGHT,
246 image::imageops::FilterType::Triangle,
247 );
248
249 let input_tensors =
250 OcrUtils::substract_mean_normalize(&src_resize, &MEAN_VALUES, &NORM_VALUES);
251
252 let target_w_raw = (CRNN_DST_HEIGHT as f32 * max_wh_ratio) as u32;
257 let target_w = target_w_raw.max(resized_w);
258 let input_tensors = if max_wh_ratio > 0.0 && target_w > resized_w {
259 let shape = input_tensors.shape();
260 let c = shape[1];
261 let h = shape[2];
262 let mut padded = ndarray::Array4::<f32>::zeros((1, c, h, target_w as usize));
263 padded
264 .slice_mut(ndarray::s![.., .., .., ..resized_w as usize])
265 .assign(&input_tensors);
266 padded
267 } else {
268 input_tensors
269 };
270 let effective_target_w = if max_wh_ratio > 0.0 { target_w } else { resized_w };
272
273 let input_tensors = Tensor::from_array(input_tensors)?;
274
275 let outputs = session.run(inputs![self.input_names[0].clone() => input_tensors]?)?;
277
278 let (_, red_data) = outputs.iter().next().unwrap();
279
280 let (shape_vec, src_data) = crate::compat::tensor_extract_with_shape_f32(&red_data)?;
282 let timesteps = shape_vec[1] as usize;
284 let vocab = shape_vec[2] as usize;
285
286 let (line, selection) = Self::score_to_text_line(&src_data, timesteps, vocab, &self.keys)?;
287 Ok((line, selection, effective_target_w as usize, timesteps))
288 }
289
290 fn score_to_text_line(
299 output_data: &[f32],
300 timesteps: usize,
301 vocab: usize,
302 keys: &[String],
303 ) -> Result<(TextLine, Vec<CtcSelection>), OcrError> {
304 let mut text_line = TextLine::default();
305 let mut selection: Vec<CtcSelection> = Vec::with_capacity(timesteps);
306 let mut last_index = 0usize;
307 let mut text_score_sum = 0.0f32;
308 let mut text_score_count = 0usize;
309
310 for i in 0..timesteps {
311 let start = i * vocab;
312 let stop = (i + 1) * vocab;
313 let slice = &output_data[start..stop.min(output_data.len())];
314
315 let (max_index, max_value) = slice
316 .iter()
317 .enumerate()
318 .fold((0usize, f32::MIN), |(max_idx, max_val), (idx, &val)| {
319 if val > max_val { (idx, val) } else { (max_idx, max_val) }
320 });
321
322 if max_index > 0 && max_index < keys.len() && !(i > 0 && max_index == last_index) {
323 text_line.text.push_str(&keys[max_index]);
324 selection.push(CtcSelection { timestep: i, score: max_value });
325 text_score_sum += max_value;
326 text_score_count += 1;
327 }
328 last_index = max_index;
329 }
330
331 text_line.text_score = if text_score_count > 0 {
332 text_score_sum / text_score_count as f32
333 } else {
334 0.0
335 };
336 Ok((text_line, selection))
337 }
338}
339
340pub(crate) fn selection_to_word_ranges(text: &str, selection: &[CtcSelection]) -> Vec<WordRange> {
351 let chars: Vec<char> = text.chars().collect();
352 debug_assert_eq!(chars.len(), selection.len(),
353 "selection deve essere indicizzato 1:1 con text.chars()");
354
355 let mut words = Vec::new();
356 let mut buf_text = String::new();
357 let mut buf_first: Option<usize> = None; let mut buf_last: usize = 0;
359 let mut buf_score_sum = 0.0f32;
360 let mut buf_score_n = 0usize;
361
362 let flush = |words: &mut Vec<WordRange>,
363 buf_text: &mut String,
364 buf_first: &mut Option<usize>,
365 buf_last: usize,
366 score_sum: f32,
367 score_n: usize| {
368 if let Some(first) = *buf_first {
369 if !buf_text.is_empty() {
370 words.push(WordRange {
371 text: std::mem::take(buf_text),
372 start_ts: selection[first].timestep,
373 end_ts: selection[buf_last].timestep,
374 score: if score_n > 0 { score_sum / score_n as f32 } else { 0.0 },
375 });
376 }
377 *buf_first = None;
378 }
379 };
380
381 for (i, ch) in chars.iter().enumerate() {
382 if ch.is_alphanumeric() {
383 buf_text.push(*ch);
384 if buf_first.is_none() { buf_first = Some(i); }
385 buf_last = i;
386 buf_score_sum += selection[i].score;
387 buf_score_n += 1;
388 } else {
389 flush(&mut words, &mut buf_text, &mut buf_first, buf_last, buf_score_sum, buf_score_n);
390 buf_score_sum = 0.0;
391 buf_score_n = 0;
392 }
393 }
394 flush(&mut words, &mut buf_text, &mut buf_first, buf_last, buf_score_sum, buf_score_n);
395 words
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn sel(timesteps: &[(usize, f32)]) -> Vec<CtcSelection> {
403 timesteps.iter().map(|&(t, s)| CtcSelection { timestep: t, score: s }).collect()
404 }
405
406 #[test]
407 fn word_grouping_simple() {
408 let text = "Hello world";
410 let selection = sel(&[
411 (0, 0.9), (2, 0.9), (3, 0.9), (4, 0.9), (5, 0.9), (7, 0.0), (10, 0.9), (11, 0.9), (12, 0.9), (13, 0.9), (14, 0.9), ]);
415 let words = selection_to_word_ranges(text, &selection);
416 assert_eq!(words.len(), 2);
417 assert_eq!(words[0].text, "Hello");
418 assert_eq!(words[0].start_ts, 0);
419 assert_eq!(words[0].end_ts, 5);
420 assert_eq!(words[1].text, "world");
421 assert_eq!(words[1].start_ts, 10);
422 assert_eq!(words[1].end_ts, 14);
423 }
424
425 #[test]
426 fn word_grouping_punctuation() {
427 let text = "Hello, world!";
429 let selection = sel(&[
430 (0, 0.9), (1, 0.9), (2, 0.9), (3, 0.9), (4, 0.9), (5, 0.5), (6, 0.0), (7, 0.9), (8, 0.9), (9, 0.9), (10, 0.9), (11, 0.9),(12, 0.5), ]);
436 let words = selection_to_word_ranges(text, &selection);
437 assert_eq!(words.len(), 2);
438 assert_eq!(words[0].text, "Hello");
439 assert_eq!(words[1].text, "world");
440 }
441
442 #[test]
443 fn word_grouping_accented() {
444 let text = "città è grande";
446 let n = text.chars().count();
447 let selection: Vec<CtcSelection> = (0..n)
448 .map(|i| CtcSelection { timestep: i, score: 0.9 })
449 .collect();
450 let words = selection_to_word_ranges(text, &selection);
451 assert_eq!(words.len(), 3);
452 assert_eq!(words[0].text, "città");
453 assert_eq!(words[1].text, "è");
454 assert_eq!(words[2].text, "grande");
455 }
456
457 #[test]
458 fn word_grouping_empty_selection() {
459 let words = selection_to_word_ranges("", &[]);
460 assert!(words.is_empty());
461 }
462
463 #[test]
464 fn word_grouping_only_punctuation() {
465 let text = "...!?";
466 let selection = sel(&[(0, 0.5), (1, 0.5), (2, 0.5), (3, 0.5), (4, 0.5)]);
467 let words = selection_to_word_ranges(text, &selection);
468 assert!(words.is_empty(), "solo punctuation → nessun word");
469 }
470}