小票字段提取:为什么按行切分会错位
识别出文字之后,真正的麻烦才开始。OCR 返回的是若干条「文本 + 外框坐标」,而小票是典型的多栏排版:左边商品名,右边价格,中间大量空白。如果按最直觉的方式处理——把 y 坐标接近的文本归为一行——结果会大量错位。
错位是怎么发生的
问题在于商品名和价格不在同一条水平线上。商品名较长时会折成两行,价格只占一行,往往对齐在第二行。这时候按 y 聚类,价格会和商品名的第二行归到一起,第一行落单,最终拼出来的记录是「半个商品名 + 别人的价格」。
用伪代码描述这个直觉做法,问题就很明显:
for box in boxes: rows[round(center_y / line_height)].append(box)
这里隐含了两个假设:所有文本基线对齐、行高固定。小票上这两个假设都不成立。
思路一:先分列,再对齐
既然是多栏排版,就先做列的划分,再做行内配对:
- 横向投影所有文本框的 x 区间,找出贯穿整页的空白带,用它把页面切成左右几栏。
- 右栏通常只有价格和数量,左栏是名称。两栏分别按 y 排序。
- 用纵向重叠率而不是中心点距离做匹配:左栏某个名称块与右栏某个价格块在 y 方向上有重叠,就配成一条记录。
比中心点距离稳,是因为名称折行后中心点会偏,但它的纵向范围仍然覆盖价格那一行。重叠率匹配对折行天然宽容。
思路二:靠金额自己配对
还有一种不看几何、看内容的做法,适合结构更乱的票据:
- 先用正则把全页所有「形如金额」的文本抽出来,作为候选价格集合。
- 对每个商品名块,从它右侧最近的候选价格里按顺序取一个。
- 用合计校验反过来验证配对是否正确:所有配上的金额求和,与票面上的「合计」比对,误差在容差内就认为配对成立。
第三步是关键。小票一定会打印合计金额,这是一个免费的自校验信号。如果求和结果与合计差得远,说明中间有配错或漏配,可以整体回退到另一种策略,或者直接标记为需要人工复核。
只要票据上有可校验的汇总字段(合计、件数、找零),就一定要用它来反验抽取结果。这比调任何模型参数都有效。
两种思路怎么选
我的判断是按票据版式的规整程度分:
- 版式规整、栏目清晰(多数商超小票):走思路一,几何匹配更直接,不依赖金额正则的准确率。
- 版式混乱、栏目不固定(餐厅结账单、手写补充):走思路二,靠合计校验兜底,容错性更好。
- 实际工程里通常是两条都实现,先用几何匹配,用合计校验做验证,校验不过再切到金额配对重跑一遍。
还有几个容易被忽略的坑
- 负号和小数点会被识别成别的东西。折扣行的减号常被识别成「-」「—」「.」,金额正则必须把这个变体都覆盖。
- 数量乘单价的行不代表最终金额。「苹果 2 x 5.00 10.00」这种行里有两个金额,取哪个取决于你要的是单价还是小计,别默认取最后一个。
- 同一张小票上可能有多个汇总。小计、折扣、应付、实付、找零,字段名不同但都长得像金额。靠正则匹配关键字时,「小计」和「合计」只差一个字,必须精确匹配再加位置权重。
这几条都是被实际样本打回来才注意到的,比模型本身的问题更消耗时间。
← 返回首页