17 个 OCR 文字框,按 y 聚类成 7 行,剔掉标题和页码后剩 5 行,再按 x 聚类成 3 列, 还原出的表格 15 个格子全对。全部参数只有两个阈值:18 和 40 像素。
上一节排行程,这一节处理文档。 表格识别是办公自动化的高频需求,而它难的地方不在认字,在还原结构。
一、OCR 给的不是表格,是一堆框
很多人以为 OCR 直接吐表格。不是。它吐的是「某个位置有什么字」的列表,类似 (x=30, y=60, 研发部)。
行列结构是算出来的。这一步叫「表格结构还原」,它跟 OCR 是两件事,可以分开做、分开评。
Demo 造了 17 个框:3 个表头加 4 行 3 列的数据,再加一个标题和一个页码。
真实场景里框会更多、更乱:合并单元格、跨行表头、斜线表头、框重叠、框切碎——这些都要额外处理。
二、第一步:按 y 聚成行
把 y 坐标相近的框归成一行。Demo 用的阈值是 18 像素:相邻 y 差小于等于 18 就算同一行。 17 个框聚成 7 行:标题 1 行、表头 1 行、数据 4 行、页码 1 行。 这个阈值怎么定?看行高。行高 40 像素的话,阈值取行高的一半左右比较稳。 取太小会把一行拆成两行,取太大会把两行并成一行。 聚完之后按 x 排序,一行里的字就自然从左到右排好了。
三、第二步:剔噪声、按 x 聚成列
标题和页码各占一行,而且只有 1 个框。所以规则很简单:整行只有 1 个框的,当噪声剔掉。 Demo 剔掉 2 行,剩 5 行。这个规则有风险——真表格里也可能有「合计:100」这种独占一行的单元格。 所以实际系统还要看这一行有没有「合计」「总计」这类关键词。 然后按 x 聚列,阈值 40 像素。三个 x 值 30、180、300 相距很远,干净地聚成 3 列。 两道阈值就是这套方法的全部参数。 调错了就串行串列,而且错得很隐蔽——表格看着还挺正常,就是内容错位了。
四、15 个格子全对,但别高兴太早
还原出的表格 15 个格子全部正确。 但这个 15 全对是我自己造数据时就注定的事——坐标是我给的,间隔是我设的。 真实 OCR 的输出会带坐标误差(±2 像素很常见),还会漏字、错字、把「38」认成「3B」。 这些误差都会传导到结构还原这一步。 所以要评这套方法,得分开评两段:识别准确率(字对不对)和结构准确率(行列对不对)。 混在一起评,出了问题不知道改哪儿。 这也是所有流水线系统的通用原则:每一段单独可测,否则整条链就是个黑盒。
五、动手 Demo:从一堆文字框还原行列结构
按固定坐标造 17 个 OCR 文字框(含标题和页码两个噪声);按 y 差 18 像素聚成行,打印每行的框; 剔掉单框行;再按 x 差 40 像素聚成列,把每个框填进对应格子;最后和真值逐格对比,算格子正确率。
def group_by_y(boxes, tol=18):
ys = sorted({b[1] for b in boxes})
groups, cur = [], [ys[0]]
for v in ys[1:]:
if v - cur[-1] <= tol:
cur.append(v)
else:
groups.append(cur)
cur = [v]
groups.append(cur)
return groups
实跑输出:
--------------------------------------------------------------------
中文表格识别:从一堆文字框还原行列结构
--------------------------------------------------------------------
OCR 输出 17 个文字框(含标题和页码)
框的格式 (x 坐标, y 坐标, 文字)
按 y 聚类成行 7 行
第 1 行 (x=20) 某某公司 2026 年部门预算表
第 2 行 (x=30) 部门 | (x=180) 人数 | (x=300) 预算(万元)
第 3 行 (x=30) 研发部 | (x=180) 42 | (x=300) 380
第 4 行 (x=30) 市场部 | (x=180) 18 | (x=300) 260
第 5 行 (x=30) 生产部 | (x=180) 76 | (x=300) 510
第 6 行 (x=30) 财务部 | (x=180) 9 | (x=300) 95
第 7 行 (x=330) 第 1 页
剔除单框行 剩下 5 行(去掉 2 行噪声)
按 x 聚类成列 3 列
还原出的表格:
部门 人数 预算(万元)
---------------------------
研发部 42 380
市场部 18 260
生产部 76 510
财务部 9 95
格子正确率 15 / 15
这一步叫「表格结构还原」:OCR 只给出「哪个位置有什么字」,
行列是算出来的——先把 y 相近的框聚成行,再把 x 相近的聚成列。
两道阈值(本例 18 和 40 像素)就是全部参数,调错了就串行串列。
沙箱里没有 OCR 引擎(没装 pytesseract,也没有可用的识别模型),
所以上面这批框是我按固定坐标造的。真实流程是:图像 → OCR → 这批框 →
本节这段代码。后半段是真的,前半段是假设的。
17 个框聚成 7 行,逐行打印出来能看出标题、表头、四行数据、页码都分得很清楚。 剔掉单框行之后剩 5 行,刚好是表头加四行数据。这两行噪声剔得干净,是因为它们各自只有一个框。 按 x 聚列得到 3 列,坐标 30、180、300 相距 150 和 120,远超 40 的阈值,所以聚得很干脆。 格子正确率 15 / 15。但这个满分是数据造的,不说明方法在真实扫描件上也这么好。
30 秒实验
- 把 y 阈值从 18 改成 60,看会不会把两行并成一行。
- 把 x 阈值从 40 改成 160,看会不会把两列并成一列。
- 想一想:为什么要把「识别准确率」和「结构准确率」分开评?
常见误区
- 「OCR 直接出表格」——它只出框,行列是后算的。
- 「阈值随便设」——错了就串行串列,而且看着还挺正常。
- 「15 全对说明方法好」——坐标是自己造的,真实扫描件没这么整齐。
自测三问(点开对答案)
1. 表格识别里「结构还原」做的是什么? —— 把一堆带坐标的文字框聚成行、聚成列,还原成二维表。 2. 本例剔噪声用的是什么规则?有什么风险? —— 整行只有 1 个框就剔掉;风险是「合计」这类独占一行的单元格会被误删。 3. 为什么要分开评识别和结构两段? —— 混在一起评,出错时不知道是认字错了还是行列错了。表格之后是车牌——同一个「先定位再识别」的套路,但约束更强。
- ☐ (本节完,下一节 5.2.6 实例6:使用AI识别车牌)