Views
No views yet
handwritten, printed and annotation region crops from Rukopys.| value | |
|---|---|
| Architecture | CRNN: 6-layer CNN → BiLSTM (hidden 256) → linear CTC head |
| Parameters | ~6.25M |
| Trained | from scratch (no pretrained backbone), silver→gold curriculum |
| Alphabet | 340 characters + CTC blank |
| Handles | handwritten, printed, annotation |
| Gold-val CER / WER | 0.1539 / 0.4188 |
| Input | a single grayscale line crop, height 32 px, width ≤ 512 px, scaled to [-1, 1] |
| Output | the transcribed string (metric-normalized character set) |
Hukyl/trocr-large-rukopys.2,2 · 2,2 · (2,1) · (2,1) so
width resolution is preserved for the sequence).Linear(512 → 256) → log-softmax → CTC (blank index 0).і ї є ґ ў ѣ), Latin, digits, punctuation, and a long tail of typographic /
mathematical symbols (–—«»“”№∑∫≈≤≥… etc.).[-1, 1].cv2.INTER_AREA — training used it, and INTER_LINEAR
aliases on the heavy downscale to 32 px height.1import cv2
2import torch
3import torch.nn as nn
4from huggingface_hub import hf_hub_download
5
6IMG_HEIGHT, IMG_MAX_WIDTH = 32, 512
7
8
9class CRNN(nn.Module):
10 def __init__(self, num_classes: int, rnn_hidden: int = 256) -> None:
11 super().__init__()
12 self.cnn = nn.Sequential(
13 nn.Conv2d(1, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2),
14 nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2),
15 nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(),
16 nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)),
17 nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(),
18 nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)),
19 )
20 self.rnn = nn.LSTM(512, rnn_hidden, bidirectional=True, batch_first=False)
21 self.fc = nn.Linear(rnn_hidden * 2, num_classes)
22
23 def forward(self, x):
24 feat = self.cnn(x)
25 _b, _c, h, _w = feat.shape
26 feat = feat.squeeze(2) if h == 1 else feat.mean(dim=2)
27 feat = feat.permute(2, 0, 1)
28 out, _ = self.rnn(feat)
29 return torch.log_softmax(self.fc(out), dim=-1)
30
31
32ckpt = torch.load(hf_hub_download("Hukyl/crnn-rukopys", "best.pt"), weights_only=False)
33chars = ckpt["chars"]
34model = CRNN(len(chars), ckpt["config"]["rnn_hidden"])
35model.load_state_dict(ckpt["model_state"])
36model.eval()
37
38gray = cv2.imread("line_crop.jpg", cv2.IMREAD_GRAYSCALE)
39new_w = max(4, min(int(gray.shape[1] * IMG_HEIGHT / gray.shape[0]), IMG_MAX_WIDTH))
40resized = cv2.resize(gray, (new_w, IMG_HEIGHT), interpolation=cv2.INTER_AREA)
41tensor = (torch.from_numpy(resized).float().unsqueeze(0) / 127.5 - 1.0).unsqueeze(0)
42
43with torch.inference_mode():
44 log_probs = model(tensor)[:, 0, :]
45
46# greedy CTC decode (collapse repeats, drop blank=0)
47idx, prev, out = log_probs.argmax(-1).tolist(), None, []
48for i in idx:
49 if i != prev and i != 0:
50 out.append(i)
51 prev = i
52print("".join(chars[i] for i in out))blank=0,
zero_infinity), AdamW, OneCycleLR schedule, online augmentation, best checkpoint
selected by lowest validation CER.| hyperparameter | value |
|---|---|
| epochs | 20 (silver) + 30 (gold), early stopping with patience 5 |
| batch size | 128 |
| learning rate | 1e-3 |
| weight decay | 1e-4 |
| optimizer / schedule | AdamW, OneCycleLR |
| gradient clipping | 5.0 |
| loss | CTC (blank=0, zero_infinity) |
| online augmentation | on (default profile, at working resolution) |
| eval | greedy CTC decode, full val each epoch |
| precision / device | fp16 mixed precision, CUDA |
| seed | 42 |
INTER_AREA to a 64 px working height (2× the model's 32 px
input), augmented, then resized to the final 32 px tensor. Only training crops were
augmented (no augmented validation was measured).| region type | geometric (one) | photometric (one or two) |
|---|---|---|
| handwritten, annotation | margin pad 2–15% / trim 1–5%, rotation ±1–5°, elastic distortion (α=25, σ=5), baseline warp (amp 2–8 px, freq 0.5–2.0) | paper-colour shift (LAB a±10 / b±15), Gaussian noise (σ 5–15), JPEG recompression (q 30–65), contrast/gamma (0.7–1.3 / 0.6–1.5), morphological erode/dilate (kernel 2) |
| printed | margin pad 2–15% / trim 1–5%, rotation ±1–3° | paper-colour shift (a±5 / b±10), Gaussian noise (σ 3–10), JPEG recompression (q 40–70), contrast/gamma (0.8–1.2 / 0.8–1.3), morphological erode/dilate (kernel 2) |
INTER_AREA resize (training
parity). Labels are metric-normalized so CER/WER reflect the scored character set.| metric | value |
|---|---|
| CER | 0.1539 |
| WER | 0.4188 |
| exact-match accuracy | 0.2706 |
| n_samples | 3,345 |
| class | n | CER | WER | accuracy |
|---|---|---|---|---|
| handwritten | 3,223 | 0.1507 | 0.4132 | 0.2696 |
| annotation | 79 | 0.4496 | 0.7578 | 0.3924 |
| printed | 43 | 0.2716 | 0.6267 | 0.1163 |
printed/annotation n is quite small, so measuring CER against them is quite noisy.| file | description |
|---|---|
best.pt | lowest-val-CER checkpoint (gold fine-tune, epoch 29) |
.pt payload: model weights, the alphabet, the
architecture config (rnn_hidden, img_height, img_max_width), and the
training-time metrics.annotation and printed classes have small training support and higher CER;
formula and table are unsupported.| dataset | source | license | role |
|---|---|---|---|
| Rukopys | UkrainianCatholicUniversity/rukopys | CC BY 4.0 | silver pretrain (auto-labeled) + gold fine-tune |