Spaces:
Running
Running
docs(audit): complete academic precision and reproducibility cleanup
Browse files- ACADEMIC_AUDIT.md +13 -10
- DATA_PROVENANCE.md +18 -10
- README.md +8 -6
- README_VI.md +8 -6
- RESEARCH.md +22 -11
- ocr-studio/research.html +32 -15
- ocr-training/TRAINING_ROADMAP_V25.md +15 -14
- ocr-training/configs/v25_dataset_manifest.json +108 -0
- ocr-training/scripts/generate_data_v25.py +437 -0
ACADEMIC_AUDIT.md
CHANGED
|
@@ -120,13 +120,13 @@ To adhere to rigorous academic scholarship, multiple unverified or hyperbolic as
|
|
| 120 |
To situate this project appropriately within existing scientific literature, a dedicated related work section and bibliography were added citing seminal contributions:
|
| 121 |
|
| 122 |
1. **Nguyen, M.-T., Schweyer, A.-V., Le, T.-L., Tran, T.-H., & Vu, H. (2019a)**: *Preliminary Results on Ancient Cham Glyph Recognition from Cham Inscription Images*. In 2019 International Conference on Multimedia Analysis and Pattern Recognition (MAPR 2019), IEEE, pp. 1–6. DOI: 10.1109/MAPR.2019.8743540. (Evaluated HOG, NPW, and CNN-derived features with k-NN and linear SVM).
|
| 123 |
-
2. **Nguyen, M.-T., Schweyer, A.-V., Le, T.-L., Tran, T.-H., & Vu, H. (2019b)**: *Improving Ancient Cham Glyph Recognition Using Data Augmentation and Transfer Learning*. In International Conference on Image Analysis and Processing (ICIAP 2019 Workshops: PatReCH), Springer, Cham, LNCS 11808, pp. 115–125. DOI: 10.1007/978-3-030-30754-7_12. (Evaluated data augmentation and transfer learning from
|
| 124 |
3. **Nguyen, T.-N. (2023)**: *Segmentation, Recognition and Indexing of Cham characters in Cham documents* (*Segmentation, reconnaissance et indexation des caractères Cham dans les documents Cham*). Ph.D. Dissertation, Université de La Rochelle, France.
|
| 125 |
4. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2022)**: *An effective method for text line segmentation in historical document images*. In 26th International Conference on Pattern Recognition (ICPR 2022), IEEE, pp. 2686–2692. DOI: 10.1109/ICPR56361.2022.9956617.
|
| 126 |
5. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2023)**: *A Two-Step Sequence Transformer Based Method for Cham to Latin Script Transliteration*. In Proceedings of the 7th International Workshop on Historical Document Imaging and Processing (HIP@ICDAR 2023), ACM, pp. 25–30. DOI: 10.1145/3604951.3605525.
|
| 127 |
6. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2025)**: *Text line segmentation approach combining deep learning model and traditional image processing techniques - application to transliteration of Cham manuscripts*. *Multimedia Tools and Applications*, Springer. DOI: 10.1007/s11042-025-20602-x. (Evaluates line segmentation and transliteration across 627 images, ~8,300 lines).
|
| 128 |
7. **ANR CHAMDOC Project (2019–2024)**: *Cham Documentation (CHAMDOC)*, Project ANR-19-CE27-0018, Agence Nationale de la Recherche, France.
|
| 129 |
-
8. **Digitization Archives**:
|
| 130 |
|
| 131 |
---
|
| 132 |
|
|
@@ -134,9 +134,9 @@ To situate this project appropriately within existing scientific literature, a d
|
|
| 134 |
|
| 135 |
The project openly documents four major technical limitations:
|
| 136 |
|
| 137 |
-
1. **Synthetic-to-Real Domain Gap**: The neural recognizer is trained
|
| 138 |
2. **Absence of a Large-Scale Annotated Real Manuscript Benchmark**: To date, there is no publicly available, character-level annotated historical Cham manuscript dataset of sufficient volume. Curating such a corpus requires close, sustained collaboration with native Cham scholars, paleographers, and community elders.
|
| 139 |
-
3. **Extreme Biological Degradation & Epigraphy**: Inscription rubbings and severely mold-damaged palm-leaves (where
|
| 140 |
4. **Interlinear Overlap Breaking Point (Level 5)**: When sinusoidal document warping exceeds line spacing ($\text{Amplitude} > \text{Gap}$), 1D projection and horizontal bounding boxes break down, necessitating future research into 2D polygonal baseline tracking.
|
| 141 |
|
| 142 |
---
|
|
@@ -154,16 +154,19 @@ The project openly documents four major technical limitations:
|
|
| 154 |
| File Audited | Changes Applied | Status | Verification Mechanism |
|
| 155 |
| :--- | :--- | :---: | :--- |
|
| 156 |
| `LICENSE` | Clean MIT License for code; pointers to DATA_PROVENANCE.md | **Verified** | Standard legal wording |
|
| 157 |
-
| `DATA_PROVENANCE.md` | Documented text corpora, fonts (*Noto Sans Cham*,
|
| 158 |
-
| `RESEARCH.md` | Reconciled title, scope, Unicode order, citations (DOIs, ICPR 2022, 2025 paper), detector parameters, manuscript counts, 25/20/15/15/15/10
|
| 159 |
-
| `ocr-studio/research.html` | Verbatim synchronization with RESEARCH.md;
|
| 160 |
| `ocr-studio/index.html` | Updated model picker, Tab 1 Unicode, Tab 5 empirical benchmarks | **Verified** | `node --check` static JS syntax validated (Code 0) |
|
| 161 |
-
| `README.md` & `README_VI.md` | Standardized title, qualified scope to Eastern Cham, updated synthetic
|
| 162 |
| `generate_data.py` | Fixed PRE_SIGNS (`ꨯꨰ`), MEDIAL_SIGNS (`ꨴꨵꨳꨶ`), cleaned VOWEL_DIACRITIC_SIGNS, enforced canonical Unicode order | **Verified** | Unit tests passing |
|
|
|
|
|
|
|
|
|
|
| 163 |
| `test_vol_roundtrip.py` | Added comprehensive test suites for VA initial/final, medials, pre-vowels, dependent vowels, stanzas | **Verified** | 100% test pass rate |
|
| 164 |
| `validate_unicode_source_labels.py` | Fixed PRE_SIGNS, MEDIAL_SIGNS, and regex for diacritic validation | **Verified** | Script execution clean |
|
| 165 |
| `paddleocr_cham_finetune.ipynb` | Synchronized character sets and canonical cluster reordering with generate_data.py | **Verified** | JSON validated |
|
| 166 |
-
| `ACADEMIC_AUDIT.md` |
|
| 167 |
|
| 168 |
---
|
| 169 |
|
|
@@ -193,4 +196,4 @@ To maintain full transparency with academic collaborators and external reviewers
|
|
| 193 |
|
| 194 |
---
|
| 195 |
|
| 196 |
-
**
|
|
|
|
| 120 |
To situate this project appropriately within existing scientific literature, a dedicated related work section and bibliography were added citing seminal contributions:
|
| 121 |
|
| 122 |
1. **Nguyen, M.-T., Schweyer, A.-V., Le, T.-L., Tran, T.-H., & Vu, H. (2019a)**: *Preliminary Results on Ancient Cham Glyph Recognition from Cham Inscription Images*. In 2019 International Conference on Multimedia Analysis and Pattern Recognition (MAPR 2019), IEEE, pp. 1–6. DOI: 10.1109/MAPR.2019.8743540. (Evaluated HOG, NPW, and CNN-derived features with k-NN and linear SVM).
|
| 123 |
+
2. **Nguyen, M.-T., Schweyer, A.-V., Le, T.-L., Tran, T.-H., & Vu, H. (2019b)**: *Improving Ancient Cham Glyph Recognition Using Data Augmentation and Transfer Learning*. In International Conference on Image Analysis and Processing (ICIAP 2019 Workshops: PatReCH), Springer, Cham, LNCS 11808, pp. 115–125. DOI: 10.1007/978-3-030-30754-7_12. (Evaluated data augmentation and transfer learning from scripts of similar or the same language family to ancient stone inscription glyphs).
|
| 124 |
3. **Nguyen, T.-N. (2023)**: *Segmentation, Recognition and Indexing of Cham characters in Cham documents* (*Segmentation, reconnaissance et indexation des caractères Cham dans les documents Cham*). Ph.D. Dissertation, Université de La Rochelle, France.
|
| 125 |
4. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2022)**: *An effective method for text line segmentation in historical document images*. In 26th International Conference on Pattern Recognition (ICPR 2022), IEEE, pp. 2686–2692. DOI: 10.1109/ICPR56361.2022.9956617.
|
| 126 |
5. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2023)**: *A Two-Step Sequence Transformer Based Method for Cham to Latin Script Transliteration*. In Proceedings of the 7th International Workshop on Historical Document Imaging and Processing (HIP@ICDAR 2023), ACM, pp. 25–30. DOI: 10.1145/3604951.3605525.
|
| 127 |
6. **Nguyen, T.-N., Burie, J.-C., Le, T.-L., & Schweyer, A.-V. (2025)**: *Text line segmentation approach combining deep learning model and traditional image processing techniques - application to transliteration of Cham manuscripts*. *Multimedia Tools and Applications*, Springer. DOI: 10.1007/s11042-025-20602-x. (Evaluates line segmentation and transliteration across 627 images, ~8,300 lines).
|
| 128 |
7. **ANR CHAMDOC Project (2019–2024)**: *Cham Documentation (CHAMDOC)*, Project ANR-19-CE27-0018, Agence Nationale de la Recherche, France.
|
| 129 |
+
8. **Digitization Archives**: The Historic Cham Manuscripts of Vietnam collection in the Southeast Asia Digital Library (SEADL) contains 977 digitized manuscripts comprising more than 57,800 page scans. Cham community collections preserve an estimated 3,000 physical manuscripts across Vietnam and Cambodia.
|
| 130 |
|
| 131 |
---
|
| 132 |
|
|
|
|
| 134 |
|
| 135 |
The project openly documents four major technical limitations:
|
| 136 |
|
| 137 |
+
1. **Synthetic-to-Real Domain Gap**: The neural recognizer is trained predominantly on synthetic textline images rendered with bundled *Noto Sans Cham* (SIL OFL 1.1). Real manuscripts feature idiosyncratic scribal handwriting, varying pen pressure, and ink bleed that may degrade recognition accuracy in archival field settings.
|
| 138 |
2. **Absence of a Large-Scale Annotated Real Manuscript Benchmark**: To date, there is no publicly available, character-level annotated historical Cham manuscript dataset of sufficient volume. Curating such a corpus requires close, sustained collaboration with native Cham scholars, paleographers, and community elders.
|
| 139 |
+
3. **Extreme Biological Degradation & Epigraphy**: Inscription rubbings and severely mold-damaged palm-leaves (where character ink has substantially eroded) cannot be resolved by visual CTC sequence decoders alone without integrating language modeling priors (MLMs).
|
| 140 |
4. **Interlinear Overlap Breaking Point (Level 5)**: When sinusoidal document warping exceeds line spacing ($\text{Amplitude} > \text{Gap}$), 1D projection and horizontal bounding boxes break down, necessitating future research into 2D polygonal baseline tracking.
|
| 141 |
|
| 142 |
---
|
|
|
|
| 154 |
| File Audited | Changes Applied | Status | Verification Mechanism |
|
| 155 |
| :--- | :--- | :---: | :--- |
|
| 156 |
| `LICENSE` | Clean MIT License for code; pointers to DATA_PROVENANCE.md | **Verified** | Standard legal wording |
|
| 157 |
+
| `DATA_PROVENANCE.md` | Documented text corpora, fonts (*Noto Sans Cham*, external reference fonts), rights disclaimer, and dual-generator architecture | **Verified** | Created & linked across docs |
|
| 158 |
+
| `RESEARCH.md` | Reconciled title, scope, Unicode order, citations (DOIs, ICPR 2022, 2025 paper), detector parameters, manuscript counts, dual generators (Pipeline A baseline 25/20/15/15/15/10 and Pipeline B V25 5-pillar) | **Verified** | 100% matched with benchmarks |
|
| 159 |
+
| `ocr-studio/research.html` | Verbatim synchronization with RESEARCH.md; updated title, tag pill, abstract grammar, citations, and dual generators | **Verified** | `node --check` static JS syntax validated (Code 0) |
|
| 160 |
| `ocr-studio/index.html` | Updated model picker, Tab 1 Unicode, Tab 5 empirical benchmarks | **Verified** | `node --check` static JS syntax validated (Code 0) |
|
| 161 |
+
| `README.md` & `README_VI.md` | Standardized title, qualified scope to Eastern Cham, updated synthetic pipeline to dual generators | **Verified** | Synchronized across languages |
|
| 162 |
| `generate_data.py` | Fixed PRE_SIGNS (`ꨯꨰ`), MEDIAL_SIGNS (`ꨴꨵꨳꨶ`), cleaned VOWEL_DIACRITIC_SIGNS, enforced canonical Unicode order | **Verified** | Unit tests passing |
|
| 163 |
+
| `generate_data_v25.py` | Fixed docstring (140k/10k), renamed `MINIMAL_PAIRS_UE_WA` (`ꨲ` UE vs `ꨶ` WA), synchronized with manifest | **Verified** | Code inspection & syntax clean |
|
| 164 |
+
| `v25_dataset_manifest.json` | Reconciled batch size (32/card, 64 global), stages (1-12, 13-22, 23-40), and diacritic naming with `rec_cham_v25.yml` | **Verified** | JSON syntax validated |
|
| 165 |
+
| `TRAINING_ROADMAP_V25.md` | Aligned data distribution table to 5 pillars, updated execution plan stages to 1-12, 13-22, 23-40, fixed diacritic naming | **Verified** | Documented & reconciled |
|
| 166 |
| `test_vol_roundtrip.py` | Added comprehensive test suites for VA initial/final, medials, pre-vowels, dependent vowels, stanzas | **Verified** | 100% test pass rate |
|
| 167 |
| `validate_unicode_source_labels.py` | Fixed PRE_SIGNS, MEDIAL_SIGNS, and regex for diacritic validation | **Verified** | Script execution clean |
|
| 168 |
| `paddleocr_cham_finetune.ipynb` | Synchronized character sets and canonical cluster reordering with generate_data.py | **Verified** | JSON validated |
|
| 169 |
+
| `ACADEMIC_AUDIT.md` | Documentation Audit & Reconciliation Log tracking known issues and empirical uncertainties | **Verified** | Fully documented |
|
| 170 |
|
| 171 |
---
|
| 172 |
|
|
|
|
| 196 |
|
| 197 |
---
|
| 198 |
|
| 199 |
+
**Summary of Documentation & Verification Status**: The documentation across `phucsd/Cham-OCR` has been calibrated against actual code artifacts and empirical benchmark logs. Open empirical uncertainties and ongoing data collection needs remain cataloged above to guide future research and collaborative fieldwork.
|
DATA_PROVENANCE.md
CHANGED
|
@@ -14,10 +14,10 @@ This document provides a transparent, verifiable record of the provenance, right
|
|
| 14 |
|
| 15 |
| Source / Title | Author / Editor / Institution | Web Reference / Archive | Acquisition Method | Rights / License Status | Redistributed in Repo? |
|
| 16 |
| :--- | :--- | :--- | :--- | :--- | :---: |
|
| 17 |
-
| *Akayet Inra Patra* (Classical verse epic) | Traditional Cham scribal transmission; modern editorial transliterations by Cham scholars | Academic & community preservation publications | Curated and transcribed from
|
| 18 |
-
| *Ariya Po Pareng* (Classical narrative poem) | Traditional Cham scribal transmission | Cham cultural preservation archives | Curated and transcribed from traditional manuscripts | Traditional cultural heritage
|
| 19 |
-
| Historical chronicles & folk narratives (*Dalikal*) | Cham traditional oral and written literature | Cham community collections and regional cultural publications | Digitized and normalized into Unicode | Traditional cultural heritage | Yes (normalized Unicode textlines in `cham_text.txt`) |
|
| 20 |
-
| Cham-Vietnamese-French Lexical Compendia | Historical missionary and linguistic wordlists (e.g. Aymonier & Cabaton 1906; Moussay 1971; modern pedagogical materials) | Academic library collections & SEADL references | Digital normalization into standard Unicode 16.0 Cham | Historical reference works / non-
|
| 21 |
|
| 22 |
### 1.2 Corpus Curation & Linguistic Preprocessing
|
| 23 |
* **Orthographic Normalization**: Conversion into canonical Unicode 16.0 Cham representation (`U+AA00`–`U+AA5F`).
|
|
@@ -56,10 +56,10 @@ The following typefaces were evaluated as reference designs during research expe
|
|
| 56 |
|
| 57 |
## 3. Synthetic Document & Textline Datasets
|
| 58 |
|
| 59 |
-
Due to the acute scarcity of character-level annotated historical Cham manuscript datasets, training and benchmark datasets were synthesized programmatically from `cham_text.txt`:
|
| 60 |
|
| 61 |
-
### 3.1
|
| 62 |
-
The generator
|
| 63 |
* **Evaluation & Locked Splits (15% total / 22,500 lines)**:
|
| 64 |
- `val_clean_short` (3% / 4,500 lines)
|
| 65 |
- `val_clean_long` (3% / 4,500 lines)
|
|
@@ -74,14 +74,22 @@ The generator partitions the 150,000 synthetic corpus into **15% evaluation/lock
|
|
| 74 |
- `train_noisy_long` (**15%** / 19,125 lines): Long verse lines with severe photometric degradation and spatial elastic distortion.
|
| 75 |
- `train_hard_examples` (**10%** / 12,750 lines): Adversarial minimal pairs (`ꨲ` vs `ꨶ`, `꩝꩝` vs `꩝`), verse numerals (1–99) with section marks (`꩑꩞`), and stacked subjoined clusters.
|
| 76 |
|
| 77 |
-
### 3.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
* 200 full-page synthetic documents comprising 903 ground-truth textlines across 5 difficulty levels (Standard, Aged Paper, Narrow Gap, Wavy Sinusoid, Extreme Overlap).
|
| 79 |
* Generated by `ocr-benchmark/scripts/generate_benchmark_200.py` to establish the mathematical breaking point of line segmentation and CTC recognition.
|
| 80 |
|
| 81 |
-
### 3.
|
| 82 |
* 50 targeted synthetic image crops covering 10 distinct failure modes (noise, tilt, blur, diacritic confusion, low resolution).
|
| 83 |
|
| 84 |
-
### 3.
|
| 85 |
Synthetically rendered document images and transcriptions generated by repository scripts inherit the rights and usage status of the underlying linguistic corpora and fonts. The synthetic generator code and annotations produced by the project maintainers are provided under the project's [MIT License](LICENSE), while redistribution of generated textline images remains subject to the rights status of the respective underlying source texts.
|
| 86 |
|
| 87 |
---
|
|
|
|
| 14 |
|
| 15 |
| Source / Title | Author / Editor / Institution | Web Reference / Archive | Acquisition Method | Rights / License Status | Redistributed in Repo? |
|
| 16 |
| :--- | :--- | :--- | :--- | :--- | :---: |
|
| 17 |
+
| *Akayet Inra Patra* (Classical verse epic) | Traditional Cham scribal transmission; modern editorial transliterations by Cham scholars | Academic & community preservation publications | Curated and transcribed from traditional cultural heritage texts | Traditional cultural heritage / specific edition rights unverified / non-commercial educational & preservation use (line-by-line edition mapping subject to archival verification) | Yes (as normalized Unicode textlines in `cham_text.txt`) |
|
| 18 |
+
| *Ariya Po Pareng* (Classical narrative poem) | Traditional Cham scribal transmission | Cham cultural preservation archives | Curated and transcribed from traditional manuscripts | Traditional cultural heritage / specific edition rights unverified / non-commercial educational & preservation use (line-by-line edition mapping subject to archival verification) | Yes (normalized Unicode textlines in `cham_text.txt`) |
|
| 19 |
+
| Historical chronicles & folk narratives (*Dalikal*) | Cham traditional oral and written literature | Cham community collections and regional cultural publications | Digitized and normalized into Unicode | Traditional cultural heritage / specific edition rights unverified / non-commercial educational & preservation use (line-by-line edition mapping subject to archival verification) | Yes (normalized Unicode textlines in `cham_text.txt`) |
|
| 20 |
+
| Cham-Vietnamese-French Lexical Compendia | Historical missionary and linguistic wordlists (e.g. Aymonier & Cabaton 1906; Moussay 1971; modern pedagogical materials) | Academic library collections & SEADL references | Digital normalization into standard Unicode 16.0 Cham | Historical reference works / specific edition rights unverified / non-commercial educational & preservation use (line-by-line edition mapping subject to archival verification) | Yes (vocabulary tokens merged into `cham_text.txt`) |
|
| 21 |
|
| 22 |
### 1.2 Corpus Curation & Linguistic Preprocessing
|
| 23 |
* **Orthographic Normalization**: Conversion into canonical Unicode 16.0 Cham representation (`U+AA00`–`U+AA5F`).
|
|
|
|
| 56 |
|
| 57 |
## 3. Synthetic Document & Textline Datasets
|
| 58 |
|
| 59 |
+
Due to the acute scarcity of character-level annotated historical Cham manuscript datasets, training and benchmark datasets were synthesized programmatically from `cham_text.txt`. The repository maintains two distinct synthetic generation pipelines:
|
| 60 |
|
| 61 |
+
### 3.1 Pipeline A: Baseline General Generator (`scripts/generate_data.py`)
|
| 62 |
+
The baseline generator produces a 150,000 textline corpus partitioned into **15% evaluation/locked splits** (22,500 lines) and **85% training split** (127,500 lines):
|
| 63 |
* **Evaluation & Locked Splits (15% total / 22,500 lines)**:
|
| 64 |
- `val_clean_short` (3% / 4,500 lines)
|
| 65 |
- `val_clean_long` (3% / 4,500 lines)
|
|
|
|
| 74 |
- `train_noisy_long` (**15%** / 19,125 lines): Long verse lines with severe photometric degradation and spatial elastic distortion.
|
| 75 |
- `train_hard_examples` (**10%** / 12,750 lines): Adversarial minimal pairs (`ꨲ` vs `ꨶ`, `꩝꩝` vs `꩝`), verse numerals (1–99) with section marks (`꩑꩞`), and stacked subjoined clusters.
|
| 76 |
|
| 77 |
+
### 3.2 Pipeline B: Experimental V25 Generator (`scripts/generate_data_v25.py`)
|
| 78 |
+
The V25 generator synthesizes 150,000 textline images (140,000 train + 10,000 val) synchronized with `configs/v25_dataset_manifest.json` across 5 core pillars:
|
| 79 |
+
1. **Canonical Cham Literature (43.3% / 65,000 lines)**: Classical literary vocabulary (*Po Klong Garai*, 57-stanza verse).
|
| 80 |
+
2. **Anti-Motion Blur Base (20.0% / 30,000 lines)**: Clean base images paired with dynamic in-RAM directional motion blur ($7\times 7$ to $13\times 13$) and defocus.
|
| 81 |
+
3. **Inline Bilingual Code-Switching (16.7% / 25,000 lines)**: Intra-line mixed phrases with Vietnamese diacritics and Latin annotations.
|
| 82 |
+
4. **Stanza Numerals & Boundary Punctuation (12.0% / 18,000 lines)**: Verse numbers $1$–$99$ (`꩑꩞`..`꩙꩙꩞`) and boundary marks (`꩞`, `:`, `–`).
|
| 83 |
+
5. **Adversarial Minimal Pairs (8.0% / 12,000 lines)**: Micro-stroke pairs (`ꨲ` U+AA32 Vowel Sign UE vs `ꨶ` U+AA36 Medial WA, variable Double Danda `꩝꩝` spacing 2–8px, 3-tier diacritic stacks).
|
| 84 |
+
|
| 85 |
+
### 3.3 Controlled 200-Page Synthetic Document Stress-Test (`ocr-benchmark/`)
|
| 86 |
* 200 full-page synthetic documents comprising 903 ground-truth textlines across 5 difficulty levels (Standard, Aged Paper, Narrow Gap, Wavy Sinusoid, Extreme Overlap).
|
| 87 |
* Generated by `ocr-benchmark/scripts/generate_benchmark_200.py` to establish the mathematical breaking point of line segmentation and CTC recognition.
|
| 88 |
|
| 89 |
+
### 3.4 50-Test Stratified Diagnostic Suite (`ocr-studio/data/benchmark_50_tests_results.json`)
|
| 90 |
* 50 targeted synthetic image crops covering 10 distinct failure modes (noise, tilt, blur, diacritic confusion, low resolution).
|
| 91 |
|
| 92 |
+
### 3.5 Rights Status of Synthetic Outputs
|
| 93 |
Synthetically rendered document images and transcriptions generated by repository scripts inherit the rights and usage status of the underlying linguistic corpora and fonts. The synthetic generator code and annotations produced by the project maintainers are provided under the project's [MIT License](LICENSE), while redistribution of generated textline images remains subject to the rights status of the respective underlying source texts.
|
| 94 |
|
| 95 |
---
|
README.md
CHANGED
|
@@ -38,10 +38,12 @@ An end-to-end deep learning research platform and paleographic transcription sys
|
|
| 38 |
- Dual-head multi-task learning with **Connectionist Temporal Classification (CTC)** sequence alignment and **NRTR Multi-Head Cross-Attention** auxiliary decoding.
|
| 39 |
3. **Canonical Unicode & Logical Order Normalization**:
|
| 40 |
- Automatically parses recognized grapheme clusters via `normalize_unicode` to reconstruct standard **Brahmic Logical Order** (`Base Consonant + Medials + Pre-Vowels + Dependent Vowels + Vowel Lengthener AA + Finals / Signs / Final VA`), resolving local visual permutation artifacts.
|
| 41 |
-
4. **150,000 Textline Synthetic
|
| 42 |
- Font map validation via `fontTools` cmap parsing, eliminating tofu/missing glyph artifacts.
|
| 43 |
- Dynamic In-RAM augmentation applying directional motion blur on clean disk renders to prevent double-blur degradation.
|
| 44 |
-
-
|
|
|
|
|
|
|
| 45 |
5. **Distributed Multi-GPU Cloud Training**:
|
| 46 |
- Adaptive data-parallel execution (`paddle.distributed.launch --gpus '0,1'`) on Kaggle Nvidia Tesla T4x2 and Lightning Cloud A100 SXM4.
|
| 47 |
- 3-Stage Checkpoint & Resume protocol ensuring uninterrupted progress across Kaggle's 12-hour session timeout limits.
|
|
@@ -172,13 +174,13 @@ Logged in `ocr-studio/data/benchmark_v24_vs_v25_results.json`:
|
|
| 172 |
If you utilize Cham-OCR, our synthetic dataset pipeline, or the OCR Studio in your research, please cite:
|
| 173 |
|
| 174 |
```bibtex
|
| 175 |
-
@software{
|
| 176 |
-
author = {Nguyen, Phuc},
|
| 177 |
-
title = {Cham
|
| 178 |
year = {2026},
|
| 179 |
publisher = {GitHub},
|
| 180 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 181 |
-
note = {
|
| 182 |
}
|
| 183 |
```
|
| 184 |
|
|
|
|
| 38 |
- Dual-head multi-task learning with **Connectionist Temporal Classification (CTC)** sequence alignment and **NRTR Multi-Head Cross-Attention** auxiliary decoding.
|
| 39 |
3. **Canonical Unicode & Logical Order Normalization**:
|
| 40 |
- Automatically parses recognized grapheme clusters via `normalize_unicode` to reconstruct standard **Brahmic Logical Order** (`Base Consonant + Medials + Pre-Vowels + Dependent Vowels + Vowel Lengthener AA + Finals / Signs / Final VA`), resolving local visual permutation artifacts.
|
| 41 |
+
4. **150,000 Textline Synthetic Pipelines**:
|
| 42 |
- Font map validation via `fontTools` cmap parsing, eliminating tofu/missing glyph artifacts.
|
| 43 |
- Dynamic In-RAM augmentation applying directional motion blur on clean disk renders to prevent double-blur degradation.
|
| 44 |
+
- **Dual-Generator Architecture**:
|
| 45 |
+
- *Pipeline A (Baseline: `scripts/generate_data.py`)*: 150,000 textlines partitioned into 15% evaluation/locked splits and 85% training across 6 categories: clean short (25%), clean medium (20%), clean long (15%), noisy short (15%), noisy long (15%), and hard adversarial examples (10%).
|
| 46 |
+
- *Pipeline B (Experimental V25: `scripts/generate_data_v25.py`)*: 150,000 textlines (140,000 train + 10,000 val) synchronized with `configs/v25_dataset_manifest.json` across 5 core pillars: canonical Cham literature (43.3%), anti-blur base (20.0%), inline bilingual (16.7%), stanza numerals & punctuation (12.0%), and minimal pairs (8.0%).
|
| 47 |
5. **Distributed Multi-GPU Cloud Training**:
|
| 48 |
- Adaptive data-parallel execution (`paddle.distributed.launch --gpus '0,1'`) on Kaggle Nvidia Tesla T4x2 and Lightning Cloud A100 SXM4.
|
| 49 |
- 3-Stage Checkpoint & Resume protocol ensuring uninterrupted progress across Kaggle's 12-hour session timeout limits.
|
|
|
|
| 174 |
If you utilize Cham-OCR, our synthetic dataset pipeline, or the OCR Studio in your research, please cite:
|
| 175 |
|
| 176 |
```bibtex
|
| 177 |
+
@software{cham_ocr_pipeline,
|
| 178 |
+
author = {Nguyen, Phuc H.},
|
| 179 |
+
title = {Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition},
|
| 180 |
year = {2026},
|
| 181 |
publisher = {GitHub},
|
| 182 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 183 |
+
note = {Production web service: https://ocr.cham.asia}
|
| 184 |
}
|
| 185 |
```
|
| 186 |
|
README_VI.md
CHANGED
|
@@ -83,9 +83,11 @@ Cham-OCR/
|
|
| 83 |
- Mạng nơ-ron **PP-OCRv4 SVTR-LCNet** mở rộng tensor đầu vào lên `[3, 48, 480]`.
|
| 84 |
- Cơ chế giải mã kép: CTC Loss kết hợp NRTR Multi-Head Cross-Attention.
|
| 85 |
- Chuẩn hóa thứ tự gõ Logic Chăm `normalize_unicode` (Brahmic Logical Order: Phụ âm cơ sở + Dấu phụ dưới chân/quấn RA/LA + Dấu phụ YA/WA + Nguyên âm đứng trước O/AI + Các nguyên âm phụ thuộc khác + Dấu kéo dài âm AA + Phụ âm cuối/Dấu ngắt).
|
| 86 |
-
3. **Sinh Dữ liệu Tổng hợp
|
| 87 |
-
- 150,000 dòng dữ liệu tổng hợp qua 4 gói phân tầng.
|
| 88 |
- Cơ chế lọc chữ lỗi ô vuông (tofu) bằng `fontTools` cmap và làm mờ động học trong RAM (*On-the-Fly Motion Blur*).
|
|
|
|
|
|
|
|
|
|
| 89 |
4. **Triển khai Trực tuyến**:
|
| 90 |
- Đóng gói container Docker trên **Hugging Face Spaces** (`phucsd/cham-ocr-studio`).
|
| 91 |
- Tên miền dịch vụ chính thức: **[https://ocr.cham.asia](https://ocr.cham.asia)**.
|
|
@@ -115,13 +117,13 @@ Mọi số liệu công bố đều được đối chiếu trực tiếp từ c
|
|
| 115 |
Nếu bạn sử dụng tài liệu, bộ sinh dữ liệu hoặc công cụ Cham OCR Studio trong nghiên cứu, vui lòng trích dẫn:
|
| 116 |
|
| 117 |
```bibtex
|
| 118 |
-
@software{
|
| 119 |
-
author = {Nguyen, Phuc},
|
| 120 |
-
title = {Cham
|
| 121 |
year = {2026},
|
| 122 |
publisher = {GitHub},
|
| 123 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 124 |
-
note = {
|
| 125 |
}
|
| 126 |
```
|
| 127 |
|
|
|
|
| 83 |
- Mạng nơ-ron **PP-OCRv4 SVTR-LCNet** mở rộng tensor đầu vào lên `[3, 48, 480]`.
|
| 84 |
- Cơ chế giải mã kép: CTC Loss kết hợp NRTR Multi-Head Cross-Attention.
|
| 85 |
- Chuẩn hóa thứ tự gõ Logic Chăm `normalize_unicode` (Brahmic Logical Order: Phụ âm cơ sở + Dấu phụ dưới chân/quấn RA/LA + Dấu phụ YA/WA + Nguyên âm đứng trước O/AI + Các nguyên âm phụ thuộc khác + Dấu kéo dài âm AA + Phụ âm cuối/Dấu ngắt).
|
| 86 |
+
3. **Hệ thống Sinh Dữ liệu Tổng hợp 150.000 dòng**:
|
|
|
|
| 87 |
- Cơ chế lọc chữ lỗi ô vuông (tofu) bằng `fontTools` cmap và làm mờ động học trong RAM (*On-the-Fly Motion Blur*).
|
| 88 |
+
- **Kiến trúc Hai Bộ Sinh Dữ liệu (Dual Generators)**:
|
| 89 |
+
- *Pipeline A (Bộ sinh chuẩn: `scripts/generate_data.py`)*: 150.000 dòng chia 15% kiểm thử/đóng băng và 85% huấn luyện qua 6 nhóm: ngắn sạch (25%), vừa sạch (20%), dài sạch (15%), ngắn nhiễu (15%), dài nhiễu (15%), cặp đối kháng (10%).
|
| 90 |
+
- *Pipeline B (Bộ sinh thực nghiệm V25: `scripts/generate_data_v25.py`)*: 150.000 dòng (140.000 train + 10.000 val) đồng bộ trực tiếp với `configs/v25_dataset_manifest.json` qua 5 trụ cột: ngữ liệu chuẩn (43,3%), chống mờ rung tay (20,0%), song ngữ kẹp dòng (16,7%), số khổ & dấu câu (12,0%), cặp đối kháng vi mô (8,0%).
|
| 91 |
4. **Triển khai Trực tuyến**:
|
| 92 |
- Đóng gói container Docker trên **Hugging Face Spaces** (`phucsd/cham-ocr-studio`).
|
| 93 |
- Tên miền dịch vụ chính thức: **[https://ocr.cham.asia](https://ocr.cham.asia)**.
|
|
|
|
| 117 |
Nếu bạn sử dụng tài liệu, bộ sinh dữ liệu hoặc công cụ Cham OCR Studio trong nghiên cứu, vui lòng trích dẫn:
|
| 118 |
|
| 119 |
```bibtex
|
| 120 |
+
@software{cham_ocr_pipeline,
|
| 121 |
+
author = {Nguyen, Phuc H.},
|
| 122 |
+
title = {Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition},
|
| 123 |
year = {2026},
|
| 124 |
publisher = {GitHub},
|
| 125 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 126 |
+
note = {Production web service: https://ocr.cham.asia}
|
| 127 |
}
|
| 128 |
```
|
| 129 |
|
RESEARCH.md
CHANGED
|
@@ -115,7 +115,7 @@ However, pre-vowels such as `ꨯ` (Vowel Sign O, U+AA2F) and `ꨰ` (Vowel Sign A
|
|
| 115 |
### 2.1 Early Epigraphic & Glyph Recognition
|
| 116 |
Automated processing of Cham script originated in the analysis of stone inscriptions from ancient Champa sanctuaries (Mỹ Sơn, Po Nagar, Đồng Dương). Nguyen, Schweyer, Le, Tran, & Vu (2019a) presented preliminary results on recognizing ancient Cham glyphs extracted from stone rubbings and photographs using Histogram of Oriented Gradients (HOG), Non-Parametric Weighted (NPW) features, and CNN-derived feature representations coupled with $k$-NN and linear SVM classifiers at the IEEE MAPR 2019 conference. Due to severe stone weathering, erosion, and irregular surface texture, isolated glyph classification across noisy background substrates was evaluated.
|
| 117 |
|
| 118 |
-
To mitigate extreme sample scarcity in epigraphic corpora, Nguyen, Schweyer, Le, Tran, & Vu (2019b) evaluated data augmentation strategies and transfer learning from
|
| 119 |
|
| 120 |
### 2.2 Deep Learning for Cham Epigraphy & Transliteration
|
| 121 |
In his doctoral dissertation, Nguyen (2023) developed comprehensive document image processing and understanding methodologies dedicated to ancient Cham documents. His work investigated stone inscription image restoration, automatic glyph segmentation under rough stone substrate conditions, and deep convolutional representations for historical Brahmic scripts.
|
|
@@ -123,7 +123,7 @@ In his doctoral dissertation, Nguyen (2023) developed comprehensive document ima
|
|
| 123 |
Complementing glyph recognition, Nguyen, Burie, Le, & Schweyer (2022) proposed an effective multi-stage method for text line segmentation in historical document images presented at the 26th International Conference on Pattern Recognition (ICPR 2022), addressing line curvature and touching diacritics in historical archives. Expanding to linguistic representation, Nguyen, Burie, Le, & Schweyer (2023) introduced a two-step sequence transformer model for Cham-to-Latin script transliteration presented at the HIP@ICDAR 2023 workshop. Their system demonstrated the efficacy of sequence-to-sequence transformers in mapping recognized Cham text sequences to standardized Latin phonological romanizations (*Cam-Latin*). Most recently, Nguyen, Burie, Le, & Schweyer (2025) published a comprehensive study entitled *Text line segmentation approach combining deep learning model and traditional image processing techniques - application to transliteration of Cham manuscripts* in *Multimedia Tools and Applications*, evaluating text-line segmentation and automatic transliteration across 627 manuscript images and approximately 8,300 textlines (focusing on line segmentation and transliteration rather than end-to-end character OCR benchmarking).
|
| 124 |
|
| 125 |
### 2.3 Manuscript Preservation & Digitization Initiatives
|
| 126 |
-
Beyond epigraphy on stone, historical Cham manuscripts on paper (*kertas*) and palm-leaf have been documented through preservation missions.
|
| 127 |
|
| 128 |
However, despite the preservation of tens of thousands of scanned folios, almost none of these archives possess open, machine-readable character-level or line-level bounding box ground-truth annotations suitable for training supervised deep learning models. The bottleneck for Cham OCR is therefore not an absolute absence of physical manuscripts, but rather the severe scarcity of annotated, verified training ground truth.
|
| 129 |
|
|
@@ -139,7 +139,7 @@ Prior research has focused predominantly on either **isolated glyph recognition
|
|
| 139 |
## 3. Text Detection & Indic Line Segmentation (Cham-DBNet)
|
| 140 |
|
| 141 |
### 3.1 The Interlinear Diacritic Collision Problem
|
| 142 |
-
|
| 143 |
1. **Line Merging**: Grouping adjacent lines into a single bounding box when diacritics touch.
|
| 144 |
2. **Diacritic Amputation**: Slicing off upper vowel signs or subjoined medials during rectangular cropping, which causes fatal downstream recognition errors.
|
| 145 |
|
|
@@ -211,8 +211,11 @@ def normalize_unicode(text: str) -> str:
|
|
| 211 |
### 5.1 The Data Scarcity Bottleneck
|
| 212 |
Deep neural recognition networks require hundreds of thousands of diverse labeled textline instances. However, surviving physical Cham manuscripts lack character-level bounding box ground truth. To overcome this limitation, we engineered a synthetic generation pipeline (`scripts/generate_data.py`) capable of synthesizing 150,000 diverse textline crops from authentic literary corpora.
|
| 213 |
|
| 214 |
-
### 5.2 Calibrated Synthetic Dataset Partitioning
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
| Training Sub-Category | Proportion | Line Count | Target Composition & Protocol | Target Failure Mode Addressed |
|
| 218 |
| :--- | :---: | :---: | :--- | :--- |
|
|
@@ -223,6 +226,14 @@ The synthetic dataset generator (`scripts/generate_data.py`) partitions the targ
|
|
| 223 |
| `train_noisy_long` | **15%** | 19,125 | Long verse lines with severe photometric degradation and spatial elastic distortion. | Prevents alignment collapse on degraded multi-word lines. |
|
| 224 |
| `train_hard_examples` | **10%** | 12,750 | Adversarial minimal pairs (`ꨲ` vs `ꨶ`, `꩝꩝` vs `꩝`), verse numerals (1–99) with section marks (`꩑꩞`), and stacked subjoined clusters. | Eliminates CTC misclassification of numerals as letters (`꩔` vs `ꨤ`, `꩕` vs `ꨅ`) and diacritic collapsing. |
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
### 5.3 Digital Typefaces & TrueType cmap Tofu Glyph Safeguards
|
| 227 |
When rendering synthetic text, digital Cham typefaces must be verified for glyph coverage. The primary bundled and reproducible typeface distributed directly within this repository is **Noto Sans Cham** (`Regular`, `Bold`, `Black`; SIL Open Font License 1.1, located in `ocr-training/data/fonts/`). External reference typefaces evaluated during research—including *Cham Roman*, *EFEO Cham*, and community fonts—are not redistributed in this repository and must be acquired independently if researchers wish to reproduce specific historical font variations.
|
| 228 |
|
|
@@ -324,9 +335,9 @@ if not hasattr(np, 'typeDict'): np.typeDict = {}
|
|
| 324 |
|
| 325 |
While our pipeline demonstrates substantial gains on synthetic and semi-controlled documents, rigorous academic honesty requires acknowledging the following boundaries:
|
| 326 |
|
| 327 |
-
1. **Synthetic-to-Real Domain Gap**: The recognizer is trained predominantly on synthetic textlines rendered with
|
| 328 |
2. **Absence of Large-Scale Real Manuscript Ground Truth**: Due to the acute scarcity of digitized historical Cham archives with line-level annotations, current benchmarks rely on controlled synthetic stress-tests and curated test slices. Establishing an open, expert-verified historical palm-leaf manuscript benchmark represents an urgent necessity requiring future collaborative scholarship with native Cham elders and linguists.
|
| 329 |
-
3. **Severe Biological Degradation & Epigraphy**: Inscription stone rubbings and severely mold-damaged palm leaves (where
|
| 330 |
4. **Extreme Interlinear Overlap (Level 5)**: When severe paper wrinkling causes interlinear wave amplitude to exceed line spacing ($\text{Amplitude} > \text{Gap}$), 1D projection and horizontal bounding boxes collapse, indicating the need for 2D polygonal baseline tracking models in future iterations.
|
| 331 |
|
| 332 |
---
|
|
@@ -334,13 +345,13 @@ While our pipeline demonstrates substantial gains on synthetic and semi-controll
|
|
| 334 |
## 9. BibTeX Citation & Academic References
|
| 335 |
|
| 336 |
```bibtex
|
| 337 |
-
@software{
|
| 338 |
-
author = {Nguyen, Phuc},
|
| 339 |
-
title = {Cham
|
| 340 |
year = {2026},
|
| 341 |
publisher = {GitHub},
|
| 342 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 343 |
-
note = {
|
| 344 |
}
|
| 345 |
```
|
| 346 |
|
|
|
|
| 115 |
### 2.1 Early Epigraphic & Glyph Recognition
|
| 116 |
Automated processing of Cham script originated in the analysis of stone inscriptions from ancient Champa sanctuaries (Mỹ Sơn, Po Nagar, Đồng Dương). Nguyen, Schweyer, Le, Tran, & Vu (2019a) presented preliminary results on recognizing ancient Cham glyphs extracted from stone rubbings and photographs using Histogram of Oriented Gradients (HOG), Non-Parametric Weighted (NPW) features, and CNN-derived feature representations coupled with $k$-NN and linear SVM classifiers at the IEEE MAPR 2019 conference. Due to severe stone weathering, erosion, and irregular surface texture, isolated glyph classification across noisy background substrates was evaluated.
|
| 117 |
|
| 118 |
+
To mitigate extreme sample scarcity in epigraphic corpora, Nguyen, Schweyer, Le, Tran, & Vu (2019b) evaluated data augmentation strategies and transfer learning from scripts of similar or the same language family to ancient stone inscription glyphs at the ICIAP 2019 Workshops (PatReCH), demonstrating quantitative gains on script-to-inscription transfer across weathered stone carvings.
|
| 119 |
|
| 120 |
### 2.2 Deep Learning for Cham Epigraphy & Transliteration
|
| 121 |
In his doctoral dissertation, Nguyen (2023) developed comprehensive document image processing and understanding methodologies dedicated to ancient Cham documents. His work investigated stone inscription image restoration, automatic glyph segmentation under rough stone substrate conditions, and deep convolutional representations for historical Brahmic scripts.
|
|
|
|
| 123 |
Complementing glyph recognition, Nguyen, Burie, Le, & Schweyer (2022) proposed an effective multi-stage method for text line segmentation in historical document images presented at the 26th International Conference on Pattern Recognition (ICPR 2022), addressing line curvature and touching diacritics in historical archives. Expanding to linguistic representation, Nguyen, Burie, Le, & Schweyer (2023) introduced a two-step sequence transformer model for Cham-to-Latin script transliteration presented at the HIP@ICDAR 2023 workshop. Their system demonstrated the efficacy of sequence-to-sequence transformers in mapping recognized Cham text sequences to standardized Latin phonological romanizations (*Cam-Latin*). Most recently, Nguyen, Burie, Le, & Schweyer (2025) published a comprehensive study entitled *Text line segmentation approach combining deep learning model and traditional image processing techniques - application to transliteration of Cham manuscripts* in *Multimedia Tools and Applications*, evaluating text-line segmentation and automatic transliteration across 627 manuscript images and approximately 8,300 textlines (focusing on line segmentation and transliteration rather than end-to-end character OCR benchmarking).
|
| 124 |
|
| 125 |
### 2.3 Manuscript Preservation & Digitization Initiatives
|
| 126 |
+
Beyond epigraphy on stone, historical Cham manuscripts on paper (*kertas*) and palm-leaf have been documented through preservation missions. The Historic Cham Manuscripts of Vietnam collection in the Southeast Asia Digital Library (SEADL) contains 977 digitized manuscripts comprising more than 57,800 page scans. In addition, field surveys estimate approximately 3,000 physical manuscripts preserved within Cham community families and religious dignitaries across Vietnam and Cambodia. The French National Research Agency funded the CHAMDOC project (*Cham Documentation*, ANR-19-CE27-0018, 2019–2024) to support preservation, cataloging, and linguistic documentation.
|
| 127 |
|
| 128 |
However, despite the preservation of tens of thousands of scanned folios, almost none of these archives possess open, machine-readable character-level or line-level bounding box ground-truth annotations suitable for training supervised deep learning models. The bottleneck for Cham OCR is therefore not an absolute absence of physical manuscripts, but rather the severe scarcity of annotated, verified training ground truth.
|
| 129 |
|
|
|
|
| 139 |
## 3. Text Detection & Indic Line Segmentation (Cham-DBNet)
|
| 140 |
|
| 141 |
### 3.1 The Interlinear Diacritic Collision Problem
|
| 142 |
+
In digital modeling and synthetic text rendering at nominal glyph heights (32–36px), interlinear clearance often narrows to 3–12 pixels between adjacent lines (with ascender excursions extending up to $+18\text{px}$ and descender excursions down to $-22\text{px}$ in normalized coordinate space). Because upper dependent vowels reach high above the core text band and subjoined medials plunge below, the ascenders of line $n+1$ routinely touch or intertwine with the descenders of line $n$ under dense formatting. Standard off-the-shelf text detection models (e.g., baseline DBNet trained on natural scene Latin or CJK text) frequently encounter two severe failure modes when applied to dense historical manuscripts:
|
| 143 |
1. **Line Merging**: Grouping adjacent lines into a single bounding box when diacritics touch.
|
| 144 |
2. **Diacritic Amputation**: Slicing off upper vowel signs or subjoined medials during rectangular cropping, which causes fatal downstream recognition errors.
|
| 145 |
|
|
|
|
| 211 |
### 5.1 The Data Scarcity Bottleneck
|
| 212 |
Deep neural recognition networks require hundreds of thousands of diverse labeled textline instances. However, surviving physical Cham manuscripts lack character-level bounding box ground truth. To overcome this limitation, we engineered a synthetic generation pipeline (`scripts/generate_data.py`) capable of synthesizing 150,000 diverse textline crops from authentic literary corpora.
|
| 213 |
|
| 214 |
+
### 5.2 Calibrated Synthetic Dataset Partitioning & Dual-Generator Architecture
|
| 215 |
+
To support both baseline reproducibility and ongoing experimental modeling, the repository provides two distinct synthetic dataset generation pipelines:
|
| 216 |
+
|
| 217 |
+
#### Pipeline A: Baseline General Generator (`scripts/generate_data.py`)
|
| 218 |
+
The baseline generator produces a 150,000 textline corpus partitioned into **15% evaluation/locked splits** (22,500 lines: `val_clean_short` 3%, `val_clean_long` 3%, `val_noisy_short` 3%, `val_noisy_long` 3%, `locked_test` 3%) and an **85% training split** (127,500 lines) structured across 6 calibrated sub-categories:
|
| 219 |
|
| 220 |
| Training Sub-Category | Proportion | Line Count | Target Composition & Protocol | Target Failure Mode Addressed |
|
| 221 |
| :--- | :---: | :---: | :--- | :--- |
|
|
|
|
| 226 |
| `train_noisy_long` | **15%** | 19,125 | Long verse lines with severe photometric degradation and spatial elastic distortion. | Prevents alignment collapse on degraded multi-word lines. |
|
| 227 |
| `train_hard_examples` | **10%** | 12,750 | Adversarial minimal pairs (`ꨲ` vs `ꨶ`, `꩝꩝` vs `꩝`), verse numerals (1–99) with section marks (`꩑꩞`), and stacked subjoined clusters. | Eliminates CTC misclassification of numerals as letters (`꩔` vs `ꨤ`, `꩕` vs `ꨅ`) and diacritic collapsing. |
|
| 228 |
|
| 229 |
+
#### Pipeline B: Experimental V25 Generator (`scripts/generate_data_v25.py`)
|
| 230 |
+
For the V25 experimental iteration, a dedicated multi-threaded generator (`generate_data_v25.py`) synthesizes 150,000 textline images (140,000 train + 10,000 validation) directly synchronized with `configs/v25_dataset_manifest.json` across 5 core pillars:
|
| 231 |
+
1. **Canonical Cham Literature (43.3% / 65,000 lines)**: Classical literary vocabulary (*Po Klong Garai*, 57-stanza verse).
|
| 232 |
+
2. **Anti-Motion Blur Base (20.0% / 30,000 lines)**: Clean base images paired with dynamic in-RAM directional motion blur ($7\times 7$ to $13\times 13$) and defocus.
|
| 233 |
+
3. **Inline Bilingual Code-Switching (16.7% / 25,000 lines)**: Intra-line mixed phrases with Vietnamese diacritics and Latin annotations.
|
| 234 |
+
4. **Stanza Numerals & Boundary Punctuation (12.0% / 18,000 lines)**: Verse numbers $1$–$99$ (`꩑꩞`..`꩙꩙꩞`) and boundary marks (`꩞`, `:`, `–`).
|
| 235 |
+
5. **Adversarial Minimal Pairs (8.0% / 12,000 lines)**: Micro-stroke pairs (`ꨲ` U+AA32 Vowel Sign UE vs `ꨶ` U+AA36 Medial WA, variable Double Danda `꩝꩝` spacing 2–8px, 3-tier diacritic stacks).
|
| 236 |
+
|
| 237 |
### 5.3 Digital Typefaces & TrueType cmap Tofu Glyph Safeguards
|
| 238 |
When rendering synthetic text, digital Cham typefaces must be verified for glyph coverage. The primary bundled and reproducible typeface distributed directly within this repository is **Noto Sans Cham** (`Regular`, `Bold`, `Black`; SIL Open Font License 1.1, located in `ocr-training/data/fonts/`). External reference typefaces evaluated during research—including *Cham Roman*, *EFEO Cham*, and community fonts—are not redistributed in this repository and must be acquired independently if researchers wish to reproduce specific historical font variations.
|
| 239 |
|
|
|
|
| 335 |
|
| 336 |
While our pipeline demonstrates substantial gains on synthetic and semi-controlled documents, rigorous academic honesty requires acknowledging the following boundaries:
|
| 337 |
|
| 338 |
+
1. **Synthetic-to-Real Domain Gap**: The neural recognizer is trained predominantly on synthetic textlines rendered with bundled *Noto Sans Cham* (SIL OFL 1.1). Historical manuscripts feature idiosyncratic scribal hands, variable ink viscosity, and non-standard ligature variations that may exhibit lower recognition confidence in the field.
|
| 339 |
2. **Absence of Large-Scale Real Manuscript Ground Truth**: Due to the acute scarcity of digitized historical Cham archives with line-level annotations, current benchmarks rely on controlled synthetic stress-tests and curated test slices. Establishing an open, expert-verified historical palm-leaf manuscript benchmark represents an urgent necessity requiring future collaborative scholarship with native Cham elders and linguists.
|
| 340 |
+
3. **Severe Biological Degradation & Epigraphy**: Inscription stone rubbings and severely mold-damaged palm leaves (where character ink has substantially eroded) remain beyond the capabilities of pure vision-based CTC sequence models. Integrating masked language models (MLMs) trained on historical Cham corpora is an active subject of future study.
|
| 341 |
4. **Extreme Interlinear Overlap (Level 5)**: When severe paper wrinkling causes interlinear wave amplitude to exceed line spacing ($\text{Amplitude} > \text{Gap}$), 1D projection and horizontal bounding boxes collapse, indicating the need for 2D polygonal baseline tracking models in future iterations.
|
| 342 |
|
| 343 |
---
|
|
|
|
| 345 |
## 9. BibTeX Citation & Academic References
|
| 346 |
|
| 347 |
```bibtex
|
| 348 |
+
@software{cham_ocr_pipeline,
|
| 349 |
+
author = {Nguyen, Phuc H.},
|
| 350 |
+
title = {Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition},
|
| 351 |
year = {2026},
|
| 352 |
publisher = {GitHub},
|
| 353 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 354 |
+
note = {Production web service: https://ocr.cham.asia}
|
| 355 |
}
|
| 356 |
```
|
| 357 |
|
ocr-studio/research.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
|
| 8 |
<!-- Google Fonts: Google Sans Flex & JetBrains Mono -->
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -750,10 +750,10 @@
|
|
| 750 |
<span class="tag-pill">Not Peer Reviewed</span>
|
| 751 |
<span class="tag-pill tag-pill-green">PaddleOCR PP-OCRv4</span>
|
| 752 |
<span class="tag-pill tag-pill-blue">Brahmic Paleography</span>
|
| 753 |
-
<span class="tag-pill">
|
| 754 |
</div>
|
| 755 |
|
| 756 |
-
<h1 class="paper-title">
|
| 757 |
|
| 758 |
<div class="author-box">
|
| 759 |
<div class="author-info">
|
|
@@ -771,7 +771,7 @@
|
|
| 771 |
<section id="abstract" class="abstract-box">
|
| 772 |
<div class="abstract-title">Abstract</div>
|
| 773 |
<p class="abstract-text">
|
| 774 |
-
Historical manuscripts of the Cham civilization—primarily preserved in Eastern Cham (<em>Akhar Thrah</em>) alongside Western Cham (<em>Cam Srak</em>) traditions—represent a fragile, millennia-old Brahmic epigraphic and literary heritage in Southeast Asia. While this project validates an end-to-end deep learning OCR pipeline specifically on Unicode Eastern Cham (<em>Akhar Thrah</em>), Western Cham (<em>Cam Srak</em>), real physical manuscripts, and epigraphic stone carvings represent active and ongoing future extensions. Automatic transcription via Optical Character Recognition (OCR) has historically faced steep barriers due to complex Brahmic abugida orthography, multi-directional dependent vowel diacritics, predominantly continuous script (<em>scriptio continua</em>) in classical manuscripts, physical carrier degradation, and tight interlinear diacritic collisions. In this technical report, we describe the design, implementation, and empirical evaluation of an end-to-end deep learning pipeline and diagnostic transcription workbench engineered for Cham documents: (1) a hybrid text detection and line segmentation engine combining a fine-tuned <strong>Mobile DBNet (PPLCNetV3)</strong> with an <strong>Indic Valley-Cut Heuristic Engine</strong> that preserves boundary strokes via mathematical set-difference masking (<code>difference_update</code>) and \(0.40 \times \text{median line height}\) vertical safety cushions; (2) a neural recognition backbone based on <strong>PP-OCRv4 SVTR-LCNet</strong> with expanded input dimensions \([3, 48, 480]\), joint Connectionist Temporal Classification (CTC) alignment, and post-OCR deterministic Unicode cluster canonicalization; and (3) a 150,000-textline synthetic dataset pipeline featuring on-the-fly motion blur, TrueType cmap tofu filtering, and hard-example adversarial mining for verse numerals (\(1\)–\(99\)) and confusing diacritic minimal pairs. Evaluations across a <strong>controlled 200-page synthetic document stress-test</strong> (903 textlines across 5 difficulty levels) and a <strong>50-test stratified failure-mode benchmark</strong> demonstrate
|
| 775 |
</p>
|
| 776 |
<div class="keywords-strip">
|
| 777 |
<strong>Keywords:</strong> Optical Character Recognition (OCR), Cham Script, Akhar Thrah, Cam Srak, Brahmic Paleography, Differentiable Binarization (DBNet), SVTR-LCNet, Connectionist Temporal Classification (CTC), Indic Line Segmentation.
|
|
@@ -852,7 +852,7 @@
|
|
| 852 |
Automated processing of Cham script originated in the analysis of stone inscriptions from ancient Champa sanctuaries (Mỹ Sơn, Po Nagar, Đồng Dương). Nguyen, Schweyer, Le, Tran, & Vu (2019a) presented preliminary results on recognizing ancient Cham glyphs extracted from stone rubbings and photographs using Histogram of Oriented Gradients (HOG), Non-Parametric Weighted (NPW) features, and CNN-derived feature representations coupled with $k$-NN and linear SVM classifiers at the IEEE MAPR 2019 conference. Due to severe stone weathering, erosion, and irregular surface texture, isolated glyph classification across noisy background substrates was evaluated.
|
| 853 |
</p>
|
| 854 |
<p>
|
| 855 |
-
To mitigate extreme sample scarcity in epigraphic corpora, Nguyen, Schweyer, Le, Tran, & Vu (2019b) evaluated data augmentation strategies and transfer learning from
|
| 856 |
</p>
|
| 857 |
|
| 858 |
<h3 id="sec2-2">2.2 Deep Learning for Cham Epigraphy & Transliteration</h3>
|
|
@@ -865,7 +865,7 @@
|
|
| 865 |
|
| 866 |
<h3 id="sec2-3">2.3 Manuscript Preservation & Digitization Initiatives</h3>
|
| 867 |
<p>
|
| 868 |
-
Beyond epigraphy on stone, historical Cham manuscripts on paper (<em>kertas</em>) and palm-leaf have been documented through preservation missions.
|
| 869 |
</p>
|
| 870 |
<p>
|
| 871 |
However, despite the preservation of tens of thousands of scanned folios, almost none of these archives possess open, machine-readable character-level or line-level bounding box ground-truth annotations suitable for training supervised deep learning models. The bottleneck for Cham OCR is therefore not an absolute absence of physical manuscripts, but rather the severe scarcity of annotated, verified training ground truth.
|
|
@@ -889,7 +889,7 @@
|
|
| 889 |
|
| 890 |
<h3 id="sec3-1">3.1 The Interlinear Diacritic Collision Problem</h3>
|
| 891 |
<p>
|
| 892 |
-
|
| 893 |
</p>
|
| 894 |
<ul>
|
| 895 |
<li>Merging adjacent lines into single monolithic boxes.</li>
|
|
@@ -1090,9 +1090,14 @@
|
|
| 1090 |
Deep neural OCR networks require hundreds of thousands of diverse labeled textlines. However, surviving physical Cham manuscripts lack open character-level bounding box ground truth. To overcome this limitation, we engineered an algorithmic synthesizer (<code>scripts/generate_data.py</code>) capable of rendering 150,000 diverse textline crops from authentic literary corpora.
|
| 1091 |
</p>
|
| 1092 |
|
| 1093 |
-
<h3 id="sec5-2">5.2 Calibrated Synthetic Dataset Partitioning</h3>
|
| 1094 |
<p>
|
| 1095 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
</p>
|
| 1097 |
<div class="table-responsive">
|
| 1098 |
<table>
|
|
@@ -1152,6 +1157,18 @@
|
|
| 1152 |
</table>
|
| 1153 |
</div>
|
| 1154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
<h3 id="sec5-3">5.3 Digital Typefaces & TrueType cmap Tofu Glyph Safeguards</h3>
|
| 1156 |
<p>
|
| 1157 |
When rendering synthetic text, digital Cham typefaces must be verified for glyph coverage. The primary bundled and reproducible typeface distributed directly within this repository is <strong>Noto Sans Cham</strong> (<code>Regular</code>, <code>Bold</code>, <code>Black</code>; SIL Open Font License 1.1, located in <code>ocr-training/data/fonts/</code>). External reference typefaces evaluated during research—including <em>Cham Roman</em>, <em>EFEO Cham</em>, and community fonts—are not redistributed in this repository and must be acquired independently if researchers wish to reproduce specific historical font variations.
|
|
@@ -1554,9 +1571,9 @@ if not hasattr(np, 'typeDict'): np.typeDict = {}</code></pre>
|
|
| 1554 |
While our pipeline demonstrates substantial gains on synthetic and semi-controlled documents, rigorous academic honesty requires acknowledging the following boundaries:
|
| 1555 |
</p>
|
| 1556 |
<ul>
|
| 1557 |
-
<li><strong>Synthetic-to-Real Domain Gap</strong>: The recognizer is trained predominantly on synthetic textlines rendered with
|
| 1558 |
<li><strong>Absence of Large-Scale Real Manuscript Ground Truth</strong>: Due to the acute scarcity of digitized historical Cham archives with line-level annotations, current benchmarks rely on controlled synthetic stress-tests and curated test slices. Establishing an open, expert-verified historical palm-leaf manuscript benchmark represents an urgent necessity requiring future collaborative scholarship with native Cham elders and linguists.</li>
|
| 1559 |
-
<li><strong>Severe Biological Degradation & Epigraphy</strong>: Inscription stone rubbings and severely mold-damaged palm leaves (where
|
| 1560 |
<li><strong>Extreme Interlinear Overlap (Level 5)</strong>: When severe paper wrinkling causes interlinear wave amplitude to exceed line spacing (\(\text{Amplitude} > \text{Gap}\)), 1D projection and horizontal bounding boxes collapse, indicating the need for 2D polygonal baseline tracking models in future iterations.</li>
|
| 1561 |
</ul>
|
| 1562 |
</section>
|
|
@@ -1574,13 +1591,13 @@ if not hasattr(np, 'typeDict'): np.typeDict = {}</code></pre>
|
|
| 1574 |
<span>BibTeX Citation</span>
|
| 1575 |
<button class="copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('bibtex-code').innerText)">Copy BibTeX</button>
|
| 1576 |
</div>
|
| 1577 |
-
<pre id="bibtex-code"><code>@software{
|
| 1578 |
-
author = {Nguyen, Phuc},
|
| 1579 |
-
title = {Cham
|
| 1580 |
year = {2026},
|
| 1581 |
publisher = {GitHub},
|
| 1582 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 1583 |
-
note = {
|
| 1584 |
}</code></pre>
|
| 1585 |
</div>
|
| 1586 |
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition — Technical Report</title>
|
| 7 |
|
| 8 |
<!-- Google Fonts: Google Sans Flex & JetBrains Mono -->
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
| 750 |
<span class="tag-pill">Not Peer Reviewed</span>
|
| 751 |
<span class="tag-pill tag-pill-green">PaddleOCR PP-OCRv4</span>
|
| 752 |
<span class="tag-pill tag-pill-blue">Brahmic Paleography</span>
|
| 753 |
+
<span class="tag-pill">Unicode Eastern Cham (Akhar Thrah)</span>
|
| 754 |
</div>
|
| 755 |
|
| 756 |
+
<h1 class="paper-title">Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition</h1>
|
| 757 |
|
| 758 |
<div class="author-box">
|
| 759 |
<div class="author-info">
|
|
|
|
| 771 |
<section id="abstract" class="abstract-box">
|
| 772 |
<div class="abstract-title">Abstract</div>
|
| 773 |
<p class="abstract-text">
|
| 774 |
+
Historical manuscripts of the Cham civilization—primarily preserved in Eastern Cham (<em>Akhar Thrah</em>) alongside Western Cham (<em>Cam Srak</em>) traditions—represent a fragile, millennia-old Brahmic epigraphic and literary heritage in Southeast Asia. While this project validates an end-to-end deep learning OCR pipeline specifically on Unicode Eastern Cham (<em>Akhar Thrah</em>), Western Cham (<em>Cam Srak</em>), real physical manuscripts, and epigraphic stone carvings represent active and ongoing future extensions. Automatic transcription via Optical Character Recognition (OCR) has historically faced steep barriers due to complex Brahmic abugida orthography, multi-directional dependent vowel diacritics, predominantly continuous script (<em>scriptio continua</em>) in classical manuscripts, physical carrier degradation, and tight interlinear diacritic collisions. In this technical report, we describe the design, implementation, and empirical evaluation of an end-to-end deep learning pipeline and diagnostic transcription workbench engineered for Cham documents: (1) a hybrid text detection and line segmentation engine combining a fine-tuned <strong>Mobile DBNet (PPLCNetV3)</strong> with an <strong>Indic Valley-Cut Heuristic Engine</strong> that preserves boundary strokes via mathematical set-difference masking (<code>difference_update</code>) and \(0.40 \times \text{median line height}\) vertical safety cushions; (2) a neural recognition backbone based on <strong>PP-OCRv4 SVTR-LCNet</strong> with expanded input dimensions \([3, 48, 480]\), joint Connectionist Temporal Classification (CTC) alignment, and post-OCR deterministic Unicode cluster canonicalization; and (3) a 150,000-textline synthetic dataset pipeline featuring on-the-fly motion blur, TrueType cmap tofu filtering, and hard-example adversarial mining for verse numerals (\(1\)–\(99\)) and confusing diacritic minimal pairs. Evaluations across a <strong>controlled 200-page synthetic document stress-test</strong> (903 textlines across 5 difficulty levels) and a <strong>50-test stratified failure-mode benchmark</strong> demonstrate robust textline detection with a <strong>100.0% line detection rate</strong> across standard, aged paper, and narrow-gap folios (Levels 1–3) and graceful degradation up to Level 4 (99.44% detection), before encountering an empirical breaking point at Level 5 (61.45% detection) where interlinear wave amplitude exceeds the interlinear gap. For textline recognition, our validated baseline model (<strong>V24</strong>) achieves <strong>13.88% mean CER</strong> across the 50-test suite (reducing noise CER from 50.59% down to 8.82% over V23), while experimental checkpoints (<strong>V25</strong>) remain under active training and evaluation. Multi-GPU distributed scaling benchmarks across Kaggle Tesla T4x2, Cloud A100, L40S, and L4 characterize hardware efficiency for non-profit cultural preservation. Finally, we discuss critical limitations regarding synthetic-to-real domain gaps and provide a production reference deployed at <code>https://ocr.cham.asia</code>.
|
| 775 |
</p>
|
| 776 |
<div class="keywords-strip">
|
| 777 |
<strong>Keywords:</strong> Optical Character Recognition (OCR), Cham Script, Akhar Thrah, Cam Srak, Brahmic Paleography, Differentiable Binarization (DBNet), SVTR-LCNet, Connectionist Temporal Classification (CTC), Indic Line Segmentation.
|
|
|
|
| 852 |
Automated processing of Cham script originated in the analysis of stone inscriptions from ancient Champa sanctuaries (Mỹ Sơn, Po Nagar, Đồng Dương). Nguyen, Schweyer, Le, Tran, & Vu (2019a) presented preliminary results on recognizing ancient Cham glyphs extracted from stone rubbings and photographs using Histogram of Oriented Gradients (HOG), Non-Parametric Weighted (NPW) features, and CNN-derived feature representations coupled with $k$-NN and linear SVM classifiers at the IEEE MAPR 2019 conference. Due to severe stone weathering, erosion, and irregular surface texture, isolated glyph classification across noisy background substrates was evaluated.
|
| 853 |
</p>
|
| 854 |
<p>
|
| 855 |
+
To mitigate extreme sample scarcity in epigraphic corpora, Nguyen, Schweyer, Le, Tran, & Vu (2019b) evaluated data augmentation strategies and transfer learning from scripts of similar or the same language family to ancient stone inscription glyphs at the ICIAP 2019 Workshops (PatReCH), demonstrating quantitative gains on script-to-inscription transfer across weathered stone carvings.
|
| 856 |
</p>
|
| 857 |
|
| 858 |
<h3 id="sec2-2">2.2 Deep Learning for Cham Epigraphy & Transliteration</h3>
|
|
|
|
| 865 |
|
| 866 |
<h3 id="sec2-3">2.3 Manuscript Preservation & Digitization Initiatives</h3>
|
| 867 |
<p>
|
| 868 |
+
Beyond epigraphy on stone, historical Cham manuscripts on paper (<em>kertas</em>) and palm-leaf have been documented through preservation missions. The Historic Cham Manuscripts of Vietnam collection in the Southeast Asia Digital Library (SEADL) contains 977 digitized manuscripts comprising more than 57,800 page scans. In addition, field surveys estimate approximately 3,000 physical manuscripts preserved within Cham community families and religious dignitaries across Vietnam and Cambodia. The French National Research Agency funded the CHAMDOC project (<em>Cham Documentation</em>, ANR-19-CE27-0018, 2019–2024) to support preservation, cataloging, and linguistic documentation.
|
| 869 |
</p>
|
| 870 |
<p>
|
| 871 |
However, despite the preservation of tens of thousands of scanned folios, almost none of these archives possess open, machine-readable character-level or line-level bounding box ground-truth annotations suitable for training supervised deep learning models. The bottleneck for Cham OCR is therefore not an absolute absence of physical manuscripts, but rather the severe scarcity of annotated, verified training ground truth.
|
|
|
|
| 889 |
|
| 890 |
<h3 id="sec3-1">3.1 The Interlinear Diacritic Collision Problem</h3>
|
| 891 |
<p>
|
| 892 |
+
In digital modeling and synthetic text rendering at nominal glyph heights (32–36px), interlinear clearance often narrows to 3–12 pixels between adjacent lines (with ascender excursions extending up to \(+18\text{px}\) and descender excursions down to \(-22\text{px}\) in normalized coordinate space). Because upper dependent vowels reach high above the core text band and subjoined medials plunge below, the ascenders of line \(n+1\) routinely touch or intertwine with the descenders of line \(n\) under dense formatting. Standard off-the-shelf text detection models (such as baseline DBNet trained on natural scene Latin or CJK text) frequently encounter two severe failure modes when applied to dense historical manuscripts:
|
| 893 |
</p>
|
| 894 |
<ul>
|
| 895 |
<li>Merging adjacent lines into single monolithic boxes.</li>
|
|
|
|
| 1090 |
Deep neural OCR networks require hundreds of thousands of diverse labeled textlines. However, surviving physical Cham manuscripts lack open character-level bounding box ground truth. To overcome this limitation, we engineered an algorithmic synthesizer (<code>scripts/generate_data.py</code>) capable of rendering 150,000 diverse textline crops from authentic literary corpora.
|
| 1091 |
</p>
|
| 1092 |
|
| 1093 |
+
<h3 id="sec5-2">5.2 Calibrated Synthetic Dataset Partitioning & Dual-Generator Architecture</h3>
|
| 1094 |
<p>
|
| 1095 |
+
To support both baseline reproducibility and ongoing experimental modeling, the repository provides two distinct synthetic dataset generation pipelines:
|
| 1096 |
+
</p>
|
| 1097 |
+
|
| 1098 |
+
<h4 style="margin-top: 1.25rem; font-size: 1.05rem; color: var(--text-primary);">Pipeline A: Baseline General Generator (<code>scripts/generate_data.py</code>)</h4>
|
| 1099 |
+
<p>
|
| 1100 |
+
The baseline generator produces a 150,000 textline corpus partitioned into <strong>15% evaluation/locked splits</strong> (22,500 lines: <code>val_clean_short</code> 3%, <code>val_clean_long</code> 3%, <code>val_noisy_short</code> 3%, <code>val_noisy_long</code> 3%, <code>locked_test</code> 3%) and an <strong>85% training split</strong> (127,500 lines) structured across 6 calibrated sub-categories:
|
| 1101 |
</p>
|
| 1102 |
<div class="table-responsive">
|
| 1103 |
<table>
|
|
|
|
| 1157 |
</table>
|
| 1158 |
</div>
|
| 1159 |
|
| 1160 |
+
<h4 style="margin-top: 1.5rem; font-size: 1.05rem; color: var(--text-primary);">Pipeline B: Experimental V25 Generator (<code>scripts/generate_data_v25.py</code>)</h4>
|
| 1161 |
+
<p>
|
| 1162 |
+
For the V25 experimental iteration, a dedicated multi-threaded generator (<code>generate_data_v25.py</code>) synthesizes 150,000 textline images (140,000 train + 10,000 validation) directly synchronized with <code>configs/v25_dataset_manifest.json</code> across 5 core pillars:
|
| 1163 |
+
</p>
|
| 1164 |
+
<ol style="margin-bottom: 1.5rem; line-height: 1.7; color: var(--text-secondary);">
|
| 1165 |
+
<li><strong>Canonical Cham Literature (43.3% / 65,000 lines)</strong>: Classical literary vocabulary (<em>Po Klong Garai</em>, 57-stanza verse).</li>
|
| 1166 |
+
<li><strong>Anti-Motion Blur Base (20.0% / 30,000 lines)</strong>: Clean base images paired with dynamic in-RAM directional motion blur (\(7\times 7\) to \(13\times 13\)) and defocus.</li>
|
| 1167 |
+
<li><strong>Inline Bilingual Code-Switching (16.7% / 25,000 lines)</strong>: Intra-line mixed phrases with Vietnamese diacritics and Latin annotations.</li>
|
| 1168 |
+
<li><strong>Stanza Numerals & Boundary Punctuation (12.0% / 18,000 lines)</strong>: Verse numbers \(1\)–\(99\) (<span class="cham-script">꩑꩞</span>..<span class="cham-script">꩙꩙꩞</span>) and boundary marks (<span class="cham-script">꩞</span>, <code>:</code>, <code>–</code>).</li>
|
| 1169 |
+
<li><strong>Adversarial Minimal Pairs (8.0% / 12,000 lines)</strong>: Micro-stroke pairs (<span class="cham-script">ꨲ</span> U+AA32 Vowel Sign UE vs <span class="cham-script">ꨶ</span> U+AA36 Medial WA, variable Double Danda <span class="cham-script">꩝꩝</span> spacing 2–8px, 3-tier diacritic stacks).</li>
|
| 1170 |
+
</ol>
|
| 1171 |
+
|
| 1172 |
<h3 id="sec5-3">5.3 Digital Typefaces & TrueType cmap Tofu Glyph Safeguards</h3>
|
| 1173 |
<p>
|
| 1174 |
When rendering synthetic text, digital Cham typefaces must be verified for glyph coverage. The primary bundled and reproducible typeface distributed directly within this repository is <strong>Noto Sans Cham</strong> (<code>Regular</code>, <code>Bold</code>, <code>Black</code>; SIL Open Font License 1.1, located in <code>ocr-training/data/fonts/</code>). External reference typefaces evaluated during research—including <em>Cham Roman</em>, <em>EFEO Cham</em>, and community fonts—are not redistributed in this repository and must be acquired independently if researchers wish to reproduce specific historical font variations.
|
|
|
|
| 1571 |
While our pipeline demonstrates substantial gains on synthetic and semi-controlled documents, rigorous academic honesty requires acknowledging the following boundaries:
|
| 1572 |
</p>
|
| 1573 |
<ul>
|
| 1574 |
+
<li><strong>Synthetic-to-Real Domain Gap</strong>: The neural recognizer is trained predominantly on synthetic textlines rendered with bundled <em>Noto Sans Cham</em> (SIL OFL 1.1). Historical manuscripts feature idiosyncratic scribal hands, variable ink viscosity, and non-standard ligature variations that may exhibit lower recognition confidence in the field.</li>
|
| 1575 |
<li><strong>Absence of Large-Scale Real Manuscript Ground Truth</strong>: Due to the acute scarcity of digitized historical Cham archives with line-level annotations, current benchmarks rely on controlled synthetic stress-tests and curated test slices. Establishing an open, expert-verified historical palm-leaf manuscript benchmark represents an urgent necessity requiring future collaborative scholarship with native Cham elders and linguists.</li>
|
| 1576 |
+
<li><strong>Severe Biological Degradation & Epigraphy</strong>: Inscription stone rubbings and severely mold-damaged palm leaves (where character ink has substantially eroded) remain beyond the capabilities of pure vision-based CTC sequence models. Integrating masked language models (MLMs) trained on historical Cham corpora is an active subject of future study.</li>
|
| 1577 |
<li><strong>Extreme Interlinear Overlap (Level 5)</strong>: When severe paper wrinkling causes interlinear wave amplitude to exceed line spacing (\(\text{Amplitude} > \text{Gap}\)), 1D projection and horizontal bounding boxes collapse, indicating the need for 2D polygonal baseline tracking models in future iterations.</li>
|
| 1578 |
</ul>
|
| 1579 |
</section>
|
|
|
|
| 1591 |
<span>BibTeX Citation</span>
|
| 1592 |
<button class="copy-btn" onclick="navigator.clipboard.writeText(document.getElementById('bibtex-code').innerText)">Copy BibTeX</button>
|
| 1593 |
</div>
|
| 1594 |
+
<pre id="bibtex-code"><code>@software{cham_ocr_pipeline,
|
| 1595 |
+
author = {Nguyen, Phuc H.},
|
| 1596 |
+
title = {Cham-OCR: A Deep-Learning OCR Pipeline for Unicode Eastern Cham, toward Historical Manuscript and Epigraphic Recognition},
|
| 1597 |
year = {2026},
|
| 1598 |
publisher = {GitHub},
|
| 1599 |
url = {https://github.com/phucsd/Cham-OCR},
|
| 1600 |
+
note = {Production web service: https://ocr.cham.asia}
|
| 1601 |
}</code></pre>
|
| 1602 |
</div>
|
| 1603 |
|
ocr-training/TRAINING_ROADMAP_V25.md
CHANGED
|
@@ -12,14 +12,14 @@ flowchart TD
|
|
| 12 |
E1["Mờ rung tay (Motion Blur)<br/>CER vọt 38.74% (Gấp 2.7 lần)<br/>Sót 8.3% dòng chữ"]
|
| 13 |
E2["Song ngữ nội dòng (Inline Mixed)<br/>CER vọt 27.84%<br/>Mô hình Chăm mù chữ Việt, Việt mù Chăm"]
|
| 14 |
E3["Mất dấu ngắt câu ꩞, :, –<br/>Gây nối nhầm dòng đoạn văn<br/>Paragraph Flow F1 tụt còn 70.7%"]
|
| 15 |
-
E4["Nhầm cặp dấu phụ chân ꨲ (
|
| 16 |
end
|
| 17 |
|
| 18 |
subgraph Solutions_V25["4 Trụ Cột Huấn Luyện Cho Mô Hình V25"]
|
| 19 |
P1["Trụ cột 1: Data Augmentation Mờ Động Cực Hạn (On-the-Fly)<br/>(Directional Motion Blur 7x7 đến 13x13, Defocus trong RAM)"]
|
| 20 |
P2["Trụ cột 2: Từ Điển Hợp Nhất Chăm - Việt Động Chuẩn Hóa NFC<br/>(build_dict_v25.py giữ thứ tự V24 + Preflight checks + Weight Surgery)"]
|
| 21 |
P3["Trụ cột 3: Gói Dữ Liệu Đặc Trị Số Khổ & Ngắt Câu Biên<br/>(Dãy số 1-99 ꩑꩞..꩙꩙꩞, bảo tồn ꩞, :, –, padding biên)"]
|
| 22 |
-
P4["Trụ cột 4: Cặp Đối Kháng Hard-Examples Minimal Pairs<br/>(Phân biệt ꨲ vs ꨶ, bảo tồn ꨯ, ꨯꨱ, Double Danda ꩝꩝ 2-8px)"]
|
| 23 |
end
|
| 24 |
|
| 25 |
E1 ==> P1
|
|
@@ -67,10 +67,10 @@ flowchart TD
|
|
| 67 |
|
| 68 |
### Trụ Cột 4: Cặp Đối Kháng Hard-Examples Cho Dấu Phụ Dễ Nhầm
|
| 69 |
- **Thực trạng**:
|
| 70 |
-
- Dấu `ꨲ` (
|
| 71 |
- Dấu Double Danda `꩝꩝` hay bị gộp thành Single Danda `꩝`.
|
| 72 |
-
- Nguyên âm
|
| 73 |
-
- **Giải pháp cho v25 (Gói
|
| 74 |
- Sinh **mẫu cặp từ tối thiểu (Minimal Pairs)**:
|
| 75 |
- `{Phụ âm} + ꨲ` đối sánh trực tiếp với `{Phụ âm} + ꨶ` (ví dụ `ꨀꨲꩆ` vs `ꨀꨶꩆ`, `ꨓꨆꨴꨲꨩ` vs `ꨓꨆꨴꨶꨩ`, `ꨚꨲ` vs `ꨚꨶ`).
|
| 76 |
- Dấu Double Danda `꩝꩝` với khoảng cách thay đổi từ 2px đến 8px.
|
|
@@ -82,14 +82,15 @@ flowchart TD
|
|
| 82 |
|
| 83 |
Theo tệp đặc tả chuẩn `configs/v25_dataset_manifest.json`:
|
| 84 |
|
| 85 |
-
| Gói Dữ Liệu (
|
| 86 |
| :--- | :---: | :---: | :--- | :--- |
|
| 87 |
-
| **Gói 1: Ngữ liệu Chăm chuẩn &
|
| 88 |
-
| **Gói 2:
|
| 89 |
-
| **Gói 3:
|
| 90 |
-
| **Gói 4:
|
|
|
|
| 91 |
| **TỔNG CỘNG TẬP TRAIN v25** | **140,000** | **100%** | **Toàn diện mọi điều kiện thực tế** | **Ảnh gốc sinh sạch trên đĩa** |
|
| 92 |
-
| **TẬP KIỂM THỬ (VALIDATION)** | **10,000** | — | **Đánh giá khách quan, cân đối
|
| 93 |
| **TỔNG QUY MÔ DATASET** | **150,000** | — | **Đồng bộ duy nhất với Manifest** | **On-the-fly Blur: 35% qua RecAug** |
|
| 94 |
|
| 95 |
> [!IMPORTANT]
|
|
@@ -175,9 +176,9 @@ flowchart LR
|
|
| 175 |
### Bước 4: Khởi Động Huấn Luyện 3 Chặng Trên Kaggle GPU T4x2
|
| 176 |
- Sử dụng cấu hình [rec_cham_v25.yml](file:///e:/Phuc's%20Data/Github/Cham-OCR/ocr-training/configs/rec_cham_v25.yml).
|
| 177 |
- Tự động nạp thông tin xác thực bảo mật qua [kaggle_auth.py](file:///e:/Phuc's%20Data/Github/Cham-OCR/ocr-training/scripts/kaggle_auth.py) và `.env`.
|
| 178 |
-
- **Chặng 1** (Epoch 1 ->
|
| 179 |
-
- **Chặng 2** (Epoch
|
| 180 |
-
- **Chặng 3** (Epoch
|
| 181 |
|
| 182 |
### Bước 5: Đóng Gói Inference & Kiểm Thử Nghiệm Thu (Non-Regression Gate)
|
| 183 |
- Xuất model inference:
|
|
|
|
| 12 |
E1["Mờ rung tay (Motion Blur)<br/>CER vọt 38.74% (Gấp 2.7 lần)<br/>Sót 8.3% dòng chữ"]
|
| 13 |
E2["Song ngữ nội dòng (Inline Mixed)<br/>CER vọt 27.84%<br/>Mô hình Chăm mù chữ Việt, Việt mù Chăm"]
|
| 14 |
E3["Mất dấu ngắt câu ꩞, :, –<br/>Gây nối nhầm dòng đoạn văn<br/>Paragraph Flow F1 tụt còn 70.7%"]
|
| 15 |
+
E4["Nhầm cặp dấu phụ chân ꨲ (Vowel Sign UE) vs ꨶ (Medial WA)<br/>Khoảng cách Double Danda ꩝꩝"]
|
| 16 |
end
|
| 17 |
|
| 18 |
subgraph Solutions_V25["4 Trụ Cột Huấn Luyện Cho Mô Hình V25"]
|
| 19 |
P1["Trụ cột 1: Data Augmentation Mờ Động Cực Hạn (On-the-Fly)<br/>(Directional Motion Blur 7x7 đến 13x13, Defocus trong RAM)"]
|
| 20 |
P2["Trụ cột 2: Từ Điển Hợp Nhất Chăm - Việt Động Chuẩn Hóa NFC<br/>(build_dict_v25.py giữ thứ tự V24 + Preflight checks + Weight Surgery)"]
|
| 21 |
P3["Trụ cột 3: Gói Dữ Liệu Đặc Trị Số Khổ & Ngắt Câu Biên<br/>(Dãy số 1-99 ꩑꩞..꩙꩙꩞, bảo tồn ꩞, :, –, padding biên)"]
|
| 22 |
+
P4["Trụ cột 4: Cặp Đối Kháng Hard-Examples Minimal Pairs<br/>(Phân biệt ꨲ (Vowel Sign UE) vs ꨶ (Medial WA), bảo tồn ꨯ (Vowel Sign E), ꨯꨱ, Double Danda ꩝꩝ 2-8px)"]
|
| 23 |
end
|
| 24 |
|
| 25 |
E1 ==> P1
|
|
|
|
| 67 |
|
| 68 |
### Trụ Cột 4: Cặp Đối Kháng Hard-Examples Cho Dấu Phụ Dễ Nhầm
|
| 69 |
- **Thực trạng**:
|
| 70 |
+
- Dấu `ꨲ` (Vowel Sign UE, U+AA32) và `ꨶ` (Medial WA, U+AA36) có cấu trúc vi mô rất tương đồng.
|
| 71 |
- Dấu Double Danda `꩝꩝` hay bị gộp thành Single Danda `꩝`.
|
| 72 |
+
- Nguyên âm phụ thuộc `ꨯ` (Vowel Sign E, U+AA2F) và `ꨯꨱ` (Vowel Sign AU, U+AA2F U+AA31) dễ bị nuốt khi nét vẽ thanh mảnh.
|
| 73 |
+
- **Giải pháp cho v25 (Gói 5: 12,000 dòng)**:
|
| 74 |
- Sinh **mẫu cặp từ tối thiểu (Minimal Pairs)**:
|
| 75 |
- `{Phụ âm} + ꨲ` đối sánh trực tiếp với `{Phụ âm} + ꨶ` (ví dụ `ꨀꨲꩆ` vs `ꨀꨶꩆ`, `ꨓꨆꨴꨲꨩ` vs `ꨓꨆꨴꨶꨩ`, `ꨚꨲ` vs `ꨚꨶ`).
|
| 76 |
- Dấu Double Danda `꩝꩝` với khoảng cách thay đổi từ 2px đến 8px.
|
|
|
|
| 82 |
|
| 83 |
Theo tệp đặc tả chuẩn `configs/v25_dataset_manifest.json`:
|
| 84 |
|
| 85 |
+
| Gói Dữ Liệu (Pillar / Package) | Số Lượng Dòng (Train) | Tỷ Lệ | Mục Tiêu Kỹ Thuật | Đặc Điểm Augmentation |
|
| 86 |
| :--- | :---: | :---: | :--- | :--- |
|
| 87 |
+
| **Gói 1: Ngữ liệu Chăm chuẩn & Văn học kinh điển** | 65,000 | 43.3% | Nền tảng từ vựng Chăm chuẩn xác (Po Klong Garai, sử thi, thơ 57 khổ) | Nền giấy cổ, texture, tiêu chuẩn sạch |
|
| 88 |
+
| **Gói 2: Ngữ liệu đặc trị Mờ Rung Tay (Anti-Blur Base)** | 30,000 | 20.0% | Tăng cường độ thích nghi mờ rung tay | Ảnh cơ sở sinh sạch, làm mờ on-the-fly trong RAM |
|
| 89 |
+
| **Gói 3: Song ngữ nội dòng (Chăm + Việt kẹp dòng)** | 25,000 | 16.7% | Nhận diện mượt mà Chăm kẹp Việt/Latin | Font kết hợp NotoSansCham + NotoSans |
|
| 90 |
+
| **Gói 4: Số thứ tự khổ thơ & Dấu câu biên** | 18,000 | 12.0% | Nhận diện số 1-99, dấu `꩞`, `:`, `–` | Padding biên biến thiên 0-12px |
|
| 91 |
+
| **Gói 5: Cặp đối kháng Hard-Examples Minimal Pairs** | 12,000 | 8.0% | Khắc phục triệt để `ꨲ` (UE) vs `ꨶ` (WA), `꩝꩝` vs `꩝` | Minimal pairs, zoom nét chân, tổ hợp 3 tầng |
|
| 92 |
| **TỔNG CỘNG TẬP TRAIN v25** | **140,000** | **100%** | **Toàn diện mọi điều kiện thực tế** | **Ảnh gốc sinh sạch trên đĩa** |
|
| 93 |
+
| **TẬP KIỂM THỬ (VALIDATION)** | **10,000** | — | **Đánh giá khách quan, cân đối 5 nhóm** | **Cố định hạt giống ngẫu nhiên** |
|
| 94 |
| **TỔNG QUY MÔ DATASET** | **150,000** | — | **Đồng bộ duy nhất với Manifest** | **On-the-fly Blur: 35% qua RecAug** |
|
| 95 |
|
| 96 |
> [!IMPORTANT]
|
|
|
|
| 176 |
### Bước 4: Khởi Động Huấn Luyện 3 Chặng Trên Kaggle GPU T4x2
|
| 177 |
- Sử dụng cấu hình [rec_cham_v25.yml](file:///e:/Phuc's%20Data/Github/Cham-OCR/ocr-training/configs/rec_cham_v25.yml).
|
| 178 |
- Tự động nạp thông tin xác thực bảo mật qua [kaggle_auth.py](file:///e:/Phuc's%20Data/Github/Cham-OCR/ocr-training/scripts/kaggle_auth.py) và `.env`.
|
| 179 |
+
- **Chặng 1** (Epoch 1 -> 12, ~7.5h): Khởi động với initial weights từ Bước 3. Ngắt chặng tại epoch 12 (`stage_end_epoch: 12`), xuất checkpoint `latest` & `best_accuracy`.
|
| 180 |
+
- **Chặng 2** (Epoch 13 -> 22, ~6.5h): Resume thông qua `Global.checkpoints` trỏ vào full checkpoint Chặng 1 (`.pdparams + .pdopt + .states`). Ngắt chặng tại epoch 22 (`stage_end_epoch: 22`).
|
| 181 |
+
- **Chặng 3** (Epoch 23 -> 40, ~11.0h): Resume từ full checkpoint Chặng 2, hoàn thành toàn bộ 40 epochs, xuất model `inference` v25.
|
| 182 |
|
| 183 |
### Bước 5: Đóng Gói Inference & Kiểm Thử Nghiệm Thu (Non-Regression Gate)
|
| 184 |
- Xuất model inference:
|
ocr-training/configs/v25_dataset_manifest.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"name": "Cham-OCR V25 Unified Dataset Manifest",
|
| 4 |
+
"version": "v25.0",
|
| 5 |
+
"created_at": "2026-09-11",
|
| 6 |
+
"description": "Source of truth thống nhất cho toàn bộ quy mô, phân bổ dữ liệu và chính sách huấn luyện V25",
|
| 7 |
+
"data_distribution": {
|
| 8 |
+
"total_samples": 150000,
|
| 9 |
+
"train_samples": 140000,
|
| 10 |
+
"val_samples": 10000,
|
| 11 |
+
"train_val_ratio": "93.3% / 6.7%",
|
| 12 |
+
"packages": [
|
| 13 |
+
{
|
| 14 |
+
"id": "pkg_01_cham_canonical",
|
| 15 |
+
"name": "Ngữ liệu Chăm chuẩn & Văn học kinh điển",
|
| 16 |
+
"count": 65000,
|
| 17 |
+
"percentage": "43.3%",
|
| 18 |
+
"description": "Nền tảng từ vựng Chăm chuẩn xác từ sử thi Po Klong Garai, thơ 57 khổ, truyền thuyết và truyện cổ tích."
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "pkg_02_anti_blur_base",
|
| 22 |
+
"name": "Ngữ liệu đặc trị Mờ Rung Tay (Anti-Blur Base)",
|
| 23 |
+
"count": 30000,
|
| 24 |
+
"percentage": "20.0%",
|
| 25 |
+
"description": "Ảnh cơ sở được sinh sạch, áp dụng Directional Motion Blur (7x7-13x13) và Defocus on-the-fly trong DataLoader."
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"id": "pkg_03_inline_mixed_bilingual",
|
| 29 |
+
"name": "Song ngữ Chăm - Việt kẹp nội dòng (Inline Mixed)",
|
| 30 |
+
"count": 25000,
|
| 31 |
+
"percentage": "16.7%",
|
| 32 |
+
"description": "Các cấu trúc Chăm kèm chú thích dịch nghĩa tiếng Việt có dấu, ngày tháng năm và ký hiệu."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "pkg_04_stanza_and_punctuation",
|
| 36 |
+
"name": "Số thứ tự khổ thơ Chăm 1-99 & Dấu ngắt câu biên",
|
| 37 |
+
"count": 18000,
|
| 38 |
+
"percentage": "12.0%",
|
| 39 |
+
"description": "Dãy số khổ thơ Chăm 1-99 (꩑꩞..꩙꩙꩞), bảo tồn dấu ngắt ꩞, dấu hai chấm :, gạch ngang – và padding biên."
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"id": "pkg_05_minimal_pairs_hard",
|
| 43 |
+
"name": "Cặp đối kháng Hard-Examples Minimal Pairs",
|
| 44 |
+
"count": 12000,
|
| 45 |
+
"percentage": "8.0%",
|
| 46 |
+
"description": "Tập trung phân biệt cặp dấu chân ꨲ (Vowel Sign UE, U+AA32) vs ꨶ (Medial WA, U+AA36), khoảng cách Double Danda ꩝꩝ (2-8px), tổ hợp dấu 3 tầng."
|
| 47 |
+
}
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"training_policy": {
|
| 51 |
+
"image_shape": [3, 48, 480],
|
| 52 |
+
"image_shape_rationale": "Chiều cao 48px bảo vệ dấu phụ mỏng; chiều rộng 480px chống co bẹp nét chữ trên dòng song ngữ dài.",
|
| 53 |
+
"batch_size_per_card": 32,
|
| 54 |
+
"global_batch_size": 64,
|
| 55 |
+
"num_workers": 2,
|
| 56 |
+
"amp_enabled": true,
|
| 57 |
+
"amp_level": "O1",
|
| 58 |
+
"blur_augmentation": "on_the_fly",
|
| 59 |
+
"blur_rationale": "Tuyệt đối không sinh ảnh mờ tĩnh lưu đĩa để tránh lỗi double-blur; biến dạng mờ được sinh động ngẫu nhiên trên RAM.",
|
| 60 |
+
"max_text_length_policy": "P99_dynamic_scan",
|
| 61 |
+
"max_text_length_rationale": "Quét toàn bộ nhãn tập train/val và đặt max_text_length = P99 để không bị cắt cụt nhãn song ngữ.",
|
| 62 |
+
"kaggle_execution_plan": {
|
| 63 |
+
"timeout_limit_hours": 12.0,
|
| 64 |
+
"stages": [
|
| 65 |
+
{
|
| 66 |
+
"stage": 1,
|
| 67 |
+
"epochs": "1 -> 12",
|
| 68 |
+
"est_hours": 7.5,
|
| 69 |
+
"checkpoint_save": "save_epoch_step: 1",
|
| 70 |
+
"output": "ch_PP-OCRv4_rec_v25_stage1/latest"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"stage": 2,
|
| 74 |
+
"epochs": "13 -> 22",
|
| 75 |
+
"est_hours": 6.5,
|
| 76 |
+
"resume_from": "stage 1 latest (pdparams + pdopt + states)",
|
| 77 |
+
"output": "ch_PP-OCRv4_rec_v25_stage2/latest"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"stage": 3,
|
| 81 |
+
"epochs": "23 -> 40",
|
| 82 |
+
"est_hours": 11.0,
|
| 83 |
+
"resume_from": "stage 2 latest (pdparams + pdopt + states)",
|
| 84 |
+
"output": "ch_PP-OCRv4_rec_v25_final/latest"
|
| 85 |
+
}
|
| 86 |
+
]
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
"acceptance_gates": {
|
| 90 |
+
"non_regression_clean_a4": {
|
| 91 |
+
"description": "Đảm bảo độ chính xác trên văn bản Chăm in chuẩn A4 không bị suy giảm khi tối ưu các miền dữ liệu mới.",
|
| 92 |
+
"metric": "CER",
|
| 93 |
+
"max_allowed_delta": "+0.50%"
|
| 94 |
+
},
|
| 95 |
+
"anti_blur_target": {
|
| 96 |
+
"metric": "CER on motion blur tests",
|
| 97 |
+
"target": "< 18.0%"
|
| 98 |
+
},
|
| 99 |
+
"mixed_bilingual_target": {
|
| 100 |
+
"metric": "CER on inline mixed Cham-Viet",
|
| 101 |
+
"target": "< 12.0%"
|
| 102 |
+
},
|
| 103 |
+
"paragraph_flow_f1": {
|
| 104 |
+
"metric": "Paragraph reconstruction F1",
|
| 105 |
+
"target": "> 88.0%"
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
ocr-training/scripts/generate_data_v25.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Master Synthetic Dataset Generator for Cham OCR Model V25.
|
| 5 |
+
Optimized for 32-core Data Prep CPU Machine on Lightning AI Studios.
|
| 6 |
+
|
| 7 |
+
Produces 140,000 Train + 10,000 Val (150,000 total) line images across 5 core pillars:
|
| 8 |
+
1. Standard & Authentic Cham Literature (Po Klong Garai, 57-stanza poem, administration)
|
| 9 |
+
2. Extreme Anti-Motion Blur & Defocus Augmentation (Kernels 7x7 to 13x13, downsampling)
|
| 10 |
+
3. Intra-line Bilingual Code-Switching (Cham phrase + Vietnamese diacritics + Latin)
|
| 11 |
+
4. Full Stanza Digits (1-99: ꩑꩞..꩙꩙꩞) & Boundary Punctuation (꩞, :, –, ?, !)
|
| 12 |
+
5. Minimal Pairs Hard-Examples (Vowel Sign UE ꨲ U+AA32 vs Medial WA ꨶ U+AA36, ꨯꨮꨩ vs ꨯꨱ, ꩝꩝ variable gap 2-8px)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import math
|
| 18 |
+
import random
|
| 19 |
+
import time
|
| 20 |
+
import json
|
| 21 |
+
import unicodedata
|
| 22 |
+
import argparse
|
| 23 |
+
import cv2
|
| 24 |
+
import numpy as np
|
| 25 |
+
from multiprocessing import Pool, cpu_count
|
| 26 |
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
|
| 27 |
+
|
| 28 |
+
# UTF-8 stdout encoding
|
| 29 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 30 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 31 |
+
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
|
| 32 |
+
sys.stderr.reconfigure(encoding='utf-8')
|
| 33 |
+
|
| 34 |
+
# NumPy 2.x compatibility monkeypatch
|
| 35 |
+
if not hasattr(np, 'sctypes'):
|
| 36 |
+
np.sctypes = {
|
| 37 |
+
'int': [np.int8, np.int16, np.int32, np.int64],
|
| 38 |
+
'uint': [np.uint8, np.uint16, np.uint32, np.uint64],
|
| 39 |
+
'float': [np.float16, np.float32, np.float64],
|
| 40 |
+
'complex': [np.complex64, np.complex128],
|
| 41 |
+
'others': [bool, object, bytes, str]
|
| 42 |
+
}
|
| 43 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 44 |
+
if not hasattr(np, 'int'): np.int = int
|
| 45 |
+
if not hasattr(np, 'float'): np.float = float
|
| 46 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 47 |
+
|
| 48 |
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 49 |
+
TRAINING_DIR = os.path.dirname(SCRIPT_DIR)
|
| 50 |
+
PROJECT_ROOT = os.path.dirname(TRAINING_DIR)
|
| 51 |
+
|
| 52 |
+
# Fonts
|
| 53 |
+
FONT_CHAM_REG = os.path.join(TRAINING_DIR, "data", "fonts", "NotoSansCham-Regular.ttf")
|
| 54 |
+
FONT_CHAM_BOLD = os.path.join(TRAINING_DIR, "data", "fonts", "NotoSansCham-Bold.ttf")
|
| 55 |
+
|
| 56 |
+
# ==============================================================================
|
| 57 |
+
# 1. Ký Tự & Ngữ Liệu Tiếng Chăm & Song Ngữ V25
|
| 58 |
+
# ==============================================================================
|
| 59 |
+
CHAM_CONSONANTS = list("ꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨀꨁꨂꨃꨄꨅ")
|
| 60 |
+
CHAM_DIGITS_LIST = list("꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙")
|
| 61 |
+
|
| 62 |
+
def to_cham_num(n):
|
| 63 |
+
"""Chuyển số nguyên n thành chuỗi chữ số Chăm"""
|
| 64 |
+
return "".join(CHAM_DIGITS_LIST[int(d)] for d in str(n))
|
| 65 |
+
|
| 66 |
+
# Ngữ liệu từ bài thơ 57 khổ và truyền thuyết Po Klong Garai
|
| 67 |
+
CHAM_VOCAB_POOL = [
|
| 68 |
+
"ꨛꨯꨮ", "ꨆꨵꨯꨱꩃ", "ꨈꨣꩈ", "ꨧꨯꨱꩃ", "ꨚꩆ", "ꨆꩀ", "ꨨꨤꨩ", "ꨟꨐꨪꩌ", "ꨕꨶꨩ", "ꨝꨯꨱꩍ",
|
| 69 |
+
"ꨝꨰ", "ꨙꨮ", "ꨎꨤꨭꩀ", "ꨟꨐꨭꩌ", "ꨀꨳꨩ", "ꨝꨵꨯꨱꩍ", "ꨝꨩ", "ꨧꨩ", "ꨝꨵꩍ", "ꨀꨇꩆ",
|
| 70 |
+
"ꨗꨯꨱ", "ꨝꨣꨭꨥ", "ꨟꩃ", "ꨓꨮꩊ", "ꨊꨯꨱꩀ", "ꨣꨈꨵꨰ", "ꨚꨢꨯꨱꩌ", "ꨘꩆ", "ꨤꩄ", "ꨆꨳꨮꩃ",
|
| 71 |
+
"ꨧꨤꨪꩍ", "ꨕꨨꨵꩀ", "ꨨꨕꨯꩌ", "ꨀꨣꩀ", "ꨗꨫ", "ꨢꨭꩅ", "ꨚꨰꩀ", "ꨝꨪ", "ꨝꩀ", "ꨆꨮꨭ",
|
| 72 |
+
"ꨝꨴꨬ", "ꨆꨩ", "ꨨꨲꨩ", "ꨡꨯꩍ", "ꨀꨦꨪꩅ", "ꨟꩀ", "ꨟꨰ", "ꨝꨭꩍ", "ꨕꨫ", "ꨕꨤꩌ",
|
| 73 |
+
"ꨅꨩ", "ꨨꨕꨳꨮꩇ", "ꨀꨗꩀ", "ꨓꨌꨯꨱꨥ", "ꨙꩃ", "ꨀꨝꨪꩍ", "ꨤꨝꨭꩀ", "ꨎꨥ", "ꨥꨮꩀ", "ꨢꨮ",
|
| 74 |
+
"ꨐꨭꨩ", "ꨓꨴꨩ", "ꨚꨤꨬ", "ꨂꩀ", "ꨀꨗꨯꨱꩃ", "ꨧꨝꨭꩃ", "ꨈꨮꩇ", "ꨀꨧꨪꩅ", "ꨢꨮꨭ", "ꨕꨘꩀ",
|
| 75 |
+
"ꨎꨮ", "ꨚꨶꨮꩄ", "ꨚꨆꨴꨲꨩ", "ꨟꨪꩆ", "ꨙꨩ", "ꨝꨳꩀ", "ꨚꨗꨶꨮꩄ", "ꨔꨮꨭ", "ꨕꨯꨱꩀ", "ꨓꨳꩆ",
|
| 76 |
+
"ꨀꨶꨮꩆ", "ꨓꨝꨶꨮꩆ", "ꨚꨎꨳꩀ", "ꨆꨵꨮꨭ", "ꨀꨮꩈ", "ꨧꨝꩀ", "ꨈꨗꨴꨮꩍ", "ꨚꩀ", "ꨨꨚꨭꩍ", "ꨤꨪꨎꩃ",
|
| 77 |
+
"ꨝꨵꨬ", "ꨣꨬ", "ꨈꨴꨮꩇ", "ꨧꩃ", "ꨠꨎꨰꩀ", "ꨚꨙꨳꩀ", "ꨤꨯꨩ", "ꨤꨮꩍ", "ꨚꨕꨬ", "ꨈꨵꨰꩍ",
|
| 78 |
+
"ꨀꨤꨩ", "ꨨꨤꨶꨬ", "ꨆꨭꨢꨮꨭ", "ꨤꨪꨔꨬ", "ꨨꨤꨬ", "ꨇꨪꩀ", "ꨙꨪꩍ", "ꨥꩉ", "ꨕꨴꨬ", "ꨁꨗꨩ",
|
| 79 |
+
"ꨈꨭꨣꩈ", "ꨤꨳꩍ", "ꨣꨭꩇ", "ꨤꨶꩀ", "ꨓꨟꨩ", "ꨓꨗꩍ", "ꨣꨪꨢꨩ", "ꨖꨪꩅ", "ꨀꨧꩉ", "ꨚꨓꨯꨱ",
|
| 80 |
+
"ꨇꨪꩆ", "ꨨꨶꩀ", "ꨃꨥ", "ꨧꨣꨥꨩ", "ꨚꨙꩉ", "ꨢꩍ", "ꨄꨰ", "ꨆꨯꨱꩍ", "ꨚꨓꨬ", "ꨓꨭꩍ",
|
| 81 |
+
"ꨓꨌꨬ", "ꨌꨪꩍ", "ꨈꩆ", "ꨆꨴꨲꩍ", "ꨓꨟꨯꨱꩃ", "ꨨꨭꨩ", "ꨗꩌ", "ꨈꩍ", "ꨆꨕꨯꨱꩍ", "ꨕꨨꨵꨮꨭ",
|
| 82 |
+
"ꨌꨙꨳꩀ", "ꨓꨆꨶꨯꩈ", "ꨀꨥꨰꩅ", "ꨗꨆꨓꨯꨮꨩ", "ꨀꨆꨢꨯꨮꩅ", "ꨓꨆꨴꨲꨩ", "ꨚꨟꨆꨴꨲꨩ", "ꨣꨳꨪꩌ", "ꨨꨈꨴꨮꩌ"
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
# Minimal pairs cho Vowel Sign UE ꨲ (U+AA32) vs Consonant Sign Medial WA ꨶ (U+AA36)
|
| 86 |
+
MINIMAL_PAIRS_UE_WA = [
|
| 87 |
+
("ꨀꨲꩆ", "ꨀꨶꩆ"),
|
| 88 |
+
("ꨓꨆꨴꨲꨩ", "ꨓꨆꨴꨶꨩ"),
|
| 89 |
+
("ꨚꨴꨲꨩ", "ꨚꨴꨶꨩ"),
|
| 90 |
+
("ꨆꨲꩆ", "ꨆꨶꩆ"),
|
| 91 |
+
("ꨟꨲꩆ", "ꨟꨶꩆ"),
|
| 92 |
+
("ꨣꨲꨩ", "ꨣꨶꨩ"),
|
| 93 |
+
("ꨚꨟꨆꨴꨲꨩ", "ꨚꨟꨆꨴꨶꨩ"),
|
| 94 |
+
("ꨚꨲ", "ꨚꨶ"),
|
| 95 |
+
("ꨆꨲ", "ꨆꨶ"),
|
| 96 |
+
("ꨀꨲ", "ꨀꨶ")
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
# Cụm dấu 3 tầng & tổ hợp nguyên âm phức tạp
|
| 100 |
+
COMPLEX_CLUSTERS = [
|
| 101 |
+
"ꨣꨳꨪꩌ", "ꨓꨳꨪꩌ", "ꨚꨳꨪꩌ", "ꨕꨨꨵꩀ", "ꨝꨪꨗꨳꨪ",
|
| 102 |
+
"ꨨꨕꨳꨮꩇ", "ꨞꨯꨚꨓꨪꩍ", "ꨧꨪꩆꨝꨳꨪ", "ꨆꨵꨯꨱꩃ", "ꨚꨗꨴꨯꨱꩃ",
|
| 103 |
+
"ꨗꨆꨓꨯꨮꨩ", "ꨚꨓꨯꨮꨩ", "ꨤꨯꨮꨩ", "ꨨꨝꨳꨯꨮꩆ", "ꨨꨈꨴꨮꩌ", "ꨣꨪꨈꨮꩌ"
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
# Mẫu song ngữ nội dòng (Cham phrase + Viet translation/commentary)
|
| 107 |
+
BILINGUAL_TEMPLATES = [
|
| 108 |
+
("ꨛꨯꨮ ꨆꨵꨯꨱꩃ ꨈꨣꩈ", "Vua Pô Klông Gia-rai"),
|
| 109 |
+
("ꨛꨯꨮ ꨚꩆ", "Pô Păt - Po Pat"),
|
| 110 |
+
("ꨚꨰꩀ ꨨꨤꨩ", "đắp bờ đập ngăn nước"),
|
| 111 |
+
("ꨕꨶꨩ ꨝꨯꨱꩍ ꨝꨰ", "dẫn nguồn nước vào ruộng"),
|
| 112 |
+
("ꨞꩇ ꨂꨣꩃ ꨌꩌ", "xứ sở người Chăm Panduranga"),
|
| 113 |
+
("yutꨢꨮ", "bờ đập nước sông lớn"),
|
| 114 |
+
("ꨁꨗꨩ ꨈꨭꨣꩈ", "Bà Thầy truyền dạy giáo lý"),
|
| 115 |
+
("ꨤꨪꨔꨬ ꨓꨝꨳꩀ", "cơm nắm gói trong lá chuối"),
|
| 116 |
+
("ꨝꨣꨭꨥ ꨟꩃ ꨣꨈꨵꨰ", "sau cuộc thi tài kết thúc"),
|
| 117 |
+
("ꨕꨨꨵꨀ ꨨꨕꨯꩌ", "nước chảy quanh năm trên đồng"),
|
| 118 |
+
("ꨎꨤꨭꩀ ꨟꨐꨭꩌ", "mở rộng hệ thống mương máng"),
|
| 119 |
+
("ꨣꨈꨵꨰ ꨚꨤꨬ ꨂꩀ", "thi tài đắp đập ngăn sông"),
|
| 120 |
+
("ꨓꩀ ꨕꨫ ꨚꨰꩀ ꨨꨤꨩ", "đến lúc đắp bờ đập nước"),
|
| 121 |
+
("ꨝꨰ ꨘꩆ ꨀꨦꨪꩅ", "ruộng lúa tốt tươi màu mỡ"),
|
| 122 |
+
("ꨣꨈꨵꨰ ꨧꨝꨭꩃ", "thi tài đắp đập đá lớn")
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
# ==============================================================================
|
| 126 |
+
# 2. Bộ Sinh Nội Dung Văn Bản (V25 Text Sampler)
|
| 127 |
+
# ==============================================================================
|
| 128 |
+
def sample_v25_text(pillar_type):
|
| 129 |
+
"""
|
| 130 |
+
Sinh dòng văn bản theo từng trụ cột kỹ thuật:
|
| 131 |
+
- pillar 1: Cham chuẩn (chuẩn bị cho Anti-Motion Blur)
|
| 132 |
+
- pillar 2: Song ngữ nội dòng (Cham + Viet + Latin)
|
| 133 |
+
- pillar 3: Số khổ thơ 1-99 & Dấu ngắt câu (꩑꩞..꩔꩓꩞, :, –, ?)
|
| 134 |
+
- pillar 4: Cặp đối kháng Minimal Pairs (ꨲ vs ꨶ, ꩝꩝ variable gap, ꨯꨮꨩ)
|
| 135 |
+
"""
|
| 136 |
+
if pillar_type == "bilingual":
|
| 137 |
+
# Trụ cột 2: Song ngữ nội dòng
|
| 138 |
+
pair = random.choice(BILINGUAL_TEMPLATES)
|
| 139 |
+
style = random.choice(["bracket", "colon", "hyphen", "en_dash"])
|
| 140 |
+
if style == "bracket":
|
| 141 |
+
return f"{pair[0]} ({pair[1]})"
|
| 142 |
+
elif style == "colon":
|
| 143 |
+
return f"{pair[0]}: {pair[1]}"
|
| 144 |
+
elif style == "en_dash":
|
| 145 |
+
return f"{pair[0]} – {pair[1]}"
|
| 146 |
+
else:
|
| 147 |
+
return f"{pair[0]} - {pair[1]}"
|
| 148 |
+
|
| 149 |
+
elif pillar_type == "stanza_punct":
|
| 150 |
+
# Trụ cột 3: Số khổ 1-99 & Dấu ngắt câu
|
| 151 |
+
num_val = random.randint(1, 99)
|
| 152 |
+
cham_num_str = to_cham_num(num_val)
|
| 153 |
+
sep = random.choice(["꩞ ", "꩝ "])
|
| 154 |
+
words = random.sample(CHAM_VOCAB_POOL, random.randint(2, 4))
|
| 155 |
+
content = " ".join(words)
|
| 156 |
+
end_mark = random.choice(["꩞", "꩝꩝", "꩝", ":", "?", "!"])
|
| 157 |
+
return f"{cham_num_str}{sep}{content}{end_mark}"
|
| 158 |
+
|
| 159 |
+
elif pillar_type == "minimal_pairs":
|
| 160 |
+
# Trụ cột 4: Cặp đối kháng
|
| 161 |
+
sub = random.choice(["ue_wa", "double_danda", "complex_clusters"])
|
| 162 |
+
if sub == "ue_wa":
|
| 163 |
+
pair = random.choice(MINIMAL_PAIRS_UE_WA)
|
| 164 |
+
target = pair[0] if random.random() < 0.5 else pair[1]
|
| 165 |
+
surrounding = random.sample(CHAM_VOCAB_POOL, random.randint(2, 3))
|
| 166 |
+
return f"{target} " + " ".join(surrounding) + random.choice(["꩝", "꩝꩝", "꩞", ""])
|
| 167 |
+
elif sub == "double_danda":
|
| 168 |
+
words = random.sample(CHAM_VOCAB_POOL, random.randint(3, 5))
|
| 169 |
+
return " ".join(words) + "꩝꩝"
|
| 170 |
+
else:
|
| 171 |
+
c = random.choice(COMPLEX_CLUSTERS)
|
| 172 |
+
words = random.sample(CHAM_VOCAB_POOL, random.randint(2, 3))
|
| 173 |
+
return f"{c} " + " ".join(words) + random.choice(["꩝", "꩞", ""])
|
| 174 |
+
|
| 175 |
+
else:
|
| 176 |
+
# Chuẩn (sẽ áp dụng Motion Blur nếu thuộc pillar anti_blur)
|
| 177 |
+
words = random.sample(CHAM_VOCAB_POOL, random.randint(3, 6))
|
| 178 |
+
text = " ".join(words)
|
| 179 |
+
if random.random() < 0.5:
|
| 180 |
+
text += random.choice(["꩝", "꩝꩝", "꩞", ""])
|
| 181 |
+
return text
|
| 182 |
+
|
| 183 |
+
# ==============================================================================
|
| 184 |
+
# 3. Kỹ Thuật Render Ảnh & Tinh Chỉnh Nét Chữ
|
| 185 |
+
# ==============================================================================
|
| 186 |
+
def render_line_v25(text, is_bold=False, font_size=26, pad_left=12, pad_right=12, danda_gap=3):
|
| 187 |
+
"""
|
| 188 |
+
Render dòng chữ Chăm / Song ngữ.
|
| 189 |
+
Hỗ trợ biến thiên khoảng cách Double Danda '꩝꩝' từ 2px đến 8px.
|
| 190 |
+
"""
|
| 191 |
+
font_path = FONT_CHAM_BOLD if is_bold else FONT_CHAM_REG
|
| 192 |
+
font = ImageFont.truetype(font_path, font_size)
|
| 193 |
+
|
| 194 |
+
# Nếu có ꩝꩝ ở cuối và cần biến thiên khoảng cách
|
| 195 |
+
if text.endswith("꩝꩝") and danda_gap != 3:
|
| 196 |
+
base_text = text[:-2]
|
| 197 |
+
bbox_base = font.getbbox(base_text) if base_text else (0, 0, 0, font_size)
|
| 198 |
+
w_base = (bbox_base[2] - bbox_base[0]) if base_text else 0
|
| 199 |
+
|
| 200 |
+
bbox_danda = font.getbbox("꩝")
|
| 201 |
+
w_danda = bbox_danda[2] - bbox_danda[0]
|
| 202 |
+
h_line = max(bbox_base[3] - bbox_base[1], bbox_danda[3] - bbox_danda[1]) + 16
|
| 203 |
+
w_total = pad_left + w_base + (6 if base_text else 0) + w_danda + danda_gap + w_danda + pad_right
|
| 204 |
+
|
| 205 |
+
img = Image.new("RGB", (max(w_total, 60), max(h_line, 48)), (255, 255, 255))
|
| 206 |
+
draw = ImageDraw.Draw(img)
|
| 207 |
+
|
| 208 |
+
cur_x = pad_left
|
| 209 |
+
y = 8
|
| 210 |
+
if base_text:
|
| 211 |
+
draw.text((cur_x, y), base_text, fill=(20, 20, 25), font=font)
|
| 212 |
+
cur_x += w_base + 6
|
| 213 |
+
draw.text((cur_x, y), "꩝", fill=(20, 20, 25), font=font)
|
| 214 |
+
cur_x += w_danda + danda_gap
|
| 215 |
+
draw.text((cur_x, y), "꩝", fill=(20, 20, 25), font=font)
|
| 216 |
+
return img
|
| 217 |
+
|
| 218 |
+
# Render thông thường
|
| 219 |
+
bbox = font.getbbox(text)
|
| 220 |
+
tw = bbox[2] - bbox[0]
|
| 221 |
+
th = bbox[3] - bbox[1]
|
| 222 |
+
w = tw + pad_left + pad_right
|
| 223 |
+
h = max(th + 16, 48)
|
| 224 |
+
|
| 225 |
+
img = Image.new("RGB", (max(w, 60), h), (255, 255, 255))
|
| 226 |
+
draw = ImageDraw.Draw(img)
|
| 227 |
+
x = pad_left - bbox[0]
|
| 228 |
+
y = 8 - bbox[1]
|
| 229 |
+
draw.text((x, y), text, fill=(20, 20, 25), font=font)
|
| 230 |
+
return img
|
| 231 |
+
|
| 232 |
+
# ==============================================================================
|
| 233 |
+
# 4. Pipeline Biến Dạng Thực Địa (Augmentations) - Đặc Trị Motion Blur
|
| 234 |
+
# ==============================================================================
|
| 235 |
+
def apply_heavy_motion_blur(img_np, ksize=9, angle=45.0):
|
| 236 |
+
"""Tạo ma trận directional motion blur kernel mô phỏng rung tay máy ảnh di động."""
|
| 237 |
+
kernel = np.zeros((ksize, ksize), dtype=np.float32)
|
| 238 |
+
rad = math.radians(angle)
|
| 239 |
+
dx = math.cos(rad)
|
| 240 |
+
dy = math.sin(rad)
|
| 241 |
+
cx, cy = ksize // 2, ksize // 2
|
| 242 |
+
for r in range(-ksize // 2, ksize // 2 + 1):
|
| 243 |
+
kx = int(round(cx + r * dx))
|
| 244 |
+
ky = int(round(cy + r * dy))
|
| 245 |
+
if 0 <= kx < ksize and 0 <= ky < ksize:
|
| 246 |
+
kernel[ky, kx] = 1.0
|
| 247 |
+
if kernel.sum() > 0:
|
| 248 |
+
kernel /= kernel.sum()
|
| 249 |
+
else:
|
| 250 |
+
kernel[cx, cy] = 1.0
|
| 251 |
+
return cv2.filter2D(img_np, -1, kernel)
|
| 252 |
+
|
| 253 |
+
def augment_image_v25(img_pil, is_motion_blur_pillar=False, bake_blur=False):
|
| 254 |
+
"""
|
| 255 |
+
Áp dụng các phép biến dạng hình ảnh thực tế:
|
| 256 |
+
- Nếu bake_blur=False (MẶC ĐỊNH CHO V25): Không nướng (bake) motion blur nặng vào ảnh đĩa.
|
| 257 |
+
Mọi thao tác làm mờ được thực hiện On-the-fly trong DataLoader qua RecAug để tránh double-blur.
|
| 258 |
+
- Giữ biến dạng nhẹ: nhiễu hạt giấy cổ, độ tương phản và chuẩn hóa chiều cao 48px.
|
| 259 |
+
"""
|
| 260 |
+
img_bgr = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
|
| 261 |
+
h, w = img_bgr.shape[:2]
|
| 262 |
+
|
| 263 |
+
if bake_blur and is_motion_blur_pillar:
|
| 264 |
+
# Chỉ áp dụng nếu người dùng cố ý bật bake_blur
|
| 265 |
+
ksize = random.choice([7, 9, 11, 13])
|
| 266 |
+
angle = random.uniform(0.0, 180.0)
|
| 267 |
+
img_bgr = apply_heavy_motion_blur(img_bgr, ksize=ksize, angle=angle)
|
| 268 |
+
if random.random() < 0.4:
|
| 269 |
+
scale = random.uniform(0.5, 0.75)
|
| 270 |
+
small = cv2.resize(img_bgr, (max(int(w * scale), 20), max(int(h * scale), 16)), interpolation=cv2.INTER_LINEAR)
|
| 271 |
+
img_bgr = cv2.resize(small, (w, h), interpolation=cv2.INTER_CUBIC)
|
| 272 |
+
|
| 273 |
+
# Thêm nhiễu nền giấy cổ nhẹ
|
| 274 |
+
if random.random() < 0.6:
|
| 275 |
+
noise = np.random.normal(0, random.uniform(1.0, 3.5), (h, w, 3)).astype(np.float32)
|
| 276 |
+
img_bgr = np.clip(img_bgr.astype(np.float32) + noise, 0, 255).astype(np.uint8)
|
| 277 |
+
|
| 278 |
+
# Thay đổi độ tương phản và độ sáng
|
| 279 |
+
if random.random() < 0.5:
|
| 280 |
+
alpha = random.uniform(0.85, 1.15)
|
| 281 |
+
beta = random.uniform(-10, 10)
|
| 282 |
+
img_bgr = np.clip(img_bgr.astype(np.float32) * alpha + beta, 0, 255).astype(np.uint8)
|
| 283 |
+
|
| 284 |
+
# Đảm bảo resize về chuẩn height 48px cho V25
|
| 285 |
+
target_h = 48
|
| 286 |
+
scale = target_h / float(h)
|
| 287 |
+
new_w = max(int(round(w * scale)), 32)
|
| 288 |
+
img_resized = cv2.resize(img_bgr, (new_w, target_h), interpolation=cv2.INTER_CUBIC)
|
| 289 |
+
return img_resized
|
| 290 |
+
|
| 291 |
+
# ==============================================================================
|
| 292 |
+
# 5. Hàm Worker Đa Tiến Trình (Multiprocessing Task)
|
| 293 |
+
# ==============================================================================
|
| 294 |
+
def generate_sample_worker(args):
|
| 295 |
+
"""Worker tạo một mẫu ảnh và nhãn."""
|
| 296 |
+
sample_idx, pillar_type, out_img_path, bake_blur = args
|
| 297 |
+
|
| 298 |
+
# 1. Sinh văn bản và chuẩn hóa tuyệt đối Unicode NFC
|
| 299 |
+
raw_text = sample_v25_text(pillar_type)
|
| 300 |
+
text = unicodedata.normalize('NFC', raw_text)
|
| 301 |
+
|
| 302 |
+
# 2. Render ảnh
|
| 303 |
+
is_bold = (random.random() < 0.35)
|
| 304 |
+
pad_l = random.randint(2, 14)
|
| 305 |
+
pad_r = random.randint(2, 14)
|
| 306 |
+
danda_gap = random.randint(2, 8) if "꩝꩝" in text else 3
|
| 307 |
+
|
| 308 |
+
img_pil = render_line_v25(text, is_bold=is_bold, pad_left=pad_l, pad_right=pad_r, danda_gap=danda_gap)
|
| 309 |
+
|
| 310 |
+
# 3. Biến dạng (Augmentation sạch cho lưu trữ đĩa)
|
| 311 |
+
is_blur_pillar = (pillar_type == "anti_blur")
|
| 312 |
+
img_final = augment_image_v25(img_pil, is_motion_blur_pillar=is_blur_pillar, bake_blur=bake_blur)
|
| 313 |
+
|
| 314 |
+
# 4. Ghi file ảnh
|
| 315 |
+
cv2.imwrite(out_img_path, img_final)
|
| 316 |
+
|
| 317 |
+
# 5. Trả về tên file tương đối và nhãn
|
| 318 |
+
rel_path = os.path.basename(os.path.dirname(out_img_path)) + "/" + os.path.basename(out_img_path)
|
| 319 |
+
return f"{rel_path}\t{text}\n"
|
| 320 |
+
|
| 321 |
+
# ==============================================================================
|
| 322 |
+
# 6. Điều Phối Huấn Luyện Toàn Cục (Master Generator Loop)
|
| 323 |
+
# ==============================================================================
|
| 324 |
+
def main():
|
| 325 |
+
default_manifest = os.path.join(TRAINING_DIR, "configs", "v25_dataset_manifest.json")
|
| 326 |
+
parser = argparse.ArgumentParser(description="Generate Cham OCR V25 Synthetic Dataset (Manifest-Aligned)")
|
| 327 |
+
parser.add_argument("--manifest", type=str, default=default_manifest, help="Đường dẫn file manifest JSON chuẩn")
|
| 328 |
+
parser.add_argument("--output_dir", type=str, default="./data/cham_synthetic_v25", help="Thư mục xuất dữ liệu")
|
| 329 |
+
parser.add_argument("--num_train", type=int, default=None, help="Số mẫu Train (mặc định lấy từ manifest: 140,000)")
|
| 330 |
+
parser.add_argument("--num_val", type=int, default=None, help="Số mẫu Val (mặc định lấy từ manifest: 10,000)")
|
| 331 |
+
parser.add_argument("--bake_blur", action="store_true", default=False, help="Bake mờ nặng vào ảnh đĩa (mặc định False: on-the-fly)")
|
| 332 |
+
parser.add_argument("--workers", type=int, default=None, help="Số tiến trình CPU song song (mặc định cpu_count)")
|
| 333 |
+
args = parser.parse_args()
|
| 334 |
+
|
| 335 |
+
# Nạp cấu hình từ manifest nếu có
|
| 336 |
+
num_train = args.num_train
|
| 337 |
+
num_val = args.num_val
|
| 338 |
+
pillars = ["standard", "anti_blur", "bilingual", "stanza_punct", "minimal_pairs"]
|
| 339 |
+
weights = [0.433, 0.200, 0.167, 0.120, 0.080] # Tương ứng 65k, 30k, 25k, 18k, 12k
|
| 340 |
+
|
| 341 |
+
if os.path.exists(args.manifest):
|
| 342 |
+
try:
|
| 343 |
+
with open(args.manifest, "r", encoding="utf-8") as f:
|
| 344 |
+
manifest_data = json.load(f)
|
| 345 |
+
dist = manifest_data.get("data_distribution", {})
|
| 346 |
+
if num_train is None:
|
| 347 |
+
num_train = dist.get("train_samples", 140000)
|
| 348 |
+
if num_val is None:
|
| 349 |
+
num_val = dist.get("val_samples", 10000)
|
| 350 |
+
print(f"📖 Đã nạp cấu hình phân bổ dữ liệu từ Manifest: {args.manifest}")
|
| 351 |
+
except Exception as e:
|
| 352 |
+
print(f"⚠️ Không đọc được manifest ({e}), dùng thông số mặc định.")
|
| 353 |
+
|
| 354 |
+
if num_train is None: num_train = 140000
|
| 355 |
+
if num_val is None: num_val = 10000
|
| 356 |
+
|
| 357 |
+
num_workers = args.workers or cpu_count()
|
| 358 |
+
print("=" * 75)
|
| 359 |
+
print("🚀 MASTER SYNTHETIC DATASET GENERATOR V25 (MANIFEST SYNCHRONIZED)")
|
| 360 |
+
print(f" • Số CPU Workers song song : {num_workers} tiến trình")
|
| 361 |
+
print(f" • Quy mô mẫu Huấn luyện : {num_train:,} ảnh")
|
| 362 |
+
print(f" • Quy mô mẫu Kiểm thử : {num_val:,} ảnh")
|
| 363 |
+
print(f" • Tổng quy mô dữ liệu : {num_train + num_val:,} ảnh")
|
| 364 |
+
print(f" • Chính sách Mờ Động Học : {'Bake tĩnh trên đĩa' if args.bake_blur else 'On-The-Fly trong DataLoader (Khuyến nghị)'}")
|
| 365 |
+
print(f" • Thư mục đầu ra : {args.output_dir}")
|
| 366 |
+
print("=" * 75)
|
| 367 |
+
|
| 368 |
+
train_img_dir = os.path.join(args.output_dir, "train_images")
|
| 369 |
+
val_img_dir = os.path.join(args.output_dir, "val_images")
|
| 370 |
+
os.makedirs(train_img_dir, exist_ok=True)
|
| 371 |
+
os.makedirs(val_img_dir, exist_ok=True)
|
| 372 |
+
|
| 373 |
+
# Chuẩn bị danh sách tham số cho Train
|
| 374 |
+
print(f"📦 Chuẩn bị danh mục tác vụ cho {num_train:,} mẫu Train...")
|
| 375 |
+
train_tasks = []
|
| 376 |
+
for i in range(num_train):
|
| 377 |
+
p_type = random.choices(pillars, weights=weights)[0]
|
| 378 |
+
img_name = f"train_{i+1:07d}.png"
|
| 379 |
+
out_path = os.path.join(train_img_dir, img_name)
|
| 380 |
+
train_tasks.append((i, p_type, out_path, args.bake_blur))
|
| 381 |
+
|
| 382 |
+
# Chuẩn bị danh sách tham số cho Val
|
| 383 |
+
print(f"📦 Chuẩn bị danh mục tác vụ cho {num_val:,} mẫu Val...")
|
| 384 |
+
val_tasks = []
|
| 385 |
+
for i in range(num_val):
|
| 386 |
+
p_type = random.choices(pillars, weights=weights)[0]
|
| 387 |
+
img_name = f"val_{i+1:06d}.png"
|
| 388 |
+
out_path = os.path.join(val_img_dir, img_name)
|
| 389 |
+
val_tasks.append((i, p_type, out_path, args.bake_blur))
|
| 390 |
+
|
| 391 |
+
t_start = time.time()
|
| 392 |
+
|
| 393 |
+
# 1. Sinh tập Train với đa tiến trình
|
| 394 |
+
print(f"\n⚡ Đang sinh tập Train ({num_train:,} ảnh) trên {num_workers} CPU cores...")
|
| 395 |
+
train_label_path = os.path.join(args.output_dir, "train_label.txt")
|
| 396 |
+
with open(train_label_path, "w", encoding="utf-8") as f_train:
|
| 397 |
+
with Pool(processes=num_workers) as pool:
|
| 398 |
+
chunksize = 250
|
| 399 |
+
completed = 0
|
| 400 |
+
for label_line in pool.imap_unordered(generate_sample_worker, train_tasks, chunksize=chunksize):
|
| 401 |
+
f_train.write(label_line)
|
| 402 |
+
completed += 1
|
| 403 |
+
if completed % 25000 == 0 or completed == num_train:
|
| 404 |
+
elapsed = time.time() - t_start
|
| 405 |
+
fps = completed / max(elapsed, 1e-5)
|
| 406 |
+
print(f" [Train Progress] {completed:,} / {num_train:,} ảnh ({completed/num_train*100:.1f}%) - Tốc độ: {fps:.1f} ảnh/s")
|
| 407 |
+
|
| 408 |
+
t_train_done = time.time()
|
| 409 |
+
print(f"✅ Hoàn thành tập Train trong {t_train_done - t_start:.2f}s (~{(t_train_done - t_start)/60:.1f} phút)")
|
| 410 |
+
|
| 411 |
+
# 2. Sinh tập Val với đa tiến trình
|
| 412 |
+
if num_val > 0:
|
| 413 |
+
print(f"\n⚡ Đang sinh tập Val ({num_val:,} ảnh) trên {num_workers} CPU cores...")
|
| 414 |
+
val_label_path = os.path.join(args.output_dir, "val_label.txt")
|
| 415 |
+
with open(val_label_path, "w", encoding="utf-8") as f_val:
|
| 416 |
+
with Pool(processes=num_workers) as pool:
|
| 417 |
+
chunksize = 250
|
| 418 |
+
completed = 0
|
| 419 |
+
for label_line in pool.imap_unordered(generate_sample_worker, val_tasks, chunksize=chunksize):
|
| 420 |
+
f_val.write(label_line)
|
| 421 |
+
completed += 1
|
| 422 |
+
if completed % 5000 == 0 or completed == num_val:
|
| 423 |
+
print(f" [Val Progress] {completed:,} / {num_val:,} ảnh ({completed/num_val*100:.1f}%)")
|
| 424 |
+
else:
|
| 425 |
+
val_label_path = os.path.join(args.output_dir, "val_label.txt")
|
| 426 |
+
print(f"\n❄️ Bỏ qua sinh tập Val (num_val = 0). Giữ nguyên tập Val đóng băng tại: {val_label_path}")
|
| 427 |
+
|
| 428 |
+
total_time = time.time() - t_start
|
| 429 |
+
print("=" * 75)
|
| 430 |
+
print(f"🎉 HOÀN TẤT SINH TOÀN BỘ BỘ DỮ LIỆU V25 ({num_train + num_val:,} ẢNH)!")
|
| 431 |
+
print(f" • Tổng thời gian thực thi: {total_time:.2f}s (~{total_time/60:.2f} phút)")
|
| 432 |
+
print(f" • Tệp nhãn Train : {train_label_path}")
|
| 433 |
+
print(f" • Tệp nhãn Val : {val_label_path}")
|
| 434 |
+
print("=" * 75)
|
| 435 |
+
|
| 436 |
+
if __name__ == "__main__":
|
| 437 |
+
main()
|