This commit is contained in:
2026-04-23 14:10:58 +07:00
parent a899c5093f
commit 9f8e103457
5 changed files with 520 additions and 0 deletions
Binary file not shown.
Binary file not shown.
+302
View File
@@ -0,0 +1,302 @@
import os
import gc
import cv2
import numpy as np
from PIL import Image, ImageFilter, ImageEnhance
from pdf2image import convert_from_path, pdfinfo_from_path
from paddleocr import PaddleOCR
from vietocr.tool.predictor import Predictor
from vietocr.tool.config import Cfg
import torch
# ============================================================
# CONFIGURATION - Tuned for MAXIMUM ACCURACY
# ============================================================
INPUT_FOLDER = "input_pdf_folder"
OUTPUT_FOLDER = "output_txt_folder"
MODEL_FOLDER = "models"
VIETOCR_MODEL_PATH = os.path.join(MODEL_FOLDER, "vgg_transformer.pth")
# --- Quality Settings ---
DPI = 600 # Maximum practical DPI for OCR
PADDING = 8 # Pixels of padding around each text box
MIN_BOX_WIDTH = 15 # Minimum box width in pixels (filter noise)
MIN_BOX_HEIGHT = 10 # Minimum box height in pixels (filter noise)
MIN_BOX_AREA = 300 # Minimum box area in pixels² (filter tiny artifacts)
Y_TOLERANCE = 20 # Pixels tolerance for same-line grouping (higher DPI = more tolerance)
# --- PaddleOCR Detection Tuning ---
# Lower thresholds = catch more text, but may also catch noise
# Higher thresholds = miss some text, but results are cleaner
DET_DB_THRESH = 0.25 # Binarization threshold (default 0.3, lower = more sensitive)
DET_DB_BOX_THRESH = 0.45 # Box confidence threshold (default 0.6, lower = more boxes)
DET_DB_UNCLIP_RATIO = 1.8 # Box expansion ratio (default 1.5, higher = larger boxes)
DET_LIMIT_SIDE_LEN = 2560 # Max image side length for detection (default 960, higher = more detail)
def setup_directories():
"""Create necessary directories if they don't exist."""
os.makedirs(INPUT_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
os.makedirs(MODEL_FOLDER, exist_ok=True)
def download_vietocr_weights():
"""Download VietOCR weights if not present with corruption protection."""
if not os.path.exists(VIETOCR_MODEL_PATH):
print("Downloading VietOCR weights (vgg_transformer)...")
import requests
temp_path = VIETOCR_MODEL_PATH + ".tmp"
url = "https://vocr.vn/data/vietocr/vgg_transformer.pth"
try:
response = requests.get(url, stream=True)
if response.status_code == 200:
with open(temp_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Verify the file is a valid torch checkpoint
try:
torch.load(temp_path, map_location='cpu')
except Exception:
os.remove(temp_path)
raise Exception("Downloaded file is corrupted. Retrying on next run.")
os.rename(temp_path, VIETOCR_MODEL_PATH)
print("Download completed and verified successfully.")
else:
raise Exception(f"Failed to download weights. Status code: {response.status_code}")
except Exception as e:
if os.path.exists(temp_path):
os.remove(temp_path)
raise e
def preprocess_for_detection(pil_img):
"""
Station 1a: Prepare image for PaddleOCR text detection.
Uses CLAHE + denoising on grayscale for optimal box detection.
Returns a 3-channel image ready for PaddleOCR.
"""
img = np.array(pil_img)
if len(img.shape) == 3 and img.shape[2] == 3:
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
else:
gray = img
# CLAHE for contrast enhancement
clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
# Light denoising (preserve edges)
denoised = cv2.fastNlMeansDenoising(enhanced, None, 8, 7, 21)
# Convert back to 3-channel for PaddleOCR
return cv2.cvtColor(denoised, cv2.COLOR_GRAY2RGB)
def preprocess_for_recognition(pil_img):
"""
Station 1b: Prepare the ORIGINAL high-res image for VietOCR recognition.
Uses the original color/grayscale image (not the detection-preprocessed one)
with mild sharpening for best character recognition.
Returns a numpy array used for cropping individual text boxes.
"""
# Sharpen the original PIL image mildly
sharpened = pil_img.filter(ImageFilter.SHARPEN)
# Boost contrast slightly
enhancer = ImageEnhance.Contrast(sharpened)
enhanced = enhancer.enhance(1.3)
return np.array(enhanced)
def get_padded_crop(img, box, padding=PADDING):
"""
Extract a padded crop from the image based on the bounding box.
Uses perspective transform for rotated boxes to get a clean crop.
"""
box_arr = np.array(box).astype(np.float32)
# Calculate width and height of the oriented bounding box
width = int(max(
np.linalg.norm(box_arr[0] - box_arr[1]),
np.linalg.norm(box_arr[2] - box_arr[3])
))
height = int(max(
np.linalg.norm(box_arr[0] - box_arr[3]),
np.linalg.norm(box_arr[1] - box_arr[2])
))
if width < MIN_BOX_WIDTH or height < MIN_BOX_HEIGHT:
return None
if width * height < MIN_BOX_AREA:
return None
# Add padding to the destination dimensions
dst_width = width + 2 * padding
dst_height = height + 2 * padding
# Define destination points with padding offset
dst_pts = np.array([
[padding, padding],
[padding + width, padding],
[padding + width, padding + height],
[padding, padding + height]
], dtype=np.float32)
# Perspective transform to handle rotated text
M = cv2.getPerspectiveTransform(box_arr, dst_pts)
cropped = cv2.warpPerspective(
img, M, (dst_width, dst_height),
borderMode=cv2.BORDER_REPLICATE
)
return cropped
def sort_boxes(boxes):
"""
Logical Sorting & Column Handling:
Sort bounding boxes primarily by Y-coordinate and secondarily by X-coordinate
to ensure correct reading order for multi-column medical leaflets.
"""
if not boxes:
return []
# First sort purely by y-coordinate (top-left y)
boxes.sort(key=lambda b: b[0][1])
sorted_boxes = []
current_line = []
current_y = boxes[0][0][1]
for box in boxes:
y1 = box[0][1]
if abs(y1 - current_y) <= Y_TOLERANCE:
current_line.append(box)
else:
current_line.sort(key=lambda b: b[0][0])
sorted_boxes.extend(current_line)
current_line = [box]
current_y = y1
if current_line:
current_line.sort(key=lambda b: b[0][0])
sorted_boxes.extend(current_line)
return sorted_boxes
def process_pdf(pdf_path, detector, recognizer):
"""Process a single PDF file through the OCR pipeline."""
pdf_name = os.path.basename(pdf_path)
txt_filename = os.path.splitext(pdf_name)[0] + ".txt"
txt_filepath = os.path.join(OUTPUT_FOLDER, txt_filename)
try:
info = pdfinfo_from_path(pdf_path)
total_pages = info["Pages"]
with open(txt_filepath, 'w', encoding='utf-8') as f_out:
for page_num in range(1, total_pages + 1):
print(f"Processing [{pdf_name}] - Page [{page_num}/{total_pages}]...")
# Station 0: PDF to Image Conversion (600 DPI for maximum detail)
images = convert_from_path(
pdf_path, dpi=DPI,
first_page=page_num, last_page=page_num
)
if not images:
continue
pil_img = images[0]
# Station 1a: Preprocess for detection (grayscale, denoised)
detection_img = preprocess_for_detection(pil_img)
# Station 1b: Preprocess for recognition (original, sharpened)
recognition_img = preprocess_for_recognition(pil_img)
# Station 2: Text Detection (PaddleOCR)
dt_boxes = detector.ocr(detection_img, rec=False, cls=False)
if dt_boxes and len(dt_boxes) > 0 and dt_boxes[0] is not None:
boxes = dt_boxes[0]
boxes = sort_boxes(boxes)
recognized_count = 0
for box in boxes:
# Station 3: Crop from the RECOGNITION image (not detection image)
# This gives VietOCR the highest quality input
cropped_cv = get_padded_crop(recognition_img, box)
if cropped_cv is None:
continue
# Convert to PIL for VietOCR
cropped_pil = Image.fromarray(cropped_cv)
if cropped_pil.mode != 'RGB':
cropped_pil = cropped_pil.convert('RGB')
# Recognize text
text = recognizer.predict(cropped_pil)
text = text.strip()
# Filter out very short garbage text (single chars that are likely noise)
if len(text) >= 1:
f_out.write(text + "\n")
recognized_count += 1
print(f" → Detected {len(boxes)} boxes, recognized {recognized_count} text lines.")
# Memory cleanup
del images, pil_img, detection_img, recognition_img
gc.collect()
print(f"Processing [{pdf_name}] - Page [{page_num}/{total_pages}]... Done.")
except Exception as e:
print(f"Error processing {pdf_name}: {e}")
import traceback
traceback.print_exc()
def main():
setup_directories()
# Initialize PaddleOCR detector (CPU ONLY, tuned for maximum accuracy)
print("Initializing Station 2: PaddleOCR Detector (CPU, MAX ACCURACY mode)...")
detector = PaddleOCR(
use_angle_cls=True, # Enable angle classification for rotated text
lang='vi',
use_gpu=False,
show_log=False,
det_db_thresh=DET_DB_THRESH,
det_db_box_thresh=DET_DB_BOX_THRESH,
det_db_unclip_ratio=DET_DB_UNCLIP_RATIO,
det_limit_side_len=DET_LIMIT_SIDE_LEN,
use_dilation=True, # Dilate text regions for better detection
)
# Initialize VietOCR recognizer (CPU ONLY, beam search for max accuracy)
print("Initializing Station 3: VietOCR Recognizer (MAX ACCURACY mode)...")
download_vietocr_weights()
config = Cfg.load_config_from_name('vgg_transformer')
config['weights'] = VIETOCR_MODEL_PATH
config['device'] = 'cpu'
config['predictor']['beamsearch'] = True # Beam search is slower but more accurate
recognizer = Predictor(config)
# Scan input folder for PDFs
pdf_files = [f for f in os.listdir(INPUT_FOLDER) if f.lower().endswith('.pdf')]
if not pdf_files:
print(f"No PDF files found in {INPUT_FOLDER}/. Please add PDF files and run again.")
return
print(f"Found {len(pdf_files)} PDF files to process.")
print(f"Settings: DPI={DPI}, Padding={PADDING}px, BeamSearch=ON")
print(f"Detection: thresh={DET_DB_THRESH}, box_thresh={DET_DB_BOX_THRESH}, "
f"unclip={DET_DB_UNCLIP_RATIO}, max_side={DET_LIMIT_SIDE_LEN}")
print("=" * 60)
for pdf_file in pdf_files:
pdf_path = os.path.join(INPUT_FOLDER, pdf_file)
process_pdf(pdf_path, detector, recognizer)
print("=" * 60)
print("Batch processing completed.")
if __name__ == "__main__":
main()
+211
View File
@@ -0,0 +1,211 @@
S.I.S - SOFT
In Digital Health Solutions
TÀI LIỆU HƯỞNG DÂN
đầu đọc
N
Setup
TP. Hồ Chí Minh, 09/2025
1/5
L. CÁCH XỬ LÝ: ÁP DỤNG CHO HẦU HẾT MÁY QUÉT
Thực hiện:
BƯỚC 1:
Trước tiên, bạn cần chuyển máy quét sang chế độ USB-COM bằng cách quét mã bên dưới
(USB-COM)
Sau khi quét thành công, máy sẽ phát ra tiếng TíT TíT liên tục 3 lần
BƯỚC 2:
Ban cần kiểm tra tên và giao thức kết nối có đúng là USB-COM hay không bằng cách KíCH CHUỘT
TRÁI vào My Computer(?This PC ? ?với win 10), sau đó chọn Manager.
Saved to this PC
WPS PDF
Insert
de
es New Roman
13
AII
Documents
Web
Settings
People
Folders
Photos
Apps
ab
Best match
Font
Device Manager
Control panel
Xs
21
Search the web
Device Manager
Control panel
Results
Q
2 de - Seemore search results
2 the document has
Deepseek
7
Open
je due to edits you
ived from others.
Quidepi
itinue and jump to the
Quadellsupport
Q
deepseekai
Apps
Q
Github Desktop
Power Bl Desktop (January 2023)
Defrese Defragment and Optimize Drives
Remote Desktop Connection
Settings
c
Vietnamese
Hộp thoại sẽ xuất hiện như hình bên dưới. Chọn ô: Device Manager - ? mở Ports (Com 8 LPT)
2/5
Computer Management
0
x
File
Action
View
Help
Computer Management (Local
KyThuat TanPhat
Actions
'f System Tools
x
1
Audio inputs and outputs
Device Manager
x
Task Scheduler
x
Bluetooth
E] Event Viewer
Computer
More Actions
x
2
p
2
E Shared Folders
Disk drives
Local Users and Groups
1
Display adapters
X
Performance
1
Firmware
x
Device Manager
Wi Human Interface Devices
2
Storage
IDE ATA/ATAPI controllers
X
Disk Management
Keyboards
to Is Services and Applications
O Mice and other pointing devices
2
I Monitors
x
7 Network adapters
- Ports (COM 8 LPT)
Communications Port (COM1)
Bộ USB Serial Device (COM5) - 4
La Print queues
- A Printers
- DJ Processors
I Software devices
s ly Sound, video and game controllers
Say Storage controllers
- La System devices
It is 3 Universal Serial Bus controllers
Hộp thoại xuất hiện, bạn tiến hành kiểm tra COM port name, tại ví dụ này là COM5 và có chữ USB
SERIAL DEVICE ở đầu dòng (ở mối máy thì COM sẽ khác nhau có thể Coml, Com3, Com 4.
cần lưu ý phải xác định tên COM port cho chính xác, để thực hiện cấu hình tại bước sau)
3/5
BƯỚC 3:
Sau đó, bạn cần cài đặt ứng dụng, link tải: https:/drive.google.com/file/d/1XDI
jD5pL6 v IpEQJDm/Pgko2YtLPCM/view/usp-sharing
BƯỚC 4:
Ở bước này, bạn sẽ thiết lập cấu hình Encoding cho ứng dụng hãng thiết bị (đã được cài đặt ở bước 3).
Sau khi cài đặt thành công sẽ có một Application Tray tại góc phải bên dưới màn hình với
Icon chữ W (Bottom Right)
v
w
E
Al
4
120%
4v)
8:48 SA
g
Click chuột phải vào Icon chọn Settings.
About
Settings
Command
Suspend
V
Quit
w
Ef
4
120%
41)
18:49 sa
g
4/5
Tại giao diện Setting, chọn đúng theo thông số bên dưới (Bỏ TÍCH USB SCANER Để HIỆN COM
PORT)
w
WIME
coMport
USB Serial Device (com5)
Select coM port to connect from combo box.
JUSB Scanner
Port Settings
Code Page
Select the code page what encodes the label you
65001: utf8
will scan.
Window Message
Select Windows Message type what is sent to the
WMLCHAR
active window. Try other type if you have any
trouble about output.
Auto Connection
WIME searches selected coM port and try
Enable
connecting if enabled.
Control Characters
Configure how to output control characters, like
Output Settings
Carrige Return, Escape. Select option for each
characters
Delay Time
After Character
0
[msec]
Set delay time in milli-sencond after character/key
output. Set this time longer if some characters
After Key
50
[msec]
drop out.
Set Default
OK
Cancel
Lưu ý: phần COM PORT cần phải trùng tên với COM PORT NAME được xác định tại BƯớc 2
BƯỚC 5:
Nếu lỗi, hoặc không thấy đổ dữ liệu ra Notepad,word... thì hãy khởi động lại máy và kiểm tra kết quả
trên ứng dụng word, excel, notepad..
GHI CHÚ: Reset máy về chế độ quét bình thường khi không sử dụng để quét căn cước công dân thì
quét vào mã cấu hình sau.
Set All Defaults
5/5
+7
View File
@@ -0,0 +1,7 @@
pdf2image
opencv-python-headless
paddleocr
paddlepaddle
vietocr
numpy
Pillow