manishw7 commited on
Commit
a00b760
·
1 Parent(s): 11f7e9d

Restore: 1:1 copy of original backend routing and preprocessing logic

Browse files
Files changed (1) hide show
  1. app.py +85 -92
app.py CHANGED
@@ -5,7 +5,7 @@ import numpy as np
5
  import cv2
6
  from PIL import Image
7
  from peft import PeftModel
8
- from transformers import AutoTokenizer, TrOCRProcessor, ViTImageProcessor, VisionEncoderDecoderModel
9
  from cnn_model import CharacterClassifier
10
 
11
  # --- CONFIGURATION ---
@@ -16,16 +16,9 @@ CNN_MODEL_PATH = "devanagari-cnn-classifier.pt"
16
  IS_SPACE = "SPACE_ID" in os.environ
17
  device = "cuda" if torch.cuda.is_available() else "cpu"
18
 
19
- # --- MODEL INITIALIZATION ---
20
- print(f"System: Initializing DevGen OCR (Env: {'Hugging Face Space' if IS_SPACE else 'Local'})")
21
-
22
- try:
23
- processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
24
- except Exception:
25
- image_processor = ViTImageProcessor.from_pretrained("google/vit-base-patch16-224-in21k")
26
- tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
27
- processor = TrOCRProcessor(image_processor=image_processor, tokenizer=tokenizer)
28
-
29
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
30
  model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
31
  model.to(device)
@@ -33,99 +26,99 @@ model.eval()
33
 
34
  cnn_engine = CharacterClassifier(model_path=CNN_MODEL_PATH, device=device)
35
 
36
- # --- REFINED ROUTING LOGIC ---
37
- def classify_input(image):
38
- """Robustly detect if the image is a single character or a word."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  gray = image.convert("L")
40
  arr = np.array(gray)
41
-
42
- # Binarize to find the ink
43
  threshold = min(arr.mean() * 0.75, 200)
44
  binary = (arr < threshold).astype(np.uint8)
45
 
46
- # Find bounding box of ink
47
- coords = np.column_stack(np.where(binary > 0))
48
- if len(coords) == 0:
49
- return "word", 0.5, "empty"
50
-
51
- y0, x0 = coords.min(axis=0)
52
- y1, x1 = coords.max(axis=0)
53
 
54
- w = x1 - x0 + 1
55
- h = y1 - y0 + 1
 
56
  aspect_ratio = w / max(h, 1)
 
57
 
58
- # Logic: Words in Devanagari are wide. Characters are nearly square.
59
- # threshold 1.5 is the sweet spot for Devanagari characters vs words.
60
- if aspect_ratio > 1.5:
61
- return "word", 0.9, f"Wide (AR: {aspect_ratio:.1f})"
62
- return "character", 0.9, f"Square (AR: {aspect_ratio:.1f})"
63
-
64
- def smart_predict(image, manual_mode):
65
- if image is None:
66
- return None, "Upload an image.", "None"
67
 
68
- try:
69
- # Determine mode
70
- if manual_mode == "Automatic":
71
- mode, conf, reason = classify_input(image)
72
- else:
73
- mode = manual_mode.lower()
74
- conf, reason = 1.0, "Manual override"
75
-
76
- status = f"Mode: {mode.upper()} | {reason}"
77
-
78
- if mode == "character" and cnn_engine.available:
79
- result = cnn_engine.predict(image)
80
- return result["text"], status, "CNN Classifier"
81
- else:
82
- # Word Recognition Pipeline (TrOCR)
83
- image_rgb = image.convert("RGB")
84
- pixel_values = processor(image_rgb, return_tensors="pt").pixel_values.to(device)
85
- with torch.no_grad():
86
- generated_ids = model.base_model.generate(
87
- pixel_values=pixel_values,
88
- num_beams=4,
89
- max_new_tokens=64,
90
- early_stopping=True,
91
- decoder_start_token_id=model.config.decoder_start_token_id
92
- )
93
- text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
94
- return text, status, "TrOCR + LoRA"
95
-
96
- except Exception as e:
97
- return f"Error: {str(e)}", "System failure", "Error"
98
 
99
- # --- PREMIUM UI ---
100
- CSS = """
101
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600&family=Inter:wght@400;500&display=swap');
102
- .gradio-container { background: #0f172a !important; color: white !important; font-family: 'Inter', sans-serif !important; }
103
- .glass-card { background: rgba(30, 41, 59, 0.7) !important; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); border-radius: 20px; padding: 25px; box-shadow: 0 20px 40px rgba(0,0,0,0.3); }
104
- h1 { font-family: 'Outfit', sans-serif; font-size: 2.5rem; background: linear-gradient(90deg, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-fill-color: transparent; margin-bottom: 0.5rem; }
105
- .output-box { font-size: 2rem !important; text-align: center; background: rgba(0,0,0,0.2) !important; border-radius: 12px; border: 1px solid #475569 !important; }
106
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- with gr.Blocks(css=CSS, theme=gr.themes.Default()) as demo:
109
- with gr.Column(elem_classes="glass-card"):
110
- gr.Markdown("# 🕉️ DevGen Smart OCR")
111
- gr.Markdown("Automatic character and word recognition suite.")
112
-
113
- with gr.Row():
114
- with gr.Column():
115
- input_img = gr.Image(type="pil", label="Input Image")
116
- mode_selector = gr.Radio(["Automatic", "Word", "Character"], value="Automatic", label="Recognition Mode")
117
- submit_btn = gr.Button("Recognize", variant="primary")
118
-
119
- with gr.Column():
120
- output_text = gr.Textbox(label="Recognition Result", elem_classes="output-box")
121
- status_msg = gr.Markdown("Ready.")
122
- model_info = gr.Textbox(label="Model Engine", interactive=False)
123
 
124
- submit_btn.click(
125
- fn=smart_predict,
126
- inputs=[input_img, mode_selector],
127
- outputs=[output_text, status_msg, model_info]
128
- )
129
 
130
  if __name__ == "__main__":
131
  demo.launch()
 
5
  import cv2
6
  from PIL import Image
7
  from peft import PeftModel
8
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel
9
  from cnn_model import CharacterClassifier
10
 
11
  # --- CONFIGURATION ---
 
16
  IS_SPACE = "SPACE_ID" in os.environ
17
  device = "cuda" if torch.cuda.is_available() else "cpu"
18
 
19
+ # --- INITIALIZATION ---
20
+ print(f"System: Restoring Original DevGen Logic...")
21
+ processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
 
 
 
 
 
 
 
22
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
23
  model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
24
  model.to(device)
 
26
 
27
  cnn_engine = CharacterClassifier(model_path=CNN_MODEL_PATH, device=device)
28
 
29
+ # --- ORIGINAL ROUTING LOGIC (1:1 from image_router.py) ---
30
+ def _flood_fill(binary, visited, start_y, start_x, h, w):
31
+ stack = [(start_y, start_x)]
32
+ size = 0
33
+ while stack:
34
+ y, x = stack.pop()
35
+ if y < 0 or y >= h or x < 0 or x >= w or visited[y, x] or not binary[y, x]:
36
+ continue
37
+ visited[y, x] = True
38
+ size += 1
39
+ stack.extend([(y + 1, x), (y - 1, x), (y, x + 1), (y, x - 1)])
40
+ return size
41
+
42
+ def _count_blobs(binary, min_size=10):
43
+ h, w = binary.shape
44
+ visited = np.zeros_like(binary, dtype=bool)
45
+ count = 0
46
+ for y in range(h):
47
+ for x in range(w):
48
+ if binary[y, x] and not visited[y, x]:
49
+ size = _flood_fill(binary, visited, y, x, h, w)
50
+ if size >= min_size:
51
+ count += 1
52
+ return count
53
+
54
+ def original_classify_input(image):
55
  gray = image.convert("L")
56
  arr = np.array(gray)
 
 
57
  threshold = min(arr.mean() * 0.75, 200)
58
  binary = (arr < threshold).astype(np.uint8)
59
 
60
+ rows = np.any(binary, axis=1)
61
+ cols = np.any(binary, axis=0)
62
+ if not rows.any() or not cols.any():
63
+ return "character", "no_ink"
 
 
 
64
 
65
+ rmin, rmax = np.where(rows)[0][[0, -1]]
66
+ cmin, cmax = np.where(cols)[0][[0, -1]]
67
+ w, h = cmax - cmin + 1, rmax - rmin + 1
68
  aspect_ratio = w / max(h, 1)
69
+ blob_count = _count_blobs(binary, min_size=max(binary.size * 0.001, 10))
70
 
71
+ # decision logic (exact copy)
72
+ is_character = True
73
+ if aspect_ratio > 2.5: is_character = False
74
+ elif aspect_ratio > 1.8 and blob_count >= 3: is_character = False
75
+ elif blob_count >= 4: is_character = False
76
+ elif aspect_ratio < 1.3 and blob_count <= 2: is_character = True
77
+ elif blob_count == 1 and aspect_ratio < 1.5: is_character = True
78
+ elif aspect_ratio > 1.6: is_character = False
 
79
 
80
+ return "character" if is_character else "word", f"AR: {aspect_ratio:.2f}, Blobs: {blob_count}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ # --- MAIN INFERENCE ---
83
+ def predict(image, manual_mode):
84
+ if image is None: return None, "Upload image.", ""
85
+
86
+ if manual_mode == "Automatic":
87
+ mode, reason = original_classify_input(image)
88
+ else:
89
+ mode, reason = manual_mode.lower(), "Manual"
90
+
91
+ if mode == "character" and cnn_engine.available:
92
+ # Use exact CNN preprocessing from CharacterClassifier
93
+ result = cnn_engine.predict(image)
94
+ return result["text"], f"Original Logic: {mode.upper()} ({reason})", "CNN Classifier"
95
+ else:
96
+ # Standard TrOCR inference
97
+ pixel_values = processor(image.convert("RGB"), return_tensors="pt").pixel_values.to(device)
98
+ with torch.no_grad():
99
+ gen_ids = model.base_model.generate(
100
+ pixel_values=pixel_values,
101
+ num_beams=4,
102
+ max_new_tokens=64,
103
+ decoder_start_token_id=model.config.decoder_start_token_id
104
+ )
105
+ text = processor.batch_decode(gen_ids, skip_special_tokens=True)[0]
106
+ return text, f"Original Logic: {mode.upper()} ({reason})", "TrOCR + LoRA"
107
 
108
+ # --- UI ---
109
+ with gr.Blocks(theme=gr.themes.Default(), css=".gradio-container {background: #0f172a; color: white;}") as demo:
110
+ gr.Markdown("# 🕉️ DevGen OCR (Original Logic)")
111
+ with gr.Row():
112
+ with gr.Column():
113
+ inp = gr.Image(type="pil", label="Input")
114
+ mode = gr.Radio(["Automatic", "Word", "Character"], value="Automatic", label="Mode")
115
+ btn = gr.Button("Recognize", variant="primary")
116
+ with gr.Column():
117
+ out = gr.Textbox(label="Result")
118
+ status = gr.Markdown("Ready.")
119
+ eng = gr.Textbox(label="Engine", interactive=False)
 
 
 
120
 
121
+ btn.click(predict, [inp, mode], [out, status, eng])
 
 
 
 
122
 
123
  if __name__ == "__main__":
124
  demo.launch()