File size: 10,789 Bytes
fbcc547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
---
license: apache-2.0
base_model:
  - PaddlePaddle/PP-OCRv6_tiny_det_onnx
  - PaddlePaddle/PP-OCRv6_tiny_rec_onnx
pipeline_tag: image-to-text
converted_by: Lucie666
language:
  - multilingual
tags:
  - burn
  - burnpack
  - rust
  - ocr
  - paddleocr
  - onnx
  - format-conversion
library_name: burn
---

# ppocrv6-tiny-burnpack

**Original models:** https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_det_onnx and
https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_rec_onnx
**Original authors:** the PaddleOCR team at Baidu β€” PP-OCRv6 (2026), the tiny tier of a
three-tier OCR family (1.5 M to 34.5 M parameters): a DBNet text detector (PPLCNetV4
backbone, RepLKFPN neck) and a CTC text recogniser (PPLCNetV4, 6 904-character
dictionary, 49 languages)
**Converted by:** [Lucie666](https://huggingface.co/Lucie666), using `burn-onnx` β€” format only

---

**This is not an original model, and no part of it is my work.** It is a mechanical format
conversion of
[PaddlePaddle/PP-OCRv6_tiny_det_onnx](https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_det_onnx)
and
[PaddlePaddle/PP-OCRv6_tiny_rec_onnx](https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_rec_onnx)
β€” nothing was trained, fine-tuned, distilled, quantised or modified. No new weights were
produced. All credit belongs to the authors above.

If you are looking for the models themselves, go to the two PaddlePaddle repositories
above. This repository only exists so people running [Burn](https://burn.dev) don't each
redo the conversion.

> Note on metadata: Hugging Face's `base_model_relation` field only accepts `adapter`,
> `merge`, `quantized` or `finetune`. None describes a pure format conversion, so the
> field is deliberately left unset rather than filled with an inaccurate value β€” these
> weights are **not** quantised, they are the original f32 values.

The files `det.bpk` and `rec.bpk` hold the *same* weights as the upstream `inference.onnx`
files, re-serialised into Burn's `burnpack` format so they can be loaded by a pure-Rust
inference stack β€” no Python, no Paddle, no ONNX Runtime at inference time. `dict.txt` is
the recogniser's character list, taken verbatim from the upstream `inference.yml`.

## Why this exists

A complete, embeddable OCR in 6 MB: 1.7 MB of detector and 4.4 MB of recogniser, no
tokenizer, Latin script with accents and CJK in the same dictionary. It is the OCR of
[rag3weaver](https://github.com/L-Defraiteur/rag3db) (`src/burn_ppocr.rs`, feature
`burn-ocr`): images go in, lines of text with boxes and confidences come out, on the same
Burn/wgpu stack as its embedders and rerankers.

## Provenance

```
PaddlePaddle/PP-OCRv6_tiny_det_onnx   inference.onnx   (opset 14, 1 780 590 bytes)
        β”‚
        β”‚  fix_onnx.py   (3 nodes: auto_pad SAME_UPPER β†’ explicit pads, no weight touched)
        β–Ό
det_pads.onnx
        β”‚
        β”‚  burn-onnx 0.22.0-pre.1   (mechanical ONNX β†’ Burn conversion, LoadStrategy::Bytes)
        β–Ό
det.bpk          weights, burnpack format
det.rs           model graph, generated Rust source (not distributed here)

PaddlePaddle/PP-OCRv6_tiny_rec_onnx   inference.onnx   (opset 11, 4 462 639 bytes)
        β”‚                                               = rec_pads.onnx, byte for byte
        β”‚  burn-onnx 0.22.0-pre.1
        β–Ό
rec.bpk          weights, burnpack format
rec.rs           model graph, generated Rust source (not distributed here)
        +
dict.txt         PostProcess.character_dict of rec_inference.yml, one entry per line
```

Nothing in this pipeline is hand-written, except the three-attribute patch below.

**Why the detector needed a patch.** Two `Conv` and one `MaxPool` (2Γ—2 kernel, stride 1)
in the detector are declared with `auto_pad = SAME_UPPER`. `burn-onnx 0.22.0-pre.1`
rejects that form when the input has dynamic dimensions (`auto_pad SAME_UPPER/SAME_LOWER
requires static input shape … Use explicit pads instead`). `fix_onnx.py` rewrites those
three nodes as `pads = [0, 0, 1, 1]` β€” the exact padding `SAME_UPPER` means for a
2Γ—2/stride-1 kernel (total `k βˆ’ 1 = 1`, placed bottom/right). Same arithmetic, same
weights, and height/width stay dynamic. The recogniser needed nothing: `rec_pads.onnx` is
the upstream file unchanged (same sha256).

**A caveat before you regenerate.** Burnpack serialisation is *not* byte-deterministic:
two builds from the same ONNX produce files of identical size but different bytes. The
tensor *values* are unaffected. The checksums below verify **this download**, not a
reproduction.

## Reproducing it

```bash
pip install onnx                      # only `onnx`, no runtime
python fix_onnx.py inference.onnx det_pads.onnx pads     # detector only
```

```rust
// build.rs
use burn_onnx::{LoadStrategy, ModelGen};

fn main() {
    ModelGen::new()
        .input("det_pads.onnx")           // patched PaddlePaddle/PP-OCRv6_tiny_det_onnx
        .out_dir("model/")
        .load_strategy(LoadStrategy::Bytes)
        .run_from_script();
    ModelGen::new()
        .input("rec_pads.onnx")           // = PaddlePaddle/PP-OCRv6_tiny_rec_onnx inference.onnx
        .out_dir("model/")
        .load_strategy(LoadStrategy::Bytes)
        .run_from_script();
}
```

Both models are far under the 2 GB ONNX protobuf limit. `0.22.0-pre.1` is used for
consistency with a `burn 0.22.0-pre.2` runtime: generated code and runtime must match
versions. The generated sources carry the absolute path of the input ONNX in their first
line; rag3weaver replaces it with the upstream repository name.

## Verifying the download

```
det.bpk          sha256  73a139fa82b9fc8f7c03b66ab3c3dc9e959e8c1f4d95b2da09b4e50529e76b04
                 size    1 737 476 bytes  (1.66 MiB)
rec.bpk          sha256  53bfcb22a068cc6991f2b8b3ba0782a1aac3c54c16895ae7138eb4e755169436
                 size    4 443 368 bytes  (4.24 MiB)
dict.txt         sha256  c5cbe34ef40c29c4df07ed012bf96569cb69a2d2a01a07027e9f13cb832bd9cd
                 size    27 156 bytes  (6 904 lines, UTF-8)
det_pads.onnx    sha256  f74ec758df06b1f77cde82a44bc840cbb39f8b7cf1573f373ea052a1e8d93ae6
                 size    1 780 566 bytes
rec_pads.onnx    sha256  9ef676d6ed3c88256a2d92c640c44f25b0c40947e111b14b8be8f594091563e6
                 size    4 462 639 bytes  (identical to upstream inference.onnx)
```

`SHA256SUMS` covers every file in the repository except this card; `sha256sum -c SHA256SUMS` checks them all.

## Model interface

Both graphs are backend-agnostic Burn modules (`burn 0.22`: `Tensor<D>` on the crate's
default backend, `Model::from_bytes(bytes, &device)` loads a burnpack).

```rust
// det.bpk β€” PP-OCRv6_tiny_det
pub fn forward(&self, x: Tensor<4>) -> Tensor<4>   // [B, 3, H, W] β†’ [B, 1, H, W], sigmoid
// rec.bpk β€” PP-OCRv6_tiny_rec
pub fn forward(&self, x: Tensor<4>) -> Tensor<3>   // [B, 3, 48, W] β†’ [B, W/8, 6906], softmax
```

`B`, `H`, `W` are dynamic at run time (checked on 320Γ—320, 224Γ—416 and 2464Γ—736 for the
detector; `W` = 160/320/640 and `B` = 1/2 for the recogniser). The recogniser's 6 906
classes are: index `0` = CTC blank, `1..=6904` = line `i` of `dict.txt`, `6905` = space.

## Pre- and post-processing

Not included in the graphs β€” this is what PaddleOCR does around them (from the upstream
`inference.yml` files, shipped here as `det_inference.yml` / `rec_inference.yml`, and
PaddleOCR's `operators.py`, `predict_rec.py`, `db_postprocess.py`, `rec_postprocess.py`):

* **Detector.** Resize so the *shorter* side is at least 736 (`limit_type min`, cap 4000
  on the longer side), then round height and width to a multiple of 32. Normalise
  `x/255` with mean `[0.485, 0.456, 0.406]`, std `[0.229, 0.224, 0.225]` applied to the
  channels **in BGR order** (PaddleOCR decodes with OpenCV and never swaps). CHW.
  Post-processing (`DBPostProcess`): binarise at `thresh 0.2`, contours β†’ minimum-area
  boxes, drop sides < 3 px, score = mean probability inside the box β‰₯ `box_thresh 0.4`,
  unclip by `d = area Γ— 1.4 / perimeter`, drop sides < 5 px, `max_candidates 3000`,
  scale back to the source image.
* **Recogniser.** Crop each box (rotate 90Β° if height/width β‰₯ 1.5), resize to height 48
  and width `ceil(48 Β· w/h)`, capped at `48 Γ— max(320/48, max w/h of the batch)`;
  `(x/255 βˆ’ 0.5) / 0.5` in BGR, zero-pad on the right, batches of 6 sorted by aspect
  ratio. Greedy CTC: argmax per step, merge consecutive repeats, drop blanks; confidence
  = mean probability of the kept characters.

rag3weaver's `src/burn_ppocr.rs` reimplements all of this in Rust (with axis-aligned
boxes instead of `minAreaRect`).

## Numerical parity

Checked against **onnxruntime 1.29** (CPU) running the very same ONNX files, fed with the
same pre-processed input tensors (so only the networks are compared), on a 400Γ—120 test
image with two lines of text (detector input `[1, 3, 736, 2464]`, recogniser input
`[2, 3, 48, 320]`):

```
                          max|Ξ”|     mean|Ξ”|     values with |Ξ”| > 1e-3
det map  [1, 1, 736, 2464]   1.81e-3    1.77e-6     87 of 1 813 504
rec probs [2, 40, 6906]      1.44e-5    9.2e-11     0
```

The 87 detector pixels sit on glyph edges, where the sigmoid is steep on a 6Γ— upscaled
image. Burn's ndarray backend shows the same gap against onnxruntime (1.89e-3) while
wgpu and ndarray agree to 9.7e-5: it is f32 accumulation noise from a different operation
order, not a backend defect. Boxes are identical, and both stacks decode the same text
with the same confidences β€” `"Hello rag3weaver"` (0.987) and `"OCR 2026"` (0.984).
`ppocr_ref.py` is the oracle script.

Runtime backend for this check: Burn + wgpu/Vulkan on an AMD Radeon AI PRO R9700
(Navi 48, RDNA4, gfx1201) via RADV.

## License

Apache-2.0, inherited from
[PaddlePaddle/PP-OCRv6_tiny_det_onnx](https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_det_onnx)
and
[PaddlePaddle/PP-OCRv6_tiny_rec_onnx](https://huggingface.co/PaddlePaddle/PP-OCRv6_tiny_rec_onnx)
([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR), Apache-2.0). All credit for the
models belongs to their authors:

```bibtex
@misc{zhang2026ppocrv6,
  title={PP-OCRv6: From 1.5M to 34.5M Parameters, Surpassing Billion-Scale VLMs on OCR Tasks},
  author={Yubo Zhang and Xueqing Wang and Manhui Lin and Yue Zhang and Penglongyi Deng and Ting Sun and Tingquan Gao and Zelun Zhang and Jiaxuan Liu and Changda Zhou and Hongen Liu and Suyin Liang and Cheng Cui and Yi Liu and Dianhai Yu and Yanjun Ma},
  year={2026},
  eprint={2606.13108},
  archivePrefix={arXiv},
  primaryClass={cs.CV},
  url={https://arxiv.org/abs/2606.13108},
}

@misc{paddleocr,
  title={PaddleOCR: Awesome multilingual OCR and Document Parsing toolkits based on PaddlePaddle},
  author={{PaddlePaddle Authors}},
  howpublished={\url{https://github.com/PaddlePaddle/PaddleOCR}},
  year={2020}
}
```