ranbac commited on
Commit
469e4bb
·
verified ·
1 Parent(s): ac29fc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -136
app.py CHANGED
@@ -1,176 +1,220 @@
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
- det_db_thresh=0.3,
40
- det_db_box_thresh=0.5,
41
- )
42
- print("Model đã sẵn sàng!")
43
-
44
- # --- 4. HÀM XỬ LÝ ẢNH NÂNG CAO ---
45
- def preprocess_red_handwriting(pil_image):
46
- """
47
- Kỹ thuật tách mực đỏ trên giấy kẻ:
48
- - Mực đỏ hấp thụ ánh sáng xanh lá (Green).
49
- - 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.
50
- """
51
- # Convert PIL sang OpenCV
52
- img = np.array(pil_image)
53
- img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
54
-
55
- # 1. Tách lấy kênh Green (Kênh hiệu quả nhất với mực đỏ)
56
- b, g, r = cv2.split(img)
57
 
58
- # 2. Tăng tương phản cục bộ (CLAHE) để làm rõ nét chữ
59
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
60
- enhanced = clahe.apply(g)
61
-
62
- # 3. Nhị phân hóa (Adaptive Threshold) để loại bỏ nền giấy và dòng kẻ mờ
63
- # Block size 21, C 10 giúp giữ lại nét chữ mà xóa nhiễu nền
64
- binary = cv2.adaptiveThreshold(
65
- enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
66
- cv2.THRESH_BINARY, 21, 10
67
- )
68
-
69
- # 4. Khử nhiễu đốm nhỏ (Denoise)
70
- clean = cv2.fastNlMeansDenoising(binary, None, 10, 7, 21)
71
 
72
- # Chuyển lại sang RGB để đưa vào Model
73
- processed_pil = Image.fromarray(cv2.cvtColor(clean, cv2.COLOR_GRAY2RGB))
74
- return processed_pil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- # --- 5. HÀM DỰ ĐOÁN CHÍNH ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def predict(image):
78
- if image is None:
79
- return None, None, "Vui lòng tải ảnh lên.", ""
80
 
81
  try:
82
- # Bước 1: Xử lý ảnh để làm rõ chữ
83
- processed_image = preprocess_red_handwriting(image)
 
84
 
85
- # Bước 2: Chạy OCR trên ảnh ĐÃ XỬ LÝ
86
- # Chuyển sang numpy array cho Paddle
87
- img_np = np.array(processed_image)
88
- result = ocr.ocr(img_np, cls=True)
89
-
90
- # Bước 3: Vẽ kết quả lên ảnh GỐC (để người dùng dễ đối chiếu)
91
- draw_img = image.copy()
92
- draw = ImageDraw.Draw(draw_img)
93
 
94
- try:
95
- font = ImageFont.truetype(FONT_PATH, 24) if FONT_PATH else ImageFont.load_default()
96
- except:
97
- font = ImageFont.load_default()
98
-
99
- texts_output = []
100
- raw_data_log = []
101
-
102
- if result and result[0]:
103
- # Sắp xếp kết quả từ trên xuống dưới theo tọa độ Y
104
- # result[0] là list các box. Mỗi item: [ [[x1,y1],...], (text, confidence) ]
105
- sorted_res = sorted(result[0], key=lambda x: x[0][0][1])
106
-
107
- for line in sorted_res:
108
- box = line[0] # Tọa độ 4 điểm
109
- txt_obj = line[1] # (Text, Score)
110
- text_content = txt_obj[0]
111
- score = txt_obj[1]
112
-
113
- # Log debug
114
- raw_data_log.append(f"Confidence: {score:.2f} | Text: {text_content}")
115
-
116
- # Chỉ lấy kết quả có độ tin cậy > 0.5 để lọc rác
117
- if score > 0.5:
118
- texts_output.append(text_content)
119
-
120
- # Vẽ khung và chữ
121
- poly = [(p[0], p[1]) for p in box]
122
- draw.polygon(poly, outline="red", width=2)
123
-
124
- # Vẽ nền cho chữ để dễ đọc
125
- if hasattr(draw, "textbbox"):
126
- bbox = draw.textbbox((poly[0][0], poly[0][1]-25), text_content, font=font)
127
- draw.rectangle(bbox, fill="red")
128
-
129
- draw.text((poly[0][0], poly[0][1]-25), text_content, fill="white", font=font)
130
- else:
131
- texts_output.append("Không tìm thấy văn bản nào.")
132
-
133
- final_text = "\n".join(texts_output)
134
- debug_info = "\n".join(raw_data_log)
135
-
136
- return draw_img, processed_image, final_text, debug_info
137
 
138
  except Exception as e:
139
  import traceback
140
- return image, image, f"Lỗi hệ thống: {str(e)}", traceback.format_exc()
141
-
142
- # --- 6. GIAO DIỆN GRADIO ---
143
- css = """
144
- .container { max-width: 1200px; margin: auto; }
145
- """
146
 
147
- with gr.Blocks(css=css, title="Handwriting OCR Pro") as iface:
148
- gr.Markdown("# 🧧 AI Nhận Diện Chữ Viết Tay Tiếng Trung (Bản Tối Ưu)")
149
- gr.Markdown("Hệ thống tối ưu riêng cho **Mực đỏ** trên **Giấy kẻ ngang**.")
150
-
151
- with gr.Row():
152
- with gr.Column(scale=1):
153
- input_img = gr.Image(type="pil", label="Tải ảnh gốc lên")
154
- run_btn = gr.Button("🚀 BẮT ĐẦU NHẬN DIỆN", variant="primary", size="lg")
155
-
156
- with gr.Column(scale=1):
157
- with gr.Tabs():
158
- with gr.TabItem("🖼️ Kết quả Overlay"):
159
- output_overlay = gr.Image(type="pil", label="Ảnh gốc + Chữ nhận diện")
160
- with gr.TabItem("🌑 Ảnh đã xử lý (AI Vision)"):
161
- output_processed = gr.Image(type="pil", label="Cách AI nhìn thấy ảnh này")
162
- gr.Markdown("*Đây là ảnh sau khi lọc bỏ dòng kẻ và tách mực đỏ.*")
163
 
164
  with gr.Row():
165
  with gr.Column():
166
- output_text = gr.Textbox(label="📄 Văn bản trích xuất", lines=10, show_copy_button=True)
 
 
167
  with gr.Column():
168
- output_debug = gr.Textbox(label="🐞 Debug Log (Độ tin cậy)", lines=10)
169
-
170
- run_btn.click(
 
 
 
 
 
 
171
  fn=predict,
172
  inputs=input_img,
173
- outputs=[output_overlay, output_processed, output_text, output_debug]
174
  )
175
 
176
  if __name__ == "__main__":
 
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=True, 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 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Ử LÝ Ả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ử 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__":