ranbac commited on
Commit
02a63b3
·
verified ·
1 Parent(s): 9e8099e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -170
app.py CHANGED
@@ -1,105 +1,77 @@
1
  import os
2
- import logging
3
- import re
4
- import requests
5
- import numpy as np
6
- from PIL import Image, ImageDraw, ImageFont
7
- import gradio as gr
8
- from paddleocr import PaddleOCR
9
 
10
- # --- CẤU HÌNH HỆ THỐNG (Tránh lỗi xung đột thư viện) ---
11
  os.environ["FLAGS_use_mkldnn"] = "0"
12
  os.environ["FLAGS_enable_mkldnn"] = "0"
13
  os.environ["DN_ENABLE_MKLDNN"] = "0"
14
  os.environ["CPP_MIN_LOG_LEVEL"] = "3"
15
 
16
- # Tắt log thừa của Paddle
 
 
 
 
 
 
 
 
17
  logging.getLogger("ppocr").setLevel(logging.WARNING)
18
 
19
- # --- QUẢN FONT CHỮ (Tự động tải font theo ngôn ngữ) ---
20
- FONTS_DIR = "./fonts"
21
- if not os.path.exists(FONTS_DIR):
22
- os.makedirs(FONTS_DIR)
23
 
24
- def get_font_path(lang_code):
25
- """
26
- Trả về đường dẫn font phù hợp với ngôn ngữ.
27
- Tự động tải nếu chưa có file.
28
- """
29
- font_config = {
30
- 'ch': {
31
- 'filename': 'simfang.ttf',
32
- 'url': 'https://github.com/StellarCN/scp_zh/raw/master/fonts/SimFang.ttf'
33
- },
34
- 'vi': {
35
- 'filename': 'roboto.ttf',
36
- 'url': 'https://github.com/google/fonts/raw/main/apache/roboto/Roboto-Regular.ttf'
37
- }
38
- }
39
-
40
- cfg = font_config.get(lang_code, font_config['ch']) # Mặc định là Trung nếu lỗi
41
- font_path = os.path.join(FONTS_DIR, cfg['filename'])
42
-
43
  if not os.path.exists(font_path):
44
- print(f"📡 Đang tải font cho '{lang_code}' từ internet...")
45
  try:
46
- r = requests.get(cfg['url'], allow_redirects=True)
 
 
 
47
  with open(font_path, 'wb') as f:
48
  f.write(r.content)
49
- print(f"Đã tải xong: {font_path}")
50
- except Exception as e:
51
- print(f" Lỗi tải font: {e}")
52
  return None
53
-
54
  return font_path
55
 
56
- # --- QUẢN LÝ MODEL (Cache Model) ---
57
- # Biến toàn cục lưu các model đã load để tránh khởi tạo lại nhiều lần
58
- loaded_models = {}
59
 
60
- def get_ocr_model(lang_code):
61
- """
62
- Lấy model OCR từ cache hoặc khởi tạo mới nếu chưa có.
63
- """
64
- if lang_code in loaded_models:
65
- return loaded_models[lang_code]
66
-
67
- print(f"🚀 Đang khởi tạo Model PaddleOCR ngôn ngữ: {lang_code}...")
68
- try:
69
- # use_angle_cls=True giúp xoay ảnh nếu văn bản bị nghiêng
70
- model = PaddleOCR(use_angle_cls=True, lang=lang_code, show_log=False)
71
- loaded_models[lang_code] = model
72
- print(f"✅ Model {lang_code} đã sẵn sàng!")
73
- return model
74
- except Exception as e:
75
- print(f"❌ Lỗi khởi tạo model: {e}")
76
- return None
77
-
78
- # --- HÀM VẼ KHUNG VÀ CHỮ LÊN ẢNH ---
79
  def universal_draw(image, raw_data, font_path):
80
  if image is None: return image
81
 
82
- # Chuyển về PIL Image để vẽ
83
  if isinstance(image, np.ndarray):
84
  image = Image.fromarray(image)
85
 
 
86
  canvas = image.copy()
87
  draw = ImageDraw.Draw(canvas)
88
 
89
- # Load Font
90
  try:
91
- font_size = 20
92
  font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
93
  except:
94
  font = ImageFont.load_default()
95
 
96
- # Hàm phụ: Parse tọa độ box từ nhiều định dạng dữ liệu khác nhau
97
  def parse_box(b):
98
  try:
99
  if hasattr(b, 'tolist'): b = b.tolist()
100
- # Dạng 4 điểm [[x1,y1], [x2,y2], ...]
101
  if len(b) > 0 and isinstance(b[0], list): return [tuple(p) for p in b]
102
- # Dạng [xmin, ymin, xmax, ymax]
103
  if len(b) == 4 and isinstance(b[0], (int, float)):
104
  return [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])]
105
  return None
@@ -107,46 +79,54 @@ def universal_draw(image, raw_data, font_path):
107
 
108
  items_to_draw = []
109
 
110
- # Hàm đệ quy tìm text và box trong JSON
111
- def hunt(data):
112
- if isinstance(data, dict):
113
- box = None; text = None
114
- # Tìm box
115
- for k in ['points', 'box', 'dt_boxes', 'poly']:
116
- if k in data: box = parse_box(data[k]); break
117
- # Tìm text
118
- for k in ['transcription', 'text', 'rec_text', 'label']:
119
- if k in data: text = data[k]; break
120
-
121
- if box and text: items_to_draw.append((box, text)); return
122
- for v in data.values(): hunt(v)
123
-
124
- elif isinstance(data, (list, tuple)):
125
- # Cấu trúc phổ biến của PaddleOCR v3/v4: [[[x,y]...], (text, conf)]
126
- if len(data) == 2 and isinstance(data[0], list) and len(data[0]) == 4:
127
- box = parse_box(data[0])
128
- txt_obj = data[1]
129
- text = txt_obj[0] if isinstance(txt_obj, (list, tuple)) else txt_obj
130
- if box and isinstance(text, str): items_to_draw.append((box, text)); return
131
-
132
- for item in data: hunt(item)
133
-
134
- hunt(raw_data)
135
-
136
- # Thực hiện vẽ
 
 
 
 
 
 
 
 
 
137
  for box, txt in items_to_draw:
138
  try:
139
  # Vẽ khung đỏ
140
- draw.polygon(box, outline="red", width=2)
141
-
142
- # Tính toán vị trí vẽ chữ (nền đỏ chữ trắng)
143
  txt_x, txt_y = box[0]
144
- if hasattr(draw, "textbbox"): # Pillow mới
145
- left, top, right, bottom = draw.textbbox((txt_x, txt_y), txt, font=font)
146
- draw.rectangle((left-2, top-2, right+2, bottom+2), fill="red")
147
- draw.text((txt_x, txt_y), txt, fill="white", font=font)
148
- else: # Pillow cũ
149
- draw.text((txt_x, txt_y - font_size), txt, fill="red", font=font)
150
  except: continue
151
 
152
  return canvas
@@ -161,114 +141,88 @@ def deep_extract_text(data):
161
  for item in data: found_texts.extend(deep_extract_text(item))
162
  elif isinstance(data, dict):
163
  for val in data.values(): found_texts.extend(deep_extract_text(val))
 
164
  return found_texts
165
 
166
  def clean_text_result(text_list):
167
  cleaned = []
168
- # Các từ khóa rác hệ thống cần loại bỏ
169
- block_list = ['min', 'max', 'general', 'header', 'footer', 'structure']
170
  for t in text_list:
171
  t = t.strip()
172
- # Loại bỏ text quá ngắn nếu không phải tiếng Trung/Việt
173
- if len(t) < 2 and not any(u'\u4e00' <= c <= u'\u9fff' for c in t): continue
174
  if t.lower().endswith(('.ttf', '.json', '.pdparams', '.yml', '.log')): continue
175
  if t.lower() in block_list: continue
 
176
  cleaned.append(t)
177
  return cleaned
178
 
179
- # --- MAIN PREDICT FUNCTION ---
180
- def predict(image, lang_option):
181
- if image is None: return None, "Vui lòng tải ảnh lên.", "No Data"
182
 
183
- # 1. Xác định ngôn ngữ
184
- lang_code = 'vi' if lang_option == "Tiếng Việt" else 'ch'
185
-
186
  try:
187
- # 2. Lấy Model Font
188
- ocr_model = get_ocr_model(lang_code)
189
- font_path = get_font_path(lang_code)
190
-
191
- if ocr_model is None:
192
- return image, "Lỗi khởi tạo Model PaddleOCR.", "Error init model"
193
-
194
- # 3. Chuẩn bị ảnh
195
  original_pil = image.copy() if isinstance(image, Image.Image) else Image.fromarray(image).copy()
196
  image_np = np.array(image)
197
-
198
- # 4. CHẠY OCR
199
- # cls=True: Tự động sửa góc nghiêng (ví dụ ảnh chụp ngược 180 độ)
200
- raw_result = ocr_model.ocr(image_np, cls=True)
201
-
202
- # 5. Xử lý hiển thị (Overlay)
203
- # Kiểm tra xem Paddle có unwarp ảnh (nắn thẳng) không?
204
  target_image_for_drawing = original_pil
205
 
206
- # Logic check unwarping (nếu dùng các model structure nâng cao)
207
  if isinstance(raw_result, list) and len(raw_result) > 0 and isinstance(raw_result[0], dict):
208
- if 'doc_preprocessor_res' in raw_result[0]:
209
  proc_res = raw_result[0]['doc_preprocessor_res']
 
210
  if 'output_img' in proc_res:
211
- target_image_for_drawing = Image.fromarray(proc_res['output_img'])
 
 
212
 
213
- # Vẽ kết quả lên ảnh
214
- annotated_image = universal_draw(target_image_for_drawing, raw_result, font_path)
215
 
216
- # 6. Trích xuất văn bản thuần
217
  all_texts = deep_extract_text(raw_result)
218
  final_texts = clean_text_result(all_texts)
219
  text_output = "\n".join(final_texts) if final_texts else "Không tìm thấy văn bản."
220
 
221
- # Debug info
222
- debug_info = f"Language: {lang_code}\nModel Loaded: {lang_code in loaded_models}\nFont Used: {font_path}\n\nRaw Data Preview:\n{str(raw_result)[:1000]}"
 
223
 
224
  return annotated_image, text_output, debug_info
225
 
226
  except Exception as e:
227
  import traceback
228
- return image, f"Lỗi hệ thống: {str(e)}", traceback.format_exc()
229
-
230
- # --- GIAO DIỆN GRADIO ---
231
- css = """
232
- footer {visibility: hidden}
233
- .gradio-container {min-height: 0px !important}
234
- """
235
 
236
- with gr.Blocks(title="PaddleOCR Multi-Language", css=css, theme=gr.themes.Soft()) as iface:
237
- gr.Markdown("# 🈯 PaddleOCR - Hỗ trợ Tiếng Việt & Trung")
238
- gr.Markdown("Tải ảnh lên, chọn ngôn ngữ và xem AI nhận diện tọa độ + văn bản.")
239
 
240
  with gr.Row():
241
- # CỘT TRÁI: INPUT
242
- with gr.Column(scale=1):
243
- input_img = gr.Image(type="pil", label="Ảnh đầu vào (Input)")
244
-
245
- # Dropdown chọn ngôn ngữ
246
- lang_dropdown = gr.Dropdown(
247
- choices=["Tiếng Trung (Chinese)", "Tiếng Việt"],
248
- value="Tiếng Việt",
249
- label="Chọn ngôn ngữ xử lý",
250
- info="Lần đầu chọn ngôn ngữ mới sẽ tốn chút th��i gian để tải model."
251
- )
252
-
253
- submit_btn = gr.Button("🚀 CHẠY NHẬN DIỆN (RUN OCR)", variant="primary", size="lg")
254
 
255
- # CỘT PHẢI: OUTPUT
256
- with gr.Column(scale=2):
257
  with gr.Tabs():
258
- with gr.TabItem("🖼️ Kết quả Hình ảnh"):
259
- output_img = gr.Image(type="pil", label="Ảnh đã vẽ khung")
260
- with gr.TabItem("📝 Văn bản (Text)"):
261
- output_txt = gr.Textbox(label="Nội dung trích xuất", lines=15)
262
- with gr.TabItem("🐞 Debug Info"):
263
- output_debug = gr.Textbox(label="Dữ liệu kỹ thuật", lines=15)
264
 
265
- # SỰ KIỆN CLICK
266
  submit_btn.click(
267
  fn=predict,
268
- inputs=[input_img, lang_dropdown],
269
  outputs=[output_img, output_txt, output_debug]
270
  )
271
 
272
  if __name__ == "__main__":
273
- # Server name="0.0.0.0" để có thể truy cập từ máy khác trong mạng LAN nếu cần
274
- iface.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
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) - Ngôn ngữ: Tiếng Việt...")
 
 
 
21
 
22
+ try:
23
+ # THAY ĐỔI 1: Chuyển lang='ch' thành lang='vi'
24
+ ocr = PaddleOCR(use_textline_orientation=True, use_doc_orientation_classify=False,
25
+ use_doc_unwarping=False, lang='vi')
26
+ except Exception as e:
27
+ print(f"Lỗi khởi tạo: {e}. Chuyển về chế độ mặc định.")
28
+ ocr = PaddleOCR(lang='vi')
29
+
30
+ print("Model đã sẵn sàng!")
31
+
32
+ # --- TẢI FONT (HỖ TRỢ TIẾNG VIỆT) ---
33
+ def check_and_download_font():
34
+ # THAY ĐỔI 2: Sử dụng font Roboto để hiển thị đúng dấu Tiếng Việt
35
+ font_path = "./Roboto-Regular.ttf"
 
 
 
 
 
36
  if not os.path.exists(font_path):
 
37
  try:
38
+ print("Đang tải font Roboto hỗ trợ tiếng Việt...")
39
+ # URL Font Roboto chuẩn từ Google Fonts
40
+ url = "https://github.com/google/fonts/raw/main/apache/roboto/Roboto-Regular.ttf"
41
+ r = requests.get(url, allow_redirects=True)
42
  with open(font_path, 'wb') as f:
43
  f.write(r.content)
44
+ print("Đã tải xong font.")
45
+ except:
46
+ print("Không tải được font. Sẽ sử dụng font mặc định hệ thống (có thể lỗi dấu).")
47
  return None
 
48
  return font_path
49
 
50
+ FONT_PATH = check_and_download_font()
 
 
51
 
52
+ # --- HÀM VẼ ĐA NĂNG ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def universal_draw(image, raw_data, font_path):
54
  if image is None: return image
55
 
56
+ # Đảm bảo image PIL
57
  if isinstance(image, np.ndarray):
58
  image = Image.fromarray(image)
59
 
60
+ # Copy để vẽ
61
  canvas = image.copy()
62
  draw = ImageDraw.Draw(canvas)
63
 
 
64
  try:
65
+ font_size = 24
66
  font = ImageFont.truetype(font_path, font_size) if font_path else ImageFont.load_default()
67
  except:
68
  font = ImageFont.load_default()
69
 
70
+ # Hàm parse box
71
  def parse_box(b):
72
  try:
73
  if hasattr(b, 'tolist'): b = b.tolist()
 
74
  if len(b) > 0 and isinstance(b[0], list): return [tuple(p) for p in b]
 
75
  if len(b) == 4 and isinstance(b[0], (int, float)):
76
  return [(b[0], b[1]), (b[2], b[1]), (b[2], b[3]), (b[0], b[3])]
77
  return None
 
79
 
80
  items_to_draw = []
81
 
82
+ # Logic tìm box/text
83
+ # Ưu tiên cấu trúc PaddleX: rec_texts + dt_polys
84
+ processed = False
85
+ if isinstance(raw_data, list) and len(raw_data) > 0 and isinstance(raw_data[0], dict):
86
+ data_dict = raw_data[0]
87
+ texts = data_dict.get('rec_texts')
88
+ boxes = data_dict.get('dt_polys', data_dict.get('rec_polys', data_dict.get('dt_boxes')))
89
+
90
+ if texts and boxes and isinstance(texts, list) and isinstance(boxes, list):
91
+ for i in range(min(len(texts), len(boxes))):
92
+ txt = texts[i]
93
+ box = parse_box(boxes[i])
94
+ if box and txt: items_to_draw.append((box, txt))
95
+ processed = True
96
+
97
+ # Fallback Logic
98
+ if not processed:
99
+ def hunt(data):
100
+ if isinstance(data, dict):
101
+ box = None; text = None
102
+ for k in ['points', 'box', 'dt_boxes', 'poly']:
103
+ if k in data: box = parse_box(data[k]); break
104
+ for k in ['transcription', 'text', 'rec_text', 'label']:
105
+ if k in data: text = data[k]; break
106
+ if box and text: items_to_draw.append((box, text)); return
107
+ for v in data.values(): hunt(v)
108
+ elif isinstance(data, (list, tuple)):
109
+ if len(data) == 2 and isinstance(data[0], list) and len(data[0]) == 4:
110
+ box = parse_box(data[0])
111
+ txt_obj = data[1]
112
+ text = txt_obj[0] if isinstance(txt_obj, (list, tuple)) else txt_obj
113
+ if box and isinstance(text, str): items_to_draw.append((box, text)); return
114
+ for item in data: hunt(item)
115
+ hunt(raw_data)
116
+
117
+ # Vẽ
118
  for box, txt in items_to_draw:
119
  try:
120
  # Vẽ khung đỏ
121
+ draw.polygon(box, outline="red", width=3)
122
+ # Vẽ chữ
 
123
  txt_x, txt_y = box[0]
124
+ if hasattr(draw, "textbbox"):
125
+ text_bbox = draw.textbbox((txt_x, txt_y), txt, font=font, anchor="lb")
126
+ draw.rectangle(text_bbox, fill="red")
127
+ draw.text((txt_x, txt_y), txt, fill="white", font=font, anchor="lb")
128
+ else:
129
+ draw.text((txt_x, txt_y - font_size), txt, fill="white", font=font)
130
  except: continue
131
 
132
  return canvas
 
141
  for item in data: found_texts.extend(deep_extract_text(item))
142
  elif isinstance(data, dict):
143
  for val in data.values(): found_texts.extend(deep_extract_text(val))
144
+ elif hasattr(data, '__dict__'): found_texts.extend(deep_extract_text(data.__dict__))
145
  return found_texts
146
 
147
  def clean_text_result(text_list):
148
  cleaned = []
149
+ block_list = ['min', 'max', 'general', 'header', 'footer', 'structure']
 
150
  for t in text_list:
151
  t = t.strip()
152
+ # Giữ lại nếu tự Unicode thông thường (bao gồm tiếng Việt)
153
+ if len(t) < 2 and not re.search(r'\w', t): continue
154
  if t.lower().endswith(('.ttf', '.json', '.pdparams', '.yml', '.log')): continue
155
  if t.lower() in block_list: continue
156
+ if not re.search(r'[\w\u00C0-\u1EF9]', t): continue # Regex mở rộng cho tiếng Việt
157
  cleaned.append(t)
158
  return cleaned
159
 
160
+ # --- MAIN PREDICT ---
161
+ def predict(image):
162
+ if image is None: return None, "Chưa ảnh.", "No Data"
163
 
 
 
 
164
  try:
165
+ # Chuẩn bị ảnh đầu vào
 
 
 
 
 
 
 
166
  original_pil = image.copy() if isinstance(image, Image.Image) else Image.fromarray(image).copy()
167
  image_np = np.array(image)
168
+
169
+ # 1. OCR
170
+ raw_result = ocr.ocr(image_np)
171
+
172
+ # 2. XỬ LÝ ẢNH ĐỂ VẼ (KEY FIX: Lấy ảnh từ Preprocessor nếu có)
 
 
173
  target_image_for_drawing = original_pil
174
 
175
+ # Kiểm tra xem Paddle chỉnh sửa ảnh không (dựa vào key 'doc_preprocessor_res')
176
  if isinstance(raw_result, list) and len(raw_result) > 0 and isinstance(raw_result[0], dict):
177
+ if 'doc_preprocessor_res' in raw_result[0]:
178
  proc_res = raw_result[0]['doc_preprocessor_res']
179
+ # Nếu có ảnh đầu ra đã chỉnh sửa (output_img)
180
  if 'output_img' in proc_res:
181
+ print("Phát hiện ảnh đã qua xử lý hình học. Đang đồng bộ tọa độ...")
182
+ numpy_img = proc_res['output_img']
183
+ target_image_for_drawing = Image.fromarray(numpy_img)
184
 
185
+ # 3. Vẽ lên ảnh ĐÚNG (Target Image)
186
+ annotated_image = universal_draw(target_image_for_drawing, raw_result, FONT_PATH)
187
 
188
+ # 4. Xử Text
189
  all_texts = deep_extract_text(raw_result)
190
  final_texts = clean_text_result(all_texts)
191
  text_output = "\n".join(final_texts) if final_texts else "Không tìm thấy văn bản."
192
 
193
+ # Debug Info
194
+ debug_str = str(raw_result)[:1000]
195
+ debug_info = f"Used Image Source: {'Preprocessed' if target_image_for_drawing != original_pil else 'Original'}\nData Preview:\n{debug_str}..."
196
 
197
  return annotated_image, text_output, debug_info
198
 
199
  except Exception as e:
200
  import traceback
201
+ return image, f"Lỗi: {str(e)}", traceback.format_exc()
 
 
 
 
 
 
202
 
203
+ # --- GIAO DIỆN ---
204
+ with gr.Blocks(title="PaddleOCR Perfect Overlay (Vietnamese)") as iface:
205
+ gr.Markdown("## PaddleOCR Vietnamese - High Precision Overlay")
206
 
207
  with gr.Row():
208
+ with gr.Column():
209
+ input_img = gr.Image(type="pil", label="Input Image")
210
+ submit_btn = gr.Button("RUN OCR", variant="primary")
 
 
 
 
 
 
 
 
 
 
211
 
212
+ with gr.Column():
 
213
  with gr.Tabs():
214
+ with gr.TabItem("🖼️ Kết quả Khớp Tọa Độ"):
215
+ output_img = gr.Image(type="pil", label="Overlay Result")
216
+ with gr.TabItem("📝 Văn bản"):
217
+ output_txt = gr.Textbox(label="Text Content", lines=15)
218
+ with gr.TabItem("🐞 Debug"):
219
+ output_debug = gr.Textbox(label="Debug Info", lines=15)
220
 
 
221
  submit_btn.click(
222
  fn=predict,
223
+ inputs=input_img,
224
  outputs=[output_img, output_txt, output_debug]
225
  )
226
 
227
  if __name__ == "__main__":
228
+ iface.launch(server_name="0.0.0.0", server_port=7860)