IncreasingLoss commited on
Commit
8afeea9
·
verified ·
1 Parent(s): 4755b2f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +528 -528
app.py CHANGED
@@ -1,529 +1,529 @@
1
- import gradio as gr
2
- import pycorrector.corrector
3
- import torch
4
- import torch.nn as nn
5
- import torch.nn.functional as F
6
- from torchvision import transforms
7
- import numpy as np
8
- from ultralytics import YOLO
9
- from llama_cpp import Llama
10
- from pypinyin import pinyin
11
- import re
12
- from PIL import Image, ImageDraw, ImageEnhance
13
- import json
14
- import os
15
- import kenlm
16
-
17
- from pycorrector.corrector import Corrector
18
- #import protobuf
19
- from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
20
-
21
- """yolo model"""
22
- user_device = "cuda" if torch.cuda.is_available() else "cpu"
23
- detection_model = YOLO("model/yolo/yolo_chinese_m.pt").to(user_device).eval()
24
-
25
-
26
-
27
- """LW-Vit Classifier"""
28
-
29
- class HSwish(nn.Module):
30
- """Hard Swish activation function"""
31
- def forward(self, x):
32
- out = x * F.relu6(x + 3, inplace=True) / 6
33
- return out
34
-
35
- class MV2_Block(nn.Module):
36
- """MobileNetV2 Inverted Residual Block with h-swish activation"""
37
- def __init__(self, in_channels, expand_channels, out_channels, stride):
38
- super().__init__()
39
- self.stride = stride
40
- self.use_res_connect = self.stride == 1 and in_channels == out_channels
41
-
42
- self.expand = nn.Sequential(
43
- nn.Conv2d(in_channels, expand_channels, kernel_size=1, stride=1, padding=0, bias=False),
44
- nn.BatchNorm2d(expand_channels),
45
- HSwish()
46
- )
47
-
48
- self.depthwise = nn.Sequential(
49
- nn.Conv2d(expand_channels, expand_channels, kernel_size=3, stride=stride,
50
- padding=1, groups=expand_channels, bias=False),
51
- nn.BatchNorm2d(expand_channels),
52
- HSwish()
53
- )
54
-
55
- self.project = nn.Sequential(
56
- nn.Conv2d(expand_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
57
- nn.BatchNorm2d(out_channels)
58
- )
59
-
60
- def forward(self, x):
61
- if self.use_res_connect:
62
- return x + self.project(self.depthwise(self.expand(x)))
63
- else:
64
- return self.project(self.depthwise(self.expand(x)))
65
-
66
- class LW_ViT_Transformer_Block(nn.Module):
67
- """Lightweight Vision Transformer Block"""
68
- def __init__(self, in_channels, out_channels, patch_size=2, heads=4, dim=128):
69
- super().__init__()
70
- self.patch_size = patch_size
71
- self.in_channels = in_channels
72
- self.out_channels = out_channels
73
- self.dim = dim
74
-
75
- # Downsampling and channel adjustment
76
- self.downsample = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=1)
77
- self.channel_adjust = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1)
78
-
79
- # Calculate patch dimension
80
- patch_dim = out_channels * patch_size * patch_size
81
- # Add projection layer if needed
82
- self.projection = nn.Identity() if patch_dim == dim else nn.Linear(patch_dim, dim)
83
-
84
- # Transformer components
85
- self.norm1 = nn.LayerNorm(dim)
86
- self.attn = nn.MultiheadAttention(embed_dim=dim, num_heads=heads, batch_first=True)
87
- self.norm2 = nn.LayerNorm(dim)
88
- self.ffn = nn.Sequential(
89
- nn.Linear(dim, dim * 4),
90
- nn.GELU(),
91
- nn.Linear(dim * 4, dim)
92
- )
93
-
94
- # Final processing
95
- self.final_conv = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
96
- self.norm_out = nn.BatchNorm2d(out_channels)
97
- self.act = HSwish()
98
-
99
- def forward(self, x):
100
- # Initial downsampling and channel adjustment
101
- x = self.downsample(x)
102
- x = self.channel_adjust(x)
103
-
104
- B, C, H, W = x.shape
105
-
106
- # Convert feature map to patches and embed
107
- # Reshape to [B, C, H/P, P, W/P, P]
108
- x_reshaped = x.reshape(B, C, H // self.patch_size, self.patch_size,
109
- W // self.patch_size, self.patch_size)
110
- # Permute to [B, H/P, W/P, C, P, P]
111
- x_permuted = x_reshaped.permute(0, 2, 4, 1, 3, 5)
112
- # Reshape to [B, H/P * W/P, C * P * P]
113
- patches = x_permuted.reshape(B, (H // self.patch_size) * (W // self.patch_size), -1)
114
-
115
- # Apply projection if needed
116
- patches = self.projection(patches)
117
-
118
- # Apply transformer operations
119
- normed_patches = self.norm1(patches)
120
- attn_out, _ = self.attn(normed_patches, normed_patches, normed_patches)
121
- patches = patches + attn_out
122
-
123
- normed_patches = self.norm2(patches)
124
- ffn_out = self.ffn(normed_patches)
125
- patches = patches + ffn_out
126
-
127
- # Reshape back to feature map
128
- # Reshape to [B, H/P, W/P, C, P, P]
129
- x_back = patches.reshape(B, H // self.patch_size, W // self.patch_size,
130
- C, self.patch_size, self.patch_size)
131
- # Permute to [B, C, H/P, P, W/P, P]
132
- x_back = x_back.permute(0, 3, 1, 4, 2, 5)
133
- # Reshape to [B, C, H, W]
134
- x_out = x_back.reshape(B, C, H, W)
135
-
136
- # Final processing
137
- out = self.final_conv(x_out)
138
- out = self.norm_out(out)
139
- out = self.act(out)
140
-
141
- return out
142
-
143
- class LW_ViT(nn.Module):
144
- """Lightweight Vision Transformer for Chinese Character Recognition"""
145
- def __init__(self, base_channels=16, num_classes=3892):
146
- super().__init__()
147
-
148
- # Stem layer
149
- self.stem = nn.Sequential(
150
- nn.Conv2d(3, base_channels, 3, stride=2, padding=1, bias=False),
151
- nn.BatchNorm2d(base_channels),
152
- HSwish()
153
- )
154
-
155
- # Feature extraction layers
156
- self.features = nn.Sequential(
157
- # MV2 blocks
158
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
159
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
160
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
161
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
162
-
163
- # Transformer block
164
- LW_ViT_Transformer_Block(
165
- in_channels=base_channels,
166
- out_channels=2*base_channels,
167
- patch_size=2,
168
- heads=4,
169
- dim=128
170
- ),
171
-
172
- # Final convolutional layer
173
- nn.Conv2d(2*base_channels, 4*base_channels, kernel_size=1, stride=1),
174
- nn.BatchNorm2d(4*base_channels),
175
- HSwish()
176
- )
177
-
178
- # Global pooling and classifier
179
- self.global_pool = nn.AdaptiveAvgPool2d(1)
180
- self.dropout = nn.Dropout(0.1) # Added dropout for regularization
181
-
182
- self.classifier = nn.Sequential(
183
- nn.Linear(4*base_channels, 8*base_channels),
184
- nn.BatchNorm1d(8*base_channels),
185
- HSwish(),
186
- nn.Dropout(0.1), # Added dropout for regularization
187
- nn.Linear(8*base_channels, num_classes)
188
- )
189
-
190
- def forward(self, x):
191
- x = self.stem(x)
192
- x = self.features(x)
193
- x = self.global_pool(x)
194
- x = torch.flatten(x, 1)
195
- x = self.dropout(x)
196
- x = self.classifier(x)
197
- return x
198
-
199
- classifier = LW_ViT()
200
- classifier.load_state_dict(torch.load("model/LW_Vit/chinese_Character_Classification_LW_ViT.pth", weights_only=True))
201
- classifier.to(user_device).eval()
202
-
203
- """load classes dict"""
204
- with open("model/LW_Vit/chinese_classes.json", "r", encoding="utf-8") as f:
205
- classes_dict = json.load(f)
206
-
207
- """transforms"""
208
- class ToSquare(object):
209
- """
210
- Transform to make images square by padding the shorter dimension
211
- """
212
- def __init__(self, fill=0):
213
- self.fill = fill # Fill value for padding (0 = black)
214
-
215
- def __call__(self, img):
216
- w, h = img.size
217
-
218
- # If already square, return as is
219
- if w == h:
220
- return img
221
-
222
- # Calculate target size (max dimension)
223
- max_dim = max(w, h)
224
-
225
- # Calculate padding
226
- pad_w = (max_dim - w) // 2
227
- pad_h = (max_dim - h) // 2
228
-
229
- # Handle odd dimensions (extra pixel on one side)
230
- pad_w_extra = (max_dim - w) % 2
231
- pad_h_extra = (max_dim - h) % 2
232
-
233
- # Create padding tuple (left, top, right, bottom)
234
- padding = (pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra)
235
-
236
- # Create new padded image
237
- padded_img = transforms.functional.pad(img, padding, fill=self.fill)
238
-
239
- return padded_img
240
-
241
- class ConvertToRGB(object):
242
- """Convert image to RGB mode"""
243
- def __call__(self, img):
244
- # Convert any image mode to RGB (including RGBA)
245
- return img.convert('RGB')
246
-
247
- class Contrast(object):
248
- """Randomly adjust contrast"""
249
- def __init__(self, p=0.3, factor_range=(0.5, 1.5)):
250
- self.p = p
251
- self.factor_range = factor_range
252
-
253
- def __call__(self, img):
254
- enhancer = ImageEnhance.Contrast(img)
255
- return enhancer.enhance(1.5)
256
-
257
- test_transforms = transforms.Compose([
258
- ToSquare(fill=255),
259
- transforms.Resize(128),
260
- ConvertToRGB(),
261
- transforms.ToTensor(),
262
- transforms.Normalize(mean=[0.5], std=[0.5])
263
- ])
264
-
265
-
266
- """import llm"""
267
- llm = Llama(
268
- model_path="model/llms/Yi-1.5-9B-Chat-Q6_K.gguf",
269
- n_ctx=4096, # Context window size
270
- n_gpu_layers=-1, # -1 means use all available GPU layers
271
- verbose=False # Set to True for debugging information
272
- )
273
-
274
-
275
- """app functions"""
276
- def convert_to_pil(inputimg):
277
- # convert input to pil image
278
- if inputimg is None:
279
- return None
280
- if isinstance(inputimg, np.ndarray):
281
- pil_img = Image.fromarray(inputimg)
282
- elif hasattr(inputimg, 'convert'): # Check if it's already a PIL Image
283
- pil_img = inputimg
284
- else:
285
- pil_img = Image.fromarray(np.array(inputimg))
286
- pil_img = pil_img.convert("RGB")
287
- return pil_img
288
-
289
- def draw_bboxes(img, results):
290
- # Create a copy of the input image to draw on
291
- draw_img = img.copy()
292
- draw = ImageDraw.Draw(draw_img)
293
- # Draw bounding boxes for detected objects
294
- for box in results[0].boxes.xyxy:
295
- x1, y1, x2, y2 = map(int, box) # Convert to integers
296
- draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
297
- return draw_img
298
-
299
-
300
- from statistics import mean
301
-
302
- def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
303
- """
304
- Crop and return image regions in proper reading order.
305
-
306
- For vertical Chinese:
307
- - Group into columns by x-coordinate (clusters whose centers are within avg_width/2)
308
- - Order columns right-to-left
309
- - Within each column, order top-to-bottom by y
310
-
311
- For horizontal:
312
- - Group into rows by y-coordinate (clusters within avg_height/2)
313
- - Order rows top-to-bottom
314
- - Within each row, order left-to-right by x
315
- """
316
- # 1) If bboxes is a Tensor, convert to Python list
317
- if hasattr(bboxes, "tolist"):
318
- bboxes = bboxes.tolist()
319
-
320
- # 2) Single box -> trivial crop
321
- if len(bboxes) <= 1:
322
- return [img.crop(tuple(map(int, bbox))) for bbox in bboxes]
323
-
324
- # 3) Compute centers and average glyph size
325
- centers = [((x1+x2)/2, (y1+y2)/2) for x1,y1,x2,y2 in bboxes]
326
- widths = [x2 - x1 for x1,y1,x2,y2 in bboxes]
327
- heights = [y2 - y1 for x1,y1,x2,y2 in bboxes]
328
- avg_w = mean(widths)
329
- avg_h = mean(heights)
330
-
331
- # 4) Choose clustering axis and threshold
332
- if written_vertical:
333
- coords = [c[0] for c in centers] # x-coords
334
- threshold = avg_w / 2
335
- else:
336
- coords = [c[1] for c in centers] # y-coords
337
- threshold = avg_h / 2
338
-
339
- # 5) Sort glyphs by the clustering axis
340
- order = sorted(range(len(coords)), key=lambda i: coords[i])
341
-
342
- # 6) Build clusters
343
- clusters = []
344
- current = [order[0]]
345
- current_mean = coords[order[0]]
346
-
347
- for idx in order[1:]:
348
- c = coords[idx]
349
- # if within threshold of this cluster’s mean, add; else start new
350
- if abs(c - current_mean) <= threshold:
351
- current.append(idx)
352
- # update cluster mean incrementally
353
- current_mean = mean([coords[i] for i in current])
354
- else:
355
- clusters.append(current)
356
- current = [idx]
357
- current_mean = c
358
- clusters.append(current)
359
-
360
- # 7) Order clusters
361
- if written_vertical:
362
- # Chinese vertical: rightmost column first
363
- clusters.sort(
364
- key=lambda grp: mean(coords[i] for i in grp),
365
- reverse=True
366
- )
367
- else:
368
- # Horizontal: top row first
369
- clusters.sort(
370
- key=lambda grp: mean(coords[i] for i in grp)
371
- )
372
-
373
- # 8) Within each cluster, sort by the orthogonal axis
374
- final_indices = []
375
- for grp in clusters:
376
- if written_vertical:
377
- # sort top-to-bottom by y
378
- grp.sort(key=lambda i: centers[i][1])
379
- else:
380
- # sort left-to-right by x
381
- grp.sort(key=lambda i: centers[i][0])
382
- final_indices.extend(grp)
383
-
384
- # 9) Crop and return
385
- crops = []
386
- for i in final_indices:
387
- x1, y1, x2, y2 = map(int, bboxes[i])
388
- crops.append(img.crop((x1, y1, x2, y2)))
389
-
390
- return crops
391
-
392
-
393
- def move_slider(threshhold_slider, input_img):
394
- pil_img = convert_to_pil(input_img)
395
- with torch.inference_mode():
396
- results = detection_model(source=pil_img, conf=threshhold_slider)
397
- draw_img = draw_bboxes(pil_img, results)
398
- return draw_img
399
-
400
- def select_image(evt: gr.SelectData):
401
- # Get the selected index from the gallery
402
- selected_index = evt.index
403
- # Return the path of the selected image
404
- return example_images[selected_index]
405
-
406
- def translate_text(threshhold_slider, is_vertical, input_img):
407
- # convert input to pil image
408
- pil_img = convert_to_pil(input_img)
409
-
410
- with torch.inference_mode():
411
- results = detection_model(source=pil_img, conf=threshhold_slider)
412
- sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
413
- chinese_text = ""
414
- for crop in sorted_cropped_images:
415
- crop = test_transforms(crop)
416
- crop = crop.to(user_device)
417
- logits = classifier(crop.unsqueeze(dim=0))
418
- class_idx = logits.argmax(dim=1)
419
- chinese_charcter = classes_dict[class_idx]
420
- chinese_text += chinese_charcter
421
-
422
- #chinese_text = "这是一句测试语句,用来检验我的深度翻译计算机语言模型。"
423
- pinyin_text = pinyin(chinese_text)
424
- pinyin_sentence = " "
425
- for pin in pinyin_text:
426
- pinyin_sentence += f"{pin[0]} "
427
-
428
- # Enhanced prompt for better translation quality
429
- prompt = f""" You are a professional Chinese to English translator.
430
- 1. Translate the following Chinese text to natural, fluent English:
431
- "{chinese_text}"
432
- 2. Respond only with the translated English text.
433
- English translation: """
434
-
435
- #try:
436
- output = llm(
437
- prompt,
438
- max_tokens=min(len(chinese_text)*2, 512), # More reasonable token limit with safety cap
439
- stop=["</s>", "\n\n", "Chinese:", "Chinese text:"], # Enhanced stop tokens
440
- echo=False,
441
- temperature=0.3, # Slightly higher temperature for more natural language
442
- frequency_penalty=.5, # Penalize repetition of tokens - higher values discourage repetition
443
- presence_penalty=.5, # Penalize tokens that have appeared at all - prevents topic repetition
444
- top_p=0.9 # Nucleus sampling - restricts to top 90% most likely tokens for more coherent output
445
- )
446
-
447
- # Extract the translation
448
- translation = output["choices"][0]["text"].strip()
449
- try:
450
- start = translation.index('"') + 1
451
- # find index of last double-quote
452
- end = translation.rindex('"')
453
- translation = translation[start:end]
454
- except:
455
- pass
456
-
457
- return chinese_text, pinyin_sentence, translation
458
-
459
-
460
- css = """
461
- .centered-examples {
462
- margin: 0 auto !important;
463
- justify-content: center !important;
464
- gap: 8px !important;
465
- min-height: 150px !important; /* Added minimum height */
466
- }
467
- .centered-examples .thumb {
468
- height: 100px !important;
469
- width: 100px !important;
470
- object-fit: cover !important;
471
- margin: 5px !important; /* Added margin between thumbs */
472
- }
473
- /* 1) Global override: remove any forced sizing on .fixed-height anywhere */
474
- .fixed-height.svelte-842rpi.svelte-842rpi {
475
- min-height: 0 !important; /* cancel the global min-height */
476
- max-height: none !important; /* cancel the global max-height */
477
- height: auto !important; /* allow auto height */
478
- }
479
- /* 2) Same-query override: mirror Gradio’s media query exactly */
480
- @media (min-width: 1280px) {
481
- /* target the exact same class chain inside the breakpoint */
482
- .fixed-height.svelte-842rpi.svelte-842rpi {
483
- min-height: 0 !important; /* zero-out the 55vh/min-height there */
484
- max-height: none !important; /* remove the viewport-height cap */
485
- height: auto !important; /* let content dictate height */
486
- }
487
- }
488
- """
489
-
490
-
491
- """gradio app"""
492
- example_dir = "examples"
493
- example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
494
- if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
495
- with gr.Blocks(css=css) as program:
496
- gr.Markdown("## DeepTranslate: Chinese OCR with translation to english")
497
- gr.Markdown("Upload or select an image and move the slider to detect characters.")
498
- gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
499
- #inputs
500
- with gr.Column(scale=1):
501
- gallery = gr.Gallery(value=example_images,
502
- label="Example Images (Click to Select)",
503
- columns=6,
504
- allow_preview=False,
505
- elem_id="my_media_gallery",
506
- elem_classes=["centered-examples"])
507
- with gr.Row(scale=2):
508
- input_img = gr.Image(label="Input Image ")
509
- detection_img = gr.Image(label="Detection Image", interactive= False)
510
-
511
- # slider and button
512
- with gr.Column(scale=1):
513
- with gr.Row(scale=3):
514
- threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshhold", step = 0.01)
515
- translate_button = gr.Button("Translate To English", variant="primary")
516
- is_vertical = gr.Checkbox(value=False, label="Vertiacl Chinese Text?", interactive=True)
517
- # outputs
518
- with gr.Column(scale=1):
519
- with gr.Row(scale=3):
520
- chinese_text = gr.TextArea(label="Chinese Text", max_lines= 1000, interactive=False)
521
- pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines= 1000, interactive=False)
522
- english_text = gr.TextArea(label="English Text", max_lines= 1000, interactive=False)
523
- #function, calling
524
- threshhold_slider.change(fn= move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
525
- translate_button.click(fn=translate_text, inputs= [threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
526
- gallery.select(fn=select_image, inputs=None, outputs=input_img)
527
-
528
- if __name__ == "__main__":
529
  program.launch(share=False)
 
1
+ import gradio as gr
2
+ import pycorrector.corrector
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from torchvision import transforms
7
+ import numpy as np
8
+ from ultralytics import YOLO
9
+ from llama_cpp import Llama
10
+ from pypinyin import pinyin
11
+ import re
12
+ from PIL import Image, ImageDraw, ImageEnhance
13
+ import json
14
+ import os
15
+ import kenlm
16
+
17
+ from pycorrector.corrector import Corrector
18
+ #import protobuf
19
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
20
+
21
+ """yolo model"""
22
+ user_device = "cuda" if torch.cuda.is_available() else "cpu"
23
+ detection_model = YOLO("model/yolo_chinese_m.pt").to(user_device).eval()
24
+
25
+
26
+
27
+ """LW-Vit Classifier"""
28
+
29
+ class HSwish(nn.Module):
30
+ """Hard Swish activation function"""
31
+ def forward(self, x):
32
+ out = x * F.relu6(x + 3, inplace=True) / 6
33
+ return out
34
+
35
+ class MV2_Block(nn.Module):
36
+ """MobileNetV2 Inverted Residual Block with h-swish activation"""
37
+ def __init__(self, in_channels, expand_channels, out_channels, stride):
38
+ super().__init__()
39
+ self.stride = stride
40
+ self.use_res_connect = self.stride == 1 and in_channels == out_channels
41
+
42
+ self.expand = nn.Sequential(
43
+ nn.Conv2d(in_channels, expand_channels, kernel_size=1, stride=1, padding=0, bias=False),
44
+ nn.BatchNorm2d(expand_channels),
45
+ HSwish()
46
+ )
47
+
48
+ self.depthwise = nn.Sequential(
49
+ nn.Conv2d(expand_channels, expand_channels, kernel_size=3, stride=stride,
50
+ padding=1, groups=expand_channels, bias=False),
51
+ nn.BatchNorm2d(expand_channels),
52
+ HSwish()
53
+ )
54
+
55
+ self.project = nn.Sequential(
56
+ nn.Conv2d(expand_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
57
+ nn.BatchNorm2d(out_channels)
58
+ )
59
+
60
+ def forward(self, x):
61
+ if self.use_res_connect:
62
+ return x + self.project(self.depthwise(self.expand(x)))
63
+ else:
64
+ return self.project(self.depthwise(self.expand(x)))
65
+
66
+ class LW_ViT_Transformer_Block(nn.Module):
67
+ """Lightweight Vision Transformer Block"""
68
+ def __init__(self, in_channels, out_channels, patch_size=2, heads=4, dim=128):
69
+ super().__init__()
70
+ self.patch_size = patch_size
71
+ self.in_channels = in_channels
72
+ self.out_channels = out_channels
73
+ self.dim = dim
74
+
75
+ # Downsampling and channel adjustment
76
+ self.downsample = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=1)
77
+ self.channel_adjust = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1)
78
+
79
+ # Calculate patch dimension
80
+ patch_dim = out_channels * patch_size * patch_size
81
+ # Add projection layer if needed
82
+ self.projection = nn.Identity() if patch_dim == dim else nn.Linear(patch_dim, dim)
83
+
84
+ # Transformer components
85
+ self.norm1 = nn.LayerNorm(dim)
86
+ self.attn = nn.MultiheadAttention(embed_dim=dim, num_heads=heads, batch_first=True)
87
+ self.norm2 = nn.LayerNorm(dim)
88
+ self.ffn = nn.Sequential(
89
+ nn.Linear(dim, dim * 4),
90
+ nn.GELU(),
91
+ nn.Linear(dim * 4, dim)
92
+ )
93
+
94
+ # Final processing
95
+ self.final_conv = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
96
+ self.norm_out = nn.BatchNorm2d(out_channels)
97
+ self.act = HSwish()
98
+
99
+ def forward(self, x):
100
+ # Initial downsampling and channel adjustment
101
+ x = self.downsample(x)
102
+ x = self.channel_adjust(x)
103
+
104
+ B, C, H, W = x.shape
105
+
106
+ # Convert feature map to patches and embed
107
+ # Reshape to [B, C, H/P, P, W/P, P]
108
+ x_reshaped = x.reshape(B, C, H // self.patch_size, self.patch_size,
109
+ W // self.patch_size, self.patch_size)
110
+ # Permute to [B, H/P, W/P, C, P, P]
111
+ x_permuted = x_reshaped.permute(0, 2, 4, 1, 3, 5)
112
+ # Reshape to [B, H/P * W/P, C * P * P]
113
+ patches = x_permuted.reshape(B, (H // self.patch_size) * (W // self.patch_size), -1)
114
+
115
+ # Apply projection if needed
116
+ patches = self.projection(patches)
117
+
118
+ # Apply transformer operations
119
+ normed_patches = self.norm1(patches)
120
+ attn_out, _ = self.attn(normed_patches, normed_patches, normed_patches)
121
+ patches = patches + attn_out
122
+
123
+ normed_patches = self.norm2(patches)
124
+ ffn_out = self.ffn(normed_patches)
125
+ patches = patches + ffn_out
126
+
127
+ # Reshape back to feature map
128
+ # Reshape to [B, H/P, W/P, C, P, P]
129
+ x_back = patches.reshape(B, H // self.patch_size, W // self.patch_size,
130
+ C, self.patch_size, self.patch_size)
131
+ # Permute to [B, C, H/P, P, W/P, P]
132
+ x_back = x_back.permute(0, 3, 1, 4, 2, 5)
133
+ # Reshape to [B, C, H, W]
134
+ x_out = x_back.reshape(B, C, H, W)
135
+
136
+ # Final processing
137
+ out = self.final_conv(x_out)
138
+ out = self.norm_out(out)
139
+ out = self.act(out)
140
+
141
+ return out
142
+
143
+ class LW_ViT(nn.Module):
144
+ """Lightweight Vision Transformer for Chinese Character Recognition"""
145
+ def __init__(self, base_channels=16, num_classes=3892):
146
+ super().__init__()
147
+
148
+ # Stem layer
149
+ self.stem = nn.Sequential(
150
+ nn.Conv2d(3, base_channels, 3, stride=2, padding=1, bias=False),
151
+ nn.BatchNorm2d(base_channels),
152
+ HSwish()
153
+ )
154
+
155
+ # Feature extraction layers
156
+ self.features = nn.Sequential(
157
+ # MV2 blocks
158
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
159
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
160
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
161
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
162
+
163
+ # Transformer block
164
+ LW_ViT_Transformer_Block(
165
+ in_channels=base_channels,
166
+ out_channels=2*base_channels,
167
+ patch_size=2,
168
+ heads=4,
169
+ dim=128
170
+ ),
171
+
172
+ # Final convolutional layer
173
+ nn.Conv2d(2*base_channels, 4*base_channels, kernel_size=1, stride=1),
174
+ nn.BatchNorm2d(4*base_channels),
175
+ HSwish()
176
+ )
177
+
178
+ # Global pooling and classifier
179
+ self.global_pool = nn.AdaptiveAvgPool2d(1)
180
+ self.dropout = nn.Dropout(0.1) # Added dropout for regularization
181
+
182
+ self.classifier = nn.Sequential(
183
+ nn.Linear(4*base_channels, 8*base_channels),
184
+ nn.BatchNorm1d(8*base_channels),
185
+ HSwish(),
186
+ nn.Dropout(0.1), # Added dropout for regularization
187
+ nn.Linear(8*base_channels, num_classes)
188
+ )
189
+
190
+ def forward(self, x):
191
+ x = self.stem(x)
192
+ x = self.features(x)
193
+ x = self.global_pool(x)
194
+ x = torch.flatten(x, 1)
195
+ x = self.dropout(x)
196
+ x = self.classifier(x)
197
+ return x
198
+
199
+ classifier = LW_ViT()
200
+ classifier.load_state_dict(torch.load("model/chinese_Character_Classification_handwritten_LW_ViT.pth", weights_only=True))
201
+ classifier.to(user_device).eval()
202
+
203
+ """load classes dict"""
204
+ with open("model/LW_Vit/chinese_classes.json", "r", encoding="utf-8") as f:
205
+ classes_dict = json.load(f)
206
+
207
+ """transforms"""
208
+ class ToSquare(object):
209
+ """
210
+ Transform to make images square by padding the shorter dimension
211
+ """
212
+ def __init__(self, fill=0):
213
+ self.fill = fill # Fill value for padding (0 = black)
214
+
215
+ def __call__(self, img):
216
+ w, h = img.size
217
+
218
+ # If already square, return as is
219
+ if w == h:
220
+ return img
221
+
222
+ # Calculate target size (max dimension)
223
+ max_dim = max(w, h)
224
+
225
+ # Calculate padding
226
+ pad_w = (max_dim - w) // 2
227
+ pad_h = (max_dim - h) // 2
228
+
229
+ # Handle odd dimensions (extra pixel on one side)
230
+ pad_w_extra = (max_dim - w) % 2
231
+ pad_h_extra = (max_dim - h) % 2
232
+
233
+ # Create padding tuple (left, top, right, bottom)
234
+ padding = (pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra)
235
+
236
+ # Create new padded image
237
+ padded_img = transforms.functional.pad(img, padding, fill=self.fill)
238
+
239
+ return padded_img
240
+
241
+ class ConvertToRGB(object):
242
+ """Convert image to RGB mode"""
243
+ def __call__(self, img):
244
+ # Convert any image mode to RGB (including RGBA)
245
+ return img.convert('RGB')
246
+
247
+ class Contrast(object):
248
+ """Randomly adjust contrast"""
249
+ def __init__(self, p=0.3, factor_range=(0.5, 1.5)):
250
+ self.p = p
251
+ self.factor_range = factor_range
252
+
253
+ def __call__(self, img):
254
+ enhancer = ImageEnhance.Contrast(img)
255
+ return enhancer.enhance(1.5)
256
+
257
+ test_transforms = transforms.Compose([
258
+ ToSquare(fill=255),
259
+ transforms.Resize(128),
260
+ ConvertToRGB(),
261
+ transforms.ToTensor(),
262
+ transforms.Normalize(mean=[0.5], std=[0.5])
263
+ ])
264
+
265
+
266
+ """import llm"""
267
+ llm = Llama(
268
+ model_path="model/llms/Yi-1.5-9B-Chat-Q6_K.gguf",
269
+ n_ctx=4096, # Context window size
270
+ n_gpu_layers=-1, # -1 means use all available GPU layers
271
+ verbose=False # Set to True for debugging information
272
+ )
273
+
274
+
275
+ """app functions"""
276
+ def convert_to_pil(inputimg):
277
+ # convert input to pil image
278
+ if inputimg is None:
279
+ return None
280
+ if isinstance(inputimg, np.ndarray):
281
+ pil_img = Image.fromarray(inputimg)
282
+ elif hasattr(inputimg, 'convert'): # Check if it's already a PIL Image
283
+ pil_img = inputimg
284
+ else:
285
+ pil_img = Image.fromarray(np.array(inputimg))
286
+ pil_img = pil_img.convert("RGB")
287
+ return pil_img
288
+
289
+ def draw_bboxes(img, results):
290
+ # Create a copy of the input image to draw on
291
+ draw_img = img.copy()
292
+ draw = ImageDraw.Draw(draw_img)
293
+ # Draw bounding boxes for detected objects
294
+ for box in results[0].boxes.xyxy:
295
+ x1, y1, x2, y2 = map(int, box) # Convert to integers
296
+ draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
297
+ return draw_img
298
+
299
+
300
+ from statistics import mean
301
+
302
+ def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
303
+ """
304
+ Crop and return image regions in proper reading order.
305
+
306
+ For vertical Chinese:
307
+ - Group into columns by x-coordinate (clusters whose centers are within avg_width/2)
308
+ - Order columns right-to-left
309
+ - Within each column, order top-to-bottom by y
310
+
311
+ For horizontal:
312
+ - Group into rows by y-coordinate (clusters within avg_height/2)
313
+ - Order rows top-to-bottom
314
+ - Within each row, order left-to-right by x
315
+ """
316
+ # 1) If bboxes is a Tensor, convert to Python list
317
+ if hasattr(bboxes, "tolist"):
318
+ bboxes = bboxes.tolist()
319
+
320
+ # 2) Single box -> trivial crop
321
+ if len(bboxes) <= 1:
322
+ return [img.crop(tuple(map(int, bbox))) for bbox in bboxes]
323
+
324
+ # 3) Compute centers and average glyph size
325
+ centers = [((x1+x2)/2, (y1+y2)/2) for x1,y1,x2,y2 in bboxes]
326
+ widths = [x2 - x1 for x1,y1,x2,y2 in bboxes]
327
+ heights = [y2 - y1 for x1,y1,x2,y2 in bboxes]
328
+ avg_w = mean(widths)
329
+ avg_h = mean(heights)
330
+
331
+ # 4) Choose clustering axis and threshold
332
+ if written_vertical:
333
+ coords = [c[0] for c in centers] # x-coords
334
+ threshold = avg_w / 2
335
+ else:
336
+ coords = [c[1] for c in centers] # y-coords
337
+ threshold = avg_h / 2
338
+
339
+ # 5) Sort glyphs by the clustering axis
340
+ order = sorted(range(len(coords)), key=lambda i: coords[i])
341
+
342
+ # 6) Build clusters
343
+ clusters = []
344
+ current = [order[0]]
345
+ current_mean = coords[order[0]]
346
+
347
+ for idx in order[1:]:
348
+ c = coords[idx]
349
+ # if within threshold of this cluster’s mean, add; else start new
350
+ if abs(c - current_mean) <= threshold:
351
+ current.append(idx)
352
+ # update cluster mean incrementally
353
+ current_mean = mean([coords[i] for i in current])
354
+ else:
355
+ clusters.append(current)
356
+ current = [idx]
357
+ current_mean = c
358
+ clusters.append(current)
359
+
360
+ # 7) Order clusters
361
+ if written_vertical:
362
+ # Chinese vertical: rightmost column first
363
+ clusters.sort(
364
+ key=lambda grp: mean(coords[i] for i in grp),
365
+ reverse=True
366
+ )
367
+ else:
368
+ # Horizontal: top row first
369
+ clusters.sort(
370
+ key=lambda grp: mean(coords[i] for i in grp)
371
+ )
372
+
373
+ # 8) Within each cluster, sort by the orthogonal axis
374
+ final_indices = []
375
+ for grp in clusters:
376
+ if written_vertical:
377
+ # sort top-to-bottom by y
378
+ grp.sort(key=lambda i: centers[i][1])
379
+ else:
380
+ # sort left-to-right by x
381
+ grp.sort(key=lambda i: centers[i][0])
382
+ final_indices.extend(grp)
383
+
384
+ # 9) Crop and return
385
+ crops = []
386
+ for i in final_indices:
387
+ x1, y1, x2, y2 = map(int, bboxes[i])
388
+ crops.append(img.crop((x1, y1, x2, y2)))
389
+
390
+ return crops
391
+
392
+
393
+ def move_slider(threshhold_slider, input_img):
394
+ pil_img = convert_to_pil(input_img)
395
+ with torch.inference_mode():
396
+ results = detection_model(source=pil_img, conf=threshhold_slider)
397
+ draw_img = draw_bboxes(pil_img, results)
398
+ return draw_img
399
+
400
+ def select_image(evt: gr.SelectData):
401
+ # Get the selected index from the gallery
402
+ selected_index = evt.index
403
+ # Return the path of the selected image
404
+ return example_images[selected_index]
405
+
406
+ def translate_text(threshhold_slider, is_vertical, input_img):
407
+ # convert input to pil image
408
+ pil_img = convert_to_pil(input_img)
409
+
410
+ with torch.inference_mode():
411
+ results = detection_model(source=pil_img, conf=threshhold_slider)
412
+ sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
413
+ chinese_text = ""
414
+ for crop in sorted_cropped_images:
415
+ crop = test_transforms(crop)
416
+ crop = crop.to(user_device)
417
+ logits = classifier(crop.unsqueeze(dim=0))
418
+ class_idx = logits.argmax(dim=1)
419
+ chinese_charcter = classes_dict[class_idx]
420
+ chinese_text += chinese_charcter
421
+
422
+ #chinese_text = "这是一句测试语句,用来检验我的深度翻译计算机语言模型。"
423
+ pinyin_text = pinyin(chinese_text)
424
+ pinyin_sentence = " "
425
+ for pin in pinyin_text:
426
+ pinyin_sentence += f"{pin[0]} "
427
+
428
+ # Enhanced prompt for better translation quality
429
+ prompt = f""" You are a professional Chinese to English translator.
430
+ 1. Translate the following Chinese text to natural, fluent English:
431
+ "{chinese_text}"
432
+ 2. Respond only with the translated English text.
433
+ English translation: """
434
+
435
+ #try:
436
+ output = llm(
437
+ prompt,
438
+ max_tokens=min(len(chinese_text)*2, 512), # More reasonable token limit with safety cap
439
+ stop=["</s>", "\n\n", "Chinese:", "Chinese text:"], # Enhanced stop tokens
440
+ echo=False,
441
+ temperature=0.3, # Slightly higher temperature for more natural language
442
+ frequency_penalty=.5, # Penalize repetition of tokens - higher values discourage repetition
443
+ presence_penalty=.5, # Penalize tokens that have appeared at all - prevents topic repetition
444
+ top_p=0.9 # Nucleus sampling - restricts to top 90% most likely tokens for more coherent output
445
+ )
446
+
447
+ # Extract the translation
448
+ translation = output["choices"][0]["text"].strip()
449
+ try:
450
+ start = translation.index('"') + 1
451
+ # find index of last double-quote
452
+ end = translation.rindex('"')
453
+ translation = translation[start:end]
454
+ except:
455
+ pass
456
+
457
+ return chinese_text, pinyin_sentence, translation
458
+
459
+
460
+ css = """
461
+ .centered-examples {
462
+ margin: 0 auto !important;
463
+ justify-content: center !important;
464
+ gap: 8px !important;
465
+ min-height: 150px !important; /* Added minimum height */
466
+ }
467
+ .centered-examples .thumb {
468
+ height: 100px !important;
469
+ width: 100px !important;
470
+ object-fit: cover !important;
471
+ margin: 5px !important; /* Added margin between thumbs */
472
+ }
473
+ /* 1) Global override: remove any forced sizing on .fixed-height anywhere */
474
+ .fixed-height.svelte-842rpi.svelte-842rpi {
475
+ min-height: 0 !important; /* cancel the global min-height */
476
+ max-height: none !important; /* cancel the global max-height */
477
+ height: auto !important; /* allow auto height */
478
+ }
479
+ /* 2) Same-query override: mirror Gradio’s media query exactly */
480
+ @media (min-width: 1280px) {
481
+ /* target the exact same class chain inside the breakpoint */
482
+ .fixed-height.svelte-842rpi.svelte-842rpi {
483
+ min-height: 0 !important; /* zero-out the 55vh/min-height there */
484
+ max-height: none !important; /* remove the viewport-height cap */
485
+ height: auto !important; /* let content dictate height */
486
+ }
487
+ }
488
+ """
489
+
490
+
491
+ """gradio app"""
492
+ example_dir = "examples"
493
+ example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
494
+ if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
495
+ with gr.Blocks(css=css) as program:
496
+ gr.Markdown("## DeepTranslate: Chinese OCR with translation to english")
497
+ gr.Markdown("Upload or select an image and move the slider to detect characters.")
498
+ gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
499
+ #inputs
500
+ with gr.Column(scale=1):
501
+ gallery = gr.Gallery(value=example_images,
502
+ label="Example Images (Click to Select)",
503
+ columns=6,
504
+ allow_preview=False,
505
+ elem_id="my_media_gallery",
506
+ elem_classes=["centered-examples"])
507
+ with gr.Row(scale=2):
508
+ input_img = gr.Image(label="Input Image ")
509
+ detection_img = gr.Image(label="Detection Image", interactive= False)
510
+
511
+ # slider and button
512
+ with gr.Column(scale=1):
513
+ with gr.Row(scale=3):
514
+ threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshhold", step = 0.01)
515
+ translate_button = gr.Button("Translate To English", variant="primary")
516
+ is_vertical = gr.Checkbox(value=False, label="Vertiacl Chinese Text?", interactive=True)
517
+ # outputs
518
+ with gr.Column(scale=1):
519
+ with gr.Row(scale=3):
520
+ chinese_text = gr.TextArea(label="Chinese Text", max_lines= 1000, interactive=False)
521
+ pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines= 1000, interactive=False)
522
+ english_text = gr.TextArea(label="English Text", max_lines= 1000, interactive=False)
523
+ #function, calling
524
+ threshhold_slider.change(fn= move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
525
+ translate_button.click(fn=translate_text, inputs= [threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
526
+ gallery.select(fn=select_image, inputs=None, outputs=input_img)
527
+
528
+ if __name__ == "__main__":
529
  program.launch(share=False)