Spaces:
Running
Running
Download ocr-training/scripts/generate_data_v23_1.py from phucsd/cham-ocr-studio: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/phucsd/cham-ocr-studio/resolve/main/ocr-training/scripts/generate_data_v23_1.py
- Command line
-
hf download hf://spaces/phucsd/cham-ocr-studio/ocr-training/scripts/generate_data_v23_1.py
-
curl -L -o generate_data_v23_1.py https://huggingface.co/spaces/phucsd/cham-ocr-studio/resolve/main/ocr-training/scripts/generate_data_v23_1.py
11.1 kB
| import os | |
| import sys | |
| import json | |
| import random | |
| import re | |
| import numpy as np | |
| from PIL import Image | |
| sys.path.insert(0, '.') | |
| from scripts.generate_data import ( | |
| draw_text_strip, | |
| apply_augmentations, | |
| unicode_to_visual, | |
| get_augmentation_pipeline, | |
| apply_morphological_and_resolution_noise, | |
| add_paper_noise, | |
| FontValidator | |
| ) | |
| # 1. Setup constants | |
| CHAM_CONSONANTS = "ꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨀꨁꨂꨃꨄꨅ" | |
| CHAM_DIACRITICS = "ꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨳꨴꨵꨶꩀꩂꩃꩄꩅꩆꩇꩈꩉꩊꩋꩌꩍ" | |
| CHAM_PUNCT = "꩜꩝꩞꩟" | |
| CHAM_DIGITS = "꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙" | |
| # Target Latin digits and punctuation | |
| LATIN_DIGITS = "0123456789" | |
| PUNCTUATION_CHARS = "[]().,;:/-" | |
| HARD_EXAMPLES_TEMPLATES = [ | |
| "ꨆꨴꨯꩃ (1)", | |
| "ꨟꨧꨮꩌ [2]", | |
| "ꨓꨴꩀ, ꨕꨠꩀ.", | |
| "ꨀꨣꩌ 123", | |
| "ꨌꨰꩀ (2024)", | |
| "[ꨆꨴꨯꩃ] ꨙꩃ", | |
| "ꨚꨴꨯꨱꩃ, ꨕꨫ." | |
| ] | |
| def load_corpus_tokens(): | |
| corpus_path = 'data/corpus/cham_text.txt' | |
| if not os.path.exists(corpus_path): | |
| # Fallback words | |
| return ["ꨆꨴꨯꩃ", "ꨟꨧꨮꩌ", "ꨓꨴꩀ", "ꨕꨠꩀ", "ꨀꨣꩌ", "ꨌꨰꩀ", "ꨙꩃ", "ꨚꨴꨯꨱꩃ", "ꨕꨫ"] | |
| with open(corpus_path, 'r', encoding='utf-8') as f: | |
| text = f.read() | |
| # Split by whitespace | |
| tokens = text.split() | |
| # Filter to retain mostly Cham words | |
| tokens = [t for t in tokens if len(t) >= 1] | |
| return tokens if tokens else ["ꨆꨴꨯꩃ", "ꨟꨧꨮꩌ"] | |
| def main(): | |
| PROJECT_ROOT = '.' | |
| fonts_dir = os.path.join(PROJECT_ROOT, 'data', 'fonts') | |
| output_dir = os.path.join(PROJECT_ROOT, 'data', 'cham_synthetic_v23_1') | |
| os.makedirs(output_dir, exist_ok=True) | |
| # Find fonts | |
| font_files = [os.path.join(fonts_dir, f) for f in os.listdir(fonts_dir) if f.endswith(('.ttf', '.otf'))] | |
| if not font_files: | |
| print("❌ No fonts found in data/fonts/") | |
| return | |
| validator = FontValidator(font_files) | |
| tokens = load_corpus_tokens() | |
| # Helper to generate random Cham word/sentence | |
| def gen_cham_word(): | |
| return random.choice(tokens) | |
| def gen_cham_sentence(n_words=3): | |
| return ' '.join(gen_cham_word() for _ in range(n_words)) | |
| # Stats trackers | |
| stats = { | |
| "latin_digit_count": 0, | |
| "bracket_count": 0, | |
| "period_count": 0, | |
| "comma_count": 0, | |
| "colon_count": 0, | |
| "semicolon_count": 0, | |
| "slash_count": 0, | |
| "dash_count": 0 | |
| } | |
| def track_stats(text): | |
| for char in text: | |
| if char in LATIN_DIGITS: | |
| stats["latin_digit_count"] += 1 | |
| elif char in "[]()": | |
| stats["bracket_count"] += 1 | |
| elif char == '.': | |
| stats["period_count"] += 1 | |
| elif char == ',': | |
| stats["comma_count"] += 1 | |
| elif char == ':': | |
| stats["colon_count"] += 1 | |
| elif char == ';': | |
| stats["semicolon_count"] += 1 | |
| elif char == '/': | |
| stats["slash_count"] += 1 | |
| elif char == '-': | |
| stats["dash_count"] += 1 | |
| # 2. Dataset sampling functions | |
| def sample_normal(): | |
| # 70% normal Cham lines | |
| n_words = random.randint(2, 6) | |
| return gen_cham_sentence(n_words) | |
| def sample_mixed(): | |
| # 20% mixed Cham + digits / punctuation | |
| sentence = gen_cham_sentence(random.randint(2, 4)) | |
| r = random.random() | |
| if r < 0.3: | |
| # append number | |
| sentence += f" {random.randint(0, 999)}" | |
| elif r < 0.6: | |
| # insert dash or slash | |
| sep = random.choice([" - ", " / ", " (2026) ", " [ref] "]) | |
| sentence = gen_cham_sentence(random.randint(1, 2)) + sep + gen_cham_sentence(random.randint(1, 2)) | |
| else: | |
| # punctuation at end or between | |
| sentence += random.choice([".", ",", " (ref).", " [ref];", ":"]) | |
| return sentence | |
| def sample_hard(): | |
| # 10% hard examples with brackets / periods / commas / line references | |
| r = random.random() | |
| if r < 0.4: | |
| # Choose from templates | |
| return random.choice(HARD_EXAMPLES_TEMPLATES) | |
| else: | |
| # Generate custom hard | |
| w1 = gen_cham_word() | |
| w2 = gen_cham_word() | |
| digit = random.randint(0, 99) | |
| hard_type = random.choice([ | |
| f"{w1} ({digit})", | |
| f"{w1} [{digit}]", | |
| f"[{w1}] {w2}", | |
| f"{w1}, {w2}.", | |
| f"{w1} - {w2}", | |
| f"{w1}/{w2}", | |
| f"{digit}/{random.randint(1,12)}/{random.randint(2000, 2030)}" | |
| ]) | |
| return hard_type | |
| # Generation loop | |
| num_samples = 8000 | |
| n_normal = int(num_samples * 0.70) # 5600 | |
| n_mixed = int(num_samples * 0.20) # 1600 | |
| n_hard = num_samples - n_normal - n_mixed # 800 | |
| categories = ( | |
| ['normal'] * n_normal + | |
| ['mixed'] * n_mixed + | |
| ['hard'] * n_hard | |
| ) | |
| random.shuffle(categories) | |
| # Setup directories | |
| train_dir = os.path.join(output_dir, 'train') | |
| val_dir = os.path.join(output_dir, 'val') | |
| os.makedirs(train_dir, exist_ok=True) | |
| os.makedirs(val_dir, exist_ok=True) | |
| transform_pipeline = get_augmentation_pipeline() | |
| train_labels = [] | |
| val_labels = [] | |
| print("Generating train and val images...") | |
| # We split 8000 into 7200 train and 800 val | |
| for idx, cat in enumerate(categories): | |
| if cat == 'normal': | |
| text = sample_normal() | |
| elif cat == 'mixed': | |
| text = sample_mixed() | |
| else: | |
| text = sample_hard() | |
| track_stats(text) | |
| # Draw image | |
| selected_font = random.choice(font_files) | |
| clean_img = draw_text_strip(text, selected_font, img_width=320, img_height=48) | |
| # Apply augmentations (only train gets heavy augs, val gets light morph noise) | |
| is_val = (idx % 10 == 0) | |
| if not is_val: | |
| augmented_img = apply_augmentations(clean_img, transform_pipeline) | |
| save_subdir = train_dir | |
| label_prefix = 'train/' | |
| else: | |
| img_morph = apply_morphological_and_resolution_noise(clean_img) | |
| img_np = np.array(img_morph) | |
| augmented = transform_pipeline(image=img_np) | |
| final_img = Image.fromarray(augmented['image']) | |
| augmented_img = add_paper_noise(final_img, strength='light') | |
| save_subdir = val_dir | |
| label_prefix = 'val/' | |
| img_name = f"synth_{idx:06d}.png" | |
| save_path = os.path.join(save_subdir, img_name) | |
| augmented_img.save(save_path) | |
| visual_text = unicode_to_visual(text) | |
| label_line = f"{label_prefix}{img_name}\t{visual_text}\n" | |
| if not is_val: | |
| train_labels.append(label_line) | |
| else: | |
| val_labels.append(label_line) | |
| # Save label files | |
| with open(os.path.join(output_dir, 'train_label.txt'), 'w', encoding='utf-8') as f: | |
| f.writelines(train_labels) | |
| with open(os.path.join(output_dir, 'val_label.txt'), 'w', encoding='utf-8') as f: | |
| f.writelines(val_labels) | |
| # Write train label stats | |
| all_lengths = [len(unicode_to_visual(l.split('\t')[1].strip())) for l in train_labels] | |
| arr = np.array(all_lengths) | |
| bins_range = [(1, 10), (11, 25), (26, 50), (51, 80)] | |
| dist = {} | |
| for start_b, end_b in bins_range: | |
| dist[f"{start_b}-{end_b}"] = int(np.sum((arr >= start_b) & (arr <= end_b))) | |
| train_label_stats = { | |
| "count": len(all_lengths), | |
| "min": int(np.min(arr)) if len(arr) > 0 else 0, | |
| "max": int(np.max(arr)) if len(arr) > 0 else 0, | |
| "mean": round(float(np.mean(arr)), 2) if len(arr) > 0 else 0.0, | |
| "median": round(float(np.median(arr)), 2) if len(arr) > 0 else 0.0, | |
| "bins": dist | |
| } | |
| with open('output/v23_1_training/train_label_stats.json', 'w', encoding='utf-8') as f: | |
| json.dump(train_label_stats, f, indent=2) | |
| with open('output/v23_1_training/punctuation_coverage_stats.json', 'w', encoding='utf-8') as f: | |
| json.dump(stats, f, indent=2) | |
| print("✅ Synthetic dataset generated successfully.") | |
| print(" Train size:", len(train_labels)) | |
| print(" Val size:", len(val_labels)) | |
| # 3. Generate locked evaluation splits | |
| # Splits: | |
| # clean_cham_200 (200) | |
| # hard_diacritic_200 (200) | |
| # long_line_200 (200) | |
| # punctuation_digit_300 (300) | |
| # mixed_cham_punctuation_300 (300) | |
| eval_dir = os.path.join(output_dir, 'eval') | |
| os.makedirs(eval_dir, exist_ok=True) | |
| def generate_eval_split(split_name, size, sampler_fn, draw_fn=None): | |
| labels = [] | |
| for i in range(size): | |
| text = sampler_fn() | |
| selected_font = random.choice(font_files) | |
| if draw_fn: | |
| img = draw_fn(text, selected_font) | |
| else: | |
| img = draw_text_strip(text, selected_font, img_width=320, img_height=48) | |
| img_name = f"{split_name}_{i:04d}.png" | |
| img.save(os.path.join(eval_dir, img_name)) | |
| visual_text = unicode_to_visual(text) | |
| labels.append(f"eval/{img_name}\t{visual_text}\n") | |
| with open(os.path.join(output_dir, f"{split_name}_label.txt"), 'w', encoding='utf-8') as f: | |
| f.writelines(labels) | |
| print(f" Generated evaluation split {split_name}: {size} images.") | |
| # samplers for eval splits | |
| def sampler_clean(): | |
| return gen_cham_sentence(random.randint(2, 4)) | |
| def sampler_hard_diacritic(): | |
| # Inject hard cluster drills | |
| sentence = gen_cham_sentence(random.randint(1, 2)) + " " + random.choice(["ꨨꨰꨳ", "ꨟꨧꨮꩌ", "ꨓꨌꨯꨱꩍ", "ꨝꨪꨗꩆ"]) + " " + gen_cham_sentence(random.randint(1, 2)) | |
| return sentence | |
| def sampler_long(): | |
| return gen_cham_sentence(random.randint(7, 10)) | |
| def sampler_punctuation_digit(): | |
| # Mostly numbers, brackets, punctuation | |
| digit1 = random.randint(0, 999) | |
| digit2 = random.randint(2000, 2030) | |
| puncs = random.choice([ | |
| f"({digit1})", | |
| f"[{digit1}]", | |
| f"{digit1}/{digit2}", | |
| f"{digit1} - {digit2}", | |
| ".,;:/-" | |
| ]) | |
| return puncs | |
| def sampler_mixed_cham_punc(): | |
| return sample_mixed() | |
| generate_eval_split('clean_cham_200', 200, sampler_clean) | |
| generate_eval_split('hard_diacritic_200', 200, sampler_hard_diacritic) | |
| generate_eval_split('long_line_200', 200, sampler_long) | |
| generate_eval_split('punctuation_digit_300', 300, sampler_punctuation_digit) | |
| generate_eval_split('mixed_cham_punctuation_300', 300, sampler_mixed_cham_punc) | |
| if __name__ == '__main__': | |
| main() | |