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,402 @@
1
+ """
2
+ AsterOCR Multi-Source Unified Dataset Packer (Industrial LMDB)
3
+ Merges multiple diverse real and synthetic data streams into a single unified LMDB database:
4
+ 1. Real Vietnamese Handwriting from VNOnDB (InkData_line_processed in ocr_dataset.zip)
5
+ 2. Real Receipts, Bills, Invoices & Vouchers (meta in ocr_dataset.zip)
6
+ 3. Real Scanned Documents (vi_00, vi_01 in ocr_dataset.zip)
7
+ 4. Millions of Synthetic Realistic Vietnamese Handwriting Lines (notebook paper, pen pressure, slants)
8
+ 5. Millions of Synthetic Realistic Thermal Receipts (dot-matrix, store names, VAT, amounts)
9
+ 6. Open-Source Hugging Face Vietnamese Handwriting datasets
10
+
11
+ Output Format:
12
+ - Single LMDB directory: dataset_unified.lmdb (data.mdb)
13
+ - image-%09d: compressed JPEG bytes (zero NTFS file fragmentation)
14
+ - label-%09d: UTF-8 Vietnamese ground-truth string
15
+ - source-%09d: source tag string (real_handwriting, real_receipt, synth_handwriting, etc.)
16
+ - num-samples: total number of samples
17
+ - dataset_summary.json: complete category and distribution report
18
+ """
19
+
20
+ import os
21
+ import io
22
+ import sys
23
+ import json
24
+ import time
25
+ import random
26
+ import zipfile
27
+ import argparse
28
+
29
+ # Suppress OpenBLAS / MKL multi-threading collisions in child worker processes
30
+ os.environ["OMP_NUM_THREADS"] = "1"
31
+ os.environ["OPENBLAS_NUM_THREADS"] = "1"
32
+ os.environ["MKL_NUM_THREADS"] = "1"
33
+ os.environ["VECLIB_MAXIMUM_THREADS"] = "1"
34
+ os.environ["NUMEXPR_NUM_THREADS"] = "1"
35
+
36
+ from multiprocessing import Pool, cpu_count
37
+ import numpy as np
38
+ from PIL import Image
39
+
40
+ if sys.platform == "win32":
41
+ try:
42
+ sys.stdout.reconfigure(encoding='utf-8')
43
+ sys.stderr.reconfigure(encoding='utf-8')
44
+ except Exception:
45
+ pass
46
+
47
+ try:
48
+ import lmdb
49
+ except ImportError:
50
+ lmdb = None
51
+
52
+ # Ensure root in sys.path
53
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
54
+ from asterocr.synthesizer.handwriting_generator import (
55
+ RealisticHandwritingRenderer,
56
+ RealisticReceiptRenderer,
57
+ sample_handwritten_text,
58
+ sample_receipt_text,
59
+ _worker_chunk_handwriting
60
+ )
61
+
62
+ def _read_real_zip_sample(zip_ref, img_path, txt_path, target_height=32):
63
+ """Safely extracts, resizes, and encodes a sample from the real OCR zip."""
64
+ try:
65
+ img_bytes = zip_ref.read(img_path)
66
+ txt_bytes = zip_ref.read(txt_path)
67
+ text = txt_bytes.decode('utf-8', errors='ignore').strip()
68
+ if not text:
69
+ return None, None
70
+
71
+ img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
72
+ w, h = img.size
73
+ if w < 4 or h < 4:
74
+ return None, None
75
+
76
+ new_w = max(16, int((float(target_height) / max(1, h)) * w))
77
+ resized_img = img.resize((new_w, target_height), Image.LANCZOS)
78
+
79
+ buf = io.BytesIO()
80
+ resized_img.save(buf, format="JPEG", quality=92)
81
+ return buf.getvalue(), text
82
+ except Exception:
83
+ return None, None
84
+
85
+ def pack_unified_dataset(
86
+ output_dir="dataset_unified.lmdb",
87
+ zip_path="dataset_download/ocr_dataset.zip",
88
+ index_path="dataset_download/ocr_dataset_index.json",
89
+ num_synth_handwriting=100000,
90
+ num_synth_receipt=50000,
91
+ include_real_zip=True,
92
+ max_real_handwriting=None,
93
+ max_real_receipts=None,
94
+ max_real_documents=None,
95
+ hf_parquet_files=None,
96
+ num_workers=10,
97
+ target_height=32
98
+ ):
99
+ """
100
+ Master pipeline to assemble the unified OCR dataset.
101
+ """
102
+ if lmdb is None:
103
+ raise ImportError("Gói 'lmdb' chưa được cài đặt! Hãy chạy: pip install lmdb")
104
+
105
+ num_workers = min(10, max(1, num_workers)) # Cap at 10 workers for CPU stability
106
+ os.makedirs(output_dir, exist_ok=True)
107
+
108
+ print("=" * 72)
109
+ print(" ASTER-OCR UNIFIED MULTI-SOURCE DATASET PACKER (LMDB)")
110
+ print("=" * 72)
111
+ print(f" Thư mục đích : {output_dir}")
112
+ print(f" Số luồng CPU tối đa : {num_workers} workers")
113
+ print(f" Chiều cao ảnh chuẩn hóa: {target_height} px")
114
+ print(f" Chữ viết tay giả lập : {num_synth_handwriting:,} mẫu")
115
+ print(f" Biên lai giả lập : {num_synth_receipt:,} mẫu")
116
+ print("=" * 72)
117
+
118
+ # 1. Estimate total samples to size the LMDB map
119
+ total_estimated = num_synth_handwriting + num_synth_receipt
120
+ real_items = []
121
+
122
+ if include_real_zip and os.path.exists(zip_path) and os.path.exists(index_path):
123
+ print(f"[*] Đang tải chỉ mục dữ liệu thực tế từ: {index_path}...")
124
+ try:
125
+ with open(index_path, "r", encoding="utf-8") as f:
126
+ all_index = json.load(f)
127
+
128
+ # Categorize real items
129
+ hw_items = [x for x in all_index if "InkData" in x[0]]
130
+ rc_items = [x for x in all_index if x[0].startswith("meta/")]
131
+ doc_items = [x for x in all_index if x[0].startswith(("vi_00/", "vi_01/"))]
132
+ other_items = [x for x in all_index if x[0].startswith(("random/", "en_00/", "en_01/"))]
133
+
134
+ print(f" - Viết tay thật VNOnDB : {len(hw_items):,} mẫu")
135
+ print(f" - Hóa đơn / biên lai : {len(rc_items):,} mẫu")
136
+ print(f" - Tài liệu in tiếng Việt: {len(doc_items):,} mẫu")
137
+ print(f" - Mã số & song ngữ : {len(other_items):,} mẫu")
138
+
139
+ # Slice according to quotas
140
+ if max_real_handwriting is not None:
141
+ hw_items = hw_items[:max_real_handwriting]
142
+ if max_real_receipts is not None:
143
+ rc_items = rc_items[:max_real_receipts]
144
+ if max_real_documents is not None:
145
+ doc_items = doc_items[:max_real_documents]
146
+
147
+ real_items.extend([(item, "real_handwriting") for item in hw_items])
148
+ real_items.extend([(item, "real_receipt") for item in rc_items])
149
+ real_items.extend([(item, "real_document") for item in doc_items])
150
+
151
+ total_estimated += len(real_items)
152
+ print(f"[+] Sẽ gom {len(real_items):,} mẫu thực tế từ kho zip!")
153
+ except Exception as e:
154
+ print(f"[-] Lỗi khi nạp chỉ mục zip: {e}")
155
+
156
+ # Map size: ~35KB per sample, minimum 64MB, with 1.3x buffer
157
+ map_size = max(1024 * 1024 * 64, int(total_estimated * 45 * 1024))
158
+
159
+ print(f"[*] Khởi tạo LMDB môi trường: map_size = {map_size // (1024 * 1024):,} MB...")
160
+ env = lmdb.open(output_dir, map_size=map_size)
161
+ txn = env.begin(write=True)
162
+
163
+ samples_written = 0
164
+ uncommitted = 0
165
+ commit_interval = 5000
166
+ start_time = time.time()
167
+
168
+ stats = {
169
+ "real_handwriting": 0,
170
+ "real_receipt": 0,
171
+ "real_document": 0,
172
+ "synth_handwriting": 0,
173
+ "synth_receipt": 0,
174
+ "hf_handwriting": 0,
175
+ }
176
+
177
+ try:
178
+ # -------------------------------------------------------------
179
+ # PHASE 1: Stream Real Images from ZIP directly into LMDB
180
+ # -------------------------------------------------------------
181
+ if real_items and os.path.exists(zip_path):
182
+ print("\n[GIAI ĐOẠN 1] Bắt đầu nạp dữ liệu thực tế (Viết tay thật + Hóa đơn + Tài liệu)...")
183
+ with zipfile.ZipFile(zip_path, "r") as z:
184
+ for idx, (item, source_tag) in enumerate(real_items):
185
+ img_path, txt_path = item
186
+ img_bytes, text = _read_real_zip_sample(z, img_path, txt_path, target_height)
187
+ if img_bytes is None or not text:
188
+ continue
189
+
190
+ samples_written += 1
191
+ img_key = f"image-{samples_written:09d}".encode("utf-8")
192
+ lbl_key = f"label-{samples_written:09d}".encode("utf-8")
193
+ src_key = f"source-{samples_written:09d}".encode("utf-8")
194
+
195
+ txn.put(img_key, img_bytes)
196
+ txn.put(lbl_key, text.encode("utf-8"))
197
+ txn.put(src_key, source_tag.encode("utf-8"))
198
+
199
+ stats[source_tag] = stats.get(source_tag, 0) + 1
200
+ uncommitted += 1
201
+
202
+ if uncommitted >= commit_interval:
203
+ txn.commit()
204
+ txn = env.begin(write=True)
205
+ uncommitted = 0
206
+
207
+ if samples_written % 1000 == 0 or idx == len(real_items) - 1:
208
+ elapsed = max(0.001, time.time() - start_time)
209
+ speed = samples_written / elapsed
210
+ sys.stdout.write(
211
+ f"\r[*] [Real Data] Đã nạp: {samples_written:,} mẫu | "
212
+ f"Tốc độ: {speed:,.0f} mẫu/s "
213
+ )
214
+ sys.stdout.flush()
215
+
216
+ print(f"\n[+] Đã nạp xong toàn bộ dữ liệu thực tế!")
217
+
218
+ # -------------------------------------------------------------
219
+ # PHASE 2: Parallel Synthetic Generation (Handwriting & Receipts)
220
+ # -------------------------------------------------------------
221
+ synth_tasks = []
222
+ chunk_size = 1000
223
+
224
+ # Handwriting tasks
225
+ curr_cnt = 0
226
+ while curr_cnt < num_synth_handwriting:
227
+ cnt = min(chunk_size, num_synth_handwriting - curr_cnt)
228
+ synth_tasks.append((samples_written + 1, cnt, "handwriting", target_height))
229
+ curr_cnt += cnt
230
+ samples_written += cnt # pre-allocate ID space
231
+
232
+ # Receipt tasks
233
+ curr_rc_cnt = 0
234
+ while curr_rc_cnt < num_synth_receipt:
235
+ cnt = min(chunk_size, num_synth_receipt - curr_rc_cnt)
236
+ synth_tasks.append((samples_written + 1, cnt, "receipt", target_height))
237
+ curr_rc_cnt += cnt
238
+ samples_written += cnt
239
+
240
+ # Reset actual counter for writing
241
+ samples_written -= (num_synth_handwriting + num_synth_receipt)
242
+
243
+ if synth_tasks:
244
+ print(f"\n[GIAI ĐOẠN 2] Sinh dữ liệu viết tay & hóa đơn song song ({num_workers} workers)...")
245
+ synth_start_time = time.time()
246
+ with Pool(num_workers) as pool:
247
+ for chunk_data in pool.imap_unordered(_worker_chunk_handwriting, synth_tasks, chunksize=1):
248
+ for _, img_bytes, text, source_type in chunk_data:
249
+ samples_written += 1
250
+ img_key = f"image-{samples_written:09d}".encode("utf-8")
251
+ lbl_key = f"label-{samples_written:09d}".encode("utf-8")
252
+ src_key = f"source-{samples_written:09d}".encode("utf-8")
253
+
254
+ txn.put(img_key, img_bytes)
255
+ txn.put(lbl_key, text.encode("utf-8"))
256
+ txn.put(src_key, source_type.encode("utf-8"))
257
+
258
+ stats[source_type] = stats.get(source_type, 0) + 1
259
+ uncommitted += 1
260
+
261
+ if uncommitted >= commit_interval:
262
+ txn.commit()
263
+ txn = env.begin(write=True)
264
+ uncommitted = 0
265
+
266
+ elapsed = max(0.001, time.time() - start_time)
267
+ speed = samples_written / elapsed
268
+ pct = (samples_written / max(1, total_estimated)) * 100
269
+
270
+ mdb_path = os.path.join(output_dir, "data.mdb")
271
+ db_mb = os.path.getsize(mdb_path) / (1024 * 1024) if os.path.exists(mdb_path) else 0
272
+
273
+ sys.stdout.write(
274
+ f"\r[*] [Synth Parallel] Đã ghi: {samples_written:,}/{total_estimated:,} ({pct:.1f}%) | "
275
+ f"Tốc độ: {speed:,.0f} mẫu/s | LMDB: {db_mb:,.1f} MB "
276
+ )
277
+ sys.stdout.flush()
278
+
279
+ # -------------------------------------------------------------
280
+ # PHASE 3: HuggingFace Parquet integration (if any specified)
281
+ # -------------------------------------------------------------
282
+ if hf_parquet_files:
283
+ print("\n[GIAI ĐOẠN 3] Nạp dữ liệu bổ sung từ Hugging Face Parquet...")
284
+ try:
285
+ import pandas as pd
286
+ for pq_file in hf_parquet_files:
287
+ if os.path.exists(pq_file):
288
+ df = pd.read_parquet(pq_file)
289
+ for _, row in df.iterrows():
290
+ # Find text and image bytes
291
+ raw_bytes = None
292
+ text = None
293
+ for col in df.columns:
294
+ if "text" in col.lower() or "label" in col.lower() or "transcription" in col.lower():
295
+ text = str(row[col]).strip()
296
+ elif "image" in col.lower():
297
+ val = row[col]
298
+ if isinstance(val, dict) and "bytes" in val:
299
+ raw_bytes = val["bytes"]
300
+ elif isinstance(val, bytes):
301
+ raw_bytes = val
302
+
303
+ if raw_bytes and text:
304
+ try:
305
+ img = Image.open(io.BytesIO(raw_bytes)).convert("RGB")
306
+ w, h = img.size
307
+ new_w = max(16, int((float(target_height) / max(1, h)) * w))
308
+ resized = img.resize((new_w, target_height), Image.LANCZOS)
309
+ buf = io.BytesIO()
310
+ resized.save(buf, format="JPEG", quality=90)
311
+
312
+ samples_written += 1
313
+ img_key = f"image-{samples_written:09d}".encode("utf-8")
314
+ lbl_key = f"label-{samples_written:09d}".encode("utf-8")
315
+ src_key = f"source-{samples_written:09d}".encode("utf-8")
316
+
317
+ txn.put(img_key, buf.getvalue())
318
+ txn.put(lbl_key, text.encode("utf-8"))
319
+ txn.put(src_key, b"hf_handwriting")
320
+
321
+ stats["hf_handwriting"] = stats.get("hf_handwriting", 0) + 1
322
+ uncommitted += 1
323
+ if uncommitted >= commit_interval:
324
+ txn.commit()
325
+ txn = env.begin(write=True)
326
+ uncommitted = 0
327
+ except Exception:
328
+ continue
329
+ except Exception as e:
330
+ print(f"[-] Lỗi nạp HuggingFace Parquet: {e}")
331
+
332
+ # Final commit and write metadata header
333
+ txn.put(b"num-samples", str(samples_written).encode("utf-8"))
334
+ txn.commit()
335
+
336
+ finally:
337
+ env.sync()
338
+ env.close()
339
+
340
+ total_time = max(0.001, time.time() - start_time)
341
+ avg_speed = samples_written / total_time
342
+ mdb_size_mb = os.path.getsize(os.path.join(output_dir, "data.mdb")) / (1024 * 1024)
343
+
344
+ # Save comprehensive summary JSON
345
+ summary = {
346
+ "dataset_name": "AsterOCR Unified Multi-Source Dataset",
347
+ "output_dir": output_dir,
348
+ "total_samples": samples_written,
349
+ "target_height": target_height,
350
+ "distribution": stats,
351
+ "size_mb": round(mdb_size_mb, 2),
352
+ "total_seconds": round(total_time, 2),
353
+ "avg_samples_per_sec": round(avg_speed, 1),
354
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
355
+ }
356
+
357
+ with open(os.path.join(output_dir, "dataset_summary.json"), "w", encoding="utf-8") as f:
358
+ json.dump(summary, f, ensure_ascii=False, indent=2)
359
+
360
+ print("\n\n" + "=" * 72)
361
+ print(" [+] HOÀN THÀNH ĐÓNG GÓI BỘ DỮ LIỆU HỢP NHẤT DUY NHẤT!")
362
+ print("=" * 72)
363
+ print(f" Tổng số mẫu trong kho : {samples_written:,} mẫu")
364
+ print(f" Thời gian hoàn thành : {total_time:.1f}s (~{total_time/60:.1f} phút)")
365
+ print(f" Tốc độ ghi trung bình : {avg_speed:,.0f} mẫu/giây")
366
+ print(f" Dung lượng LMDB (MDB) : {mdb_size_mb:,.2f} MB")
367
+ print(f" Phân bố nguồn dữ liệu :")
368
+ for k, v in stats.items():
369
+ if v > 0:
370
+ pct = (v / max(1, samples_written)) * 100
371
+ print(f" * {k:<20}: {v:>10,} mẫu ({pct:.1f}%)")
372
+ print("=" * 72)
373
+ return summary
374
+
375
+ def main():
376
+ parser = argparse.ArgumentParser(description="AsterOCR Unified Multi-Source Dataset Packer")
377
+ parser.add_argument("--output_dir", type=str, default="dataset_unified.lmdb", help="Thư mục LMDB duy nhất")
378
+ parser.add_argument("--num_synth_hw", type=int, default=50000, help="Số mẫu chữ viết tay giả lập")
379
+ parser.add_argument("--num_synth_rc", type=int, default=25000, help="Số mẫu hóa đơn biên lai giả lập")
380
+ parser.add_argument("--max_real_hw", type=int, default=None, help="Giới hạn số mẫu viết tay thật (None = lấy hết 7.2k)")
381
+ parser.add_argument("--max_real_rc", type=int, default=50000, help="Giới hạn số mẫu hóa đơn thật")
382
+ parser.add_argument("--max_real_doc", type=int, default=50000, help="Giới hạn số mẫu văn bản tài liệu thật")
383
+ parser.add_argument("--no_real_zip", action="store_true", help="Không lấy dữ liệu từ zip")
384
+ parser.add_argument("--num_workers", type=int, default=10, help="Số luồng CPU tối đa (mặc định 10)")
385
+ parser.add_argument("--target_height", type=int, default=32, help="Chiều cao ảnh chuẩn hóa")
386
+
387
+ args = parser.parse_args()
388
+
389
+ pack_unified_dataset(
390
+ output_dir=args.output_dir,
391
+ num_synth_handwriting=args.num_synth_hw,
392
+ num_synth_receipt=args.num_synth_rc,
393
+ include_real_zip=not args.no_real_zip,
394
+ max_real_handwriting=args.max_real_hw,
395
+ max_real_receipts=args.max_real_rc,
396
+ max_real_documents=args.max_real_doc,
397
+ num_workers=args.num_workers,
398
+ target_height=args.target_height
399
+ )
400
+
401
+ if __name__ == "__main__":
402
+ main()
@@ -0,0 +1,93 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import requests
5
+
6
+ if sys.platform == "win32":
7
+ try:
8
+ sys.stdout.reconfigure(encoding='utf-8')
9
+ sys.stderr.reconfigure(encoding='utf-8')
10
+ except Exception:
11
+ pass
12
+
13
+ def get_filename_from_cd(cd):
14
+ if not cd:
15
+ return None
16
+ fname = re.findall(r'filename=(.+)', cd)
17
+ if len(fname) == 0:
18
+ fname = re.findall(r"filename\*=UTF-8''(.+)", cd)
19
+ if len(fname) > 0:
20
+ return fname[0].strip(' "\'')
21
+ return None
22
+
23
+ def download_file_from_google_drive(file_id, output_dir="dataset_download"):
24
+ os.makedirs(output_dir, exist_ok=True)
25
+ session = requests.Session()
26
+
27
+ # Initial request
28
+ url = f"https://drive.usercontent.google.com/download?id={file_id}&export=download&authuser=0"
29
+ headers = {
30
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
31
+ }
32
+
33
+ print(f"[*] Đang kết nối tới Google Drive (ID: {file_id})...")
34
+ res = session.get(url, headers=headers, stream=True)
35
+
36
+ content_type = res.headers.get('Content-Type', '')
37
+ if 'text/html' in content_type:
38
+ html = res.text
39
+ # Tìm token xác nhận hoặc link download trực tiếp
40
+ token_match = re.search(r'name="confirm"\s+value="([^"]+)"', html)
41
+ uuid_match = re.search(r'name="uuid"\s+value="([^"]+)"', html)
42
+ action_match = re.search(r'action="([^"]+)"', html)
43
+
44
+ if action_match and token_match:
45
+ download_url = action_match.group(1)
46
+ params = {
47
+ 'id': file_id,
48
+ 'export': 'download',
49
+ 'confirm': token_match.group(1),
50
+ }
51
+ if uuid_match:
52
+ params['uuid'] = uuid_match.group(1)
53
+ print(f"[*] Bỏ qua cảnh báo virus, gửi mã xác nhận: {token_match.group(1)}...")
54
+ res = session.get(download_url, params=params, headers=headers, stream=True)
55
+ else:
56
+ # Thử qua docs.google.com/uc
57
+ alt_url = "https://docs.google.com/uc?export=download"
58
+ res = session.get(alt_url, params={'id': file_id, 'confirm': 't'}, headers=headers, stream=True)
59
+
60
+ # Lấy tên file thực tế từ header hoặc đặt mặc định
61
+ filename = get_filename_from_cd(res.headers.get('Content-Disposition'))
62
+ if not filename:
63
+ filename = f"dataset_{file_id}.zip"
64
+
65
+ out_path = os.path.join(output_dir, filename)
66
+ print(f"[*] Đang tải về: {filename}")
67
+ total_size = int(res.headers.get('content-length', 0))
68
+ if total_size:
69
+ print(f"[*] Dung lượng: {total_size / (1024*1024):.2f} MB")
70
+
71
+ downloaded = 0
72
+ chunk_size = 1024 * 1024 # 1MB
73
+ with open(out_path, "wb") as f:
74
+ for chunk in res.iter_content(chunk_size):
75
+ if chunk:
76
+ f.write(chunk)
77
+ downloaded += len(chunk)
78
+ if total_size > 0:
79
+ percent = (downloaded / total_size) * 100
80
+ mb_down = downloaded / (1024 * 1024)
81
+ mb_tot = total_size / (1024 * 1024)
82
+ sys.stdout.write(f"\r--> Tiến độ: {percent:.1f}% ({mb_down:.1f}/{mb_tot:.1f} MB)")
83
+ else:
84
+ mb_down = downloaded / (1024 * 1024)
85
+ sys.stdout.write(f"\r--> Đã tải: {mb_down:.1f} MB")
86
+ sys.stdout.flush()
87
+
88
+ print(f"\n[+] Tải xong thành công: {out_path}")
89
+ return out_path
90
+
91
+ if __name__ == "__main__":
92
+ file_id = "1T0cmkhTgu3ahyMIwGZeby612RpVdDxOR"
93
+ download_file_from_google_drive(file_id, "dataset_download")