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
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Extreme Synthetic Data Generator (Mega Generator)
|
|
3
|
+
Generates millions of heavily degraded, real-world dirty/aged document images:
|
|
4
|
+
- Stains: Coffee stains, tea rings, mold spots, deep yellow aging
|
|
5
|
+
- Damage: Paper creases/folds cutting through text, torn borders, staple holes
|
|
6
|
+
- Scanner flaws: Dirty roller vertical streaks, black border shadows, contrast clipping
|
|
7
|
+
- Text distortions: Broken ink lines, ink bleed/blobs, motion blur, irregular handwriting
|
|
8
|
+
- Multi-scale: Tiny to massive fonts, wide/narrow aspect ratio compression
|
|
9
|
+
- Output: Standard images + comprehensive metadata JSON
|
|
10
|
+
- Multi-processing: Utilizes all CPU cores for maximum throughput
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import io
|
|
15
|
+
import sys
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import random
|
|
19
|
+
import math
|
|
20
|
+
import argparse
|
|
21
|
+
from multiprocessing import Pool, cpu_count
|
|
22
|
+
import numpy as np
|
|
23
|
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
|
|
24
|
+
import cv2
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import lmdb
|
|
28
|
+
except ImportError:
|
|
29
|
+
lmdb = None
|
|
30
|
+
|
|
31
|
+
if sys.platform == "win32":
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
34
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# Ensure root in sys.path
|
|
39
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
|
40
|
+
from asterocr.synthesizer.corpus import generate_random_corpus_text
|
|
41
|
+
from asterocr.synthesizer.generator import default_generator, SYSTEM_FONT_DIR
|
|
42
|
+
|
|
43
|
+
class MegaDegradationEngine:
|
|
44
|
+
"""Extreme real-world degradation effects engine."""
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def add_paper_aging_and_stains(img):
|
|
48
|
+
w, h = img.size
|
|
49
|
+
arr = np.array(img, dtype=np.float32)
|
|
50
|
+
|
|
51
|
+
# 1. Base yellow/brown aged tint
|
|
52
|
+
r_scale = random.uniform(0.92, 1.0)
|
|
53
|
+
g_scale = random.uniform(0.80, 0.92)
|
|
54
|
+
b_scale = random.uniform(0.60, 0.78)
|
|
55
|
+
arr[:, :, 0] *= r_scale
|
|
56
|
+
arr[:, :, 1] *= g_scale
|
|
57
|
+
arr[:, :, 2] *= b_scale
|
|
58
|
+
|
|
59
|
+
# 2. Random coffee / tea stains or moisture rings
|
|
60
|
+
if random.random() < 0.6:
|
|
61
|
+
num_stains = random.randint(1, 3)
|
|
62
|
+
for _ in range(num_stains):
|
|
63
|
+
cx, cy = random.randint(0, w), random.randint(0, h)
|
|
64
|
+
rx, ry = random.randint(15, 60), random.randint(10, 30)
|
|
65
|
+
y_idx, x_idx = np.ogrid[:h, :w]
|
|
66
|
+
dist = ((x_idx - cx) / rx) ** 2 + ((y_idx - cy) / ry) ** 2
|
|
67
|
+
mask = np.exp(-dist * 1.5)
|
|
68
|
+
# Stain darkening factor
|
|
69
|
+
stain_color = np.array([random.uniform(0.7, 0.85), random.uniform(0.6, 0.75), random.uniform(0.4, 0.6)])
|
|
70
|
+
for c in range(3):
|
|
71
|
+
arr[:, :, c] = arr[:, :, c] * (1 - mask * (1 - stain_color[c]))
|
|
72
|
+
|
|
73
|
+
# 3. Paper mold / mildew specks
|
|
74
|
+
if random.random() < 0.5:
|
|
75
|
+
num_specks = random.randint(10, 50)
|
|
76
|
+
for _ in range(num_specks):
|
|
77
|
+
sx, sy = random.randint(0, w - 1), random.randint(0, h - 1)
|
|
78
|
+
rad = random.randint(1, 3)
|
|
79
|
+
arr[max(0, sy-rad):min(h, sy+rad), max(0, sx-rad):min(w, sx+rad)] *= random.uniform(0.3, 0.6)
|
|
80
|
+
|
|
81
|
+
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def add_creases_and_folds(img):
|
|
85
|
+
"""Adds horizontal, diagonal or vertical paper fold creases cutting through text."""
|
|
86
|
+
if random.random() > 0.6:
|
|
87
|
+
return img
|
|
88
|
+
|
|
89
|
+
w, h = img.size
|
|
90
|
+
arr = np.array(img, dtype=np.float32)
|
|
91
|
+
|
|
92
|
+
# Line from one edge to another
|
|
93
|
+
x1, y1 = random.randint(0, w // 4), random.randint(0, h)
|
|
94
|
+
x2, y2 = random.randint(w * 3 // 4, w), random.randint(0, h)
|
|
95
|
+
|
|
96
|
+
cv_img = arr.copy()
|
|
97
|
+
# Dark crease shadow
|
|
98
|
+
cv2.line(cv_img, (x1, y1), (x2, y2), (random.randint(50, 90), random.randint(45, 80), random.randint(35, 70)), 1)
|
|
99
|
+
# Highlighted fold edge
|
|
100
|
+
cv2.line(cv_img, (x1, max(0, y1 - 1)), (x2, max(0, y2 - 1)), (255, 255, 240), 1)
|
|
101
|
+
|
|
102
|
+
# Blurring slightly into paper
|
|
103
|
+
cv_img = cv2.GaussianBlur(cv_img, (3, 3), 0.5)
|
|
104
|
+
# Blend back
|
|
105
|
+
alpha = random.uniform(0.4, 0.8)
|
|
106
|
+
arr = arr * (1 - alpha) + cv_img * alpha
|
|
107
|
+
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def add_scanner_streaks_and_borders(img):
|
|
111
|
+
"""Adds vertical roller streaks and dark photocopy borders."""
|
|
112
|
+
w, h = img.size
|
|
113
|
+
arr = np.array(img, dtype=np.float32)
|
|
114
|
+
|
|
115
|
+
# Vertical streaks from defective scan roller
|
|
116
|
+
if random.random() < 0.45:
|
|
117
|
+
num_streaks = random.randint(1, 3)
|
|
118
|
+
for _ in range(num_streaks):
|
|
119
|
+
sx = random.randint(5, w - 5)
|
|
120
|
+
streak_w = random.randint(1, 3)
|
|
121
|
+
dark_val = random.uniform(0.4, 0.75)
|
|
122
|
+
arr[:, max(0, sx - streak_w):min(w, sx + streak_w)] *= dark_val
|
|
123
|
+
|
|
124
|
+
# Black or dark shadow border from photocopier lid
|
|
125
|
+
if random.random() < 0.4:
|
|
126
|
+
border_side = random.choice(["top", "bottom", "left", "right"])
|
|
127
|
+
depth = random.randint(3, 8)
|
|
128
|
+
if border_side == "top":
|
|
129
|
+
arr[:depth, :] *= np.linspace(0.1, 0.9, depth)[:, None, None]
|
|
130
|
+
elif border_side == "bottom":
|
|
131
|
+
arr[-depth:, :] *= np.linspace(0.9, 0.1, depth)[:, None, None]
|
|
132
|
+
elif border_side == "left":
|
|
133
|
+
arr[:, :depth] *= np.linspace(0.1, 0.9, depth)[None, :, None]
|
|
134
|
+
else:
|
|
135
|
+
arr[:, -depth:] *= np.linspace(0.9, 0.1, depth)[None, :, None]
|
|
136
|
+
|
|
137
|
+
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def add_ink_degradations(img):
|
|
141
|
+
"""Broken characters, ink smudge, bleed, heavy erosion / dilation."""
|
|
142
|
+
cv_img = np.array(img)
|
|
143
|
+
degrade_type = random.choice(["erode", "dilate", "salt_pepper", "blur", "clip"])
|
|
144
|
+
|
|
145
|
+
if degrade_type == "erode":
|
|
146
|
+
# Thicker, smudged ink
|
|
147
|
+
k = np.ones((2, 2), np.uint8)
|
|
148
|
+
cv_img = cv2.erode(cv_img, k, iterations=random.randint(1, 2))
|
|
149
|
+
elif degrade_type == "dilate":
|
|
150
|
+
# Faint, broken, dried-out typewriter/pen ink
|
|
151
|
+
k = np.ones((2, 2), np.uint8)
|
|
152
|
+
cv_img = cv2.dilate(cv_img, k, iterations=1)
|
|
153
|
+
elif degrade_type == "salt_pepper":
|
|
154
|
+
# Heavy scanner noise
|
|
155
|
+
noise = np.random.choice([0, 1, 2], size=cv_img.shape[:2], p=[0.02, 0.02, 0.96])
|
|
156
|
+
cv_img[noise == 0] = [random.randint(10, 40)] * 3 # pepper
|
|
157
|
+
cv_img[noise == 1] = [random.randint(220, 255)] * 3 # salt
|
|
158
|
+
elif degrade_type == "blur":
|
|
159
|
+
# Motion or out-of-focus blur
|
|
160
|
+
ksize = random.choice([3, 5])
|
|
161
|
+
cv_img = cv2.GaussianBlur(cv_img, (ksize, ksize), random.uniform(0.5, 1.4))
|
|
162
|
+
else:
|
|
163
|
+
# Extreme contrast clipping (photocopy effect)
|
|
164
|
+
gray = cv2.cvtColor(cv_img, cv2.COLOR_RGB2GRAY)
|
|
165
|
+
_, thresh = cv2.threshold(gray, random.randint(120, 170), 255, cv2.THRESH_BINARY)
|
|
166
|
+
cv_img = cv2.cvtColor(thresh, cv2.COLOR_GRAY2RGB)
|
|
167
|
+
|
|
168
|
+
return Image.fromarray(cv_img)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def add_torn_edge_or_staple(img):
|
|
172
|
+
"""Simulates torn paper borders or rusted staple holes."""
|
|
173
|
+
if random.random() > 0.35:
|
|
174
|
+
return img
|
|
175
|
+
|
|
176
|
+
w, h = img.size
|
|
177
|
+
draw = ImageDraw.Draw(img)
|
|
178
|
+
|
|
179
|
+
if random.random() < 0.5:
|
|
180
|
+
# Rusted staple hole
|
|
181
|
+
hx = random.randint(6, 16)
|
|
182
|
+
hy = random.randint(6, h - 6)
|
|
183
|
+
# Hole center (black) + rust ring (brown)
|
|
184
|
+
draw.ellipse([hx-3, hy-3, hx+3, hy+3], fill=(139, 69, 19))
|
|
185
|
+
draw.ellipse([hx-2, hy-2, hx+2, hy+2], fill=(20, 20, 20))
|
|
186
|
+
else:
|
|
187
|
+
# Jagged torn edge on left or right
|
|
188
|
+
side = random.choice(["left", "right"])
|
|
189
|
+
tear_w = random.randint(4, 10)
|
|
190
|
+
poly = []
|
|
191
|
+
for y in range(0, h, 4):
|
|
192
|
+
x = random.randint(0, tear_w) if side == "left" else random.randint(w - tear_w, w)
|
|
193
|
+
poly.append((x, y))
|
|
194
|
+
if side == "left":
|
|
195
|
+
poly = [(0, 0)] + poly + [(0, h)]
|
|
196
|
+
else:
|
|
197
|
+
poly = [(w, 0)] + poly + [(w, h)]
|
|
198
|
+
draw.polygon(poly, fill=(25, 25, 25))
|
|
199
|
+
|
|
200
|
+
return img
|
|
201
|
+
|
|
202
|
+
class AsterMegaSynthesizer:
|
|
203
|
+
def __init__(self, output_dir="dataset_mega"):
|
|
204
|
+
self.output_dir = output_dir
|
|
205
|
+
self.generator = default_generator
|
|
206
|
+
self.degrader = MegaDegradationEngine()
|
|
207
|
+
|
|
208
|
+
def generate_image_and_text(self, sample_id, text=None, mode="mixed"):
|
|
209
|
+
"""Generates a single extreme-degradation OCR strip image (PIL) and its text."""
|
|
210
|
+
if not text:
|
|
211
|
+
text = generate_random_corpus_text(mode=mode)
|
|
212
|
+
|
|
213
|
+
# 1. Variable Font Size & Aspect ratio
|
|
214
|
+
font_size = random.randint(16, 32)
|
|
215
|
+
font = self.generator.get_random_font(font_size)
|
|
216
|
+
|
|
217
|
+
# 2. Render initial text
|
|
218
|
+
dummy = Image.new("RGB", (1, 1))
|
|
219
|
+
draw_dummy = ImageDraw.Draw(dummy)
|
|
220
|
+
bbox = draw_dummy.textbbox((0, 0), text, font=font)
|
|
221
|
+
tw = max(10, bbox[2] - bbox[0])
|
|
222
|
+
th = max(10, bbox[3] - bbox[1])
|
|
223
|
+
|
|
224
|
+
padx = random.randint(8, 20)
|
|
225
|
+
pady = random.randint(4, 14)
|
|
226
|
+
cw, ch = tw + padx * 2, th + pady * 2
|
|
227
|
+
|
|
228
|
+
bg_style = random.choice(["aged", "scan", "aged", "clean", "grid"])
|
|
229
|
+
canvas = self.generator.generate_background(cw, ch, style=bg_style)
|
|
230
|
+
draw = ImageDraw.Draw(canvas)
|
|
231
|
+
|
|
232
|
+
# Realistic ink colors: faded blue, typewriter purple, charcoal, stamped red
|
|
233
|
+
ink = random.choice([
|
|
234
|
+
(random.randint(15, 40), random.randint(15, 40), random.randint(15, 40)),
|
|
235
|
+
(random.randint(10, 30), random.randint(30, 60), random.randint(100, 160)), # faded blue pen
|
|
236
|
+
(random.randint(60, 90), random.randint(20, 40), random.randint(70, 100)), # violet ink
|
|
237
|
+
(random.randint(70, 110), random.randint(70, 110), random.randint(70, 110)) # faded typewriter
|
|
238
|
+
])
|
|
239
|
+
draw.text((padx - bbox[0], pady - bbox[1]), text, font=font, fill=ink)
|
|
240
|
+
|
|
241
|
+
# 3. Apply cascade of extreme degradation effects
|
|
242
|
+
active_degradations = []
|
|
243
|
+
|
|
244
|
+
# Effect A: Paper aging & stains
|
|
245
|
+
canvas = self.degrader.add_paper_aging_and_stains(canvas)
|
|
246
|
+
active_degradations.append("stains_and_aging")
|
|
247
|
+
|
|
248
|
+
# Effect B: Creases & folds crossing text
|
|
249
|
+
if random.random() < 0.6:
|
|
250
|
+
canvas = self.degrader.add_creases_and_folds(canvas)
|
|
251
|
+
active_degradations.append("paper_creases")
|
|
252
|
+
|
|
253
|
+
# Effect C: Scanner roller streaks & photocopy shadow borders
|
|
254
|
+
if random.random() < 0.55:
|
|
255
|
+
canvas = self.degrader.add_scanner_streaks_and_borders(canvas)
|
|
256
|
+
active_degradations.append("scanner_streaks")
|
|
257
|
+
|
|
258
|
+
# Effect D: Broken ink, smear, salt-pepper, photocopy thresholding
|
|
259
|
+
canvas = self.degrader.add_ink_degradations(canvas)
|
|
260
|
+
active_degradations.append("ink_degradation")
|
|
261
|
+
|
|
262
|
+
# Effect E: Torn edge / staple hole
|
|
263
|
+
if random.random() < 0.4:
|
|
264
|
+
canvas = self.degrader.add_torn_edge_or_staple(canvas)
|
|
265
|
+
active_degradations.append("torn_or_staple")
|
|
266
|
+
|
|
267
|
+
# Effect F: Rotation / Shear (-3.5 to +3.5 deg)
|
|
268
|
+
if random.random() < 0.5:
|
|
269
|
+
angle = random.uniform(-3.5, 3.5)
|
|
270
|
+
canvas = canvas.rotate(angle, resample=Image.BILINEAR, expand=True, fillcolor=(235, 230, 220))
|
|
271
|
+
active_degradations.append("affine_rotation")
|
|
272
|
+
|
|
273
|
+
# 4. Standardize to H=32 preserving aspect ratio
|
|
274
|
+
w_orig, h_orig = canvas.size
|
|
275
|
+
new_w = max(24, int((32.0 / max(1, h_orig)) * w_orig))
|
|
276
|
+
final_img = canvas.resize((new_w, 32), Image.BILINEAR)
|
|
277
|
+
return final_img, text, active_degradations
|
|
278
|
+
|
|
279
|
+
def generate_sample_bytes(self, sample_id, text=None, mode="mixed"):
|
|
280
|
+
"""Generates sample and compresses directly to JPEG bytes in RAM."""
|
|
281
|
+
final_img, text, _ = self.generate_image_and_text(sample_id, text=text, mode=mode)
|
|
282
|
+
|
|
283
|
+
# Fast in-memory JPEG compression via OpenCV SIMD
|
|
284
|
+
img_np = np.array(final_img)
|
|
285
|
+
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
|
286
|
+
quality = random.randint(65, 92)
|
|
287
|
+
_, encoded = cv2.imencode('.jpg', img_bgr, [int(cv2.IMWRITE_JPEG_QUALITY), quality])
|
|
288
|
+
return sample_id, encoded.tobytes(), text
|
|
289
|
+
|
|
290
|
+
def generate_single_sample_file(self, sample_id, text=None, mode="mixed"):
|
|
291
|
+
"""Generates and writes to disk with subfolder sharding to prevent NTFS slowdown."""
|
|
292
|
+
final_img, text, active_degradations = self.generate_image_and_text(sample_id, text=text, mode=mode)
|
|
293
|
+
|
|
294
|
+
# Shard into subdirectories of 10,000 files each
|
|
295
|
+
shard_idx = sample_id // 10000
|
|
296
|
+
shard_dir = os.path.join(self.output_dir, "images", f"{shard_idx:04d}")
|
|
297
|
+
os.makedirs(shard_dir, exist_ok=True)
|
|
298
|
+
|
|
299
|
+
filename = f"sample_{sample_id:08d}.jpg"
|
|
300
|
+
rel_path = f"images/{shard_idx:04d}/{filename}"
|
|
301
|
+
full_path = os.path.join(shard_dir, filename)
|
|
302
|
+
final_img.save(full_path, "JPEG", quality=random.randint(65, 90))
|
|
303
|
+
|
|
304
|
+
meta = {
|
|
305
|
+
"id": f"{sample_id:08d}",
|
|
306
|
+
"image_path": rel_path,
|
|
307
|
+
"text": text,
|
|
308
|
+
"width": final_img.size[0],
|
|
309
|
+
"height": 32,
|
|
310
|
+
"degradations": active_degradations,
|
|
311
|
+
"difficulty": "extreme" if len(active_degradations) >= 4 else "hard"
|
|
312
|
+
}
|
|
313
|
+
return meta
|
|
314
|
+
|
|
315
|
+
def _worker_chunk_lmdb(args):
|
|
316
|
+
"""Worker task that processes a chunk of samples directly in RAM."""
|
|
317
|
+
start_id, count, mode = args
|
|
318
|
+
synth = AsterMegaSynthesizer()
|
|
319
|
+
chunk_data = []
|
|
320
|
+
for i in range(count):
|
|
321
|
+
sid = start_id + i
|
|
322
|
+
try:
|
|
323
|
+
sid, img_bytes, text = synth.generate_sample_bytes(sid, mode=mode)
|
|
324
|
+
chunk_data.append((sid, img_bytes, text))
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
return chunk_data
|
|
328
|
+
|
|
329
|
+
def _worker_chunk_files(args):
|
|
330
|
+
"""Worker task that writes files to sharded subdirectories."""
|
|
331
|
+
start_id, count, out_dir, mode = args
|
|
332
|
+
synth = AsterMegaSynthesizer(output_dir=out_dir)
|
|
333
|
+
results = []
|
|
334
|
+
for i in range(count):
|
|
335
|
+
sid = start_id + i
|
|
336
|
+
try:
|
|
337
|
+
m = synth.generate_single_sample_file(sid, mode=mode)
|
|
338
|
+
results.append(m)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
return results
|
|
342
|
+
|
|
343
|
+
def generate_mega_dataset_lmdb(total_samples=10000, output_dir="dataset_mega.lmdb", num_workers=None, mode="mixed"):
|
|
344
|
+
"""
|
|
345
|
+
Ultra-high throughput LMDB dataset generator for millions of samples.
|
|
346
|
+
- Zero NTFS fragmentation / No freeze.
|
|
347
|
+
- Memory-mapped streaming write with atomic batch transactions.
|
|
348
|
+
- CPU multi-core parallel rendering.
|
|
349
|
+
"""
|
|
350
|
+
if lmdb is None:
|
|
351
|
+
raise ImportError("Gói 'lmdb' chưa được cài đặt! Hãy chạy: pip install lmdb")
|
|
352
|
+
|
|
353
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
354
|
+
if num_workers is None:
|
|
355
|
+
num_workers = min(10, max(1, cpu_count() - 2))
|
|
356
|
+
|
|
357
|
+
print("=" * 68)
|
|
358
|
+
print(f" ASTER-OCR MILLION-SCALE LMDB SYNTHESIZER: {total_samples:,} MẪU")
|
|
359
|
+
print(f" Chế độ nội dung : {mode.upper()} (Random 134+ Charset + Mã số + Hành chính)")
|
|
360
|
+
print(f" Số luồng CPU : {num_workers} workers song song")
|
|
361
|
+
print(f" Định dạng xuất : LMDB Chuẩn OCR (data.mdb)")
|
|
362
|
+
print(f" Thư mục đích : {output_dir}")
|
|
363
|
+
print("=" * 68)
|
|
364
|
+
|
|
365
|
+
# Calculate map_size (max virtual capacity: ~35KB per sample, min 64MB)
|
|
366
|
+
map_size = max(1024 * 1024 * 64, int(total_samples * 35 * 1024))
|
|
367
|
+
|
|
368
|
+
# Divide into task chunks
|
|
369
|
+
chunk_size = 500 if total_samples <= 50000 else 1000
|
|
370
|
+
tasks = []
|
|
371
|
+
curr_id = 1
|
|
372
|
+
while curr_id <= total_samples:
|
|
373
|
+
cnt = min(chunk_size, total_samples - curr_id + 1)
|
|
374
|
+
tasks.append((curr_id, cnt, mode))
|
|
375
|
+
curr_id += cnt
|
|
376
|
+
|
|
377
|
+
total_chunks = len(tasks)
|
|
378
|
+
start_time = time.time()
|
|
379
|
+
samples_done = 0
|
|
380
|
+
commit_interval = 5000 # Commit to LMDB every 5000 samples
|
|
381
|
+
|
|
382
|
+
env = lmdb.open(output_dir, map_size=map_size)
|
|
383
|
+
txn = env.begin(write=True)
|
|
384
|
+
uncommitted = 0
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
with Pool(num_workers) as pool:
|
|
388
|
+
for chunk_data in pool.imap_unordered(_worker_chunk_lmdb, tasks, chunksize=1):
|
|
389
|
+
for sid, img_bytes, text in chunk_data:
|
|
390
|
+
image_key = f"image-{sid:09d}".encode("utf-8")
|
|
391
|
+
label_key = f"label-{sid:09d}".encode("utf-8")
|
|
392
|
+
|
|
393
|
+
txn.put(image_key, img_bytes)
|
|
394
|
+
txn.put(label_key, text.encode("utf-8"))
|
|
395
|
+
|
|
396
|
+
samples_done += 1
|
|
397
|
+
uncommitted += 1
|
|
398
|
+
|
|
399
|
+
if uncommitted >= commit_interval:
|
|
400
|
+
txn.commit()
|
|
401
|
+
txn = env.begin(write=True)
|
|
402
|
+
uncommitted = 0
|
|
403
|
+
|
|
404
|
+
# Progress display
|
|
405
|
+
elapsed = max(0.001, time.time() - start_time)
|
|
406
|
+
speed = samples_done / elapsed
|
|
407
|
+
rem = max(0, total_samples - samples_done)
|
|
408
|
+
eta_sec = rem / max(1.0, speed)
|
|
409
|
+
eta_str = time.strftime('%H:%M:%S', time.gmtime(eta_sec))
|
|
410
|
+
|
|
411
|
+
mdb_path = os.path.join(output_dir, "data.mdb")
|
|
412
|
+
db_mb = os.path.getsize(mdb_path) / (1024 * 1024) if os.path.exists(mdb_path) else 0
|
|
413
|
+
pct = (samples_done / total_samples) * 100
|
|
414
|
+
|
|
415
|
+
sys.stdout.write(
|
|
416
|
+
f"\r[*] Tiến độ: {samples_done:,}/{total_samples:,} ({pct:.1f}%) | "
|
|
417
|
+
f"Tốc độ: {speed:,.0f} mẫu/s | ETA: {eta_str} | LMDB: {db_mb:,.1f} MB "
|
|
418
|
+
)
|
|
419
|
+
sys.stdout.flush()
|
|
420
|
+
|
|
421
|
+
# Final commit
|
|
422
|
+
txn.put(b"num-samples", str(samples_done).encode("utf-8"))
|
|
423
|
+
txn.commit()
|
|
424
|
+
finally:
|
|
425
|
+
env.sync()
|
|
426
|
+
env.close()
|
|
427
|
+
|
|
428
|
+
total_elapsed = time.time() - start_time
|
|
429
|
+
avg_speed = samples_done / max(0.001, total_elapsed)
|
|
430
|
+
final_size_mb = os.path.getsize(os.path.join(output_dir, "data.mdb")) / (1024 * 1024)
|
|
431
|
+
|
|
432
|
+
print("\n" + "=" * 68)
|
|
433
|
+
print(f"[+] HOÀN THÀNH XUẤT SẮC!")
|
|
434
|
+
print(f"[+] Tổng số mẫu đã lưu vào LMDB: {samples_done:,} mẫu")
|
|
435
|
+
print(f"[+] Thời gian thực hiện : {total_elapsed:.1f} giây (~{total_elapsed/60:.1f} phút)")
|
|
436
|
+
print(f"[+] Tốc độ trung bình : {avg_speed:,.0f} mẫu/giây")
|
|
437
|
+
print(f"[+] Dung lượng LMDB cuối cùng : {final_size_mb:,.2f} MB")
|
|
438
|
+
print(f"[+] Đường dẫn database : {output_dir}")
|
|
439
|
+
print("=" * 68)
|
|
440
|
+
|
|
441
|
+
# Save a metadata info file
|
|
442
|
+
info = {
|
|
443
|
+
"format": "lmdb",
|
|
444
|
+
"num_samples": samples_done,
|
|
445
|
+
"mode": mode,
|
|
446
|
+
"target_height": 32,
|
|
447
|
+
"size_mb": final_size_mb,
|
|
448
|
+
"created_at": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
449
|
+
}
|
|
450
|
+
with open(os.path.join(output_dir, "dataset_info.json"), "w", encoding="utf-8") as f:
|
|
451
|
+
json.dump(info, f, ensure_ascii=False, indent=2)
|
|
452
|
+
|
|
453
|
+
return output_dir
|
|
454
|
+
|
|
455
|
+
def generate_mega_dataset_files(total_samples=10000, output_dir="dataset_mega", num_workers=None, mode="mixed"):
|
|
456
|
+
"""Generates samples saving into sharded subdirectories."""
|
|
457
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
458
|
+
if num_workers is None:
|
|
459
|
+
num_workers = min(10, max(1, cpu_count() - 2))
|
|
460
|
+
|
|
461
|
+
print("=" * 68)
|
|
462
|
+
print(f" ASTER-OCR SHARDED FILES SYNTHESIZER: {total_samples:,} MẪU")
|
|
463
|
+
print(f" Số luồng CPU : {num_workers} workers song song")
|
|
464
|
+
print(f" Thư mục xuất : {output_dir}")
|
|
465
|
+
print("=" * 68)
|
|
466
|
+
|
|
467
|
+
chunk_size = 500 if total_samples <= 20000 else 1000
|
|
468
|
+
tasks = []
|
|
469
|
+
curr_id = 1
|
|
470
|
+
while curr_id <= total_samples:
|
|
471
|
+
cnt = min(chunk_size, total_samples - curr_id + 1)
|
|
472
|
+
tasks.append((curr_id, cnt, output_dir, mode))
|
|
473
|
+
curr_id += cnt
|
|
474
|
+
|
|
475
|
+
all_metadata = []
|
|
476
|
+
start_time = time.time()
|
|
477
|
+
|
|
478
|
+
with Pool(num_workers) as p:
|
|
479
|
+
for chunk in p.imap_unordered(_worker_chunk_files, tasks, chunksize=1):
|
|
480
|
+
all_metadata.extend(chunk)
|
|
481
|
+
samples_done = len(all_metadata)
|
|
482
|
+
elapsed = max(0.001, time.time() - start_time)
|
|
483
|
+
speed = samples_done / elapsed
|
|
484
|
+
pct = (samples_done / total_samples) * 100
|
|
485
|
+
sys.stdout.write(f"\r[*] Đã sinh: {samples_done:,}/{total_samples:,} ({pct:.1f}%) | Tốc độ: {speed:,.0f} mẫu/s ")
|
|
486
|
+
sys.stdout.flush()
|
|
487
|
+
|
|
488
|
+
txt_annotation_path = os.path.join(output_dir, "train_labels.txt")
|
|
489
|
+
json_annotation_path = os.path.join(output_dir, "labels.json")
|
|
490
|
+
|
|
491
|
+
with open(json_annotation_path, "w", encoding="utf-8") as f:
|
|
492
|
+
json.dump(all_metadata, f, ensure_ascii=False, indent=2)
|
|
493
|
+
|
|
494
|
+
with open(txt_annotation_path, "w", encoding="utf-8") as f:
|
|
495
|
+
for m in all_metadata:
|
|
496
|
+
f.write(f"{m['image_path']}\t{m['text']}\n")
|
|
497
|
+
|
|
498
|
+
print(f"\n[+] Đã sinh {len(all_metadata):,} mẫu ảnh thành công!")
|
|
499
|
+
print(f"[+] File nhãn Text: {txt_annotation_path}")
|
|
500
|
+
return json_annotation_path
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
parser = argparse.ArgumentParser(description="AsterOCR Million-Scale Synthetic Generator")
|
|
504
|
+
parser.add_argument("--num_samples", type=int, default=1000, help="Số mẫu cần sinh (Ví dụ: 100000, 1000000)")
|
|
505
|
+
parser.add_argument("--format", type=str, default="lmdb", choices=["lmdb", "files"], help="Định dạng lưu: 'lmdb' (khuyên dùng) hoặc 'files'")
|
|
506
|
+
parser.add_argument("--output_dir", type=str, default=None, help="Thư mục xuất dữ liệu (Mặc định: dataset_mega.lmdb hoặc dataset_mega)")
|
|
507
|
+
parser.add_argument("--workers", type=int, default=10, help="Số tiến trình CPU song song (Mặc định: 10 để tránh lag máy)")
|
|
508
|
+
parser.add_argument("--mode", type=str, default="mixed", choices=["mixed", "pure_random", "admin"], help="Chế độ chuỗi ký tự")
|
|
509
|
+
args = parser.parse_args()
|
|
510
|
+
|
|
511
|
+
if args.output_dir is None:
|
|
512
|
+
args.output_dir = "dataset_mega.lmdb" if args.format == "lmdb" else "dataset_mega"
|
|
513
|
+
|
|
514
|
+
if args.format == "lmdb":
|
|
515
|
+
generate_mega_dataset_lmdb(
|
|
516
|
+
total_samples=args.num_samples,
|
|
517
|
+
output_dir=args.output_dir,
|
|
518
|
+
num_workers=args.workers,
|
|
519
|
+
mode=args.mode
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
generate_mega_dataset_files(
|
|
523
|
+
total_samples=args.num_samples,
|
|
524
|
+
output_dir=args.output_dir,
|
|
525
|
+
num_workers=args.workers,
|
|
526
|
+
mode=args.mode
|
|
527
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Performance Evaluation Metrics
|
|
3
|
+
Calculates Character Error Rate (CER) and Word Error Rate (WER) using Levenshtein distance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def levenshtein_distance(ref, hyp):
|
|
7
|
+
"""Calculates edit distance between two sequences (characters or words)."""
|
|
8
|
+
m, n = len(ref), len(hyp)
|
|
9
|
+
dp = [[0] * (n + 1) for _ in range(m + 1)]
|
|
10
|
+
|
|
11
|
+
for i in range(m + 1):
|
|
12
|
+
dp[i][0] = i
|
|
13
|
+
for j in range(n + 1):
|
|
14
|
+
dp[0][j] = j
|
|
15
|
+
|
|
16
|
+
for i in range(1, m + 1):
|
|
17
|
+
for j in range(1, n + 1):
|
|
18
|
+
if ref[i - 1] == hyp[j - 1]:
|
|
19
|
+
dp[i][j] = dp[i - 1][j - 1]
|
|
20
|
+
else:
|
|
21
|
+
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
22
|
+
|
|
23
|
+
return dp[m][n]
|
|
24
|
+
|
|
25
|
+
def calculate_cer(references, hypotheses):
|
|
26
|
+
"""
|
|
27
|
+
Character Error Rate (CER) = Total Character Edits / Total Reference Characters
|
|
28
|
+
"""
|
|
29
|
+
total_dist = 0
|
|
30
|
+
total_len = 0
|
|
31
|
+
for ref, hyp in zip(references, hypotheses):
|
|
32
|
+
total_dist += levenshtein_distance(list(ref), list(hyp))
|
|
33
|
+
total_len += max(1, len(ref))
|
|
34
|
+
return (total_dist / total_len) if total_len > 0 else 0.0
|
|
35
|
+
|
|
36
|
+
def calculate_wer(references, hypotheses):
|
|
37
|
+
"""
|
|
38
|
+
Word Error Rate (WER) = Total Word Edits / Total Reference Words
|
|
39
|
+
"""
|
|
40
|
+
total_dist = 0
|
|
41
|
+
total_len = 0
|
|
42
|
+
for ref, hyp in zip(references, hypotheses):
|
|
43
|
+
ref_words = ref.strip().split()
|
|
44
|
+
hyp_words = hyp.strip().split()
|
|
45
|
+
total_dist += levenshtein_distance(ref_words, hyp_words)
|
|
46
|
+
total_len += max(1, len(ref_words))
|
|
47
|
+
return (total_dist / total_len) if total_len > 0 else 0.0
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
refs = ["Cộng hòa Xã hội", "Nguyễn Văn An"]
|
|
51
|
+
hyps = ["Cộng hoà Xã hội", "Nguyễn Văn An"]
|
|
52
|
+
print(f"CER: {calculate_cer(refs, hyps)*100:.2f}%")
|
|
53
|
+
print(f"WER: {calculate_wer(refs, hyps)*100:.2f}%")
|