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,371 @@
1
+ """
2
+ AsterOCR Multi-Source Blended Dataset Engine
3
+ Features:
4
+ 1. AsterZipDataset: Reads directly from 7.3GB ocr_dataset.zip without extracting to disk.
5
+ Caches sample index to zip_index.json for instant 0.05s loading.
6
+ 2. AsterSyntheticDataset: Infinite realistic Vietnamese administrative text generator.
7
+ 3. AsterMultiSourceDataset: Blends real-world scanned images + synthetic Vietnamese data
8
+ according to configurable mixing ratios (e.g. 50% Real / 50% Synthetic).
9
+ 4. ctc_collate_fn: Batch padder and CTC length tensor builder.
10
+ """
11
+
12
+ import os
13
+ import io
14
+ import json
15
+ import random
16
+ import zipfile
17
+ import torch
18
+ from torch.utils.data import Dataset, DataLoader
19
+ from PIL import Image
20
+ import numpy as np
21
+
22
+ try:
23
+ import lmdb
24
+ except ImportError:
25
+ lmdb = None
26
+
27
+ from asterocr.vocab import default_vocab
28
+ from asterocr.synthesizer.corpus import generate_random_corpus_text
29
+ from asterocr.synthesizer.generator import default_generator
30
+
31
+ class AsterLMDBDataset(Dataset):
32
+ """
33
+ Ultra-high performance zero-copy LMDB Dataset loader for AsterOCR.
34
+ Reads directly from LMDB database (e.g. dataset_mega.lmdb).
35
+ Compatible with standard CRNN / ASTER / Clova AI format:
36
+ - 'image-%09d' -> raw JPEG/PNG bytes
37
+ - 'label-%09d' -> UTF-8 encoded text string
38
+ - 'num-samples' -> total number of samples
39
+ Supports multi-processing PyTorch DataLoader via lazy environment initialization.
40
+ """
41
+ def __init__(self, lmdb_dir, target_height=32, max_samples=None):
42
+ if lmdb is None:
43
+ raise ImportError("Gói 'lmdb' chưa được cài đặt! Hãy chạy: pip install lmdb")
44
+
45
+ self.lmdb_dir = lmdb_dir
46
+ self.target_height = target_height
47
+ self.env = None
48
+
49
+ if not os.path.exists(lmdb_dir):
50
+ raise FileNotFoundError(f"Thư mục LMDB không tồn tại: {lmdb_dir}")
51
+
52
+ from asterocr.kaggle_bridge.chunker import get_or_open_lmdb_env
53
+ self.env = get_or_open_lmdb_env(self.lmdb_dir, max_readers=126)
54
+ if self.env is None:
55
+ raise FileNotFoundError(f"Không thể mở LMDB tại: {lmdb_dir}")
56
+
57
+ with self.env.begin(write=False) as txn:
58
+ num_samples_val = txn.get(b'num-samples')
59
+ if num_samples_val is None:
60
+ raise ValueError(f"Không tìm thấy khóa 'num-samples' trong LMDB: {lmdb_dir}")
61
+ total = int(num_samples_val.decode('utf-8'))
62
+ self.num_samples = min(total, max_samples) if (max_samples and max_samples > 0) else total
63
+
64
+ def _init_env(self):
65
+ if self.env is None:
66
+ from asterocr.kaggle_bridge.chunker import get_or_open_lmdb_env
67
+ self.env = get_or_open_lmdb_env(self.lmdb_dir, max_readers=126)
68
+
69
+ def __len__(self):
70
+ return self.num_samples
71
+
72
+ def __getitem__(self, idx):
73
+ self._init_env()
74
+ sample_idx = idx + 1
75
+ try:
76
+ with self.env.begin(write=False) as txn:
77
+ img_key = f"image-{sample_idx:09d}".encode("utf-8")
78
+ lbl_key = f"label-{sample_idx:09d}".encode("utf-8")
79
+
80
+ img_bytes = txn.get(img_key)
81
+ label_bytes = txn.get(lbl_key)
82
+
83
+ if img_bytes is None or label_bytes is None:
84
+ img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
85
+ text = " "
86
+ else:
87
+ img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
88
+ raw_str = label_bytes.decode("utf-8", errors="ignore").strip()
89
+ text = raw_str if raw_str else " "
90
+ except Exception:
91
+ img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
92
+ text = " "
93
+
94
+ w, h = img.size
95
+ new_w = max(16, int((float(self.target_height) / max(1, h)) * w))
96
+ # Guarantee enough width for characters so CTC feature length >= target length
97
+ min_w = len(text) * 8 + 16
98
+ new_w = min(480, max(new_w, min_w))
99
+ img = img.resize((new_w, self.target_height), Image.BILINEAR)
100
+
101
+ arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
102
+ return torch.from_numpy(arr), text
103
+
104
+ class AsterSyntheticDataset(Dataset):
105
+ """Infinite synthetic training dataset generated in real time."""
106
+ def __init__(self, size=10000, target_height=32, generator=None):
107
+ self.size = size
108
+ self.target_height = target_height
109
+ self.generator = generator or default_generator
110
+
111
+ def __len__(self):
112
+ return self.size
113
+
114
+ def __getitem__(self, idx):
115
+ text = generate_random_corpus_text()
116
+ img = self.generator.render_text(text, target_height=self.target_height)
117
+
118
+ arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
119
+ tensor = torch.from_numpy(arr)
120
+ return tensor, text
121
+
122
+ class AsterZipDataset(Dataset):
123
+ """
124
+ Directly streams image and text label pairs from ocr_dataset.zip.
125
+ Zero disk extraction required.
126
+ """
127
+ def __init__(self, zip_path="dataset_download/ocr_dataset.zip", target_height=32, max_samples=None):
128
+ self.zip_path = zip_path
129
+ self.target_height = target_height
130
+ self.pairs = [] # list of (img_name, txt_name)
131
+
132
+ index_cache = os.path.splitext(zip_path)[0] + "_index.json"
133
+
134
+ if os.path.exists(index_cache):
135
+ try:
136
+ with open(index_cache, "r", encoding="utf-8") as f:
137
+ self.pairs = json.load(f)
138
+ except Exception:
139
+ self.pairs = []
140
+
141
+ if not self.pairs and os.path.exists(zip_path):
142
+ print(f"[*] Đang lập chỉ mục {zip_path} (lần đầu tiên)...")
143
+ with zipfile.ZipFile(zip_path, "r") as z:
144
+ all_names = set(z.namelist())
145
+ # Filter for images that have matching txt files
146
+ for n in all_names:
147
+ if n.endswith(('.jpg', '.png')):
148
+ base, _ = os.path.splitext(n)
149
+ txt_n = base + ".txt"
150
+ if txt_n in all_names:
151
+ self.pairs.append((n, txt_n))
152
+
153
+ # Save index cache
154
+ try:
155
+ with open(index_cache, "w", encoding="utf-8") as f:
156
+ json.dump(self.pairs, f)
157
+ print(f"[+] Đã lưu chỉ mục: {len(self.pairs)} mẫu vào {index_cache}")
158
+ except Exception:
159
+ pass
160
+
161
+ if max_samples and len(self.pairs) > max_samples:
162
+ self.pairs = self.pairs[:max_samples]
163
+
164
+ self.zip_ref = None
165
+
166
+ def _get_zip(self):
167
+ if self.zip_ref is None:
168
+ self.zip_ref = zipfile.ZipFile(self.zip_path, "r")
169
+ return self.zip_ref
170
+
171
+ def __len__(self):
172
+ return len(self.pairs)
173
+
174
+ def __getitem__(self, idx):
175
+ if not self.pairs:
176
+ # Fallback dummy
177
+ img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
178
+ arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
179
+ return torch.from_numpy(arr), " "
180
+
181
+ img_n, txt_n = self.pairs[idx]
182
+ z = self._get_zip()
183
+
184
+ try:
185
+ raw_img = z.read(img_n)
186
+ img = Image.open(io.BytesIO(raw_img)).convert("RGB")
187
+ w, h = img.size
188
+ new_w = max(16, int((self.target_height / max(1, h)) * w))
189
+ img = img.resize((new_w, self.target_height), Image.BILINEAR)
190
+
191
+ raw_txt = z.read(txt_n).decode("utf-8", errors="ignore").strip().strip('"\'')
192
+ text = raw_txt if raw_txt else " "
193
+ except Exception:
194
+ img = Image.new("RGB", (64, self.target_height), (255, 255, 255))
195
+ text = " "
196
+
197
+ arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
198
+ return torch.from_numpy(arr), text
199
+
200
+ class AsterMultiSourceDataset(Dataset):
201
+ """
202
+ Multi-Source Blended Dataset:
203
+ Dynamically blends Real Scans/Photos (from zip) + Synthetic Vietnamese text
204
+ according to a configurable ratio (e.g. 50% Real / 50% Synthetic).
205
+ """
206
+ def __init__(self, synthetic_ds, real_ds=None, real_ratio=0.5, total_size=10000):
207
+ self.synthetic_ds = synthetic_ds
208
+ self.real_ds = real_ds
209
+ self.real_ratio = real_ratio if (real_ds and len(real_ds) > 0) else 0.0
210
+ self.total_size = total_size
211
+
212
+ def __len__(self):
213
+ return self.total_size
214
+
215
+ def __getitem__(self, idx):
216
+ # Decide whether to sample from real dataset or synthetic generator
217
+ if random.random() < self.real_ratio and self.real_ds:
218
+ real_idx = random.randint(0, len(self.real_ds) - 1)
219
+ return self.real_ds[real_idx]
220
+ else:
221
+ synth_idx = random.randint(0, len(self.synthetic_ds) - 1)
222
+ return self.synthetic_ds[synth_idx]
223
+
224
+ class AsterOmniDataset(Dataset):
225
+ """
226
+ Omni-Source Blended Dataset:
227
+ Hop nhat toan bo moi nguon du lieu (All-In-One):
228
+ - Kho Hop Nhat LMDB (Viet tay VNOnDB, Vo o ly, Hoa don in kim)
229
+ - Kho Anh Quet/Chup Thuc Te (601,282 mau trong zip)
230
+ - Kho Mega Trieu Mau LMDB (1,000,000 mau sieu toc)
231
+ - Trinh Tu Sinh Dong Python On-The-Fly (322 Font Windows, Giay cu, Muc nhoe)
232
+ Can bang ty trong mau va boc ngau nhien truc tiep cho tung batch.
233
+ """
234
+ def __init__(self, sources_dict, weights=None, total_size=50000):
235
+ self.sources = []
236
+ self.names = []
237
+ for name, ds in sources_dict.items():
238
+ if ds is not None and len(ds) > 0:
239
+ self.sources.append(ds)
240
+ self.names.append(name)
241
+
242
+ if not self.sources:
243
+ raise ValueError("Khong tim thay nguon du lieu nao kha dung cho Omni Dataset!")
244
+
245
+ self.total_size = total_size
246
+ n = len(self.sources)
247
+ if weights is None or len(weights) != n:
248
+ self.weights = [1.0 / n] * n
249
+ else:
250
+ s = sum(weights)
251
+ self.weights = [w / s for w in weights]
252
+
253
+ def __len__(self):
254
+ return self.total_size
255
+
256
+ def __getitem__(self, idx):
257
+ chosen_ds = random.choices(self.sources, weights=self.weights, k=1)[0]
258
+ rand_idx = random.randint(0, len(chosen_ds) - 1)
259
+ return chosen_ds[rand_idx]
260
+
261
+ class AsterFileDataset(Dataset):
262
+ """Loads dataset from image directory and annotation file (path\\tlabel)."""
263
+ def __init__(self, annotation_file, data_root=None, target_height=32):
264
+ self.samples = []
265
+ self.target_height = target_height
266
+ data_root = data_root or os.path.dirname(annotation_file)
267
+
268
+ with open(annotation_file, "r", encoding="utf-8") as f:
269
+ for line in f:
270
+ line = line.strip()
271
+ if not line:
272
+ continue
273
+ parts = line.split("\t")
274
+ if len(parts) >= 2:
275
+ rel_p, text = parts[0], parts[1]
276
+ else:
277
+ parts = line.split(" ", 1)
278
+ rel_p = parts[0]
279
+ text = parts[1] if len(parts) > 1 else ""
280
+
281
+ full_p = os.path.join(data_root, rel_p) if not os.path.isabs(rel_p) else rel_p
282
+ if os.path.exists(full_p):
283
+ self.samples.append((full_p, text))
284
+
285
+ def __len__(self):
286
+ return len(self.samples)
287
+
288
+ def __getitem__(self, idx):
289
+ img_path, text = self.samples[idx]
290
+ try:
291
+ img = Image.open(img_path).convert("RGB")
292
+ w, h = img.size
293
+ new_w = max(16, int((self.target_height / max(1, h)) * w))
294
+ img = img.resize((new_w, self.target_height), Image.BILINEAR)
295
+ except Exception:
296
+ img = Image.new("RGB", (32, self.target_height), (255, 255, 255))
297
+ text = " "
298
+
299
+ arr = np.array(img, dtype=np.float32).transpose(2, 0, 1) / 255.0
300
+ return torch.from_numpy(arr), text
301
+
302
+ def ctc_collate_fn(batch, vocab=None):
303
+ if vocab is None:
304
+ vocab = default_vocab
305
+
306
+ tensors, texts = zip(*batch)
307
+ batch_size = len(tensors)
308
+
309
+ encoded_targets = []
310
+ target_lengths = []
311
+
312
+ for text in texts:
313
+ tokens = vocab.encode(text)
314
+ if not tokens:
315
+ tokens = [vocab.encode(" ")[0]]
316
+ encoded_targets.extend(tokens)
317
+ target_lengths.append(len(tokens))
318
+
319
+ targets = torch.tensor(encoded_targets, dtype=torch.long)
320
+ target_lengths = torch.tensor(target_lengths, dtype=torch.long)
321
+ max_target_len = int(target_lengths.max().item()) if len(target_lengths) > 0 else 1
322
+
323
+ max_w = min(640, max([t.shape[2] for t in tensors]))
324
+ max_w = max(max_w, (max_target_len * 4) + 16)
325
+ max_w = max(32, ((max_w + 3) // 4) * 4)
326
+
327
+ padded_imgs = torch.ones((batch_size, 3, 32, max_w), dtype=torch.float32)
328
+ input_lengths = []
329
+
330
+ for i, t in enumerate(tensors):
331
+ w = min(t.shape[2], max_w)
332
+ padded_imgs[i, :, :, :w] = t[:, :, :w]
333
+ feat_len = max(1, w // 4)
334
+ t_len = int(target_lengths[i].item())
335
+ feat_len = max(feat_len, t_len)
336
+ feat_len = min(feat_len, max_w // 4)
337
+ input_lengths.append(feat_len)
338
+
339
+ input_lengths = torch.tensor(input_lengths, dtype=torch.long)
340
+
341
+ return {
342
+ "images": padded_imgs,
343
+ "targets": targets,
344
+ "input_lengths": input_lengths,
345
+ "target_lengths": target_lengths,
346
+ "texts": list(texts)
347
+ }
348
+
349
+ def create_dataloader(dataset, batch_size=32, shuffle=True, num_workers=0, pin_memory=False):
350
+ return DataLoader(
351
+ dataset,
352
+ batch_size=batch_size,
353
+ shuffle=shuffle,
354
+ num_workers=num_workers,
355
+ collate_fn=ctc_collate_fn,
356
+ pin_memory=pin_memory
357
+ )
358
+
359
+ if __name__ == "__main__":
360
+ # Test Multi-Source Mixed Dataset
361
+ synth_ds = AsterSyntheticDataset(size=50)
362
+ zip_p = "dataset_download/ocr_dataset.zip"
363
+ real_ds = AsterZipDataset(zip_p) if os.path.exists(zip_p) else None
364
+ mixed_ds = AsterMultiSourceDataset(synth_ds, real_ds, real_ratio=0.5, total_size=20)
365
+ loader = create_dataloader(mixed_ds, batch_size=4)
366
+
367
+ for batch in loader:
368
+ print("Mixed batch images:", batch["images"].shape)
369
+ print("Mixed batch texts:", batch["texts"])
370
+ break
371
+ print("[+] Multi-source dataset test passed!")