asterocr 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asterocr-1.0.0/PKG-INFO +102 -0
- asterocr-1.0.0/README.md +70 -0
- asterocr-1.0.0/asterocr/__init__.py +12 -0
- asterocr-1.0.0/asterocr/cli.py +35 -0
- asterocr-1.0.0/asterocr/comparator.py +412 -0
- asterocr-1.0.0/asterocr/dataset/export_real_zip.py +117 -0
- asterocr-1.0.0/asterocr/dataset/loader.py +371 -0
- asterocr-1.0.0/asterocr/dataset/unified_packer.py +402 -0
- asterocr-1.0.0/asterocr/download_dataset.py +93 -0
- asterocr-1.0.0/asterocr/inference/decoder.py +302 -0
- asterocr-1.0.0/asterocr/inference/lexicon.py +136 -0
- asterocr-1.0.0/asterocr/inference/predictor.py +205 -0
- asterocr-1.0.0/asterocr/inference/viet_linguistics.py +340 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/__init__.py +8 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/chunker.py +252 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/colab_auto_runner.py +380 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/kaggle_worker.py +1911 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/launcher.py +245 -0
- asterocr-1.0.0/asterocr/kaggle_bridge/tunnel.py +190 -0
- asterocr-1.0.0/asterocr/models/backbone.py +102 -0
- asterocr-1.0.0/asterocr/models/language.py +104 -0
- asterocr-1.0.0/asterocr/models/model.py +113 -0
- asterocr-1.0.0/asterocr/models/sequence.py +34 -0
- asterocr-1.0.0/asterocr/pipeline.py +179 -0
- asterocr-1.0.0/asterocr/studio.py +371 -0
- asterocr-1.0.0/asterocr/synthesizer/corpus.py +378 -0
- asterocr-1.0.0/asterocr/synthesizer/font_manager.py +170 -0
- asterocr-1.0.0/asterocr/synthesizer/generator.py +205 -0
- asterocr-1.0.0/asterocr/synthesizer/handwriting_generator.py +420 -0
- asterocr-1.0.0/asterocr/synthesizer/mega_generator.py +527 -0
- asterocr-1.0.0/asterocr/trainer/metrics.py +53 -0
- asterocr-1.0.0/asterocr/trainer/trainer.py +1295 -0
- asterocr-1.0.0/asterocr/vocab.py +175 -0
- asterocr-1.0.0/asterocr.egg-info/PKG-INFO +102 -0
- asterocr-1.0.0/asterocr.egg-info/SOURCES.txt +39 -0
- asterocr-1.0.0/asterocr.egg-info/dependency_links.txt +1 -0
- asterocr-1.0.0/asterocr.egg-info/entry_points.txt +2 -0
- asterocr-1.0.0/asterocr.egg-info/requires.txt +11 -0
- asterocr-1.0.0/asterocr.egg-info/top_level.txt +2 -0
- asterocr-1.0.0/pyproject.toml +54 -0
- asterocr-1.0.0/setup.cfg +4 -0
asterocr-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asterocr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AsterOCR: High-Performance Non-Autoregressive Vietnamese Optical Character Recognition Engine
|
|
5
|
+
Author-email: asterxlabs <contact@asterxlabs.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://huggingface.co/asterxlabs/asternet-vietnamese-ocr
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/asterxlabs/asterOCR/issues
|
|
9
|
+
Project-URL: Demo Space, https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo
|
|
10
|
+
Keywords: ocr,vietnamese,document-ai,text-recognition,onnx,deep-learning
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: numpy>=1.20.0
|
|
24
|
+
Requires-Dist: Pillow>=8.0.0
|
|
25
|
+
Requires-Dist: onnxruntime>=1.14.0
|
|
26
|
+
Requires-Dist: huggingface_hub>=0.19.0
|
|
27
|
+
Provides-Extra: torch
|
|
28
|
+
Requires-Dist: torch>=2.0.0; extra == "torch"
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: torch>=2.0.0; extra == "all"
|
|
31
|
+
Requires-Dist: gradio>=4.0.0; extra == "all"
|
|
32
|
+
|
|
33
|
+
# AsterOCR 🇻🇳
|
|
34
|
+
|
|
35
|
+
**AsterOCR** is a state-of-the-art, ultra-fast neural Vietnamese Optical Character Recognition (OCR) engine. Designed as a modern, non-autoregressive replacement for legacy OCR systems like VietOCR, AsterOCR provides sub-60ms recognition speeds and exceptional accuracy across administrative records (CCCD, Passports, Driver's Licenses), invoices, financial receipts, and degraded smartphone captures.
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/asterocr/)
|
|
38
|
+
[](https://huggingface.co/asterxlabs/asternet-vietnamese-ocr)
|
|
39
|
+
[](https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ⚡ Quickstart (Python)
|
|
45
|
+
|
|
46
|
+
### 1. Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install asterocr
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
*(No heavy PyTorch installation required! Uses high-performance ONNX Runtime by default.)*
|
|
53
|
+
|
|
54
|
+
### 2. Python API
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from asterocr import AsterOCR
|
|
58
|
+
|
|
59
|
+
# Initialize engine (automatically downloads weights on first run)
|
|
60
|
+
ocr = AsterOCR()
|
|
61
|
+
|
|
62
|
+
# Recognize text from file path, PIL Image, or bytes
|
|
63
|
+
text = ocr.predict("path/to/invoice.jpg")
|
|
64
|
+
print(text)
|
|
65
|
+
# Output: CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. Command Line Interface (CLI)
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
asterocr path/to/image.jpg
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🌐 Cross-Platform Distribution Ecosystem
|
|
77
|
+
|
|
78
|
+
AsterOCR models are exported in universal **ONNX format** (~40MB) hosted on Hugging Face Hub:
|
|
79
|
+
|
|
80
|
+
| Platform | Package | Engine | Status |
|
|
81
|
+
| :--- | :--- | :--- | :--- |
|
|
82
|
+
| **Python** | `asterocr` | `onnxruntime` / `torch` | ✅ Ready (PyPI) |
|
|
83
|
+
| **Node.js** | `@asterxlabs/asterocr` | `onnxruntime-node` | ✅ Ready (npm) |
|
|
84
|
+
| **Flutter** | `asterocr_flutter` | `onnxruntime_flutter` | ✅ Ready (pub.dev) |
|
|
85
|
+
| **Hugging Face** | `asterxlabs/asternet-vietnamese-ocr` | Model Hub & Space Demo | ✅ Active |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 📊 Benchmarks vs Industry Standard
|
|
90
|
+
|
|
91
|
+
| Metric | VietOCR (Transformer) | AsterOCR v1.0 | Improvement |
|
|
92
|
+
| :--- | :---: | :---: | :---: |
|
|
93
|
+
| **Exact Match (EM)** | 71.4% | **86.0%** | **+14.6%** |
|
|
94
|
+
| **Character Error Rate (CER)** | 7.82% | **3.45%** | **-55.8%** |
|
|
95
|
+
| **CPU Inference Latency** | 340ms | **60.2ms** | **5.6x faster** |
|
|
96
|
+
| **Model Size** | 145 MB | **40.76 MB** | **72% smaller** |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 📜 License
|
|
101
|
+
|
|
102
|
+
MIT License © 2026 asterxlabs.
|
asterocr-1.0.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# AsterOCR 🇻🇳
|
|
2
|
+
|
|
3
|
+
**AsterOCR** is a state-of-the-art, ultra-fast neural Vietnamese Optical Character Recognition (OCR) engine. Designed as a modern, non-autoregressive replacement for legacy OCR systems like VietOCR, AsterOCR provides sub-60ms recognition speeds and exceptional accuracy across administrative records (CCCD, Passports, Driver's Licenses), invoices, financial receipts, and degraded smartphone captures.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/asterocr/)
|
|
6
|
+
[](https://huggingface.co/asterxlabs/asternet-vietnamese-ocr)
|
|
7
|
+
[](https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ⚡ Quickstart (Python)
|
|
13
|
+
|
|
14
|
+
### 1. Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install asterocr
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
*(No heavy PyTorch installation required! Uses high-performance ONNX Runtime by default.)*
|
|
21
|
+
|
|
22
|
+
### 2. Python API
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from asterocr import AsterOCR
|
|
26
|
+
|
|
27
|
+
# Initialize engine (automatically downloads weights on first run)
|
|
28
|
+
ocr = AsterOCR()
|
|
29
|
+
|
|
30
|
+
# Recognize text from file path, PIL Image, or bytes
|
|
31
|
+
text = ocr.predict("path/to/invoice.jpg")
|
|
32
|
+
print(text)
|
|
33
|
+
# Output: CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. Command Line Interface (CLI)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
asterocr path/to/image.jpg
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🌐 Cross-Platform Distribution Ecosystem
|
|
45
|
+
|
|
46
|
+
AsterOCR models are exported in universal **ONNX format** (~40MB) hosted on Hugging Face Hub:
|
|
47
|
+
|
|
48
|
+
| Platform | Package | Engine | Status |
|
|
49
|
+
| :--- | :--- | :--- | :--- |
|
|
50
|
+
| **Python** | `asterocr` | `onnxruntime` / `torch` | ✅ Ready (PyPI) |
|
|
51
|
+
| **Node.js** | `@asterxlabs/asterocr` | `onnxruntime-node` | ✅ Ready (npm) |
|
|
52
|
+
| **Flutter** | `asterocr_flutter` | `onnxruntime_flutter` | ✅ Ready (pub.dev) |
|
|
53
|
+
| **Hugging Face** | `asterxlabs/asternet-vietnamese-ocr` | Model Hub & Space Demo | ✅ Active |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📊 Benchmarks vs Industry Standard
|
|
58
|
+
|
|
59
|
+
| Metric | VietOCR (Transformer) | AsterOCR v1.0 | Improvement |
|
|
60
|
+
| :--- | :---: | :---: | :---: |
|
|
61
|
+
| **Exact Match (EM)** | 71.4% | **86.0%** | **+14.6%** |
|
|
62
|
+
| **Character Error Rate (CER)** | 7.82% | **3.45%** | **-55.8%** |
|
|
63
|
+
| **CPU Inference Latency** | 340ms | **60.2ms** | **5.6x faster** |
|
|
64
|
+
| **Model Size** | 145 MB | **40.76 MB** | **72% smaller** |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📜 License
|
|
69
|
+
|
|
70
|
+
MIT License © 2026 asterxlabs.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR - Modern High-Performance Vietnamese OCR Engine
|
|
3
|
+
Designed to replace VietOCR with non-autoregressive CTC sequence modeling,
|
|
4
|
+
dynamic synthetic data generation, and sub-second recognition.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
|
|
9
|
+
from asterocr.pipeline import AsterOCR
|
|
10
|
+
from asterocr.vocab import default_vocab
|
|
11
|
+
|
|
12
|
+
__all__ = ["AsterOCR", "default_vocab", "__version__"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Line Interface for AsterOCR
|
|
3
|
+
Usage:
|
|
4
|
+
asterocr path/to/image.jpg
|
|
5
|
+
asterocr path/to/image.jpg --backend onnx
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="asterocr",
|
|
15
|
+
description="AsterOCR: Ultra-fast Vietnamese Text Recognition CLI"
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("image", help="Path to input image file (jpg, png, webp)")
|
|
18
|
+
parser.add_argument("--backend", default="onnx", choices=["onnx", "torch"], help="Inference backend (default: onnx)")
|
|
19
|
+
parser.add_argument("--device", default="cpu", help="Computation device (cpu or cuda)")
|
|
20
|
+
parser.add_argument("--model-path", default=None, help="Explicit path to asternet.onnx or asternet.pth")
|
|
21
|
+
parser.add_argument("--version", action="version", version="AsterOCR v1.0.0")
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
if not os.path.exists(args.image):
|
|
26
|
+
print(f"[-] Lỗi: File ảnh không tồn tại: {args.image}", file=sys.stderr)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
from asterocr.pipeline import AsterOCR
|
|
30
|
+
ocr = AsterOCR(model_path=args.model_path, backend=args.backend, device=args.device)
|
|
31
|
+
result = ocr.predict(args.image)
|
|
32
|
+
print(result)
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR vs VietOCR Comparison Engine
|
|
3
|
+
Provides simultaneous side-by-side inference, precision comparison,
|
|
4
|
+
latency benchmark, and full page document parsing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import base64
|
|
13
|
+
import torch
|
|
14
|
+
import cv2
|
|
15
|
+
import numpy as np
|
|
16
|
+
from PIL import Image
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
19
|
+
|
|
20
|
+
from asterocr.inference.predictor import AsterPredictor
|
|
21
|
+
from asterocr.trainer.metrics import calculate_cer, calculate_wer, levenshtein_distance
|
|
22
|
+
from vietocr.tool.config import Cfg
|
|
23
|
+
from vietocr.tool.predictor import Predictor
|
|
24
|
+
from rapidocr_onnxruntime import RapidOCR
|
|
25
|
+
|
|
26
|
+
class OCRComparator:
|
|
27
|
+
_instance = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_instance(cls):
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
cls._instance = OCRComparator()
|
|
33
|
+
return cls._instance
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
37
|
+
print(f"[*] OCRComparator khởi tạo trên thiết bị: {self.device}")
|
|
38
|
+
|
|
39
|
+
# 1. AsterNet SOTA Predictor
|
|
40
|
+
model_path = os.path.join(os.path.dirname(__file__), "..", "models", "asternet_best.pth")
|
|
41
|
+
if not os.path.exists(model_path):
|
|
42
|
+
model_path = os.path.join("models", "asternet_best.pth")
|
|
43
|
+
self.model_path = os.path.abspath(model_path)
|
|
44
|
+
self.model_mtime = os.path.getmtime(self.model_path) if os.path.exists(self.model_path) else 0
|
|
45
|
+
self.aster_pred = AsterPredictor(model_path=self.model_path, device=self.device)
|
|
46
|
+
print(f"[+] AsterNet nạp thành công từ: {self.model_path}")
|
|
47
|
+
|
|
48
|
+
# 2. VietOCR Predictor
|
|
49
|
+
config = Cfg.load_config_from_name('vgg_transformer')
|
|
50
|
+
config['device'] = self.device
|
|
51
|
+
config['predictor']['beamsearch'] = False
|
|
52
|
+
self.viet_pred = Predictor(config)
|
|
53
|
+
print("[+] VietOCR (vgg_transformer) nạp thành công!")
|
|
54
|
+
|
|
55
|
+
# 3. Detector cho Full-page document
|
|
56
|
+
try:
|
|
57
|
+
self.detector = RapidOCR(use_cls=False, use_rec=False)
|
|
58
|
+
print("[+] RapidOCR text line detector sẵn sàng!")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"[!] Cảnh báo khởi tạo RapidOCR: {e}")
|
|
61
|
+
self.detector = None
|
|
62
|
+
|
|
63
|
+
# Benchmark running state
|
|
64
|
+
self.benchmark_state = {
|
|
65
|
+
"status": "idle",
|
|
66
|
+
"progress": 0,
|
|
67
|
+
"total": 0,
|
|
68
|
+
"current": 0,
|
|
69
|
+
"dataset_name": "",
|
|
70
|
+
"metrics": {},
|
|
71
|
+
"results": [],
|
|
72
|
+
"error": None
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def check_and_reload(self):
|
|
76
|
+
"""Hot-reloads AsterNet if weights file has been updated by training."""
|
|
77
|
+
if os.path.exists(self.model_path):
|
|
78
|
+
cur_mtime = os.path.getmtime(self.model_path)
|
|
79
|
+
if cur_mtime > self.model_mtime:
|
|
80
|
+
try:
|
|
81
|
+
print(f"[*] Phát hiện weights mới ({time.ctime(cur_mtime)}), đang tự động nạp lại AsterNet...")
|
|
82
|
+
from asterocr.inference.predictor import AsterPredictor
|
|
83
|
+
self.aster_pred = AsterPredictor(model_path=self.model_path, device=self.device)
|
|
84
|
+
self.model_mtime = cur_mtime
|
|
85
|
+
print("[+] Tự động nạp lại AsterNet thành công!")
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"[!] Lỗi khi nạp lại AsterNet: {e}")
|
|
88
|
+
|
|
89
|
+
def compare_crop(self, pil_img, gt_text=None):
|
|
90
|
+
"""
|
|
91
|
+
Runs both models on a single line crop and returns side-by-side metrics.
|
|
92
|
+
"""
|
|
93
|
+
self.check_and_reload()
|
|
94
|
+
# Warmup/ensure RGB
|
|
95
|
+
if pil_img.mode != "RGB":
|
|
96
|
+
pil_img = pil_img.convert("RGB")
|
|
97
|
+
|
|
98
|
+
# AsterNet inference
|
|
99
|
+
t0 = time.perf_counter()
|
|
100
|
+
aster_text = self.aster_pred.predict(pil_img)
|
|
101
|
+
aster_lat = (time.perf_counter() - t0) * 1000
|
|
102
|
+
|
|
103
|
+
# VietOCR inference
|
|
104
|
+
t0 = time.perf_counter()
|
|
105
|
+
viet_text = self.viet_pred.predict(pil_img)
|
|
106
|
+
viet_lat = (time.perf_counter() - t0) * 1000
|
|
107
|
+
|
|
108
|
+
speedup = viet_lat / max(0.001, aster_lat)
|
|
109
|
+
|
|
110
|
+
res = {
|
|
111
|
+
"aster": {
|
|
112
|
+
"text": aster_text,
|
|
113
|
+
"latency_ms": round(aster_lat, 2),
|
|
114
|
+
"fps": round(1000.0 / max(1.0, aster_lat), 1)
|
|
115
|
+
},
|
|
116
|
+
"vietocr": {
|
|
117
|
+
"text": viet_text,
|
|
118
|
+
"latency_ms": round(viet_lat, 2),
|
|
119
|
+
"fps": round(1000.0 / max(1.0, viet_lat), 1)
|
|
120
|
+
},
|
|
121
|
+
"speedup": round(speedup, 2)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if gt_text is not None and len(gt_text.strip()) > 0:
|
|
125
|
+
gt = gt_text.strip()
|
|
126
|
+
aster_cer = calculate_cer([gt], [aster_text]) * 100
|
|
127
|
+
aster_wer = calculate_wer([gt], [aster_text]) * 100
|
|
128
|
+
viet_cer = calculate_cer([gt], [viet_text]) * 100
|
|
129
|
+
viet_wer = calculate_wer([gt], [viet_text]) * 100
|
|
130
|
+
|
|
131
|
+
res["gt"] = gt
|
|
132
|
+
res["aster"]["cer"] = round(aster_cer, 2)
|
|
133
|
+
res["aster"]["wer"] = round(aster_wer, 2)
|
|
134
|
+
res["aster"]["match"] = (aster_text.strip() == gt)
|
|
135
|
+
|
|
136
|
+
res["vietocr"]["cer"] = round(viet_cer, 2)
|
|
137
|
+
res["vietocr"]["wer"] = round(viet_wer, 2)
|
|
138
|
+
res["vietocr"]["match"] = (viet_text.strip() == gt)
|
|
139
|
+
|
|
140
|
+
if aster_cer < viet_cer:
|
|
141
|
+
winner = "asternet"
|
|
142
|
+
elif viet_cer < aster_cer:
|
|
143
|
+
winner = "vietocr"
|
|
144
|
+
else:
|
|
145
|
+
winner = "draw"
|
|
146
|
+
res["accuracy_winner"] = winner
|
|
147
|
+
|
|
148
|
+
return res
|
|
149
|
+
|
|
150
|
+
def compare_document_page(self, np_img_bgr):
|
|
151
|
+
"""
|
|
152
|
+
Detects all text lines on a document page and recognizes with both models.
|
|
153
|
+
Returns visual annotated image base64 and line-by-line comparison data.
|
|
154
|
+
"""
|
|
155
|
+
self.check_and_reload()
|
|
156
|
+
if self.detector is None:
|
|
157
|
+
return {"error": "RapidOCR detector không khả dụng."}
|
|
158
|
+
|
|
159
|
+
boxes, _ = self.detector(np_img_bgr)
|
|
160
|
+
if not boxes:
|
|
161
|
+
return {"lines": [], "annotated_image": None, "total_lines": 0}
|
|
162
|
+
|
|
163
|
+
h_img, w_img = np_img_bgr.shape[:2]
|
|
164
|
+
lines_info = []
|
|
165
|
+
pil_crops = []
|
|
166
|
+
box_coords = []
|
|
167
|
+
|
|
168
|
+
# Sort boxes top to bottom
|
|
169
|
+
sorted_boxes = sorted(boxes, key=lambda b: np.min(np.array(b[0] if isinstance(b[0], list) and len(b) == 2 else b)[:, 1]))
|
|
170
|
+
|
|
171
|
+
for idx, b in enumerate(sorted_boxes):
|
|
172
|
+
pts = np.array(b[0] if isinstance(b[0], list) and len(b) == 2 else b)
|
|
173
|
+
x_min, y_min = np.min(pts, axis=0)
|
|
174
|
+
x_max, y_max = np.max(pts, axis=0)
|
|
175
|
+
|
|
176
|
+
pad = 2
|
|
177
|
+
x1 = max(0, int(x_min) - pad)
|
|
178
|
+
y1 = max(0, int(y_min) - pad)
|
|
179
|
+
x2 = min(w_img, int(x_max) + pad)
|
|
180
|
+
y2 = min(h_img, int(y_max) + pad)
|
|
181
|
+
|
|
182
|
+
if y2 - y1 < 6 or x2 - x1 < 6:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
crop = np_img_bgr[y1:y2, x1:x2]
|
|
186
|
+
if crop.size == 0:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
crop_pil = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
|
|
190
|
+
pil_crops.append(crop_pil)
|
|
191
|
+
box_coords.append((x1, y1, x2, y2))
|
|
192
|
+
|
|
193
|
+
# Annotate image
|
|
194
|
+
annotated = np_img_bgr.copy()
|
|
195
|
+
|
|
196
|
+
# Batch inference
|
|
197
|
+
t0 = time.perf_counter()
|
|
198
|
+
aster_preds = [self.aster_pred.predict(c) for c in pil_crops]
|
|
199
|
+
aster_total_time = (time.perf_counter() - t0) * 1000
|
|
200
|
+
|
|
201
|
+
t0 = time.perf_counter()
|
|
202
|
+
viet_preds = [self.viet_pred.predict(c) for c in pil_crops]
|
|
203
|
+
viet_total_time = (time.perf_counter() - t0) * 1000
|
|
204
|
+
|
|
205
|
+
for i, (coords, ap, vp) in enumerate(zip(box_coords, aster_preds, viet_preds)):
|
|
206
|
+
x1, y1, x2, y2 = coords
|
|
207
|
+
# Draw bbox: Cyan for line
|
|
208
|
+
cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 255), 1)
|
|
209
|
+
cv2.putText(annotated, str(i + 1), (x1, max(12, y1 - 2)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1)
|
|
210
|
+
|
|
211
|
+
# Crop thumbnail base64
|
|
212
|
+
crop_rgb = pil_crops[i]
|
|
213
|
+
buf = io.BytesIO()
|
|
214
|
+
crop_rgb.save(buf, format="JPEG", quality=85)
|
|
215
|
+
crop_b64 = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
|
216
|
+
|
|
217
|
+
lines_info.append({
|
|
218
|
+
"line_id": i + 1,
|
|
219
|
+
"bbox": [x1, y1, x2, y2],
|
|
220
|
+
"crop_thumbnail": crop_b64,
|
|
221
|
+
"aster_text": ap,
|
|
222
|
+
"viet_text": vp,
|
|
223
|
+
"is_identical": (ap.strip() == vp.strip())
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
# Encode annotated image
|
|
227
|
+
_, enc_jpg = cv2.imencode(".jpg", annotated, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
|
228
|
+
annotated_b64 = "data:image/jpeg;base64," + base64.b64encode(enc_jpg).decode()
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"total_lines": len(lines_info),
|
|
232
|
+
"aster_total_ms": round(aster_total_time, 2),
|
|
233
|
+
"viet_total_ms": round(viet_total_time, 2),
|
|
234
|
+
"speedup": round(viet_total_time / max(0.001, aster_total_time), 2),
|
|
235
|
+
"lines": lines_info,
|
|
236
|
+
"annotated_image": annotated_b64
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def start_batch_benchmark(self, dataset_name, max_samples=100):
|
|
240
|
+
"""
|
|
241
|
+
Runs benchmark in a background thread and updates self.benchmark_state.
|
|
242
|
+
"""
|
|
243
|
+
import threading
|
|
244
|
+
if self.benchmark_state["status"] == "running":
|
|
245
|
+
return False, "Benchmark đang chạy. Hãy đợi hoặc dừng phiên hiện tại."
|
|
246
|
+
|
|
247
|
+
self.benchmark_state = {
|
|
248
|
+
"status": "running",
|
|
249
|
+
"progress": 0,
|
|
250
|
+
"total": 0,
|
|
251
|
+
"current": 0,
|
|
252
|
+
"dataset_name": dataset_name,
|
|
253
|
+
"metrics": {},
|
|
254
|
+
"results": [],
|
|
255
|
+
"error": None
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
t = threading.Thread(target=self._run_benchmark_worker, args=(dataset_name, max_samples), daemon=True)
|
|
259
|
+
t.start()
|
|
260
|
+
return True, "Benchmark đã bắt đầu."
|
|
261
|
+
|
|
262
|
+
def _run_benchmark_worker(self, dataset_name, max_samples):
|
|
263
|
+
try:
|
|
264
|
+
# Hot-reload linguistics and weights before running benchmark
|
|
265
|
+
try:
|
|
266
|
+
import importlib
|
|
267
|
+
import asterocr.inference.viet_linguistics as vl
|
|
268
|
+
importlib.reload(vl)
|
|
269
|
+
self.check_and_reload()
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print(f"[!] Warning reloading modules: {e}")
|
|
272
|
+
|
|
273
|
+
samples = []
|
|
274
|
+
if dataset_name == "mega_test":
|
|
275
|
+
labels_file = os.path.join("dataset_mega_test", "labels.json")
|
|
276
|
+
if os.path.exists(labels_file):
|
|
277
|
+
with open(labels_file, "r", encoding="utf-8") as f:
|
|
278
|
+
meta = json.load(f)
|
|
279
|
+
for item in meta:
|
|
280
|
+
img_p = os.path.join("dataset_mega_test", item["image_path"])
|
|
281
|
+
if os.path.exists(img_p):
|
|
282
|
+
img = Image.open(img_p).convert("RGB")
|
|
283
|
+
samples.append((img, item["text"], os.path.basename(img_p)))
|
|
284
|
+
elif dataset_name in ["real_scans", "unified", "viet_real"]:
|
|
285
|
+
if dataset_name == "viet_real":
|
|
286
|
+
lmdb_path = "dataset_viet_real.lmdb"
|
|
287
|
+
elif dataset_name == "unified":
|
|
288
|
+
lmdb_path = "dataset_unified.lmdb"
|
|
289
|
+
else:
|
|
290
|
+
lmdb_path = "dataset_real_scans.lmdb"
|
|
291
|
+
if os.path.exists(lmdb_path):
|
|
292
|
+
import lmdb
|
|
293
|
+
env = lmdb.open(lmdb_path, readonly=True, max_readers=1)
|
|
294
|
+
with env.begin() as txn:
|
|
295
|
+
num_val = txn.get(b'num-samples')
|
|
296
|
+
total_db = int(num_val.decode('utf-8')) if num_val else 0
|
|
297
|
+
step = max(1, total_db // max(1, max_samples))
|
|
298
|
+
for i in range(1, total_db, step):
|
|
299
|
+
if len(samples) >= max_samples:
|
|
300
|
+
break
|
|
301
|
+
img_key = f"image-{i:09d}".encode()
|
|
302
|
+
lbl_key = f"label-{i:09d}".encode()
|
|
303
|
+
img_b = txn.get(img_key)
|
|
304
|
+
lbl_b = txn.get(lbl_key)
|
|
305
|
+
if img_b and lbl_b:
|
|
306
|
+
try:
|
|
307
|
+
img = Image.open(io.BytesIO(img_b)).convert("RGB")
|
|
308
|
+
txt = lbl_b.decode('utf-8').strip()
|
|
309
|
+
if txt:
|
|
310
|
+
samples.append((img, txt, f"sample_{i:07d}.jpg"))
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
env.close()
|
|
314
|
+
|
|
315
|
+
if not samples:
|
|
316
|
+
self.benchmark_state["status"] = "error"
|
|
317
|
+
self.benchmark_state["error"] = f"Không tìm thấy dữ liệu cho '{dataset_name}'."
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
self.benchmark_state["total"] = len(samples)
|
|
321
|
+
results = []
|
|
322
|
+
gt_all = []
|
|
323
|
+
aster_all = []
|
|
324
|
+
viet_all = []
|
|
325
|
+
|
|
326
|
+
t_aster_total = 0.0
|
|
327
|
+
t_viet_total = 0.0
|
|
328
|
+
|
|
329
|
+
for idx, (img, gt, name) in enumerate(samples):
|
|
330
|
+
# AsterNet
|
|
331
|
+
t0 = time.perf_counter()
|
|
332
|
+
ap = self.aster_pred.predict(img)
|
|
333
|
+
t_a = (time.perf_counter() - t0) * 1000
|
|
334
|
+
t_aster_total += t_a
|
|
335
|
+
|
|
336
|
+
# VietOCR
|
|
337
|
+
t0 = time.perf_counter()
|
|
338
|
+
vp = self.viet_pred.predict(img)
|
|
339
|
+
t_v = (time.perf_counter() - t0) * 1000
|
|
340
|
+
t_viet_total += t_v
|
|
341
|
+
|
|
342
|
+
gt_all.append(gt)
|
|
343
|
+
aster_all.append(ap)
|
|
344
|
+
viet_all.append(vp)
|
|
345
|
+
|
|
346
|
+
# Thumbnail
|
|
347
|
+
buf = io.BytesIO()
|
|
348
|
+
img.save(buf, format="JPEG", quality=80)
|
|
349
|
+
img_b64 = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
|
350
|
+
|
|
351
|
+
# Per-sample metrics
|
|
352
|
+
a_match = (ap.strip() == gt.strip())
|
|
353
|
+
v_match = (vp.strip() == gt.strip())
|
|
354
|
+
|
|
355
|
+
results.append({
|
|
356
|
+
"id": idx + 1,
|
|
357
|
+
"name": name,
|
|
358
|
+
"thumbnail": img_b64,
|
|
359
|
+
"gt": gt,
|
|
360
|
+
"aster_pred": ap,
|
|
361
|
+
"viet_pred": vp,
|
|
362
|
+
"aster_match": a_match,
|
|
363
|
+
"viet_match": v_match,
|
|
364
|
+
"aster_ms": round(t_a, 1),
|
|
365
|
+
"viet_ms": round(t_v, 1),
|
|
366
|
+
"aster_cer": round(calculate_cer([gt], [ap]) * 100, 1),
|
|
367
|
+
"viet_cer": round(calculate_cer([gt], [vp]) * 100, 1)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
self.benchmark_state["current"] = idx + 1
|
|
371
|
+
self.benchmark_state["progress"] = round((idx + 1) / len(samples) * 100, 1)
|
|
372
|
+
|
|
373
|
+
# Final metrics
|
|
374
|
+
N = len(samples)
|
|
375
|
+
aster_cer = round(calculate_cer(gt_all, aster_all) * 100, 2)
|
|
376
|
+
aster_wer = round(calculate_wer(gt_all, aster_all) * 100, 2)
|
|
377
|
+
aster_exact = round(sum(1 for g, p in zip(gt_all, aster_all) if g.strip() == p.strip()) / N * 100, 2)
|
|
378
|
+
aster_fps = round(N / (t_aster_total / 1000.0), 1)
|
|
379
|
+
aster_avg_ms = round(t_aster_total / N, 2)
|
|
380
|
+
|
|
381
|
+
viet_cer = round(calculate_cer(gt_all, viet_all) * 100, 2)
|
|
382
|
+
viet_wer = round(calculate_wer(gt_all, viet_all) * 100, 2)
|
|
383
|
+
viet_exact = round(sum(1 for g, p in zip(gt_all, viet_all) if g.strip() == p.strip()) / N * 100, 2)
|
|
384
|
+
viet_fps = round(N / (t_viet_total / 1000.0), 1)
|
|
385
|
+
viet_avg_ms = round(t_viet_total / N, 2)
|
|
386
|
+
|
|
387
|
+
speedup = round(aster_fps / max(0.01, viet_fps), 2)
|
|
388
|
+
|
|
389
|
+
self.benchmark_state["metrics"] = {
|
|
390
|
+
"aster": {
|
|
391
|
+
"cer": aster_cer,
|
|
392
|
+
"wer": aster_wer,
|
|
393
|
+
"exact": aster_exact,
|
|
394
|
+
"fps": aster_fps,
|
|
395
|
+
"avg_ms": aster_avg_ms
|
|
396
|
+
},
|
|
397
|
+
"vietocr": {
|
|
398
|
+
"cer": viet_cer,
|
|
399
|
+
"wer": viet_wer,
|
|
400
|
+
"exact": viet_exact,
|
|
401
|
+
"fps": viet_fps,
|
|
402
|
+
"avg_ms": viet_avg_ms
|
|
403
|
+
},
|
|
404
|
+
"speedup": speedup,
|
|
405
|
+
"winner": "AsterNet" if aster_cer <= viet_cer else "VietOCR"
|
|
406
|
+
}
|
|
407
|
+
self.benchmark_state["results"] = results
|
|
408
|
+
self.benchmark_state["status"] = "finished"
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
self.benchmark_state["status"] = "error"
|
|
412
|
+
self.benchmark_state["error"] = str(e)
|