Image Classification
Transformers
Tibetan
dinov3
tibetan
script-classification
paleography
fine-tuned
document-analysis
Eval Results (legacy)
Instructions to use openpecha/tibetan-script-classifier with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use openpecha/tibetan-script-classifier with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-classification", model="openpecha/tibetan-script-classifier") pipe("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/hub/parrots.png")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("openpecha/tibetan-script-classifier", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| language: | |
| - bo | |
| license: apache-2.0 | |
| library_name: transformers | |
| tags: | |
| - image-classification | |
| - dinov3 | |
| - tibetan | |
| - script-classification | |
| - paleography | |
| - fine-tuned | |
| - document-analysis | |
| base_model: facebook/dinov3-vits16-pretrain-lvd1689m | |
| datasets: | |
| - custom | |
| metrics: | |
| - f1 | |
| - accuracy | |
| pipeline_tag: image-classification | |
| model-index: | |
| - name: Tibetan Script Classifier (DINOv3 ViT-S) | |
| results: | |
| - task: | |
| type: image-classification | |
| name: Tibetan Script Classification | |
| metrics: | |
| - name: Macro F1 (whole page) | |
| type: f1 | |
| value: 0.512 | |
| - name: Accuracy (whole page) | |
| type: accuracy | |
| value: 0.571 | |
| - name: Macro F1 (CLAHE patches, page-level) | |
| type: f1 | |
| value: 0.529 | |
| # Tibetan Script Classifier (DINOv3) | |
| This repository contains fine-tuned Tibetan script classification checkpoints for 18 classes, trained from the DINOv3 ViT-S backbone: | |
| - Backbone: `facebook/dinov3-vits16-pretrain-lvd1689m` | |
| - Task: 18-way script classification | |
| - Training script included: `finetune_dinov3.py` | |
| **Hugging Face access:** DINOv3 requires access approval at [huggingface.co/facebook/dinov3-vits16-pretrain-lvd1689m](https://huggingface.co/facebook/dinov3-vits16-pretrain-lvd1689m) before `from_pretrained` / downloads will work. Anyone cloning this repo will see the same gated-model error until their HF account is granted access and they are logged in (`huggingface-cli login` or `HF_TOKEN`). | |
| ## Label Set | |
| `dhumri`, `difficult`, `drathung`, `drudring`, `druring`, `druthung`, `khyuyig`, `multi_scripts`, `non_tibetan`, `peri`, `petsuk`, `trinyig`, `tsegdrig`, `tsugchung`, `tsumachug`, `uchen_sugdring`, `uchen_sugthung`, `yigchung`. | |
| ## Preprocessing (per experiment) | |
| Images for training were produced as follows (see `preprocess.py` in the parent project): | |
| - **`whole_page`:** resize so the **short edge is 224 px**, then **center crop** to 224×224 (one crop per source page). | |
| - **`patches_color`:** same short-edge resize to 224, then **sliding-window** 224×224 patches with **25% overlap** between windows (multiple crops per page). | |
| - **`patches_clahe`:** identical patch layout as `patches_color`; each patch is converted to grayscale and **CLAHE** contrast normalization is applied (`clipLimit=2.0`, `tileGridSize=(8,8)`), then saved as BGR/RGB for training. | |
| ## Training recipe | |
| - **Progressive unfreezing (defaults in `finetune_dinov3.py`):** | |
| - **Stage A — head only:** 20 epochs, backbone frozen, classifier head LR **1e-3** (backbone LR 0). | |
| - **Stage B — last 2 blocks:** 10 epochs, backbone LR **1e-5**, head LR **1e-3**. | |
| - **Stage C — last 4 blocks:** 10 epochs, backbone LR **5e-6**, head LR **5e-4**. | |
| - **Loss:** class-weighted **cross-entropy** with inverse-frequency weights over the training split (`nn.CrossEntropyLoss(weight=...)`). | |
| - **Sampling:** the published runs use a standard `DataLoader` with **`shuffle=True`**. The script also defines **`get_weighted_sampler` → `WeightedRandomSampler`** if you want to switch the train loader to explicit class-balanced sampling. | |
| - **Document-aware augmentations (train only):** `RandomRotation` **±5°** (fill white), `ColorJitter` brightness/contrast **±20%** (`0.2`), plus `RandomResizedCrop` and light `RandomErasing` as in `ScriptDataset`; **no horizontal flip**. | |
| ## Class distribution (`whole_page` split totals) | |
| The whole-page split has **5,684** samples in total (**train 3,996 / val 844 / test 844**). The **844** figure in test metrics is **only the test split** (roughly 15% of pages per class held out for testing), not the full dataset. | |
| The per-class table below sums **train + val + test** counts. A benchmark exclusion manifest (`benchmark_page_ids.json`, **88** page IDs across 18 classes) is consulted when building splits: any file whose page ID matches is skipped and counted in `splits.json` under `skipped_excluded_files_by_class`. For the published `whole_page/splits.json`, that skip map is **empty**—so either those pages were **not present** under the training `data_dir`, or they had **already been removed** before this split was built. The **5,684** totals are whatever files remained for stratified splitting. | |
| | Class | Samples | | |
| |---|---:| | |
| | dhumri | 98 | | |
| | difficult | 170 | | |
| | drathung | 129 | | |
| | drudring | 132 | | |
| | druring | 119 | | |
| | druthung | 207 | | |
| | khyuyig | 113 | | |
| | multi_scripts | 235 | | |
| | non_tibetan | 192 | | |
| | peri | 614 | | |
| | petsuk | 1388 | | |
| | trinyig | 42 | | |
| | tsegdrig | 749 | | |
| | tsugchung | 77 | | |
| | tsumachug | 178 | | |
| | uchen_sugdring | 835 | | |
| | uchen_sugthung | 240 | | |
| | yigchung | 166 | | |
| ## Experiments Included | |
| ### 1) `whole_page` | |
| - Files: `whole_page/final_model.pt`, `results.json`, `confusion_matrix.png`, `confusion_matrix.csv`, `splits.json` | |
| - Test (image-level) macro-F1: **0.5124** | |
| - Test accuracy: **0.5711** | |
| ### 2) `patches_color` | |
| - Files: `patches_color/final_model.pt`, `results.json`, `confusion_matrix.png`, `checkpoint_page_eval.json`, `splits.json` | |
| - Test (patch-level) macro-F1: **0.4899** | |
| - Re-eval **page-level** macro-F1 for shipped `final_model.pt` (`checkpoint_page_eval.json`): **0.5017** | |
| - Best **page-level** macro-F1 among stage checkpoints on the same grid: **0.5043** (**Stage A**) | |
| ### 3) `patches_clahe` | |
| - Files: `patches_clahe/final_model.pt`, `results.json`, `confusion_matrix.png`, `checkpoint_page_eval.json`, `splits.json` | |
| - Test (patch-level) macro-F1: **0.4911** | |
| - Re-eval page-level macro-F1 for shipped `final_model.pt`: **0.5261** | |
| - Best **page-level** macro-F1 among stage checkpoints: **0.529** (**Stage B**) | |
| ## Which stage produced which checkpoint? | |
| - **`final_model.pt` in each folder** is the stage with the highest **validation macro-F1** among `best_stage_*.pt` checkpoints (see `best_val_checkpoint` in each `results.json`): **Stage B** for `whole_page`, **Stage C** for both `patches_color` and `patches_clahe`. | |
| - For **page-level** quality on the patch runs, the best single stage on the re-eval grid differs: **Stage A** (`patches_color`) and **Stage B** (`patches_clahe`) beat their respective `final_model.pt` page scores—use `checkpoint_page_eval.json` if you want to deploy a stage checkpoint instead of the val-selected default. | |
| ## Which experiment won? | |
| CLAHE patches achieved the highest **page-level** macro-F1 (**0.529** on the best stage checkpoint), while **whole page** achieved the best **image-level** macro-F1 (**0.512**). **Whole page** is recommended for production due to simpler inference. | |
| ## How To Load a Checkpoint | |
| ```python | |
| import torch | |
| from pathlib import Path | |
| from finetune_dinov3 import DINOv3Classifier, DINOV3_MODEL_ID | |
| ckpt_path = Path("whole_page/final_model.pt") | |
| payload = torch.load(ckpt_path, map_location="cpu") | |
| label_to_idx = payload["label_to_idx"] | |
| idx_to_label = {v: k for k, v in label_to_idx.items()} | |
| num_classes = len(label_to_idx) | |
| model = DINOv3Classifier(DINOV3_MODEL_ID, num_classes) | |
| model.load_state_dict(payload["model_state_dict"]) | |
| model.eval() | |
| ``` | |
| ## Inference (Single Image) | |
| ```python | |
| import torch | |
| from PIL import Image | |
| from transformers import AutoImageProcessor | |
| processor = AutoImageProcessor.from_pretrained(DINOV3_MODEL_ID) | |
| img = Image.open("example.png").convert("RGB") | |
| inputs = processor(images=img, return_tensors="pt") | |
| with torch.no_grad(): | |
| logits = model(inputs["pixel_values"]) | |
| probs = torch.softmax(logits, dim=1)[0].cpu().numpy() | |
| pred_idx = int(probs.argmax()) | |
| pred_label = idx_to_label[pred_idx] | |
| print(pred_label, float(probs[pred_idx])) | |
| ``` | |
| ## Page-Level Inference (Patch Aggregation) | |
| For patch experiments (`patches_color`, `patches_clahe`), aggregate by page stem: | |
| 1. group patch probabilities by page ID (strip `_pN` suffix), | |
| 2. average probabilities per page, | |
| 3. take `argmax` of averaged probabilities. | |
| This is the same logic used in the re-evaluation script output (`checkpoint_page_eval.json`). | |
| ## Known Limitations | |
| - Class imbalance is high (for example `petsuk` and `uchen_sugdring` dominate, while `trinyig` is small). | |
| - Results can vary by preprocessing variant and by patch vs page-level evaluation protocol. | |
| - Patch-level metrics and page-level metrics are not directly interchangeable. | |
| - The model expects Tibetan manuscript-style inputs; performance can drop on out-of-domain scans or mixed/noisy pages. | |
| - Checkpoints are tied to the exact label mapping saved in each payload (`label_to_idx`). | |
| ## Reproducibility Notes | |
| - Exclusion manifest support is enabled in training (`benchmark_page_ids.json`). | |
| - Full training code used for these artifacts is included at `finetune_dinov3.py`. | |