waglesameer5 commited on
Commit
42ffac6
·
verified ·
1 Parent(s): 940d918

Create DevGen Devanagari OCR Gradio Space

Browse files
Files changed (4) hide show
  1. README.md +11 -7
  2. __pycache__/app.cpython-311.pyc +0 -0
  3. app.py +206 -0
  4. requirements.txt +9 -0
README.md CHANGED
@@ -1,13 +1,17 @@
1
  ---
2
- title: Devgen Devanagari Ocr
3
- emoji: 📊
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
1
  ---
2
+ title: DevGen Devanagari OCR
3
+ colorFrom: blue
4
+ colorTo: green
 
5
  sdk: gradio
 
 
6
  app_file: app.py
7
  pinned: false
8
+ license: mit
9
+ models:
10
+ - waglesameer5/devgen-trocr-devanagari-lora
11
  ---
12
 
13
+ # DevGen Devanagari OCR
14
+
15
+ This Space runs a Devanagari OCR demo using `paudelanil/trocr-devanagari-2` with the DevGen LoRA adapter hosted at `waglesameer5/devgen-trocr-devanagari-lora`.
16
+
17
+ Upload a word or short line image, choose a preprocessing mode, and run recognition.
__pycache__/app.cpython-311.pyc ADDED
Binary file (14.4 kB). View file
 
app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import time
6
+ from functools import lru_cache
7
+ from typing import Optional
8
+
9
+ import cv2
10
+ import gradio as gr
11
+ import numpy as np
12
+ import torch
13
+ from peft import PeftModel
14
+ from PIL import Image
15
+ from transformers import AutoTokenizer, TrOCRProcessor, ViTImageProcessor, VisionEncoderDecoderModel
16
+
17
+
18
+ BASE_MODEL = os.getenv("TROCR_BASE_MODEL", "paudelanil/trocr-devanagari-2")
19
+ ADAPTER_MODEL = os.getenv("TROCR_ADAPTER_MODEL", "waglesameer5/devgen-trocr-devanagari-lora")
20
+ FALLBACK_IMAGE_PROCESSOR = os.getenv("TROCR_FALLBACK_IMAGE_PROCESSOR", "google/vit-base-patch16-224-in21k")
21
+
22
+
23
+ def get_device() -> str:
24
+ return "cuda" if torch.cuda.is_available() else "cpu"
25
+
26
+
27
+ def image_to_bytes(image: Image.Image) -> bytes:
28
+ buffer = io.BytesIO()
29
+ image.convert("RGB").save(buffer, format="PNG")
30
+ return buffer.getvalue()
31
+
32
+
33
+ def bytes_to_cv2(image_bytes: bytes) -> np.ndarray:
34
+ nparr = np.frombuffer(image_bytes, np.uint8)
35
+ return cv2.imdecode(nparr, cv2.IMREAD_COLOR)
36
+
37
+
38
+ def cv2_to_pil(img: np.ndarray) -> Image.Image:
39
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
40
+ return Image.fromarray(rgb)
41
+
42
+
43
+ def crop_to_foreground(img: np.ndarray, padding_ratio: float = 0.18) -> np.ndarray:
44
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img
45
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
46
+ _, mask = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
47
+
48
+ kernel = np.ones((3, 3), np.uint8)
49
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
50
+ mask = cv2.dilate(mask, kernel, iterations=1)
51
+
52
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
53
+ if not contours:
54
+ return img
55
+
56
+ h, w = gray.shape[:2]
57
+ min_area = max(12, int(h * w * 0.0001))
58
+ boxes = [cv2.boundingRect(contour) for contour in contours if cv2.contourArea(contour) >= min_area]
59
+ if not boxes:
60
+ return img
61
+
62
+ x1 = min(x for x, _, _, _ in boxes)
63
+ y1 = min(y for _, y, _, _ in boxes)
64
+ x2 = max(x + bw for x, _, bw, _ in boxes)
65
+ y2 = max(y + bh for _, y, _, bh in boxes)
66
+
67
+ pad_x = max(8, int((x2 - x1) * padding_ratio))
68
+ pad_y = max(8, int((y2 - y1) * padding_ratio))
69
+ return img[max(0, y1 - pad_y):min(h, y2 + pad_y), max(0, x1 - pad_x):min(w, x2 + pad_x)]
70
+
71
+
72
+ def normalize_for_model(img: np.ndarray, target_height: int = 384, target_width: int = 384) -> np.ndarray:
73
+ h, w = img.shape[:2]
74
+ scale = min(target_height / h, target_width / w)
75
+ new_h = max(1, int(h * scale))
76
+ new_w = max(1, int(w * scale))
77
+ resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
78
+
79
+ if len(img.shape) == 3:
80
+ canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * 255
81
+ else:
82
+ canvas = np.ones((target_height, target_width), dtype=np.uint8) * 255
83
+
84
+ y_offset = (target_height - new_h) // 2
85
+ x_offset = (target_width - new_w) // 2
86
+ canvas[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
87
+ return canvas
88
+
89
+
90
+ def preprocess(image: Image.Image, mode: str) -> Image.Image:
91
+ image = image.convert("RGB")
92
+ if mode == "Original":
93
+ return image
94
+
95
+ img = bytes_to_cv2(image_to_bytes(image))
96
+ if mode == "Foreground crop":
97
+ return cv2_to_pil(crop_to_foreground(img))
98
+ if mode == "Square pad":
99
+ return cv2_to_pil(normalize_for_model(img))
100
+ if mode == "Crop + square pad":
101
+ return cv2_to_pil(normalize_for_model(crop_to_foreground(img)))
102
+
103
+ return image
104
+
105
+
106
+ def load_processor() -> TrOCRProcessor:
107
+ try:
108
+ return TrOCRProcessor.from_pretrained(BASE_MODEL)
109
+ except Exception:
110
+ try:
111
+ image_processor = ViTImageProcessor.from_pretrained(ADAPTER_MODEL)
112
+ except Exception:
113
+ image_processor = ViTImageProcessor.from_pretrained(FALLBACK_IMAGE_PROCESSOR)
114
+ tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
115
+ return TrOCRProcessor(image_processor=image_processor, tokenizer=tokenizer)
116
+
117
+
118
+ @lru_cache(maxsize=1)
119
+ def load_model() -> tuple[VisionEncoderDecoderModel, TrOCRProcessor, str]:
120
+ device = get_device()
121
+ processor = load_processor()
122
+ base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL)
123
+ base_model.config.decoder_start_token_id = processor.tokenizer.cls_token_id
124
+ base_model.config.pad_token_id = processor.tokenizer.pad_token_id
125
+ base_model.config.eos_token_id = processor.tokenizer.sep_token_id
126
+ base_model.config.vocab_size = base_model.config.decoder.vocab_size
127
+
128
+ peft_model = PeftModel.from_pretrained(base_model, ADAPTER_MODEL)
129
+ try:
130
+ model = peft_model.merge_and_unload()
131
+ except Exception:
132
+ model = peft_model
133
+
134
+ model.to(device)
135
+ model.eval()
136
+ return model, processor, device
137
+
138
+
139
+ def recognize(image: Optional[Image.Image], preprocessing: str, max_length: int) -> tuple[str, Image.Image | None, dict]:
140
+ if image is None:
141
+ return "", None, {"error": "Upload an image first."}
142
+
143
+ processed = preprocess(image, preprocessing)
144
+ model, processor, device = load_model()
145
+ started_at = time.perf_counter()
146
+
147
+ pixel_values = processor(images=processed.convert("RGB"), return_tensors="pt").pixel_values.to(device)
148
+ with torch.inference_mode():
149
+ outputs = model.generate(
150
+ pixel_values,
151
+ max_length=max_length,
152
+ num_beams=4,
153
+ return_dict_in_generate=True,
154
+ output_scores=True,
155
+ )
156
+
157
+ text = processor.batch_decode(outputs.sequences, skip_special_tokens=True)[0].strip()
158
+ elapsed_ms = round((time.perf_counter() - started_at) * 1000, 2)
159
+ details = {
160
+ "base_model": BASE_MODEL,
161
+ "adapter": ADAPTER_MODEL,
162
+ "device": device,
163
+ "preprocessing": preprocessing,
164
+ "inference_ms": elapsed_ms,
165
+ }
166
+ return text, processed, details
167
+
168
+
169
+ CSS = """
170
+ .gradio-container { max-width: 1120px !important; }
171
+ #result_text textarea { font-size: 1.35rem; line-height: 1.8; }
172
+ """
173
+
174
+
175
+ with gr.Blocks(title="DevGen Devanagari OCR", css=CSS) as demo:
176
+ gr.Markdown(
177
+ """
178
+ # DevGen Devanagari OCR
179
+
180
+ Upload a Devanagari word or short line image. The demo runs a TrOCR base model with the DevGen LoRA adapter hosted on Hugging Face.
181
+ """
182
+ )
183
+ with gr.Row():
184
+ with gr.Column(scale=1):
185
+ image_input = gr.Image(type="pil", label="Image")
186
+ preprocessing_input = gr.Radio(
187
+ ["Foreground crop", "Original", "Square pad", "Crop + square pad"],
188
+ value="Foreground crop",
189
+ label="Preprocessing",
190
+ )
191
+ max_length_input = gr.Slider(16, 128, value=64, step=1, label="Max output length")
192
+ submit = gr.Button("Recognize", variant="primary")
193
+ with gr.Column(scale=1):
194
+ text_output = gr.Textbox(label="Recognized text", lines=4, elem_id="result_text")
195
+ processed_output = gr.Image(type="pil", label="Processed image")
196
+ details_output = gr.JSON(label="Run details")
197
+
198
+ submit.click(
199
+ fn=recognize,
200
+ inputs=[image_input, preprocessing_input, max_length_input],
201
+ outputs=[text_output, processed_output, details_output],
202
+ )
203
+
204
+
205
+ if __name__ == "__main__":
206
+ demo.queue(max_size=8).launch()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ torch
3
+ torchvision
4
+ transformers
5
+ peft
6
+ Pillow
7
+ numpy
8
+ opencv-python-headless
9
+ safetensors