asterocr 1.0.0__py3-none-any.whl
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/__init__.py +12 -0
- asterocr/cli.py +35 -0
- asterocr/comparator.py +412 -0
- asterocr/dataset/export_real_zip.py +117 -0
- asterocr/dataset/loader.py +371 -0
- asterocr/dataset/unified_packer.py +402 -0
- asterocr/download_dataset.py +93 -0
- asterocr/inference/decoder.py +302 -0
- asterocr/inference/lexicon.py +136 -0
- asterocr/inference/predictor.py +205 -0
- asterocr/inference/viet_linguistics.py +340 -0
- asterocr/kaggle_bridge/__init__.py +8 -0
- asterocr/kaggle_bridge/chunker.py +252 -0
- asterocr/kaggle_bridge/colab_auto_runner.py +380 -0
- asterocr/kaggle_bridge/kaggle_worker.py +1911 -0
- asterocr/kaggle_bridge/launcher.py +245 -0
- asterocr/kaggle_bridge/tunnel.py +190 -0
- asterocr/models/backbone.py +102 -0
- asterocr/models/language.py +104 -0
- asterocr/models/model.py +113 -0
- asterocr/models/sequence.py +34 -0
- asterocr/pipeline.py +179 -0
- asterocr/studio.py +371 -0
- asterocr/synthesizer/corpus.py +378 -0
- asterocr/synthesizer/font_manager.py +170 -0
- asterocr/synthesizer/generator.py +205 -0
- asterocr/synthesizer/handwriting_generator.py +420 -0
- asterocr/synthesizer/mega_generator.py +527 -0
- asterocr/trainer/metrics.py +53 -0
- asterocr/trainer/trainer.py +1295 -0
- asterocr/vocab.py +175 -0
- asterocr-1.0.0.dist-info/METADATA +102 -0
- asterocr-1.0.0.dist-info/RECORD +36 -0
- asterocr-1.0.0.dist-info/WHEEL +5 -0
- asterocr-1.0.0.dist-info/entry_points.txt +2 -0
- asterocr-1.0.0.dist-info/top_level.txt +1 -0
asterocr/__init__.py
ADDED
|
@@ -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__"]
|
asterocr/cli.py
ADDED
|
@@ -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()
|
asterocr/comparator.py
ADDED
|
@@ -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)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Fast Real Dataset Exporter
|
|
3
|
+
Extracts unexported real document scan samples (en_00, en_01, random) from ocr_dataset.zip
|
|
4
|
+
and packages them into dataset_real_scans.lmdb for seamless streaming to Dual GPUs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import io
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
import zipfile
|
|
13
|
+
import lmdb
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
try:
|
|
18
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
19
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def export_real_zip_to_lmdb(
|
|
24
|
+
zip_path="dataset_download/ocr_dataset.zip",
|
|
25
|
+
index_path="dataset_download/ocr_dataset_index.json",
|
|
26
|
+
output_dir="dataset_real_scans.lmdb",
|
|
27
|
+
target_height=32,
|
|
28
|
+
categories=("en_00", "en_01", "random"),
|
|
29
|
+
batch_commit=5000
|
|
30
|
+
):
|
|
31
|
+
if not os.path.exists(zip_path):
|
|
32
|
+
print(f"[!] Không tìm thấy tệp zip: {zip_path}")
|
|
33
|
+
return False
|
|
34
|
+
if not os.path.exists(index_path):
|
|
35
|
+
print(f"[!] Không tìm thấy tệp index: {index_path}")
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
print(f"[*] Đang tải danh mục từ {index_path}...")
|
|
39
|
+
with open(index_path, "r", encoding="utf-8") as f:
|
|
40
|
+
all_entries = json.load(f)
|
|
41
|
+
|
|
42
|
+
# Filter targets
|
|
43
|
+
target_entries = [
|
|
44
|
+
item for item in all_entries
|
|
45
|
+
if any(item[0].startswith(cat + "/") for cat in categories)
|
|
46
|
+
]
|
|
47
|
+
total_target = len(target_entries)
|
|
48
|
+
print(f"[+] Tìm thấy {total_target:,} mẫu ảnh thật thuộc các nhóm: {categories}")
|
|
49
|
+
|
|
50
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
51
|
+
# 20GB map size ceiling
|
|
52
|
+
env = lmdb.open(output_dir, map_size=25 * 1024 * 1024 * 1024, sync=False)
|
|
53
|
+
|
|
54
|
+
t0 = time.time()
|
|
55
|
+
written_count = 0
|
|
56
|
+
txn = env.begin(write=True)
|
|
57
|
+
|
|
58
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
59
|
+
for idx, (img_rel, txt_rel) in enumerate(target_entries, 1):
|
|
60
|
+
try:
|
|
61
|
+
img_data = zf.read(img_rel)
|
|
62
|
+
txt_data = zf.read(txt_rel).decode("utf-8", errors="ignore").strip()
|
|
63
|
+
if not txt_data:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# Resize to target height 32
|
|
67
|
+
im = Image.open(io.BytesIO(img_data)).convert("RGB")
|
|
68
|
+
w, h = im.size
|
|
69
|
+
if w < 4 or h < 4:
|
|
70
|
+
continue
|
|
71
|
+
new_w = max(16, int((float(target_height) / max(1, h)) * w))
|
|
72
|
+
im_res = im.resize((new_w, target_height), Image.BILINEAR)
|
|
73
|
+
|
|
74
|
+
buf = io.BytesIO()
|
|
75
|
+
im_res.save(buf, format="JPEG", quality=90)
|
|
76
|
+
jpeg_bytes = buf.getvalue()
|
|
77
|
+
|
|
78
|
+
written_count += 1
|
|
79
|
+
img_key = f"image-{written_count:09d}".encode("utf-8")
|
|
80
|
+
lbl_key = f"label-{written_count:09d}".encode("utf-8")
|
|
81
|
+
|
|
82
|
+
txn.put(img_key, jpeg_bytes)
|
|
83
|
+
txn.put(lbl_key, txt_data.encode("utf-8"))
|
|
84
|
+
|
|
85
|
+
if written_count % batch_commit == 0:
|
|
86
|
+
txn.commit()
|
|
87
|
+
txn = env.begin(write=True)
|
|
88
|
+
elapsed = time.time() - t0
|
|
89
|
+
sps = written_count / max(0.1, elapsed)
|
|
90
|
+
print(f"--> Đã xuất: {written_count:,}/{total_target:,} mẫu ({written_count/total_target*100:.1f}%) | {sps:.0f} mẫu/s")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Finalize
|
|
96
|
+
txn.put(b"num-samples", str(written_count).encode("utf-8"))
|
|
97
|
+
txn.commit()
|
|
98
|
+
env.sync()
|
|
99
|
+
env.close()
|
|
100
|
+
|
|
101
|
+
total_time = time.time() - t0
|
|
102
|
+
info = {
|
|
103
|
+
"format": "lmdb",
|
|
104
|
+
"num_samples": written_count,
|
|
105
|
+
"categories": list(categories),
|
|
106
|
+
"source": zip_path,
|
|
107
|
+
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
108
|
+
"total_seconds": round(total_time, 1)
|
|
109
|
+
}
|
|
110
|
+
with open(os.path.join(output_dir, "dataset_info.json"), "w", encoding="utf-8") as f:
|
|
111
|
+
json.dump(info, f, indent=2, ensure_ascii=False)
|
|
112
|
+
|
|
113
|
+
print(f"\n[+] HOÀN TẤT ĐÓNG GÓI {written_count:,} MẪU VÀO {output_dir} TRONG {total_time:.1f} GIÂY!")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
export_real_zip_to_lmdb()
|