manishw7 commited on
Commit
dc17282
·
1 Parent(s): 9487c54

Final Fidelity: Mirror local Preprocess-Route-Recognize pipeline

Browse files
Files changed (2) hide show
  1. app.py +62 -90
  2. preprocessing.py +53 -0
app.py CHANGED
@@ -1,67 +1,55 @@
1
  import os
2
- import time
3
  import gradio as gr
4
  import torch
5
  import numpy as np
6
- import cv2
7
  from PIL import Image
8
  from peft import PeftModel
9
- from transformers import AutoTokenizer, TrOCRProcessor, ViTImageProcessor, VisionEncoderDecoderModel
10
  from cnn_model import CharacterClassifier
 
11
 
12
- # --- CONSTANTS (From local trocr_engine.py) ---
13
  BASE_MODEL_ID = "paudelanil/trocr-devanagari-2"
14
  ADAPTER_ID = "manishw10/devgen-trocr-devanagari-lora"
15
  CNN_MODEL_PATH = "devanagari-cnn-classifier.pt"
16
- SPECIAL_TOKEN_NAMES = ("bos_token_id", "cls_token_id", "eos_token_id", "pad_token_id", "sep_token_id")
17
 
18
- IS_SPACE = "SPACE_ID" in os.environ
19
  device = "cuda" if torch.cuda.is_available() else "cpu"
20
 
21
- # --- MODEL LOADING (Mirrored from TrOCREngine.__init__) ---
22
- print(f"System: Aligning with local TrOCREngine...")
23
-
24
- # 1. Load Processor
25
- try:
26
- processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
27
- except Exception:
28
- image_processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224-in21k")
29
- tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
30
- processor = TrOCRProcessor(image_processor=image_processor, tokenizer=tokenizer)
31
-
32
- # 2. Load and Config Model
33
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
 
 
34
  base_model.config.decoder_start_token_id = processor.tokenizer.cls_token_id
35
  base_model.config.pad_token_id = processor.tokenizer.pad_token_id
36
  base_model.config.eos_token_id = processor.tokenizer.sep_token_id
37
  base_model.config.vocab_size = base_model.config.decoder.vocab_size
38
 
39
- # 3. Apply and MERGE LoRA (Critical for consistency)
40
  peft_model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
41
  try:
42
  model = peft_model.merge_and_unload()
43
- print("System: LoRA weights merged successfully (1:1 with local).")
44
- except Exception as e:
45
- print(f"System: Merge failed, using wrapper: {e}")
46
  model = peft_model
47
-
48
  model.to(device)
49
  model.eval()
50
 
51
- # 4. Load CNN
52
  cnn_engine = CharacterClassifier(model_path=CNN_MODEL_PATH, device=device)
53
 
54
- # --- ORIGINAL ROUTING (Mirrored from image_router.py) ---
55
  def _flood_fill(binary, visited, start_y, start_x, h, w):
56
  stack = [(start_y, start_x)]
57
  size = 0
58
  while stack:
59
  y, x = stack.pop()
60
- if y < 0 or y >= h or x < 0 or x >= w or visited[y, x] or not binary[y, x]:
61
- continue
62
- visited[y, x] = True
63
  size += 1
64
- stack.extend([(y+1, x), (y-1, x), (y, x+1), (y, x-1)])
65
  return size
66
 
67
  def count_blobs(binary, min_size=10):
@@ -70,91 +58,75 @@ def count_blobs(binary, min_size=10):
70
  count = 0
71
  for y in range(h):
72
  for x in range(w):
73
- if binary[y, x] and not visited[y, x]:
74
  size = _flood_fill(binary, visited, y, x, h, w)
75
- if size >= min_size:
76
- count += 1
77
  return count
78
 
79
- def classify_input_type(image):
80
  gray = image.convert("L")
81
  arr = np.array(gray)
82
  threshold = min(arr.mean() * 0.75, 200)
83
  binary = (arr < threshold).astype(np.uint8)
84
-
85
- rows = np.any(binary, axis=1)
86
- cols = np.any(binary, axis=0)
87
- if not rows.any() or not cols.any():
88
- return "character", 0.5, 0.0, 0
89
-
90
  rmin, rmax = np.where(rows)[0][[0, -1]]
91
  cmin, cmax = np.where(cols)[0][[0, -1]]
92
  w, h = cmax - cmin + 1, rmax - rmin + 1
93
- aspect_ratio = w / max(h, 1)
94
- blob_count = count_blobs(binary, min_size=max(binary.size * 0.001, 10))
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- # DECISION LOGIC (1:1 with local image_router.py)
97
- is_character = True
98
- if aspect_ratio > 2.5: is_character = False
99
- elif aspect_ratio > 1.8 and blob_count >= 3: is_character = False
100
- elif blob_count >= 4: is_character = False
101
- elif aspect_ratio < 1.3 and blob_count <= 2: is_character = True
102
- elif blob_count == 1 and aspect_ratio < 1.5: is_character = True
103
- elif aspect_ratio > 1.6: is_character = False
104
 
105
- return ("character" if is_character else "word"), aspect_ratio, blob_count
106
-
107
- # --- PREDICT ---
108
- def predict(image, manual_mode):
109
- if image is None: return None, "Upload an image.", "", ""
 
 
110
 
111
- # 1. Routing
112
- if manual_mode == "Automatic":
113
- mode, ar, bc = classify_input_type(image)
114
- routing_status = f"Auto: {mode.upper()} (AR: {ar:.2f}, Blobs: {bc})"
115
- else:
116
- mode = manual_mode.lower()
117
- routing_status = f"Manual: {mode.upper()}"
118
-
119
  try:
120
  if mode == "character" and cnn_engine.available:
121
- result = cnn_engine.predict(image)
122
- return result["text"], routing_status, "CNN Classifier", ""
123
  else:
124
- # Word Recognition
125
- pixel_values = processor(image.convert("RGB"), return_tensors="pt").pixel_values.to(device)
126
  with torch.no_grad():
127
- outputs = model.generate(
128
- pixel_values,
129
- num_beams=4,
130
- max_length=128,
131
- early_stopping=True
132
- )
133
  text = processor.batch_decode(outputs, skip_special_tokens=True)[0]
134
- return text, routing_status, "TrOCR + LoRA", ""
135
  except Exception as e:
136
- return f"Error: {str(e)}", "Inference Failed", "None", ""
137
 
138
  # --- UI ---
139
- CSS = """
140
- .gradio-container { background: #0f172a; color: white; font-family: 'Inter', sans-serif; }
141
- .panel { background: rgba(30, 41, 59, 0.8); border-radius: 20px; padding: 20px; border: 1px solid #334155; }
142
- .result-box { font-size: 2.5rem !important; font-weight: bold; color: #818cf8; text-align: center; }
143
- """
144
-
145
- with gr.Blocks(css=CSS, theme=gr.themes.Default()) as demo:
146
- gr.Markdown("# 🕉️ DevGen OCR — Professional Engine")
147
- with gr.Row(elem_classes="panel"):
148
  with gr.Column():
149
- input_img = gr.Image(type="pil", label="Input Handwriting")
150
- mode_sel = gr.Radio(["Automatic", "Word", "Character"], value="Automatic", label="Mode")
151
- run_btn = gr.Button("Recognize", variant="primary")
152
  with gr.Column():
153
- output_text = gr.Textbox(label="Recognition Result", elem_classes="result-box")
154
- status_lbl = gr.Markdown("Engine ready.")
155
- engine_lbl = gr.Textbox(label="Model Used", interactive=False)
156
 
157
- run_btn.click(predict, [input_img, mode_sel], [output_text, status_lbl, engine_lbl])
158
 
159
  if __name__ == "__main__":
160
  demo.launch()
 
1
  import os
2
+ import io
3
  import gradio as gr
4
  import torch
5
  import numpy as np
 
6
  from PIL import Image
7
  from peft import PeftModel
8
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel
9
  from cnn_model import CharacterClassifier
10
+ from preprocessing import preprocess_for_ocr
11
 
12
+ # --- CONFIGURATION ---
13
  BASE_MODEL_ID = "paudelanil/trocr-devanagari-2"
14
  ADAPTER_ID = "manishw10/devgen-trocr-devanagari-lora"
15
  CNN_MODEL_PATH = "devanagari-cnn-classifier.pt"
 
16
 
 
17
  device = "cuda" if torch.cuda.is_available() else "cpu"
18
 
19
+ # --- MODEL INITIALIZATION (Mirror of TrOCREngine) ---
20
+ print("System: Initializing Full-Fidelity Engine...")
21
+ processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
 
 
 
 
 
 
 
 
 
22
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
23
+
24
+ # Sync Token Configs
25
  base_model.config.decoder_start_token_id = processor.tokenizer.cls_token_id
26
  base_model.config.pad_token_id = processor.tokenizer.pad_token_id
27
  base_model.config.eos_token_id = processor.tokenizer.sep_token_id
28
  base_model.config.vocab_size = base_model.config.decoder.vocab_size
29
 
30
+ # Apply and Merge PEFT (Identical to local)
31
  peft_model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
32
  try:
33
  model = peft_model.merge_and_unload()
34
+ print("System: LoRA weights merged.")
35
+ except Exception:
 
36
  model = peft_model
 
37
  model.to(device)
38
  model.eval()
39
 
40
+ # Load CNN
41
  cnn_engine = CharacterClassifier(model_path=CNN_MODEL_PATH, device=device)
42
 
43
+ # --- ORIGINAL ROUTING LOGIC (1:1 Copy) ---
44
  def _flood_fill(binary, visited, start_y, start_x, h, w):
45
  stack = [(start_y, start_x)]
46
  size = 0
47
  while stack:
48
  y, x = stack.pop()
49
+ if y<0 or y>=h or x<0 or x>=w or visited[y,x] or not binary[y,x]: continue
50
+ visited[y,x] = True
 
51
  size += 1
52
+ stack.extend([(y+1,x),(y-1,x),(y,x+1),(y,x-1)])
53
  return size
54
 
55
  def count_blobs(binary, min_size=10):
 
58
  count = 0
59
  for y in range(h):
60
  for x in range(w):
61
+ if binary[y,x] and not visited[y,x]:
62
  size = _flood_fill(binary, visited, y, x, h, w)
63
+ if size >= min_size: count += 1
 
64
  return count
65
 
66
+ def original_classify_input(image):
67
  gray = image.convert("L")
68
  arr = np.array(gray)
69
  threshold = min(arr.mean() * 0.75, 200)
70
  binary = (arr < threshold).astype(np.uint8)
71
+ rows, cols = np.any(binary, axis=1), np.any(binary, axis=0)
72
+ if not rows.any() or not cols.any(): return "character", 0, 0
 
 
 
 
73
  rmin, rmax = np.where(rows)[0][[0, -1]]
74
  cmin, cmax = np.where(cols)[0][[0, -1]]
75
  w, h = cmax - cmin + 1, rmax - rmin + 1
76
+ ar, bc = w/h, count_blobs(binary, min_size=max(binary.size * 0.001, 10))
77
+ is_char = True
78
+ if ar > 2.5: is_char = False
79
+ elif ar > 1.8 and bc >= 3: is_char = False
80
+ elif bc >= 4: is_char = False
81
+ elif ar < 1.3 and bc <= 2: is_char = True
82
+ elif bc == 1 and ar < 1.5: is_char = True
83
+ elif ar > 1.6: is_char = False
84
+ return ("character" if is_char else "word"), ar, bc
85
+
86
+ # --- MAIN INFERENCE PIPELINE ---
87
+ def predict(image):
88
+ if image is None: return None, "Upload image.", ""
89
 
90
+ # 1. PREPROCESS (Critical! 1:1 with local recognize endpoint)
91
+ # Convert PIL to bytes for the preprocessor
92
+ buf = io.BytesIO()
93
+ image.save(buf, format="PNG")
94
+ image_bytes = buf.getvalue()
 
 
 
95
 
96
+ # The original app preprocesses BEFORE routing
97
+ preprocessed_pil = preprocess_for_ocr(image_bytes)
98
+ if preprocessed_pil is None: return "Error during preprocessing", "", ""
99
+
100
+ # 2. ROUTE (Using preprocessed image)
101
+ mode, ar, bc = original_classify_input(preprocessed_pil)
102
+ status = f"Mode: {mode.upper()} (AR: {ar:.2f}, Blobs: {bc})"
103
 
 
 
 
 
 
 
 
 
104
  try:
105
  if mode == "character" and cnn_engine.available:
106
+ result = cnn_engine.predict(preprocessed_pil)
107
+ return result["text"], status, "CNN Classifier"
108
  else:
109
+ pixel_values = processor(preprocessed_pil, return_tensors="pt").pixel_values.to(device)
 
110
  with torch.no_grad():
111
+ outputs = model.generate(pixel_values, num_beams=4, max_length=128, early_stopping=True)
 
 
 
 
 
112
  text = processor.batch_decode(outputs, skip_special_tokens=True)[0]
113
+ return text, status, "TrOCR + LoRA"
114
  except Exception as e:
115
+ return f"Error: {str(e)}", "Failed", "None"
116
 
117
  # --- UI ---
118
+ with gr.Blocks(css=".gradio-container {background: #0f172a; color: white;}") as demo:
119
+ gr.Markdown("# 🕉️ DevGen OCR Full Fidelity Suite")
120
+ with gr.Row():
 
 
 
 
 
 
121
  with gr.Column():
122
+ inp = gr.Image(type="pil", label="Input Image")
123
+ btn = gr.Button("Recognize", variant="primary")
 
124
  with gr.Column():
125
+ out = gr.Textbox(label="Result", interactive=False)
126
+ lbl = gr.Label(label="Engine Status")
127
+ eng = gr.Textbox(label="Model", interactive=False)
128
 
129
+ btn.click(predict, [inp], [out, lbl, eng])
130
 
131
  if __name__ == "__main__":
132
  demo.launch()
preprocessing.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from PIL import Image
4
+ import io
5
+
6
+ def bytes_to_cv2(image_bytes: bytes) -> np.ndarray:
7
+ nparr = np.frombuffer(image_bytes, np.uint8)
8
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
9
+ return img
10
+
11
+ def cv2_to_pil(img: np.ndarray) -> Image.Image:
12
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
13
+ return Image.fromarray(rgb)
14
+
15
+ def crop_to_foreground(img: np.ndarray, padding_ratio: float = 0.18) -> np.ndarray:
16
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img
17
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
18
+ _, mask = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
19
+ kernel = np.ones((3, 3), np.uint8)
20
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
21
+ mask = cv2.dilate(mask, kernel, iterations=1)
22
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
23
+ if not contours: return img
24
+ h, w = gray.shape[:2]
25
+ min_area = max(12, int(h * w * 0.0001))
26
+ boxes = [cv2.boundingRect(contour) for contour in contours if cv2.contourArea(contour) >= min_area]
27
+ if not boxes: return img
28
+ x1, y1, x2, y2 = min(x for x,_,_,_ in boxes), min(y for _,y,_,_ in boxes), max(x+bw for x,_,bw,_ in boxes), max(y+bh for _,y,_,bh in boxes)
29
+ pad_x, pad_y = max(8, int((x2 - x1) * padding_ratio)), max(8, int((y2 - y1) * padding_ratio))
30
+ x1, y1, x2, y2 = max(0, x1 - pad_x), max(0, y1 - pad_y), min(w, x2 + pad_x), min(h, y2 + pad_y)
31
+ return img[y1:y2, x1:x2]
32
+
33
+ def normalize_for_model(img: np.ndarray, target_height: int = 384, target_width: int = 384) -> np.ndarray:
34
+ h, w = img.shape[:2]
35
+ scale = min(target_height / h, target_width / w)
36
+ new_h, new_w = int(h * scale), int(w * scale)
37
+ resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
38
+ canvas = np.ones((target_height, target_width, 3), dtype=np.uint8) * 255
39
+ y_offset, x_offset = (target_height - new_h) // 2, (target_width - new_w) // 2
40
+ canvas[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
41
+ return canvas
42
+
43
+ def preprocess_for_ocr(image_bytes: bytes) -> Image.Image:
44
+ img = bytes_to_cv2(image_bytes)
45
+ if img is None: return None
46
+ h, w = img.shape[:2]
47
+ aspect_ratio = w / float(h)
48
+ if aspect_ratio <= 1.55:
49
+ img = crop_to_foreground(img)
50
+ elif aspect_ratio > 2.2:
51
+ img = crop_to_foreground(img)
52
+ img = normalize_for_model(img)
53
+ return cv2_to_pil(img)