Qarvexium commited on
Commit
447f27a
·
verified ·
1 Parent(s): b978ae1

Upload 4 files

Browse files
qocr_tiny_v1_ready/config.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "QOCR-Tiny-v1",
3
+ "parameters": 7980689,
4
+ "input": {
5
+ "grayscale": true,
6
+ "max_height": 48,
7
+ "max_width": 384,
8
+ "aspect_ratio_preserved": true,
9
+ "crop": false,
10
+ "upscale": false
11
+ },
12
+ "encoder": {
13
+ "channels": [
14
+ 64,
15
+ 128,
16
+ 256,
17
+ 384,
18
+ 512
19
+ ],
20
+ "latent_dim": 256
21
+ },
22
+ "rnn": {
23
+ "type": "BiGRU",
24
+ "hidden": 512,
25
+ "layers": 2
26
+ },
27
+ "ctc": {
28
+ "blank_id": 0
29
+ },
30
+ "charset": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?'-:/()%&+=$@#_"
31
+ }
qocr_tiny_v1_ready/inference.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import json
4
+ import argparse
5
+
6
+ import numpy as np
7
+ from PIL import Image
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+
12
+
13
+ HERE = os.path.dirname(os.path.abspath(__file__))
14
+ MODEL_PATH = os.path.join(HERE, "qocr_tiny_v1.pt")
15
+ VOCAB_PATH = os.path.join(HERE, "vocab.json")
16
+ CONFIG_PATH = os.path.join(HERE, "config.json")
17
+
18
+ with open(VOCAB_PATH, "r", encoding="utf-8") as f:
19
+ vocab = json.load(f)
20
+
21
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
22
+ config = json.load(f)
23
+
24
+ CHARSET = vocab["charset"]
25
+ BLANK_ID = int(vocab["blank"])
26
+ VOCAB_SIZE = int(vocab["vocab_size"])
27
+
28
+ MAX_HEIGHT = int(config["input"]["max_height"])
29
+ MAX_WIDTH = int(config["input"]["max_width"])
30
+
31
+ CNN1, CNN2, CNN3, CNN4, CNN5 = [int(x) for x in config["encoder"]["channels"]]
32
+ LATENT_DIM = int(config["encoder"]["latent_dim"])
33
+ GRU_HIDDEN = int(config["rnn"]["hidden"])
34
+ GRU_LAYERS = int(config["rnn"]["layers"])
35
+
36
+ ID_TO_CHAR = {i + 1: c for i, c in enumerate(CHARSET)}
37
+
38
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
39
+ USE_AMP = (DEVICE.type == "cuda")
40
+
41
+
42
+ class ConvBNAct(nn.Module):
43
+ def __init__(self, in_channels, out_channels, stride=(1, 1)):
44
+ super().__init__()
45
+ self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
46
+ self.bn = nn.BatchNorm2d(out_channels)
47
+ self.act = nn.SiLU(inplace=True)
48
+
49
+ def forward(self, x):
50
+ return self.act(self.bn(self.conv(x)))
51
+
52
+
53
+ class DepthwiseSeparable(nn.Module):
54
+ def __init__(self, in_channels, out_channels, stride=(1, 1)):
55
+ super().__init__()
56
+ self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=stride, padding=1, groups=in_channels, bias=False)
57
+ self.depth_bn = nn.BatchNorm2d(in_channels)
58
+ self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
59
+ self.point_bn = nn.BatchNorm2d(out_channels)
60
+ self.act = nn.SiLU(inplace=True)
61
+
62
+ def forward(self, x):
63
+ x = self.depthwise(x)
64
+ x = self.depth_bn(x)
65
+ x = self.act(x)
66
+ x = self.pointwise(x)
67
+ x = self.point_bn(x)
68
+ x = self.act(x)
69
+ return x
70
+
71
+
72
+ class QOCRSmall(nn.Module):
73
+ def __init__(self, vocab_size):
74
+ super().__init__()
75
+
76
+ self.encoder = nn.Sequential(
77
+ ConvBNAct(1, CNN1, stride=(2, 1)),
78
+ ConvBNAct(CNN1, CNN2, stride=(2, 1)),
79
+ ConvBNAct(CNN2, CNN3, stride=(2, 1)),
80
+ DepthwiseSeparable(CNN3, CNN4, stride=(1, 2)),
81
+ DepthwiseSeparable(CNN4, CNN5, stride=(1, 1)),
82
+ )
83
+
84
+ self.latent_projection = nn.Sequential(
85
+ nn.Conv2d(CNN5, LATENT_DIM, kernel_size=1, bias=False),
86
+ nn.BatchNorm2d(LATENT_DIM),
87
+ nn.SiLU(inplace=True),
88
+ )
89
+
90
+ self.gru = nn.GRU(
91
+ input_size=LATENT_DIM,
92
+ hidden_size=GRU_HIDDEN,
93
+ num_layers=GRU_LAYERS,
94
+ batch_first=True,
95
+ bidirectional=True,
96
+ dropout=0.15,
97
+ )
98
+
99
+ self.norm = nn.LayerNorm(GRU_HIDDEN * 2)
100
+ self.classifier = nn.Linear(GRU_HIDDEN * 2, vocab_size)
101
+
102
+ def forward(self, x):
103
+ x = self.encoder(x)
104
+ x = self.latent_projection(x)
105
+ x = x.mean(dim=2)
106
+ x = x.transpose(1, 2)
107
+ x, _ = self.gru(x)
108
+ x = self.norm(x)
109
+ x = self.classifier(x)
110
+ return x.transpose(0, 1)
111
+
112
+
113
+ model = QOCRSmall(VOCAB_SIZE).to(DEVICE)
114
+ state_dict = torch.load(MODEL_PATH, map_location=DEVICE)
115
+ model.load_state_dict(state_dict)
116
+ model.eval()
117
+
118
+
119
+ def preprocess_image(image):
120
+ if not isinstance(image, Image.Image):
121
+ image = Image.fromarray(np.asarray(image))
122
+
123
+ image = image.convert("L")
124
+ width, height = image.size
125
+
126
+ if width <= 0 or height <= 0:
127
+ raise ValueError("Invalid image dimensions.")
128
+
129
+ scale = min(1.0, MAX_WIDTH / width, MAX_HEIGHT / height)
130
+
131
+ if scale < 1.0:
132
+ width = max(1, round(width * scale))
133
+ height = max(1, round(height * scale))
134
+ image = image.resize((width, height), Image.Resampling.LANCZOS)
135
+
136
+ array = np.asarray(image, dtype=np.float32)
137
+ array /= 255.0
138
+
139
+ tensor = torch.from_numpy(array).unsqueeze(0).unsqueeze(0).to(DEVICE)
140
+ return tensor
141
+
142
+
143
+ def decode_logits(logits):
144
+ ids = logits.argmax(dim=2)
145
+ sequence = ids[:, 0].tolist()
146
+
147
+ previous = BLANK_ID
148
+ output = []
149
+
150
+ for token in sequence:
151
+ if token != BLANK_ID and token != previous:
152
+ output.append(ID_TO_CHAR.get(token, ""))
153
+ previous = token
154
+
155
+ return "".join(output)
156
+
157
+
158
+ @torch.inference_mode()
159
+ def ocr(image):
160
+ tensor = preprocess_image(image)
161
+
162
+ if USE_AMP:
163
+ with torch.autocast(device_type="cuda", dtype=torch.float16):
164
+ logits = model(tensor)
165
+ else:
166
+ logits = model(tensor)
167
+
168
+ return decode_logits(logits)
169
+
170
+
171
+ def ocr_file(path):
172
+ with Image.open(path) as image:
173
+ return ocr(image)
174
+
175
+
176
+ def main():
177
+ parser = argparse.ArgumentParser(description="QOCR-Tiny v1 OCR")
178
+ parser.add_argument("image", help="Path to cropped text image")
179
+ args = parser.parse_args()
180
+
181
+ result = ocr_file(args.image)
182
+ print(result)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
qocr_tiny_v1_ready/qocr_tiny_v1.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6cd4136dd4bc9d876f407c826f95e167536f1922bcbad82882c528b2f04d0b3b
3
+ size 31960230
qocr_tiny_v1_ready/vocab.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "charset": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?'-:/()%&+=$@#_",
3
+ "blank": 0,
4
+ "vocab_size": 81
5
+ }