Harry Pham commited on
Commit
303d6ec
·
1 Parent(s): 644a224

update OCR

Browse files
Files changed (1) hide show
  1. src/inference.py +145 -68
src/inference.py CHANGED
@@ -31,34 +31,32 @@ def get_det_model(checkpoint="best.pt"):
31
  return _det_model
32
 
33
  def get_ocr_reader(backend="paddle"):
34
- """Khởi tạo OCR engine, ưu tiên PaddleOCR, fallback EasyOCR"""
35
  global _ocr_reader
36
  if _ocr_reader is not None:
37
  return _ocr_reader
38
-
39
  if backend == "paddle":
40
  try:
41
  from paddleocr import PaddleOCR
42
- print("[INFO] Initializing PaddleOCR (lang: vi, en)...")
43
  _ocr_reader = PaddleOCR(
44
- lang='vi', # tiếng Việt + tiếng Anh
45
- use_angle_cls=True, # tự động xoay ảnh
46
  use_gpu=(DEVICE == "cuda"),
47
  show_log=False,
48
- det_db_thresh=0.3,
49
- det_db_box_thresh=0.5,
50
- rec_algorithm='SVTR_LCNet' # mạnh cho chữ in
 
 
 
51
  )
52
  return _ocr_reader
53
- except ImportError:
54
- print("[WARN] PaddleOCR not installed, falling back to EasyOCR.")
55
  except Exception as e:
56
- print(f"[WARN] PaddleOCR init failed: {e}, fallback to EasyOCR.")
57
-
58
- # Fallback to EasyOCR
59
- import easyocr
60
- print("[INFO] Loading EasyOCR (vi, en)...")
61
- _ocr_reader = easyocr.Reader(["vi", "en"], gpu=(DEVICE == "cuda"), verbose=False)
62
  return _ocr_reader
63
 
64
  def preprocess_image(img_bgr, ocr_type="note"):
@@ -183,70 +181,149 @@ def group_rows(items, vertical_thresh_ratio=0.6):
183
  return [[it["text"] for it in row] for row in rows]
184
 
185
  def ocr_note(img_path, backend="paddle"):
186
- """OCR cho vùng Note, trả về chuỗi văn bản."""
187
  img = cv2.imread(img_path)
188
  if img is None:
189
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- items = ocr_with_backend(img, backend, ocr_type="note")
192
- if not items:
193
- # Thử lại với preprocessing khác (bỏ sharpen, chỉ CLAHE)
194
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
195
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
196
- gray = clahe.apply(gray)
197
- img2 = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
198
- items = ocr_with_backend(img2, backend, ocr_type="note")
199
- if not items:
200
- return ""
201
-
202
- # Sắp xếp theo y rồi x để tạo đoạn văn bản
203
- items_sorted = sorted(items, key=lambda x: (x["y"], x["x"]))
204
- lines = []
205
- current_line = []
206
- y_thresh = 12 # ngưỡng dòng
207
- for i, it in enumerate(items_sorted):
208
- if i == 0:
209
- current_line.append(it["text"])
210
- else:
211
- if abs(it["y"] - items_sorted[i-1]["y"]) < y_thresh:
212
- current_line.append(it["text"])
213
- else:
214
- lines.append(" ".join(current_line))
215
- current_line = [it["text"]]
216
- if current_line:
217
- lines.append(" ".join(current_line))
218
-
219
- # Post-processing: loại bỏ ký tự lạ, chuẩn hóa khoảng trắng
220
- clean_lines = []
221
- for line in lines:
222
- line = re.sub(r'[^\w\s\.\,\-\/\(\)]', '', line) # giữ chữ, số, dấu c��u cơ bản
223
- line = re.sub(r'\s+', ' ', line).strip()
224
- if len(line) > 1:
225
- clean_lines.append(line)
226
- return "\n".join(clean_lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
  def ocr_table(img_path, backend="paddle"):
229
- """OCR cho vùng Table, trả về dict rows và text."""
230
  img = cv2.imread(img_path)
231
  if img is None:
232
  return {"rows": [], "text": ""}
233
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  items = ocr_with_backend(img, backend, ocr_type="table")
235
  if not items:
236
- # Thử lại với ảnh gốc (không xóa đường kẻ) vì đôi khi đường kẻ giúp định vị ô
237
- img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
238
- img2 = cv2.adaptiveThreshold(img2, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
239
- cv2.THRESH_BINARY, 15, 5)
240
- img2 = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
241
- items = ocr_with_backend(img2, backend, ocr_type="table")
242
- if not items:
243
- return {"rows": [], "text": ""}
244
-
245
  rows = group_rows(items, vertical_thresh_ratio=0.6)
246
- # Chuyển rows thành text (các cột cách nhau bằng ' | ')
247
- text_lines = [" | ".join(row) for row in rows if row]
248
- return {"rows": rows, "text": "\n".join(text_lines)}
249
-
250
  def run_pipeline(image_path, output_dir="outputs",
251
  checkpoint="best.pt", conf_thresh=0.3,
252
  ocr_backend="paddle"):
 
31
  return _det_model
32
 
33
  def get_ocr_reader(backend="paddle"):
 
34
  global _ocr_reader
35
  if _ocr_reader is not None:
36
  return _ocr_reader
 
37
  if backend == "paddle":
38
  try:
39
  from paddleocr import PaddleOCR
40
+ print("[INFO] Initializing PaddleOCR (optimized for table & vi)...")
41
  _ocr_reader = PaddleOCR(
42
+ lang='vi',
43
+ use_angle_cls=True,
44
  use_gpu=(DEVICE == "cuda"),
45
  show_log=False,
46
+ det_db_thresh=0.2, # giảm ngưỡng để phát hiện nhiều chữ hơn
47
+ det_db_box_thresh=0.3,
48
+ det_db_unclip_ratio=1.5, # mở rộng box
49
+ rec_algorithm='SVTR_LCNet',
50
+ rec_image_shape="3,48,320", # hỗ trợ chữ dài
51
+ max_text_length=50
52
  )
53
  return _ocr_reader
 
 
54
  except Exception as e:
55
+ print(f"[WARN] PaddleOCR failed: {e}, fallback to EasyOCR")
56
+ backend = "easyocr"
57
+ if backend == "easyocr":
58
+ import easyocr
59
+ _ocr_reader = easyocr.Reader(["vi","en"], gpu=(DEVICE=="cuda"), verbose=False)
 
60
  return _ocr_reader
61
 
62
  def preprocess_image(img_bgr, ocr_type="note"):
 
181
  return [[it["text"] for it in row] for row in rows]
182
 
183
  def ocr_note(img_path, backend="paddle"):
 
184
  img = cv2.imread(img_path)
185
  if img is None:
186
  return ""
187
+
188
+ # Tiền xử lý mạnh: tăng độ phân giải, lọc nhiễu, làm sắc nét
189
+ h, w = img.shape[:2]
190
+ if w < 1000:
191
+ scale = 1200 / w
192
+ img = cv2.resize(img, (int(w*scale), int(h*scale)), interpolation=cv2.INTER_CUBIC)
193
+
194
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
195
+ # Sử dụng CLAHE mạnh hơn
196
+ clahe = cv2.createCLAHE(clipLimit=3.5, tileGridSize=(4,4))
197
+ gray = clahe.apply(gray)
198
+ # Khử nhiễu nhưng giữ cạnh
199
+ gray = cv2.bilateralFilter(gray, 9, 75, 75)
200
+ # Làm sắc nét
201
+ kernel_sharpen = np.array([[-1,-1,-1],[-1,9,-1],[-1,-1,-1]])
202
+ gray = cv2.filter2D(gray, -1, kernel_sharpen)
203
+ # Nhị phân hóa thích ứng
204
+ gray = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
205
+ img_proc = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
206
+
207
+ reader = get_ocr_reader(backend)
208
+ if hasattr(reader, 'ocr'):
209
+ result = reader.ocr(img_proc, cls=True)
210
+ if result and result[0]:
211
+ texts = [line[1][0].strip() for line in result[0] if line[1][1] > 0.3]
212
+ return "\n".join(texts)
213
+ else:
214
+ results = reader.readtext(img_proc, detail=1, paragraph=True)
215
+ texts = [text for (_, text, conf) in results if conf > 0.3]
216
+ return "\n".join(texts)
217
+ return ""
218
 
219
+ import cv2
220
+ import numpy as np
221
+
222
+ def detect_table_structure(img_bgr):
223
+ """
224
+ Phát hiện các ô trong bảng dựa trên đường kẻ.
225
+ Trả về: danh sách các ô (x1,y1,x2,y2) theo thứ tự từ trái sang phải, trên xuống dưới.
226
+ """
227
+ gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
228
+ # Nhị phân hóa
229
+ _, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV)
230
+ # Phát hiện đường kẻ ngang
231
+ horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1))
232
+ horizontal_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
233
+ # Phát hiện đường kẻ dọc
234
+ vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40))
235
+ vertical_lines = cv2.morphologyEx(binary, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
236
+ # Kết hợp
237
+ table_structure = cv2.add(horizontal_lines, vertical_lines)
238
+ # Tìm contours của các ô (các vùng kín)
239
+ contours, _ = cv2.findContours(table_structure, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
240
+ cells = []
241
+ for cnt in contours:
242
+ x, y, w, h = cv2.boundingRect(cnt)
243
+ if w > 10 and h > 10: # lọc ô nhỏ do nhiễu
244
+ cells.append((x, y, x+w, y+h))
245
+ # Loại bỏ trùng lặp và sắp xếp theo y trước, x sau
246
+ cells = sorted(set(cells), key=lambda r: (r[1], r[0]))
247
+ return cells
248
+
249
+ def ocr_cell(img_cell, backend="paddle"):
250
+ """OCR riêng một ô, trả về text đã làm sạch."""
251
+ if img_cell.size == 0:
252
+ return ""
253
+ # Tiền xử lý mạnh cho ô nhỏ
254
+ gray = cv2.cvtColor(img_cell, cv2.COLOR_BGR2GRAY)
255
+ # Resize nếu quá nhỏ
256
+ h, w = gray.shape
257
+ if w < 50 or h < 30:
258
+ scale = max(2.0, 100 / w)
259
+ gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
260
+ # CLAHE + threshold
261
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(4,4))
262
+ gray = clahe.apply(gray)
263
+ _, gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
264
+ # Đưa về BGR cho OCR engine
265
+ img_proc = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
266
+
267
+ reader = get_ocr_reader(backend)
268
+ if hasattr(reader, 'ocr'):
269
+ result = reader.ocr(img_proc, cls=True)
270
+ if result and result[0]:
271
+ texts = [line[1][0].strip() for line in result[0] if line[1][1] > 0.3]
272
+ return " ".join(texts)
273
+ else:
274
+ results = reader.readtext(img_proc, detail=1, paragraph=False)
275
+ texts = [text for (_, text, conf) in results if conf > 0.2]
276
+ return " ".join(texts)
277
+ return ""
278
 
279
  def ocr_table(img_path, backend="paddle"):
280
+ """Phiên bản cải tiến: phát hiện ô, OCR từng ô, ghép theo hàng."""
281
  img = cv2.imread(img_path)
282
  if img is None:
283
  return {"rows": [], "text": ""}
284
+
285
+ # Phát hiện cấu trúc bảng (các ô)
286
+ cells = detect_table_structure(img)
287
+ if not cells:
288
+ # Fallback: dùng phương pháp cũ (group theo dòng)
289
+ return ocr_table_fallback(img, backend)
290
+
291
+ # OCR từng ô
292
+ ocr_results = []
293
+ for (x1, y1, x2, y2) in cells:
294
+ # Mở rộng nhẹ để lấy toàn bộ chữ
295
+ pad = 2
296
+ y1 = max(0, y1-pad)
297
+ x1 = max(0, x1-pad)
298
+ y2 = min(img.shape[0], y2+pad)
299
+ x2 = min(img.shape[1], x2+pad)
300
+ cell_img = img[y1:y2, x1:x2]
301
+ text = ocr_cell(cell_img, backend)
302
+ if text:
303
+ ocr_results.append({
304
+ "text": text,
305
+ "x": (x1 + x2) // 2,
306
+ "y": (y1 + y2) // 2,
307
+ "box": (x1, y1, x2, y2)
308
+ })
309
+
310
+ if not ocr_results:
311
+ return {"rows": [], "text": ""}
312
+
313
+ # Nhóm thành các hàng dựa trên tọa độ y
314
+ rows = group_rows(ocr_results, vertical_thresh_ratio=0.5)
315
+ # Mỗi hàng đã được sắp xếp theo x, chỉ lấy text
316
+ rows_text = [[item["text"] for item in row] for row in rows]
317
+ return {"rows": rows_text, "text": "\n".join(" | ".join(r) for r in rows_text)}
318
+
319
+ def ocr_table_fallback(img, backend="paddle"):
320
+ """Phương pháp cũ dùng grouping đơn giản khi không phát hiện được cấu trúc bảng."""
321
  items = ocr_with_backend(img, backend, ocr_type="table")
322
  if not items:
323
+ return {"rows": [], "text": ""}
 
 
 
 
 
 
 
 
324
  rows = group_rows(items, vertical_thresh_ratio=0.6)
325
+ rows_text = [[it["text"] for it in row] for row in rows]
326
+ return {"rows": rows_text, "text": "\n".join(" | ".join(r) for r in rows_text)}
 
 
327
  def run_pipeline(image_path, output_dir="outputs",
328
  checkpoint="best.pt", conf_thresh=0.3,
329
  ocr_backend="paddle"):