如何使用OpenCV实现多列扫描文档的通用文本块分割以优化Tesseract OCR效果
如何使用OpenCV实现多列扫描文档的通用文本块分割以优化Tesseract OCR效果
我完全懂你这种批量处理大量多列扫描件的痛苦——试遍了Tesseract所有PSM和OEM参数,还是会把不同列的文本强行合并成一行,手动处理几万页根本不现实。你现有的OpenCV分割脚本靠边缘检测+膨胀找轮廓,对简单2列文档有效,但遇到3列、4列或者布局复杂的情况,很容易把整个页面当成一个大轮廓,问题出在这种方法太依赖边缘的连通性,而文档列之间的空白间隙有时候不够明显。
给你一套更通用的基于投影分析的分割思路,这是处理文档列分割的经典方法,能适配不同列数的扫描件:
核心思路:利用文本的水平/垂直投影定位列边界
文档的文本块在水平和垂直方向上有明显的像素密度差异——文本行的像素总和远高于空白行,列之间的空白列像素总和远低于文本列。我们可以利用这个特性来精准定位分割点。
步骤1:图像预处理(关键!)
先把图像处理成适合分析的状态,解决扫描件的明暗不均、噪点问题:
import cv2 import numpy as np def preprocess_image(img): # 灰度化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 自适应二值化(应对明暗不均的扫描件) thresh = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 ) # 去噪+轻微膨胀,让文本行更连贯 kernel = np.ones((2,2), np.uint8) thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=1) return thresh
步骤2:水平投影定位文本区域
先找到页面上的有效文本区域,排除页眉、页脚或者大面积空白:
def get_text_region(thresh_img): # 计算水平投影:每一行的白色像素总和 horizontal_proj = np.sum(thresh_img, axis=1) # 找到有文本的行的上下边界 text_rows = np.where(horizontal_proj > 0)[0] if len(text_rows) == 0: return thresh_img, 0 top = text_rows[0] bottom = text_rows[-1] # 裁剪出文本区域 return thresh_img[top:bottom, :], top
步骤3:垂直投影找列分割点
在文本区域内,计算每一列的白色像素总和,找到列之间的空白间隙作为分割点:
def find_column_dividers(text_region, min_gap_width=10): # 计算垂直投影:每一列的白色像素总和 vertical_proj = np.sum(text_region, axis=0) # 找到空白列(像素总和为0或接近0的列) gap_columns = np.where(vertical_proj < 50)[0] # 阈值可根据图像调整 # 把连续的空白列合并成间隙 dividers = [] current_gap_start = None for idx, val in enumerate(gap_columns): if idx == 0 or gap_columns[idx] != gap_columns[idx-1] + 1: current_gap_start = val # 判断间隙宽度是否达标 if idx == len(gap_columns)-1 or gap_columns[idx+1] != gap_columns[idx] + 1: gap_width = gap_columns[idx] - current_gap_start + 1 if gap_width >= min_gap_width: # 取间隙的中间位置作为分割点 divider = (current_gap_start + gap_columns[idx]) // 2 dividers.append(divider) # 加上左右边界 dividers = [0] + dividers + [text_region.shape[1]] return dividers
步骤4:分割列并保存
根据找到的分割点,把原图切成单独的列:
def segment_columns(img, dividers, top_offset): segmented_columns = [] img_height = img.shape[0] for i in range(len(dividers)-1): left = dividers[i] right = dividers[i+1] # 从原图裁剪对应列的区域(加上之前的top_offset) column_img = img[top_offset:min(top_offset+img_height, img.shape[0]), left:right] # 过滤掉过窄的无效列 if column_img.shape[1] > 20: segmented_columns.append(column_img) return segmented_columns
完整的批量处理脚本
把这些函数整合起来,处理批量文件:
import os def process_single_file(file_path, output_dir="segmented_columns"): os.makedirs(output_dir, exist_ok=True) img = cv2.imread(file_path) if img is None: print(f"无法读取文件:{file_path}") return # 预处理 thresh = preprocess_image(img) # 获取文本区域 text_region, top = get_text_region(thresh) # 找列分割点 dividers = find_column_dividers(text_region) # 分割列 columns = segment_columns(img, dividers, top) # 保存每一列 base_name = os.path.splitext(os.path.basename(file_path))[0] for idx, col in enumerate(columns): save_path = os.path.join(output_dir, f"{base_name}_col_{idx+1}.png") cv2.imwrite(save_path, col) print(f"已完成{file_path}的分割,共生成{len(columns)}列") # 批量处理命令行传入的文件 if __name__ == "__main__": import sys if len(sys.argv) < 2: print("请传入至少一个文件路径作为参数") sys.exit(1) for file_path in sys.argv[1:]: if os.path.exists(file_path): process_single_file(file_path) else: print(f"文件不存在:{file_path}")
额外优化建议
- 处理倾斜文档:如果扫描件有倾斜,在预处理前先做纠偏,可以用OpenCV的霍夫变换检测倾斜角度,确保投影分析的准确性。
- 调整阈值:不同扫描件的清晰度、字体大小不同,需要根据实际情况调整二值化的参数、垂直投影的空白阈值、最小间隙宽度等,可以先找几个典型样本调试。
- 结合Tesseract的布局分析:分割后的单列图像可以使用Tesseract的PSM=6(假设一个统一的文本块),这样OCR准确率会更高。
- 批量OCR:分割完成后,可以写个脚本批量处理所有列图像,用Tesseract或者OCRmyPDF生成可搜索的PDF,避免重复劳动。
你之前试过遍历所有PSM和OEM参数的方法,其实Tesseract的布局分析在面对复杂多列文档时确实容易失效,先通过OpenCV做物理分割,把多列拆成单列再OCR,是更可靠的路径,尤其适合几万页的批量任务。
备注:内容来源于stack exchange,提问作者drg22114
相关产品推荐
相关产品推荐

