File size: 7,049 Bytes
4c70b8d
564e314
 
4c70b8d
564e314
 
 
 
 
 
 
 
 
 
 
 
4c70b8d
564e314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49936a0
 
 
 
 
564e314
 
 
49936a0
 
 
 
564e314
 
 
49936a0
 
564e314
 
 
 
 
49936a0
564e314
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
---
language:
  - uk
license: agpl-3.0
datasets:
  - UkrainianCatholicUniversity/rukopys
base_model:
  - Kansallisarkisto/cyrillic-htr-model
tags:
  - optical-character-recognition
  - handwriting-recognition
  - document-ai
  - object-detection
  - rtdetr
  - trocr
  - ukrainian
---

# Mark One OCR Weights

This repository contains the custom inference weights for **Mark One OCR**, the
second-place solution in the final of the
[Handwritten to Data challenge](https://www.kaggle.com/competitions/handwritten-to-data).
The models detect, classify, and transcribe regions in Ukrainian handwritten
documents. They were trained or fine-tuned on the open
[RUKOPYS dataset](https://huggingface.co/datasets/UkrainianCatholicUniversity/rukopys).

The complete, supported inference pipeline—including detector ensembling,
geometry correction, TrOCR, optional Qwen page-context correction, final
post-processing, CLI, Docker, and the web app—is available at
[Mark One OCR](https://github.com/Isterikus/markone-ocr).

## Repository contents

| Path | Purpose |
|---|---|
| `detector_old.pt` | First fine-tuned Ultralytics RT-DETR-L checkpoint |
| `detector_new.pt` | Complementary fine-tuned RT-DETR-L checkpoint |
| `recognizer/` | Fine-tuned TrOCR `VisionEncoderDecoderModel` inference files |
| `recognizer_processor/` | Matching TrOCR image processor and tokenizer |
| `warp_selector.pkl` | LightGBM gate used by the conservative UVDoc geometry stage |
| `SHA256SUMS` | Checksums for every inference artifact |

The detector class order is:

```text
0 handwritten, 1 printed, 2 formula, 3 table,
4 annotation, 5 image, 6 graph
```

The two detector checkpoints are an ensemble. Running either checkpoint alone
is useful for inspection, but does not reproduce the challenge pipeline.
This is a multi-component weight bundle, so the repository root is not intended
to be loaded by a single `from_pretrained()` call or Hugging Face inference
widget. Load a component as shown below or use the complete pipeline.

## Use the complete pipeline

```bash
git clone https://github.com/Isterikus/markone-ocr
cd markone-ocr
python -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[all]'
python scripts/download_models.py

# Base detector + geometry + greedy TrOCR inference
markone-ocr infer /path/to/pages --output outputs/base

# Optional page-context correction through a running Ollama service
markone-ocr infer /path/to/pages \
  --enhance --backend ollama --model qwen3.6:latest \
  --output outputs/enhanced
```

See the GitHub README for Ollama, vLLM, Docker, web app, batch, and low-VRAM
profiles. Qwen and UVDoc weights are external dependencies and are not included
in this repository.

## Load the recognizer by itself

The recognizer expects a **cropped text region**, not a full document page.
Greedy decoding (`num_beams=1`) is the supported configuration.

```python
from pathlib import Path

import torch
from huggingface_hub import snapshot_download
from PIL import Image
from transformers import TrOCRProcessor, VisionEncoderDecoderModel

repo_id = "dkovalen/markone-ocr-weights"
root = Path(
    snapshot_download(
        repo_id,
        allow_patterns=["recognizer/*", "recognizer_processor/*"],
    )
)

processor = TrOCRProcessor.from_pretrained(root / "recognizer_processor")
model = VisionEncoderDecoderModel.from_pretrained(root / "recognizer").to("cuda").eval()

crop = Image.open("text-crop.png").convert("RGB")
pixel_values = processor(images=crop, return_tensors="pt").pixel_values.to("cuda")
with torch.inference_mode():
    token_ids = model.generate(pixel_values, num_beams=1, max_new_tokens=128)

print(processor.batch_decode(token_ids, skip_special_tokens=True)[0])
```

## Load a detector checkpoint by itself

```python
from huggingface_hub import hf_hub_download
from ultralytics import RTDETR

repo_id = "dkovalen/markone-ocr-weights"
checkpoint = hf_hub_download(repo_id, filename="detector_old.pt")
detector = RTDETR(checkpoint)
results = detector.predict("page.jpg", imgsz=1536, device=0)

for result in results:
    print(result.boxes.xyxy, result.boxes.cls, result.boxes.conf)
```

This example is raw single-checkpoint inference. Use the full Mark One pipeline
for its two-checkpoint, full-page plus sliding-window ensemble, class-aware
filters, geometry safeguards, coordinate mapping, and final post-processing.

## Evaluation

The competition metric was:

```text
score = 0.15 × Detection F1
      + 0.05 × Classification Accuracy
      + 0.30 × (1 − Region CER)
      + 0.50 × (1 − Page CER)
```

| Full pipeline configuration | Public test composite |
|---|---:|
| RT-DETR ensemble + geometry + greedy TrOCR | 0.862 |
| + Qwen3.5 9B | 0.896 |
| + Qwen3.6 35B-A3B | **0.905** |
| + Qwen3.5 122B-A10B | 0.914 |

These are results from the challenge test distribution. They are not guaranteed
for unrelated document collections. The Qwen models in the enhanced rows are
served separately and are not part of this weight bundle.

## Intended use and limitations

The release is intended for research, evaluation, and OCR applications involving
Ukrainian handwritten documents. It is especially suited to pages structurally
similar to RUKOPYS.

- The recognizer is specialized for Ukrainian Cyrillic handwriting and may
  perform poorly on other languages, scripts, layouts, or modern camera photos.
- Large tables are a weak point for the base TrOCR-only profile.
- Handwriting is ambiguous. Human review is required for legal, medical,
  identity, archival, or other high-impact uses.
- A Qwen enhancer may replace an incorrect transcription with plausible but
  still incorrect text.
- `warp_selector.pkl` is a Python pickle. Load it only from this trusted,
  checksum-verified repository; pickle files can execute code when deserialized.

## Training data and upstream models

- Dataset: [UkrainianCatholicUniversity/rukopys](https://huggingface.co/datasets/UkrainianCatholicUniversity/rukopys),
  licensed CC-BY-4.0.
- Recognizer base: [Kansallisarkisto/cyrillic-htr-model](https://huggingface.co/Kansallisarkisto/cyrillic-htr-model),
  licensed Apache-2.0.
- Detector architecture/runtime: Ultralytics RT-DETR-L.

The compact training path and recorded profiles for both released RT-DETR
checkpoints are available in
[`training/detectors`](https://github.com/Isterikus/markone-ocr/tree/main/training/detectors).
Data-curation experiments are kept separate from the model trainer. See
`THIRD_PARTY_NOTICES.md` for dependency attribution.

## License

The Mark One code and custom weights are released under **GNU AGPL-3.0-only**
because the detector pipeline uses Ultralytics RT-DETR under AGPL-3.0.
Third-party components retain their respective licenses; see `LICENSE` and
`THIRD_PARTY_NOTICES.md` for details.

## Citation

If you use Mark One OCR in research, you can cite the software as:

```bibtex
@misc{markoneocr2026,
  title        = {Mark One OCR},
  author       = {Mark One},
  year         = {2026},
  url          = {https://github.com/Isterikus/markone-ocr}
}
```