ranbac commited on
Commit
5708279
·
verified ·
1 Parent(s): e8542ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -180
app.py CHANGED
@@ -1,220 +1,178 @@
1
  import os
2
-
3
- # --- CẤU HÌNH HỆ THỐNG ---
4
- os.environ["FLAGS_use_mkldnn"] = "0"
5
- os.environ["FLAGS_enable_mkldnn"] = "0"
6
- os.environ["DN_ENABLE_MKLDNN"] = "0"
7
- os.environ["CPP_MIN_LOG_LEVEL"] = "3"
8
-
9
- import logging
10
- import re
11
  import gradio as gr
12
  from paddleocr import PaddleOCR
13
  from PIL import Image, ImageDraw, ImageFont
14
- import numpy as np
15
- import requests
16
-
17
- # Tắt log thừa
18
- logging.getLogger("ppocr").setLevel(logging.WARNING)
19
 
20
- print("Đang khởi tạo PaddleOCR (Coordinate Sync Mode)...")
21
-
22
- try:
23
- ocr = PaddleOCR(use_textline_orientation=False, use_doc_orientation_classify=False,
24
- use_doc_unwarping=False, lang='ch')
25
- except Exception as e:
26
- print(f"Lỗi khởi tạo: {e}. Chuyển về chế độ mặc định.")
27
- ocr = PaddleOCR(lang='ch')
28
-
29
- print("Model đã sẵn sàng!")
30
 
31
- # --- TẢI FONT ---
32
  def check_and_download_font():
33
  font_path = "./simfang.ttf"
34
  if not os.path.exists(font_path):
 
35
  try:
36
  url = "https://github.com/StellarCN/scp_zh/raw/master/fonts/SimFang.ttf"
37
  r = requests.get(url, allow_redirects=True)
38
  with open(font_path, 'wb') as f:
39
  f.write(r.content)
40
- except:
 
 
41
  return None
42
  return font_path
43
 
44
  FONT_PATH = check_and_download_font()
45
 
46
- # --- HÀM VẼ ĐA NĂNG ---
47
- def universal_draw(image, raw_data, font_path):
48
- if image is None: return image
49
-
50
- # Đảm bảo image là PIL
51
- if isinstance(image, np.ndarray):
52
- image = Image.fromarray(image)
53
-
54
- # Copy để vẽ
55
- canvas = image.copy()
56
- draw = ImageDraw.Draw(canvas)
57
-
58
- try:
59
- font_size = 24
60
- font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
61
- except:
62
- font = ImageFont.load_default()
63
 
64
- # Hàm parse box
65
- def parse_box(b):
66
- try:
67
- if hasattr(b, 'tolist'): b = b.tolist()
68
- if len(b) > 0 and isinstance(b[0], list): return [tuple(p) for p in b]
69
- if len(b) == 4 and isinstance(b[0], (int, float)):
70
- return [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])]
71
- return None
72
- except: return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- items_to_draw = []
 
 
 
 
 
75
 
76
- # Logic tìm box/text
77
- # Ưu tiên cấu trúc PaddleX: rec_texts + dt_polys
78
- processed = False
79
- if isinstance(raw_data, list) and len(raw_data) > 0 and isinstance(raw_data[0], dict):
80
- data_dict = raw_data[0]
81
- texts = data_dict.get('rec_texts')
82
- boxes = data_dict.get('dt_polys', data_dict.get('rec_polys', data_dict.get('dt_boxes')))
83
-
84
- if texts and boxes and isinstance(texts, list) and isinstance(boxes, list):
85
- for i in range(min(len(texts), len(boxes))):
86
- txt = texts[i]
87
- box = parse_box(boxes[i])
88
- if box and txt: items_to_draw.append((box, txt))
89
- processed = True
90
-
91
- # Fallback Logic
92
- if not processed:
93
- def hunt(data):
94
- if isinstance(data, dict):
95
- box = None; text = None
96
- for k in ['points', 'box', 'dt_boxes', 'poly']:
97
- if k in data: box = parse_box(data[k]); break
98
- for k in ['transcription', 'text', 'rec_text', 'label']:
99
- if k in data: text = data[k]; break
100
- if box and text: items_to_draw.append((box, text)); return
101
- for v in data.values(): hunt(v)
102
- elif isinstance(data, (list, tuple)):
103
- if len(data) == 2 and isinstance(data[0], list) and len(data[0]) == 4:
104
- box = parse_box(data[0])
105
- txt_obj = data[1]
106
- text = txt_obj[0] if isinstance(txt_obj, (list, tuple)) else txt_obj
107
- if box and isinstance(text, str): items_to_draw.append((box, text)); return
108
- for item in data: hunt(item)
109
- hunt(raw_data)
110
-
111
- # Vẽ
112
- for box, txt in items_to_draw:
113
- try:
114
- # Vẽ khung đỏ
115
- draw.polygon(box, outline="red", width=3)
116
- # Vẽ chữ
117
- txt_x, txt_y = box[0]
118
- if hasattr(draw, "textbbox"):
119
- text_bbox = draw.textbbox((txt_x, txt_y), txt, font=font, anchor="lb")
120
- draw.rectangle(text_bbox, fill="red")
121
- draw.text((txt_x, txt_y), txt, fill="white", font=font, anchor="lb")
122
- else:
123
- draw.text((txt_x, txt_y - font_size), txt, fill="white", font=font)
124
- except: continue
125
-
126
- return canvas
127
-
128
- # --- HÀM XỬ LÝ TEXT ---
129
- def deep_extract_text(data):
130
- found_texts = []
131
- if isinstance(data, str):
132
- if len(data.strip()) > 0: return [data]
133
- return []
134
- if isinstance(data, (list, tuple)):
135
- for item in data: found_texts.extend(deep_extract_text(item))
136
- elif isinstance(data, dict):
137
- for val in data.values(): found_texts.extend(deep_extract_text(val))
138
- elif hasattr(data, '__dict__'): found_texts.extend(deep_extract_text(data.__dict__))
139
- return found_texts
140
-
141
- def clean_text_result(text_list):
142
- cleaned = []
143
- block_list = ['min', 'max', 'general', 'header', 'footer', 'structure']
144
- for t in text_list:
145
- t = t.strip()
146
- if len(t) < 2 and not any(u'\u4e00' <= c <= u'\u9fff' for c in t): continue
147
- if t.lower().endswith(('.ttf', '.json', '.pdparams', '.yml', '.log')): continue
148
- if t.lower() in block_list: continue
149
- if not re.search(r'[\w\u4e00-\u9fff]', t): continue
150
- cleaned.append(t)
151
- return cleaned
152
-
153
- # --- MAIN PREDICT ---
154
  def predict(image):
155
- if image is None: return None, "Chưa có ảnh.", "No Data"
 
156
 
157
  try:
158
- # Chuẩn bị ảnh đầu vào
159
- original_pil = image.copy() if isinstance(image, Image.Image) else Image.fromarray(image).copy()
160
- image_np = np.array(image)
161
-
162
- # 1. OCR
163
- raw_result = ocr.ocr(image_np)
164
 
165
- # 2. XỬ ẢNH ĐỂ VẼ (KEY FIX: Lấy ảnh từ Preprocessor nếu có)
166
- target_image_for_drawing = original_pil
 
 
 
 
 
 
167
 
168
- # Kiểm tra xem Paddle có chỉnh sửa ảnh không (dựa vào key 'doc_preprocessor_res')
169
- if isinstance(raw_result, list) and len(raw_result) > 0 and isinstance(raw_result[0], dict):
170
- if 'doc_preprocessor_res' in raw_result[0]:
171
- proc_res = raw_result[0]['doc_preprocessor_res']
172
- # Nếu có ảnh đầu ra đã chỉnh sửa (output_img)
173
- if 'output_img' in proc_res:
174
- print("Phát hiện ảnh đã qua xử lý hình học. Đang đồng bộ tọa độ...")
175
- numpy_img = proc_res['output_img']
176
- target_image_for_drawing = Image.fromarray(numpy_img)
177
-
178
- # 3. Vẽ lên ảnh ĐÚNG (Target Image)
179
- annotated_image = universal_draw(target_image_for_drawing, raw_result, FONT_PATH)
180
-
181
- # 4. Xử lý Text
182
- all_texts = deep_extract_text(raw_result)
183
- final_texts = clean_text_result(all_texts)
184
- text_output = "\n".join(final_texts) if final_texts else "Không tìm thấy văn bản."
185
-
186
- # Debug Info
187
- debug_str = str(raw_result)[:1000]
188
- debug_info = f"Used Image Source: {'Preprocessed' if target_image_for_drawing != original_pil else 'Original'}\nData Preview:\n{debug_str}..."
189
-
190
- return annotated_image, text_output, debug_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  except Exception as e:
193
  import traceback
194
- return image, f"Lỗi: {str(e)}", traceback.format_exc()
 
 
 
 
 
195
 
196
- # --- GIAO DIỆN ---
197
- with gr.Blocks(title="PaddleOCR Perfect Overlay") as iface:
198
- gr.Markdown("## PaddleOCR Chinese - High Precision Overlay")
199
 
200
  with gr.Row():
201
- with gr.Column():
202
- input_img = gr.Image(type="pil", label="Input Image")
203
- submit_btn = gr.Button("RUN OCR", variant="primary")
204
 
205
- with gr.Column():
206
  with gr.Tabs():
207
- with gr.TabItem("🖼️ Kết quả Khớp Tọa Độ"):
208
- output_img = gr.Image(type="pil", label="Overlay Result")
209
- with gr.TabItem("📝 Văn bản"):
210
- output_txt = gr.Textbox(label="Text Content", lines=15)
211
- with gr.TabItem("🐞 Debug"):
212
- output_debug = gr.Textbox(label="Debug Info", lines=15)
213
-
214
- submit_btn.click(
 
 
 
 
 
215
  fn=predict,
216
  inputs=input_img,
217
- outputs=[output_img, output_txt, output_debug]
218
  )
219
 
220
  if __name__ == "__main__":
 
1
  import os
2
+ import cv2
3
+ import numpy as np
4
+ import requests
 
 
 
 
 
 
5
  import gradio as gr
6
  from paddleocr import PaddleOCR
7
  from PIL import Image, ImageDraw, ImageFont
 
 
 
 
 
8
 
9
+ # --- 1. CẤU HÌNH HỆ THỐNG ---
10
+ os.environ["FLAGS_use_mkldnn"] = "0"
11
+ os.environ["FLAGS_enable_mkldnn"] = "0"
12
+ os.environ["CPP_MIN_LOG_LEVEL"] = "3"
 
 
 
 
 
 
13
 
14
+ # --- 2. TẢI FONT CHỮ TRUNG QUỐC ---
15
  def check_and_download_font():
16
  font_path = "./simfang.ttf"
17
  if not os.path.exists(font_path):
18
+ print("Đang tải font SimFang để hiển thị tiếng Trung...")
19
  try:
20
  url = "https://github.com/StellarCN/scp_zh/raw/master/fonts/SimFang.ttf"
21
  r = requests.get(url, allow_redirects=True)
22
  with open(font_path, 'wb') as f:
23
  f.write(r.content)
24
+ print("Đã tải font thành công!")
25
+ except Exception as e:
26
+ print(f"Lỗi tải font: {e}")
27
  return None
28
  return font_path
29
 
30
  FONT_PATH = check_and_download_font()
31
 
32
+ # --- 3. KHỞI TẠO PADDLE OCR (TỐI ƯU CHO CHỮ VIẾT TAY) ---
33
+ print("Đang khởi tạo PaddleOCR...")
34
+ # det_db_thresh=0.3: Giảm ngưỡng để bắt nét mỏng
35
+ # use_angle_cls=True: Tự động xoay ảnh nếu chữ bị nghiêng
36
+ ocr = PaddleOCR(
37
+ use_angle_cls=True,
38
+ lang='ch',
39
+ show_log=False,
40
+ det_db_thresh=0.3,
41
+ det_db_box_thresh=0.5,
42
+ use_gpu=False # Đặt True nếu bạn có GPU
43
+ )
44
+ print("Model đã sẵn sàng!")
 
 
 
 
45
 
46
+ # --- 4. HÀM XỬ LÝ ẢNH NÂNG CAO ---
47
+ def preprocess_red_handwriting(pil_image):
48
+ """
49
+ Kỹ thuật tách mực đỏ trên giấy kẻ:
50
+ - Mực đỏ hấp thụ ánh sáng xanh (Green).
51
+ - Trong kênh Green, mực đỏ sẽ rất tối (gần đen), còn nền trắng/kẻ xanh sẽ sáng.
52
+ """
53
+ # Convert PIL sang OpenCV
54
+ img = np.array(pil_image)
55
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
56
+
57
+ # 1. Tách lấy kênh Green (Kênh hiệu quả nhất với mực đỏ)
58
+ b, g, r = cv2.split(img)
59
+
60
+ # 2. Tăng tương phản cục bộ (CLAHE) để làm rõ nét chữ
61
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
62
+ enhanced = clahe.apply(g)
63
+
64
+ # 3. Nhị phân hóa (Adaptive Threshold) để loại bỏ nền giấy và dòng kẻ mờ
65
+ # Block size 21, C 10 giúp giữ lại nét chữ mà xóa nhiễu nền
66
+ binary = cv2.adaptiveThreshold(
67
+ enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
68
+ cv2.THRESH_BINARY, 21, 10
69
+ )
70
 
71
+ # 4. Khử nhiễu đốm nhỏ (Denoise)
72
+ clean = cv2.fastNlMeansDenoising(binary, None, 10, 7, 21)
73
+
74
+ # Chuyển lại sang RGB để đưa vào Model
75
+ processed_pil = Image.fromarray(cv2.cvtColor(clean, cv2.COLOR_GRAY2RGB))
76
+ return processed_pil
77
 
78
+ # --- 5. HÀM DỰ ĐOÁN CHÍNH ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def predict(image):
80
+ if image is None:
81
+ return None, None, "Vui lòng tải ảnh lên.", ""
82
 
83
  try:
84
+ # Bước 1: Xử lý ảnh để làm rõ chữ
85
+ processed_image = preprocess_red_handwriting(image)
 
 
 
 
86
 
87
+ # Bước 2: Chạy OCR trên ảnh ĐÃ XỬ
88
+ # Chuyển sang numpy array cho Paddle
89
+ img_np = np.array(processed_image)
90
+ result = ocr.ocr(img_np, cls=True)
91
+
92
+ # Bước 3: Vẽ kết quả lên ảnh GỐC (để người dùng dễ đối chiếu)
93
+ draw_img = image.copy()
94
+ draw = ImageDraw.Draw(draw_img)
95
 
96
+ try:
97
+ font = ImageFont.truetype(FONT_PATH, 24) if FONT_PATH else ImageFont.load_default()
98
+ except:
99
+ font = ImageFont.load_default()
100
+
101
+ texts_output = []
102
+ raw_data_log = []
103
+
104
+ if result and result[0]:
105
+ # Sắp xếp kết quả từ trên xuống dưới theo tọa độ Y
106
+ # result[0] là list các box. Mỗi item: [ [[x1,y1],...], (text, confidence) ]
107
+ sorted_res = sorted(result[0], key=lambda x: x[0][0][1])
108
+
109
+ for line in sorted_res:
110
+ box = line[0] # Tọa độ 4 điểm
111
+ txt_obj = line[1] # (Text, Score)
112
+ text_content = txt_obj[0]
113
+ score = txt_obj[1]
114
+
115
+ # Log debug
116
+ raw_data_log.append(f"Confidence: {score:.2f} | Text: {text_content}")
117
+
118
+ # Chỉ lấy kết quả có độ tin cậy > 0.5 để lọc rác
119
+ if score > 0.5:
120
+ texts_output.append(text_content)
121
+
122
+ # Vẽ khung và chữ
123
+ poly = [(p[0], p[1]) for p in box]
124
+ draw.polygon(poly, outline="red", width=2)
125
+
126
+ # Vẽ nền cho chữ để dễ đọc
127
+ if hasattr(draw, "textbbox"):
128
+ bbox = draw.textbbox((poly[0][0], poly[0][1]-25), text_content, font=font)
129
+ draw.rectangle(bbox, fill="red")
130
+
131
+ draw.text((poly[0][0], poly[0][1]-25), text_content, fill="white", font=font)
132
+ else:
133
+ texts_output.append("Không tìm thấy văn bản nào.")
134
+
135
+ final_text = "\n".join(texts_output)
136
+ debug_info = "\n".join(raw_data_log)
137
+
138
+ return draw_img, processed_image, final_text, debug_info
139
 
140
  except Exception as e:
141
  import traceback
142
+ return image, image, f"Lỗi hệ thống: {str(e)}", traceback.format_exc()
143
+
144
+ # --- 6. GIAO DIỆN GRADIO ---
145
+ css = """
146
+ .container { max-width: 1200px; margin: auto; }
147
+ """
148
 
149
+ with gr.Blocks(css=css, title="Handwriting OCR Pro") as iface:
150
+ gr.Markdown("# 🧧 AI Nhận Diện Chữ Viết Tay Tiếng Trung (Bản Tối Ưu)")
151
+ gr.Markdown("Hệ thống tối ưu riêng cho **Mực đỏ** trên **Giấy kẻ ngang**.")
152
 
153
  with gr.Row():
154
+ with gr.Column(scale=1):
155
+ input_img = gr.Image(type="pil", label="Tải ảnh gốc lên")
156
+ run_btn = gr.Button("🚀 BẮT ĐẦU NHẬN DIỆN", variant="primary", size="lg")
157
 
158
+ with gr.Column(scale=1):
159
  with gr.Tabs():
160
+ with gr.TabItem("🖼️ Kết quả Overlay"):
161
+ output_overlay = gr.Image(type="pil", label="Ảnh gốc + Chữ nhận diện")
162
+ with gr.TabItem("🌑 Ảnh đã xử lý (AI Vision)"):
163
+ output_processed = gr.Image(type="pil", label="Cách AI nhìn thấy ảnh này")
164
+ gr.Markdown("*Đây là ảnh sau khi lọc bỏ dòng kẻ và tách mực đỏ.*")
165
+
166
+ with gr.Row():
167
+ with gr.Column():
168
+ output_text = gr.Textbox(label="📄 Văn bản trích xuất", lines=10, show_copy_button=True)
169
+ with gr.Column():
170
+ output_debug = gr.Textbox(label="🐞 Debug Log (Độ tin cậy)", lines=10)
171
+
172
+ run_btn.click(
173
  fn=predict,
174
  inputs=input_img,
175
+ outputs=[output_overlay, output_processed, output_text, output_debug]
176
  )
177
 
178
  if __name__ == "__main__":