小票字段提取:为什么按行切分会错位

2026-10-16

识别出文字之后,真正的麻烦才开始。OCR 返回的是若干条「文本 + 外框坐标」,而小票是典型的多栏排版:左边商品名,右边价格,中间大量空白。如果按最直觉的方式处理——把 y 坐标接近的文本归为一行——结果会大量错位。

错位是怎么发生的

问题在于商品名和价格不在同一条水平线上。商品名较长时会折成两行,价格只占一行,往往对齐在第二行。这时候按 y 聚类,价格会和商品名的第二行归到一起,第一行落单,最终拼出来的记录是「半个商品名 + 别人的价格」。

用伪代码描述这个直觉做法,问题就很明显:

for box in boxes: rows[round(center_y / line_height)].append(box)

这里隐含了两个假设:所有文本基线对齐、行高固定。小票上这两个假设都不成立。

思路一:先分列,再对齐

既然是多栏排版,就先做列的划分,再做行内配对:

  1. 横向投影所有文本框的 x 区间,找出贯穿整页的空白带,用它把页面切成左右几栏。
  2. 右栏通常只有价格和数量,左栏是名称。两栏分别按 y 排序。
  3. 用纵向重叠率而不是中心点距离做匹配:左栏某个名称块与右栏某个价格块在 y 方向上有重叠,就配成一条记录。

比中心点距离稳,是因为名称折行后中心点会偏,但它的纵向范围仍然覆盖价格那一行。重叠率匹配对折行天然宽容。

思路二:靠金额自己配对

还有一种不看几何、看内容的做法,适合结构更乱的票据:

  1. 先用正则把全页所有「形如金额」的文本抽出来,作为候选价格集合。
  2. 对每个商品名块,从它右侧最近的候选价格里按顺序取一个。
  3. 用合计校验反过来验证配对是否正确:所有配上的金额求和,与票面上的「合计」比对,误差在容差内就认为配对成立。

第三步是关键。小票一定会打印合计金额,这是一个免费的自校验信号。如果求和结果与合计差得远,说明中间有配错或漏配,可以整体回退到另一种策略,或者直接标记为需要人工复核。

只要票据上有可校验的汇总字段(合计、件数、找零),就一定要用它来反验抽取结果。这比调任何模型参数都有效。

两种思路怎么选

我的判断是按票据版式的规整程度分:

还有几个容易被忽略的坑

这几条都是被实际样本打回来才注意到的,比模型本身的问题更消耗时间。

← 返回首页