手机拍的发票交给 TeleOCR,多数情况下值得先直接解析一次,但“能不能直接吃”不取决于照片看起来清不清楚,而取决于解析过程里文字检测框和字段还原这两步各自有没有垮。模糊、倾斜、阴影落在不同步骤上:模糊更容易让文字识别出错字,倾斜先影响检测框的形状,侧向阴影常把两者一起拖下去。可行的做法是拿同一张发票拍出几组受控样本,先跑一遍看哪一步先变差,再决定加不加旋转矫正和去阴影预处理。
手机直拍发票可以先进 TeleOCR 直解一次,判断依据不是观感清晰度,而是解析日志里检测框是否成行、行序是否连续、关键数字是否逐位正确。模糊主要压文字识别,倾斜主要压检测框形状,阴影往往同时干扰两者。建议先用同一张发票做清晰、模糊、倾斜、带阴影四组对照,再决定是否补旋转矫正和去阴影;发票代码、发票号码、价税合计任一项漏识或错位,就该走预处理。
先备好三组对照样本,把变量控制在同一张发票上
三组对照的目的,是让每一次只改变一个拍摄因素。如果换成两张不同发票,字段数、版式、印章位置都变了,解析结果变差时你无法判断是拍摄问题还是票据本身的问题。所以底稿只用一张发票,所有样本都由它派生。
文件命名建议把发票编号、拍摄变体、光线条件写进文件名,后面对日志时不用翻相册回忆:
inv01_sharp_straight_even.jpg # 清晰、正拍、光照均匀,基线
inv01_blur_straight_even.jpg # 只把清晰度调低,其余不变
inv01_sharp_tilt15_even.jpg # 只让票据倾斜十几度,其余不变
inv01_sharp_straight_shadow.jpg # 只加侧向阴影,其余不变
“模糊”这一组不要靠手抖随机拍,抖动方向和幅度不可复现;可以用同一张已经拍好的清晰图,在本地做一次轻度降质导出,保留原图对照。“倾斜十几度”也建议在导出时旋转,而不是靠手举着估角度,这样倾斜角度是已知量。阴影组可以在手机手电或台灯下从侧上方贴着拍,让票据一半偏亮一半偏暗。
三类干扰的观察重点不一样,先列成一张对照表,跑完逐行填:
| 对照组 | 拍摄条件 | 命名后缀 | 要观察的环节 |
|---|---|---|---|
| 清晰与模糊 | 正拍、光照均匀,仅改变清晰度 | _sharp / _blur | 文字识别是否出错字、是否整行丢失 |
| 正拍与倾斜 | 清晰度一致,仅旋转十几度 | _straight / _tilt15 | 检测框是否跟着歪、行序是否错乱 |
| 均匀与带阴影 | 正拍、清晰度一致,仅加侧向阴影 | _even / _shadow | 字段是否命中、关键数字是否无误 |
用清晰正拍样本跑一遍,记录字段识别基线
基线样本 inv01_sharp_straight_even.jpg 的解析结果,是后面所有对照的比较对象,所以这一次的记录要尽量完整,不能只记“识别成功”。每次跑之前固定输入条件:长边像素(例如统一压到 1600 或 2000)、文件格式(统一导出为 JPG,避免同一批里混进 HEIC 和 PNG)、是否开启检测框坐标和行序返回。TeleOCR 不同版本或不同部署方式的接口名与参数名会有差异,下面是一个可替换的通用骨架,把 URL、字段名换成你环境实际支持的即可:
POST /ocr/invoice
Content-Type: multipart/form-data
file=@inv01_sharp_straight_even.jpg
return_boxes=true # 要求返回检测框四点坐标
return_row_index=true # 要求返回行序或行的归属
关键字段建议只圈定少数几个,字段太多会把注意力摊薄。通常把这几项作为关键字段:发票代码、发票号码、开票日期、金额、税额、价税合计。购买方和销售方名称可以一并记录,但出现个别字形差异时不必立刻判定失败,因为名称里的生僻字和括号全半角本身就不稳定。
记录模板可以用一行 CSV,跑完一组填一行,横向对比最省事:
case_id,long_edge_px,format,latency_ms,box_count,row_order_ok,fields_hit,fields_wrong
inv01_sharp_straight_even,2000,jpg,,,yes,,
这里的 latency_ms 记端到端耗时,从发起请求到拿到完整响应;“快”本身不是目标,它只是用来发现某次解析是否中途退化。检测框数量 box_count 和行序 row_order_ok 是后面两节要用到的关键列,基线这一行必须填上。
只改模糊度再跑一遍,看是字段漏识别还是整行丢失
把 inv01_blur_straight_even.jpg 用同样的参数再跑一次,然后逐字段对差异,不要只对“成功/失败”。对差异的位置比数量更重要:把两次结果的字段值和检测框按位置对齐,看变化集中在哪一块区域。
两种失败的成因不同,处理方向也不同:
- 整行丢失:基线里某一行的检测框在模糊样本里整体消失,
box_count明显减少,且减少的框集中在同一行的横条区域。这通常指向文字检测阶段,也就是模型没把这条文字当成候选文本,后面根本没有识别机会。方向是提高输入分辨率、降低压缩率,或者换更清晰的拍摄,而不是去调字段后处理。 - 单字段出错:检测框数量和行位置基本没变,但个别字段的值变了,例如数字 0 和 8、1 和 7 互换,或税额少一位。这通常指向文字识别阶段,检测到了但认错了。方向是对关键数字做二次校验(位数、小数位、价税合计的加和关系),而不是加旋转矫正。
一般规律是:轻微模糊先表现为单字段出错,继续加重才出现整行丢失。所以看模糊样本时,重点是它停在哪一级。如果只到单字段出错,可以靠后处理校验兜住;如果已经整行丢失,说明这条路的输入质量不够,应该回到拍摄或预处理环节。
换成倾斜十几度的样本,检查框线是否跟着歪
跑 inv01_sharp_tilt15_even.jpg,这一次重点不是字段值,而是检测框输出本身。若接口返回了四点坐标,就看每一个框的四个点:正常正拍时上下边基本水平,四点的纵坐标接近;倾斜样本里如果四点整体跟着票据一起转,上下边仍然平行,框的形状和票据边缘一致,说明检测阶段把倾斜吃下来了,倾斜只影响后续的透视还原。
如果返回的是可视化图片,直接看框是否贴合文字行、是否有一行文字被斜切成两段、相邻两行的框是否互相吃掉。判断依据可以归成两条:
- 框是否退化成一个包住整片区域的水平大框。若是,检测阶段没跟上倾斜,相邻列可能被并进同一行,行序会直接错。
- 框虽然是斜的,但每个框只覆盖一行、行与行的上下顺序仍然连续。这种情况下继续用原图解析通常可行,只需要在后续还原环节做透视校正。
旋转矫正该不该预先做,取决于上面两条落在哪一边。只有出现退化大框、行被切开或行序错乱时,才建议在送进 TeleOCR 之前做一次旋转矫正;如果框线都跟着歪但行序正常,预先旋转属于可省的动作,做了反而多一次重采样,可能轻微损失清晰度。
加入侧向阴影后复核,划出直接解析可用与必须先预处理的分界
跑 inv01_sharp_straight_shadow.jpg,它的特点是亮暗不均:亮侧的文字识别正常,暗侧可能出现漏检或错字。阴影样本不要只看整体是否成功,按下面三条判据逐条过,每条都有明确的观察点:
- 字段是否命中:逐字段对照基线,观察暗侧区域的字段(常是右侧金额列和票面下半部分)是否缺失。命中数比基线少,且缺失集中在暗侧,说明阴影压低了局部对比度。
- 行序是否正确:看返回的行顺序是否和票面从上到下一致,阴影边缘是否把一行拆成两行、或把两行并成一行。行序错会导致字段归属错位,比单字识别错误更难靠后处理补救。
- 关键数字是否无误:金额、税额、价税合计逐位核对,并做一次加和自检(金额加税额是否等于价税合计)。数字逐位正确是硬要求,出现错位或漏位就不算通过。
这三条判据可以沉淀成一份可复用的决策清单,每次拿到新的一批手机发票照片都可以照着走:
- 先固定输入尺寸、格式和解析参数,用清晰正拍样本跑一次,确认关键字段都能命中,作为这批照片的参照基线。
- 直接解析实际待处理的手机照片,对照基线看三条判据。三条全部通过,直接解析可用,不必预处理。
- 只有字段漏检、且漏检集中在亮度不均的区域,先加去阴影或局部对比度增强,再解析一次对比。
- 只有检测框退化成大框、行被切开或行序错乱,先加旋转矫正,再解析一次对比。
- 三条判据里有两条以上不通过,说明单纯预处理收益有限,应先改善拍摄条件(正面、均匀光、避免手抖),再重跑基线。
- 每次调整只改一个变量,改完必须回到同一张底稿复跑,否则无法判断改善来自哪一步。
边界需要说清楚:这套判断依赖你实际环境的输入尺寸、压缩方式和接口返回内容,同样的照片在不同分辨率或不同参数下结论可能不同,所以清单里的阈值不能照搬,要先用自己的一批样本跑出基线再定。另外,预处理本身也会引入新的误差,去阴影过度会压低文字笔画,旋转重采样会让细笔画发虚,因此能靠拍摄解决的,优先回到拍摄,而不是层层叠加预处理。