inviolus commited on
Commit
ef9f248
·
verified ·
1 Parent(s): 8d5da39

InviolusOCR 2026-10-31: conv CTC model, int8 keeps accuracy

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ inviolusocr.keras filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-sa-4.0
3
+ language:
4
+ - tr
5
+ library_name: keras
6
+ pipeline_tag: image-to-text
7
+ tags:
8
+ - ocr
9
+ - text-recognition
10
+ - turkish
11
+ - tinyml
12
+ - tflite
13
+ - int8
14
+ - ctc
15
+ - microcontroller
16
+ ---
17
+
18
+ # InviolusOCR: tiny Turkish line OCR for microcontrollers
19
+
20
+ A compact convolutional CTC recognizer for **one cropped line of printed
21
+ Turkish text**: 560,954 parameters and a **598 KB full-int8 TensorFlow Lite**
22
+ file that keeps almost all of the float model's accuracy.
23
+
24
+ > **Türkçe özet:** Mikrodenetleyiciler için tek satır Türkçe metin okuyan,
25
+ > 561 bin parametreli, int8 TFLite dosyası 598 KB olan küçük bir OCR modeli.
26
+ > int8 hâli, ayrılmış test satırlarının %78,4'ünü ve gerçek sahne metni
27
+ > (TS-TR) test kırpıntılarının %58,2'sini harfi harfine doğru okuyor. Ticari
28
+ > olmayan kullanım içindir (CC BY-NC-SA 4.0).
29
+
30
+ This replaces [inviolus/erdem-ocr-tr-tinyml](https://huggingface.co/inviolus/erdem-ocr-tr-tinyml)
31
+ (2026-10-09), whose int8 file lost most of its accuracy.
32
+
33
+ ## Files
34
+
35
+ | File | What it is |
36
+ |---|---|
37
+ | `inviolusocr.keras` | Float model, input `(batch, 32, 160, 1)`, output `(batch, 40, 90)` softmax |
38
+ | `inviolusocr_int8.tflite` | Full-int8 export, input `[1, 32, 160, 1]` int8 (scale 0.007843, zero point -1), output `[1, 40, 90]` int8 (scale 0.00390625, zero point -128) |
39
+ | `alphabet.json` | Character set used to decode the output |
40
+ | `benchmark.json` | All scores below, for this model and the earlier versions |
41
+
42
+ ## Model
43
+
44
+ - **Input:** one horizontal text line, grayscale, resized to 160×32, scaled
45
+ to `[-1, 1]` (`pixel / 127.5 - 1`).
46
+ - **Architecture:** 4 conv + batch-norm + pool blocks, a dense projection,
47
+ then four residual dilated 1-D convolutions (dilation 1, 2, 4, 1) instead of
48
+ an LSTM, and a dense softmax. Only ops that quantize cleanly to int8.
49
+ - **Output:** 40 timesteps × 90 classes. Class `0` is padding, `1..88` are the
50
+ characters of `alphabet.json` in order, `89` is the CTC blank.
51
+ - **Character set:** digits, ASCII letters, `ç ğ ı İ ö ş ü Ç Ğ Ö Ş Ü`, space
52
+ and `.,:;!?-_/()'%`. Up to 24 characters per line.
53
+ - **Not included:** text detection. The line must already be cropped.
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ import json
59
+ import numpy as np
60
+ import tensorflow as tf
61
+
62
+ alphabet = json.load(open("alphabet.json", encoding="utf-8"))["alphabet"]
63
+ BLANK = len(alphabet) + 1
64
+
65
+ def preprocess(path):
66
+ image = tf.io.decode_image(tf.io.read_file(path), channels=1, expand_animations=False)
67
+ image = tf.image.resize(image, [32, 160])
68
+ return (tf.cast(image, tf.float32) / 127.5 - 1.0).numpy()
69
+
70
+ def decode(ids):
71
+ text, previous = [], None
72
+ for i in ids:
73
+ if i != previous and 0 < i < BLANK:
74
+ text.append(alphabet[i - 1])
75
+ previous = i
76
+ return "".join(text)
77
+
78
+ # int8 TFLite (the file meant for devices)
79
+ interpreter = tf.lite.Interpreter(model_path="inviolusocr_int8.tflite")
80
+ interpreter.allocate_tensors()
81
+ inp, out = interpreter.get_input_details()[0], interpreter.get_output_details()[0]
82
+ scale, zero = inp["quantization"]
83
+ x = np.clip(np.round(preprocess("line.png") / scale + zero), -128, 127).astype(np.int8)
84
+ interpreter.set_tensor(inp["index"], x[np.newaxis])
85
+ interpreter.invoke()
86
+ print(decode(interpreter.get_tensor(out["index"])[0].argmax(-1)))
87
+ ```
88
+
89
+ ## Results
90
+
91
+ Exact-line accuracy (the whole line correct), all models scored on the same
92
+ rows: 12,093 held-out lines (synthetic, Zenodo, TS-TR, esengul3, STS-TR) and
93
+ the 2,078-crop official TS-TR test split (real scene text, never trained on).
94
+
95
+ ![Exact-line accuracy on held-out lines, float and int8](benchmark-1031-heldout-light.svg)
96
+
97
+ | Model | Params | int8 size | Held-out float | Held-out **int8** | TS-TR float | TS-TR **int8** |
98
+ |---|---:|---:|---:|---:|---:|---:|
99
+ | erdem-ocr 2026-10-09 (LSTM) | 631,226 | 665 KB | 58.8% | 32.9% | 45.3% | 33.5% |
100
+ | unpublished 2026-10-25 (LSTM) | 631,226 | 665 KB | 80.0% | 32.1% | 65.1% | 48.7% |
101
+ | **InviolusOCR 2026-10-31** | **560,954** | **598 KB** | 79.4% | **78.4%** | 60.0% | **58.2%** |
102
+
103
+ Character error rate of the int8 file: 9.1% held-out, 18.1% TS-TR.
104
+
105
+ The 2026-10-25 model is not published because it was trained with a dataset
106
+ that states no license.
107
+
108
+ ## Limitations
109
+
110
+ - Printed text only; no handwriting, curved text or full pages.
111
+ - Real scene text is still the weak spot (58% exact).
112
+ - **Licence plates are not read** (0% exact on 250 plate crops in a first,
113
+ unchecked test).
114
+ - Text detection and line cropping must be done before this model runs.
115
+
116
+ ## Training data
117
+
118
+ Trained for 200 minutes on CPU, starting from the 2026-10-09 weights, on
119
+ about 66,000 crops:
120
+
121
+ - Synthetic Turkish lines rendered from a 50k-word Turkish word list.
122
+ - [Turkish OCR Text Image Dataset](https://zenodo.org/records/21923181), Zenodo, CC BY 4.0.
123
+ - [Turkish Scene Text Recognition (TS-TR)](https://www.kaggle.com/datasets/serdaryildiz/turkish-scene-text-recognition-dataset), Serdar Yıldız, CC BY-NC 4.0.
124
+ - [Synthetic Turkish Scene Text (STS-TR)](https://www.kaggle.com/datasets/serdaryildiz/synthetic-turkish-scene-text-recognition-dataset), Serdar Yıldız, CC BY-NC 4.0.
125
+ - [Turkish Word OCR](https://huggingface.co/datasets/esengul3/turkish-word-ocr), esengul3, CC BY-SA 4.0.
126
+
127
+ ## License
128
+
129
+ Because of the non-commercial (TS-TR, STS-TR) and share-alike (esengul3)
130
+ terms of the training data, this model is released under
131
+ **[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)**:
132
+ non-commercial use only, credit this model and the datasets above, and share
133
+ adaptations under the same license.
134
+
135
+ Training code (MIT): [ErdemWilkinson/InviolusOCR](https://github.com/ErdemWilkinson/InviolusOCR).
alphabet.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "alphabet": "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ çğıİöşüÇĞÖŞÜ.,:;!?-_/()'%",
3
+ "blank_id": 89
4
+ }
benchmark-1031-heldout-light.svg ADDED
benchmark.json ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "old_2026-10-09": {
3
+ "params": 631226,
4
+ "tflite_bytes": 664648,
5
+ "tstr_test/float": {
6
+ "samples": 2078,
7
+ "exact": 0.45332050048123196,
8
+ "cer": 0.24171940494780966
9
+ },
10
+ "tstr_test/int8": {
11
+ "samples": 2078,
12
+ "exact": 0.33493743984600577,
13
+ "cer": 0.333771677488288
14
+ },
15
+ "heldout/float": {
16
+ "samples": 12093,
17
+ "exact": 0.5876953609526172,
18
+ "cer": 0.13569795363093237
19
+ },
20
+ "heldout/int8": {
21
+ "samples": 12093,
22
+ "exact": 0.3289506325973704,
23
+ "cer": 0.20805435130018604
24
+ },
25
+ "plates/float": {
26
+ "samples": 250,
27
+ "exact": 0.0,
28
+ "cer": 0.9095901985635826
29
+ },
30
+ "plates_nospace/float": {
31
+ "samples": 250,
32
+ "exact": 0.0,
33
+ "cer": 0.911134903640257
34
+ },
35
+ "plates/int8": {
36
+ "samples": 250,
37
+ "exact": 0.0,
38
+ "cer": 0.926489226869455
39
+ },
40
+ "plates_nospace/int8": {
41
+ "samples": 250,
42
+ "exact": 0.0,
43
+ "cer": 0.9443254817987152
44
+ }
45
+ },
46
+ "old_2026-10-10": {
47
+ "params": 631226,
48
+ "tflite_bytes": 664648,
49
+ "tstr_test/float": {
50
+ "samples": 2078,
51
+ "exact": 0.595283926852743,
52
+ "cer": 0.17489931782690885
53
+ },
54
+ "tstr_test/int8": {
55
+ "samples": 2078,
56
+ "exact": 0.4273339749759384,
57
+ "cer": 0.2717185830525191
58
+ },
59
+ "heldout/float": {
60
+ "samples": 12093,
61
+ "exact": 0.7716034069296287,
62
+ "cer": 0.09050901074405225
63
+ },
64
+ "heldout/int8": {
65
+ "samples": 12093,
66
+ "exact": 0.39518729843711237,
67
+ "cer": 0.1772891551454258
68
+ },
69
+ "plates/float": {
70
+ "samples": 250,
71
+ "exact": 0.0,
72
+ "cer": 0.8939585973806506
73
+ },
74
+ "plates_nospace/float": {
75
+ "samples": 250,
76
+ "exact": 0.0,
77
+ "cer": 0.8950749464668094
78
+ },
79
+ "plates/int8": {
80
+ "samples": 250,
81
+ "exact": 0.0,
82
+ "cer": 0.9142374313476975
83
+ },
84
+ "plates_nospace/int8": {
85
+ "samples": 250,
86
+ "exact": 0.0,
87
+ "cer": 0.917558886509636
88
+ }
89
+ },
90
+ "old_2026-10-25": {
91
+ "params": 631226,
92
+ "tflite_bytes": 664648,
93
+ "tstr_test/float": {
94
+ "samples": 2078,
95
+ "exact": 0.651106833493744,
96
+ "cer": 0.14843428947152132
97
+ },
98
+ "tstr_test/int8": {
99
+ "samples": 2078,
100
+ "exact": 0.4865255052935515,
101
+ "cer": 0.24623982904577957
102
+ },
103
+ "heldout/float": {
104
+ "samples": 12093,
105
+ "exact": 0.8003803853468949,
106
+ "cer": 0.07878971652420486
107
+ },
108
+ "heldout/int8": {
109
+ "samples": 12093,
110
+ "exact": 0.320929463325891,
111
+ "cer": 0.18703337950024995
112
+ },
113
+ "plates/float": {
114
+ "samples": 250,
115
+ "exact": 0.0,
116
+ "cer": 0.8821292775665399
117
+ },
118
+ "plates_nospace/float": {
119
+ "samples": 250,
120
+ "exact": 0.0,
121
+ "cer": 0.9250535331905781
122
+ },
123
+ "plates/int8": {
124
+ "samples": 250,
125
+ "exact": 0.0,
126
+ "cer": 0.8901563160118293
127
+ },
128
+ "plates_nospace/int8": {
129
+ "samples": 250,
130
+ "exact": 0.0,
131
+ "cer": 0.9518201284796574
132
+ }
133
+ },
134
+ "A_lstm_clean": {
135
+ "params": 631226,
136
+ "tflite_bytes": 664648,
137
+ "tstr_test/float": {
138
+ "samples": 2078,
139
+ "exact": 0.5871029836381135,
140
+ "cer": 0.17818689898906878
141
+ },
142
+ "tstr_test/int8": {
143
+ "samples": 2078,
144
+ "exact": 0.4157844080846968,
145
+ "cer": 0.2721295306977891
146
+ },
147
+ "heldout/float": {
148
+ "samples": 12093,
149
+ "exact": 0.7687091705945588,
150
+ "cer": 0.08910761262405652
151
+ },
152
+ "heldout/int8": {
153
+ "samples": 12093,
154
+ "exact": 0.4822624658893575,
155
+ "cer": 0.1620950492128404
156
+ },
157
+ "plates/float": {
158
+ "samples": 250,
159
+ "exact": 0.0,
160
+ "cer": 0.9032530629488804
161
+ },
162
+ "plates_nospace/float": {
163
+ "samples": 250,
164
+ "exact": 0.0,
165
+ "cer": 0.9089935760171306
166
+ },
167
+ "plates/int8": {
168
+ "samples": 250,
169
+ "exact": 0.0,
170
+ "cer": 0.904098014364174
171
+ },
172
+ "plates_nospace/int8": {
173
+ "samples": 250,
174
+ "exact": 0.0,
175
+ "cer": 0.9298715203426124
176
+ }
177
+ },
178
+ "B_conv": {
179
+ "params": 560954,
180
+ "tflite_bytes": 597816,
181
+ "tstr_test/float": {
182
+ "samples": 2078,
183
+ "exact": 0.5996150144369586,
184
+ "cer": 0.166762554450563
185
+ },
186
+ "tstr_test/int8": {
187
+ "samples": 2078,
188
+ "exact": 0.5822906641000962,
189
+ "cer": 0.18089915344785074
190
+ },
191
+ "heldout/float": {
192
+ "samples": 12093,
193
+ "exact": 0.7935169106094435,
194
+ "cer": 0.0860343711328378
195
+ },
196
+ "heldout/int8": {
197
+ "samples": 12093,
198
+ "exact": 0.7839245844703547,
199
+ "cer": 0.09139410429352325
200
+ },
201
+ "plates/float": {
202
+ "samples": 250,
203
+ "exact": 0.0,
204
+ "cer": 0.866497676383608
205
+ },
206
+ "plates_nospace/float": {
207
+ "samples": 250,
208
+ "exact": 0.0,
209
+ "cer": 0.8490364025695931
210
+ },
211
+ "plates/int8": {
212
+ "samples": 250,
213
+ "exact": 0.0,
214
+ "cer": 0.8601605407689058
215
+ },
216
+ "plates_nospace/int8": {
217
+ "samples": 250,
218
+ "exact": 0.0,
219
+ "cer": 0.841541755888651
220
+ }
221
+ }
222
+ }
inviolusocr.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc1e15a32a277ac49a7397fbc7a21e1415636c34f993eaa78a7121de92203271
3
+ size 2365508
inviolusocr_int8.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:693a342760ec0d44aa2ea0d367657d5f1e01dda69eb256b3bdaff1fab561721b
3
+ size 597816