Downloads · 30 days
26
44% of all-time downloads
Hukyl/crnn-rukopys
crnn-rukopys is a image-to-text model from Hukyl. Use it when you need a caption or text from an image. It is set up for pytorch. The card lists the license as apache-2.0.
A compact CRNN + CTC line recognizer for handwritten Ukrainian document text, trained from scratch on the Rukopys dataset. At ~6.25M parameters with greedy CTC decode it is CPU-friendly — a lightweight alternative to…
Downloads · 30 days
26
44% of all-time downloads
All-time downloads
59
Public
Repo size
50.1 MB
Likes
0
Public
Click a slice to open those files.
.pt25 MB · 100%
From the Hugging Face model README
A compact CRNN + CTC line recognizer for handwritten Ukrainian document text, trained from scratch on the Rukopys dataset. At ~6.25M parameters with greedy CTC decode it is CPU-friendly — a lightweight alternative to transformer recognizers.
The training data included 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) |
Recognizing cropped line/region images from handwritten Ukrainian documents, downstream of a layout detector — where a small, fast, CPU-capable recognizer is preferred over a transformer one.
For stronger recognition quality at ~90× the parameter count, see
Hukyl/trocr-large-rukopys.
CNN feature extractor → BiLSTM sequence model → linear CTC head. No pretrained backbone — trained end-to-end on Rukopys crops.
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].Note: resize crops with cv2.INTER_AREA — training used it, and INTER_LINEAR
aliases on the heavy downscale to 32 px height.
import cv2
import torch
import torch.nn as nn
from huggingface_hub import hf_hub_download
IMG_HEIGHT, IMG_MAX_WIDTH = 32, 512
class CRNN(nn.Module):
def __init__(self, num_classes: int, rnn_hidden: int = 256) -> None:
super().__init__()
self.cnn = nn.Sequential(
nn.Conv2d(1, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2),
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2, 2),
nn.Conv2d(128, 256, 3, padding=1), nn.BatchNorm2d(256), nn.ReLU(),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)),
nn.Conv2d(256, 512, 3, padding=1), nn.BatchNorm2d(512), nn.ReLU(),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(), nn.MaxPool2d((2, 1)),
)
self.rnn = nn.LSTM(512, rnn_hidden, bidirectional=True, batch_first=False)
self.fc = nn.Linear(rnn_hidden * 2, num_classes)
def forward(self, x):
feat = self.cnn(x)
_b, _c, h, _w = feat.shape
feat = feat.squeeze(2) if h == 1 else feat.mean(dim=2)
feat = feat.permute(2, 0, 1)
out, _ = self.rnn(feat)
return torch.log_softmax(self.fc(out), dim=-1)
ckpt = torch.load(hf_hub_download("Hukyl/crnn-rukopys", "best.pt"), weights_only=False)
chars = ckpt["chars"]
model = CRNN(len(chars), ckpt["config"]["rnn_hidden"])
model.load_state_dict(ckpt["model_state"])
model.eval()
gray = cv2.imread("line_crop.jpg", cv2.IMREAD_GRAYSCALE)
new_w = max(4, min(int(gray.shape[1] * IMG_HEIGHT / gray.shape[0]), IMG_MAX_WIDTH))
resized = cv2.resize(gray, (new_w, IMG_HEIGHT), interpolation=cv2.INTER_AREA)
tensor = (torch.from_numpy(resized).float().unsqueeze(0) / 127.5 - 1.0).unsqueeze(0)
with torch.inference_mode():
log_probs = model(tensor)[:, 0, :]
# greedy CTC decode (collapse repeats, drop blank=0)
idx, prev, out = log_probs.argmax(-1).tolist(), None, []
for i in idx:
if i != prev and i != 0:
out.append(i)
prev = i
print("".join(chars[i] for i in out))
The model expects single-line / single-region crops, not full pages.
Two-stage silver→gold curriculum, trained from scratch. CTC loss (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 |
Online augmentation was applied during training, at working resolution: each crop was
first downscaled with 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).
Each crop was transformed once per epoch by one geometric + one or two photometric operations at random. Each class received a separate augmentation profile that was selected to minimize the distribution shift.
| 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) |
These simulate scanner/paper variation, ink thinning/bleed, and natural handwriting deformation, widening the range of appearances beyond the raw training crops.
Protocol: full held-out gold validation split filtered to the model's classes
(3,345 crops, no subsampling), greedy CTC decode, 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 |
We also acknowledge that 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) |
The checkpoint is a self-contained .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 |