OCR在普通电子文档上通常能取得很高的字符准确率,但换到扫描合同、银行回单、学术论文、招股书等复杂版面时,漏字、串行、表格错乱会集中出现。这些问题并不总是字符识别能力不足,而是版面结构没有被提前拆解。整页图像直接送入OCR,模型很难判断双栏文章应该先读左栏还是右栏,也容易把表格线与字符混在一起。解决思路是先做布局检测,把页面切分成标题、段落、表格、图片、页眉页脚等区域,再对每个区域分别识别。

复杂文档识别为什么先做布局检测
布局检测本质上是针对文档图像的区域分割与分类任务。它的输入是一张页面图像,输出是若干候选框以及每个候选框的类别,例如标题、正文、表格、图片、公式、页眉、页脚等。与通用目标检测不同,文档版面中的目标通常没有固定形状,表格可横跨半页,图片可以嵌入文字之中,标题与正文的差异更多体现在字号和位置而不是纹理特征。
如果不做布局检测,直接对整页执行OCR,会带来三个典型问题。第一,阅读顺序错误,双栏论文的左右栏可能被交替读取,形成完全混乱的文本流。第二,表格区域被当作普通段落识别,表格线、单元格分隔符和数字会被混在一起,后续无法还原结构化数据。第三,页眉、页脚、页码等噪声会插入正文,影响下游检索和摘要任务的输入质量。先做布局检测后,OCR只在区域内工作,区域边界相当于给模型提供了上下文约束,能显著降低错行和跨区域串扰。
从工程角度看,布局检测也为后续处理提供了统一的坐标框架。所有识别结果都可以携带区域类型、页码和坐标信息,便于生成结构化文档。例如表格区域单独送入表格结构识别模型,图片区域只提取图题,正文段落按阅读顺序拼接。这种拆分方式比直接训练一个超大端到端模型更可控,也更容易调试。
主流布局检测方法:从传统规则到深度模型
早期的布局分析主要使用图像处理和启发式规则。常见流程是先对图像做灰度化、二值化和形态学膨胀,让同一区域的文字粘连成块,再用连通域分析获得候选区域。然后通过投影法判断横排或竖排,根据面积、长宽比和位置判断是否为标题、正文或图片。这类方法不需要标注数据,运行速度快,对版式固定的票据和表单效果不错。但它的阈值依赖很强,换一种字体、扫描密度或页面背景,就需要重新调整参数,难以适应版式变化较大的文档。
当前主流的布局检测已经转向深度目标检测模型。Faster R-CNN、Mask R-CNN等两阶段模型在小目标和大目标混合的版面中有较好的召回率,但推理速度偏慢。YOLO系列及其衍生模型速度快,适合大批量文档解析。DETR类模型省去了锚点和NMS,结构更简洁,不过训练时需要更多数据。实际项目中,PaddleOCR提供的PP-Structure是一个比较成熟的组合方案,它把版面分析、表格识别和OCR串在一起,适合需要快速落地的场景。下面是一个基础调用示例。
from paddleocr import PPStructure
engine = PPStructure(show_log=True)
img_path = 'contract.jpg'
result = engine(img_path)
for region in result:
print(region['type'], region['bbox'])
if 'res' in region:
print(region['res'])
上述代码会输出每个区域的类型和坐标,表格区域还会返回结构化的HTML表格。需要注意的是,PP-Structure默认模型在扫描件上的表现较好,但对拍照文档或严重倾斜图像,需要先做矫正。另一个值得关注的路线是LayoutLM系列,它把文本、视觉和布局信息联合建模,适合需要语义理解的场景,例如从合同中提取关键字段,而不仅仅是识别字符。
OCR与布局检测的协同流程设计
协同流程通常包括五个阶段:图像预处理、布局检测、区域排序、分类识别、结构合并。预处理阶段处理倾斜、噪声和分辨率问题,保证布局检测输入质量。布局检测输出区域框后,不要立即识别,而是先根据阅读顺序对区域排序。对于单栏文档,可以按纵坐标从上到下排序;对于双栏文档,需要先判断栏的位置,再在每栏内部从上到下排序。简单实现时可以按y坐标分桶,再按x坐标区分左右栏。
下面给出一个简化版区域排序示例,适用于单栏或已经切分好栏的场景。
def sort_regions(regions):
# regions: list of dict, bbox is [x0, y0, x1, y1]
# 按纵坐标分桶,桶高度为20像素,再按横坐标排序
return sorted(
regions,
key=lambda r: (round(r['bbox'][1] / 20), r['bbox'][0])
)
# 示例
regions = [
{'type': 'text', 'bbox': [100, 200, 400, 240]},
{'type': 'title', 'bbox': [100, 120, 500, 170]},
{'type': 'text', 'bbox': [100, 260, 400, 300]},
]
ordered = sort_regions(regions)
for r in ordered:
print(r['type'], r['bbox'])
这个示例只是最基础的排序。真实项目中,双栏文档需要先使用栏检测结果,把左右栏拆开,否则同一行的左右两块区域会被错误地交替排列。更可靠的做法是让布局检测模型同时输出阅读顺序,或者在版面分析后增加一个排序模型。部分开源框架已经提供阅读顺序预测能力,可以减少手工规则。
分类识别阶段要依据区域类型选择不同引擎。标题、正文、脚注等文本区域使用OCR识别即可;表格区域应先做表格结构识别,再对每个单元格做OCR;图片区域通常只提取图题,不需要识别像素内容。以表格为例,如果把表格整块送给普通OCR,很可能得到一串没有行列语义的数字和文字。正确做法是使用表格识别模型,先还原单元格拓扑,再在单元格内识别内容。
工程落地中的避坑点与优化建议
第一个容易忽视的问题是图像分辨率。布局检测模型和OCR模型通常对输入尺寸敏感,扫描件如果分辨率过低,小字号文本会被漏检;分辨率过高又会增加推理时间并可能导致检测框过碎。常见做法是限制长边在1600到2000像素左右,在速度和细节之间取平衡。对于拍照文档,先做透视矫正比盲目提高分辨率更有效。
第二个问题是表格还原。表格区域建议单独走表格结构识别,不要依赖规则画线,因为很多表格只有隐式分隔线,甚至没有线。结构识别模型能够输出单元格的行列关系,再结合OCR结果生成HTML或JSON。还需要注意空单元格和合并单元格,这是业务中最常出错的地方。
第三个问题是类别不均衡和误检。页眉、页脚和页码容易被识别成正文,污染最终文本。可以在后处理阶段根据区域坐标过滤:靠近页面顶部或底部且高度较小的区域,如果被识别为正文,可以降低置信度或直接丢弃。对于标题和正文混淆,可以结合字体大小、加粗程度和多行文本高度做二次判断。
性能优化方面,如果文档量很大,可以先用轻量检测模型过滤空白页和无关区域,再对关键区域调用高精度模型。也可以把布局检测和OCR拆成两个服务,避免单个进程内显存峰值过高。工程上建议使用队列解耦,将版面分析、文本识别、表格结构化作为流水线节点,便于单独扩容。相较直接训练一个复杂端到端模型,这种模块化方案更容易定位错误来源,也更符合持续迭代的节奏。