1use image::DynamicImage;
6use std::path::{Path, PathBuf};
7
8use crate::det::{DetModel, DetOptions};
9use crate::error::{OcrError, OcrResult};
10use crate::mnn::{Backend, InferenceConfig, PrecisionMode};
11use crate::ori::{OriModel, OriOptions};
12use crate::postprocess::TextBox;
13use crate::rec::{RecModel, RecOptions, RecognitionResult};
14
15#[derive(Debug, Clone)]
17pub struct OcrResult_ {
18 pub text: String,
20 pub confidence: f32,
22 pub bbox: TextBox,
24}
25
26impl OcrResult_ {
27 pub fn new(text: String, confidence: f32, bbox: TextBox) -> Self {
29 Self {
30 text,
31 confidence,
32 bbox,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct OcrEngineConfig {
40 pub backend: Backend,
42 pub thread_count: i32,
44 pub precision_mode: PrecisionMode,
46 pub det_options: DetOptions,
48 pub rec_options: RecOptions,
50 pub ori_options: OriOptions,
52 pub enable_parallel: bool,
54 pub min_result_confidence: f32,
56 pub ori_min_confidence: f32,
58}
59
60impl Default for OcrEngineConfig {
61 fn default() -> Self {
62 Self {
63 backend: Backend::CPU,
64 thread_count: 4,
65 precision_mode: PrecisionMode::Normal,
66 det_options: DetOptions::default(),
67 rec_options: RecOptions::default(),
68 ori_options: OriOptions::default(),
69 enable_parallel: true,
70 min_result_confidence: 0.5,
71 ori_min_confidence: 0.3,
72 }
73 }
74}
75
76impl OcrEngineConfig {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_backend(mut self, backend: Backend) -> Self {
84 self.backend = backend;
85 self
86 }
87
88 pub fn with_threads(mut self, threads: i32) -> Self {
90 self.thread_count = threads;
91 self
92 }
93
94 pub fn with_precision(mut self, precision: PrecisionMode) -> Self {
96 self.precision_mode = precision;
97 self
98 }
99
100 pub fn with_det_options(mut self, options: DetOptions) -> Self {
102 self.det_options = options;
103 self
104 }
105
106 pub fn with_rec_options(mut self, options: RecOptions) -> Self {
108 self.rec_options = options;
109 self
110 }
111
112 pub fn with_ori_options(mut self, options: OriOptions) -> Self {
114 self.ori_options = options;
115 self
116 }
117
118 pub fn with_parallel(mut self, enable: bool) -> Self {
123 self.enable_parallel = enable;
124 self
125 }
126
127 pub fn with_min_result_confidence(mut self, threshold: f32) -> Self {
132 self.min_result_confidence = threshold;
133 self
134 }
135
136 pub fn with_ori_min_confidence(mut self, threshold: f32) -> Self {
138 self.ori_min_confidence = threshold;
139 self
140 }
141
142 pub fn fast() -> Self {
144 Self {
145 precision_mode: PrecisionMode::Low,
146 det_options: DetOptions::fast(),
147 ..Default::default()
148 }
149 }
150
151 #[cfg(any(target_os = "macos", target_os = "ios"))]
153 pub fn gpu() -> Self {
154 Self {
155 backend: Backend::Metal,
156 ..Default::default()
157 }
158 }
159
160 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
162 pub fn gpu() -> Self {
163 Self {
164 backend: Backend::OpenCL,
165 ..Default::default()
166 }
167 }
168
169 fn to_inference_config(&self) -> InferenceConfig {
170 InferenceConfig {
171 thread_count: self.thread_count,
172 precision_mode: self.precision_mode,
173 backend: self.backend,
174 ..Default::default()
175 }
176 }
177}
178
179pub struct OcrEngine {
205 det_model: DetModel,
206 rec_model: RecModel,
207 ori_model: Option<OriModel>,
208 config: OcrEngineConfig,
209}
210
211impl OcrEngine {
212 fn build_with_paths(
213 det_model_path: &Path,
214 rec_model_path: &Path,
215 charset_path: &Path,
216 ori_model_path: Option<&Path>,
217 config: Option<OcrEngineConfig>,
218 ) -> OcrResult<Self> {
219 let config = config.unwrap_or_default();
220 let inference_config = config.to_inference_config();
221
222 let det_options = config.det_options.clone();
224 let rec_options = config.rec_options.clone();
225 let ori_options = config.ori_options.clone();
226
227 let det_model = DetModel::from_file(det_model_path, Some(inference_config.clone()))?
228 .with_options(det_options);
229
230 let rec_model =
231 RecModel::from_file(rec_model_path, charset_path, Some(inference_config.clone()))?
232 .with_options(rec_options);
233
234 let ori_model = match ori_model_path {
235 Some(path) => {
236 Some(OriModel::from_file(path, Some(inference_config))?.with_options(ori_options))
237 }
238 None => None,
239 };
240
241 Ok(Self {
242 det_model,
243 rec_model,
244 ori_model,
245 config,
246 })
247 }
248
249 pub fn new(
257 det_model_path: impl AsRef<Path>,
258 rec_model_path: impl AsRef<Path>,
259 charset_path: impl AsRef<Path>,
260 config: Option<OcrEngineConfig>,
261 ) -> OcrResult<Self> {
262 Self::build_with_paths(
263 det_model_path.as_ref(),
264 rec_model_path.as_ref(),
265 charset_path.as_ref(),
266 None,
267 config,
268 )
269 }
270
271 pub fn new_with_ori(
273 det_model_path: impl AsRef<Path>,
274 rec_model_path: impl AsRef<Path>,
275 charset_path: impl AsRef<Path>,
276 ori_model_path: impl AsRef<Path>,
277 config: Option<OcrEngineConfig>,
278 ) -> OcrResult<Self> {
279 Self::build_with_paths(
280 det_model_path.as_ref(),
281 rec_model_path.as_ref(),
282 charset_path.as_ref(),
283 Some(ori_model_path.as_ref()),
284 config,
285 )
286 }
287
288 pub fn from_bytes(
290 det_model_bytes: &[u8],
291 rec_model_bytes: &[u8],
292 charset_bytes: &[u8],
293 config: Option<OcrEngineConfig>,
294 ) -> OcrResult<Self> {
295 let config = config.unwrap_or_default();
296 let inference_config = config.to_inference_config();
297
298 let det_options = config.det_options.clone();
300 let rec_options = config.rec_options.clone();
301
302 let det_model = DetModel::from_bytes(det_model_bytes, Some(inference_config.clone()))?
303 .with_options(det_options);
304
305 let rec_model = RecModel::from_bytes_with_charset(
306 rec_model_bytes,
307 charset_bytes,
308 Some(inference_config.clone()),
309 )?
310 .with_options(rec_options);
311
312 Ok(Self {
313 det_model,
314 rec_model,
315 ori_model: None,
316 config,
317 })
318 }
319
320 pub fn from_bytes_with_ori(
322 det_model_bytes: &[u8],
323 rec_model_bytes: &[u8],
324 charset_bytes: &[u8],
325 ori_model_bytes: &[u8],
326 config: Option<OcrEngineConfig>,
327 ) -> OcrResult<Self> {
328 let config = config.unwrap_or_default();
329 let inference_config = config.to_inference_config();
330
331 let det_options = config.det_options.clone();
332 let rec_options = config.rec_options.clone();
333 let ori_options = config.ori_options.clone();
334
335 let det_model = DetModel::from_bytes(det_model_bytes, Some(inference_config.clone()))?
336 .with_options(det_options);
337
338 let rec_model = RecModel::from_bytes_with_charset(
339 rec_model_bytes,
340 charset_bytes,
341 Some(inference_config.clone()),
342 )?
343 .with_options(rec_options);
344
345 let ori_model = OriModel::from_bytes(ori_model_bytes, Some(inference_config))?
346 .with_options(ori_options);
347
348 Ok(Self {
349 det_model,
350 rec_model,
351 ori_model: Some(ori_model),
352 config,
353 })
354 }
355
356 pub fn det_only(
358 det_model_path: impl AsRef<Path>,
359 config: Option<OcrEngineConfig>,
360 ) -> OcrResult<DetOnlyEngine> {
361 let config = config.unwrap_or_default();
362 let inference_config = config.to_inference_config();
363
364 let det_model = DetModel::from_file(det_model_path, Some(inference_config))?
365 .with_options(config.det_options);
366
367 Ok(DetOnlyEngine { det_model })
368 }
369
370 pub fn rec_only(
372 rec_model_path: impl AsRef<Path>,
373 charset_path: impl AsRef<Path>,
374 config: Option<OcrEngineConfig>,
375 ) -> OcrResult<RecOnlyEngine> {
376 let config = config.unwrap_or_default();
377 let inference_config = config.to_inference_config();
378
379 let rec_model = RecModel::from_file(rec_model_path, charset_path, Some(inference_config))?
380 .with_options(config.rec_options);
381
382 Ok(RecOnlyEngine { rec_model })
383 }
384
385 pub fn recognize(&self, image: &DynamicImage) -> OcrResult<Vec<OcrResult_>> {
393 let corrected_image = if let Some(ori_model) = self.ori_model.as_ref() {
395 self.correct_orientation_with_model(ori_model, image.clone())
396 } else {
397 image.clone()
398 };
399
400 let boxes = self.det_model.detect_expanded(&corrected_image)?;
402
403 if boxes.is_empty() {
404 return Ok(Vec::new());
405 }
406
407 let rec_results = self.rec_model.recognize_regions(&corrected_image, &boxes)?;
410
411 let results: Vec<OcrResult_> = rec_results
413 .into_iter()
414 .zip(boxes)
415 .filter(|(rec, _)| {
416 !rec.text.is_empty() && rec.confidence >= self.config.min_result_confidence
417 })
418 .map(|(rec, bbox)| OcrResult_::new(rec.text, rec.confidence, bbox))
419 .collect();
420
421 Ok(results)
422 }
423
424 pub fn detect(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
426 self.det_model.detect(image)
427 }
428
429 pub fn recognize_text(&self, image: &DynamicImage) -> OcrResult<RecognitionResult> {
431 self.rec_model.recognize(image)
432 }
433
434 pub fn recognize_batch(&self, images: &[DynamicImage]) -> OcrResult<Vec<RecognitionResult>> {
436 self.rec_model.recognize_batch(images)
437 }
438
439 pub fn ori_model(&self) -> Option<&OriModel> {
441 self.ori_model.as_ref()
442 }
443
444 pub fn det_model(&self) -> &DetModel {
446 &self.det_model
447 }
448
449 pub fn rec_model(&self) -> &RecModel {
451 &self.rec_model
452 }
453
454 pub fn config(&self) -> &OcrEngineConfig {
456 &self.config
457 }
458
459 fn correct_orientation_with_model(
460 &self,
461 ori_model: &OriModel,
462 image: DynamicImage,
463 ) -> DynamicImage {
464 let result = match ori_model.classify(&image) {
465 Ok(result) => result,
466 Err(_) => return image,
467 };
468
469 if !result.is_valid(self.config.ori_min_confidence) {
470 return image;
471 }
472
473 if result.angle.rem_euclid(360) == 0 {
474 return image;
475 }
476
477 rotate_by_angle(&image, result.angle)
478 }
479}
480
481pub struct OcrEngineBuilder {
483 det_model_path: Option<PathBuf>,
484 rec_model_path: Option<PathBuf>,
485 charset_path: Option<PathBuf>,
486 ori_model_path: Option<PathBuf>,
487 config: Option<OcrEngineConfig>,
488}
489
490impl OcrEngineBuilder {
491 pub fn new() -> Self {
493 Self {
494 det_model_path: None,
495 rec_model_path: None,
496 charset_path: None,
497 ori_model_path: None,
498 config: None,
499 }
500 }
501
502 pub fn with_det_model_path(mut self, path: impl AsRef<Path>) -> Self {
504 self.det_model_path = Some(path.as_ref().to_path_buf());
505 self
506 }
507
508 pub fn with_rec_model_path(mut self, path: impl AsRef<Path>) -> Self {
510 self.rec_model_path = Some(path.as_ref().to_path_buf());
511 self
512 }
513
514 pub fn with_charset_path(mut self, path: impl AsRef<Path>) -> Self {
516 self.charset_path = Some(path.as_ref().to_path_buf());
517 self
518 }
519
520 pub fn with_ori_model_path(mut self, path: impl AsRef<Path>) -> Self {
522 self.ori_model_path = Some(path.as_ref().to_path_buf());
523 self
524 }
525
526 pub fn with_config(mut self, config: OcrEngineConfig) -> Self {
528 self.config = Some(config);
529 self
530 }
531
532 pub fn build(self) -> OcrResult<OcrEngine> {
534 let det_model_path = self
535 .det_model_path
536 .ok_or_else(|| OcrError::InvalidParameter("Missing det_model_path".to_string()))?;
537 let rec_model_path = self
538 .rec_model_path
539 .ok_or_else(|| OcrError::InvalidParameter("Missing rec_model_path".to_string()))?;
540 let charset_path = self
541 .charset_path
542 .ok_or_else(|| OcrError::InvalidParameter("Missing charset_path".to_string()))?;
543
544 OcrEngine::build_with_paths(
545 det_model_path.as_path(),
546 rec_model_path.as_path(),
547 charset_path.as_path(),
548 self.ori_model_path.as_deref(),
549 self.config,
550 )
551 }
552}
553
554pub struct DetOnlyEngine {
556 det_model: DetModel,
557}
558
559impl DetOnlyEngine {
560 pub fn detect(&self, image: &DynamicImage) -> OcrResult<Vec<TextBox>> {
562 self.det_model.detect(image)
563 }
564
565 pub fn detect_and_crop(&self, image: &DynamicImage) -> OcrResult<Vec<(DynamicImage, TextBox)>> {
567 self.det_model.detect_and_crop(image)
568 }
569
570 pub fn model(&self) -> &DetModel {
572 &self.det_model
573 }
574}
575
576pub struct RecOnlyEngine {
578 rec_model: RecModel,
579}
580
581impl RecOnlyEngine {
582 pub fn recognize(&self, image: &DynamicImage) -> OcrResult<RecognitionResult> {
584 self.rec_model.recognize(image)
585 }
586
587 pub fn recognize_text(&self, image: &DynamicImage) -> OcrResult<String> {
589 self.rec_model.recognize_text(image)
590 }
591
592 pub fn recognize_batch(&self, images: &[DynamicImage]) -> OcrResult<Vec<RecognitionResult>> {
594 self.rec_model.recognize_batch(images)
595 }
596
597 pub fn model(&self) -> &RecModel {
599 &self.rec_model
600 }
601}
602
603pub fn ocr_file(
616 image_path: impl AsRef<Path>,
617 det_model_path: impl AsRef<Path>,
618 rec_model_path: impl AsRef<Path>,
619 charset_path: impl AsRef<Path>,
620) -> OcrResult<Vec<OcrResult_>> {
621 let image = image::open(image_path)?;
622 let engine = OcrEngine::new(det_model_path, rec_model_path, charset_path, None)?;
623 engine.recognize(&image)
624}
625
626pub fn ocr_file_with_ori(
628 image_path: impl AsRef<Path>,
629 det_model_path: impl AsRef<Path>,
630 rec_model_path: impl AsRef<Path>,
631 charset_path: impl AsRef<Path>,
632 ori_model_path: impl AsRef<Path>,
633) -> OcrResult<Vec<OcrResult_>> {
634 let image = image::open(image_path)?;
635 let engine = OcrEngine::new_with_ori(
636 det_model_path,
637 rec_model_path,
638 charset_path,
639 ori_model_path,
640 None,
641 )?;
642 engine.recognize(&image)
643}
644
645fn rotate_by_angle(image: &DynamicImage, angle: i32) -> DynamicImage {
646 match angle.rem_euclid(360) {
648 90 => DynamicImage::ImageRgb8(image::imageops::rotate270(&image.to_rgb8())),
649 180 => DynamicImage::ImageRgb8(image::imageops::rotate180(&image.to_rgb8())),
650 270 => DynamicImage::ImageRgb8(image::imageops::rotate90(&image.to_rgb8())),
651 _ => image.clone(),
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn test_ocr_result() {
661 let bbox = TextBox::new(imageproc::rect::Rect::at(0, 0).of_size(100, 20), 0.9);
662 let result = OcrResult_::new("Hello".to_string(), 0.95, bbox);
663
664 assert_eq!(result.text, "Hello");
665 assert_eq!(result.confidence, 0.95);
666 }
667}