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.
@@ -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}%")