智绘鸿蒙 · 如 7 而至端侧 AI 能力实战手记

一天两百个单号怎么进表

智绘鸿蒙·如 7 而至

前言

仓库那边找到我:一天两百多个快递面单,两个人拿键盘敲进 Excel,敲到下午四点眼睛是花的。老板的原话是"你不是搞技术的吗,这个东西不是很简单"。

是挺简单。鸿蒙 7 的 Core Vision Kit 里那个通用文字识别就是干这个的,端侧跑、不要网络、不要调云端 API 按次付费。我在 MatePad Pro 上把整条链跑通了,最后的结果是:四张面单,三张全对,一张崩得很难看

崩的那张教会我的东西,比前三张加起来都多。这篇文章就按我干活的顺序写:先造数据,再调接口,最后处理那些接口文档不会告诉你的破事。

先说我不信 demo 图

任何 OCR 需求,拿官方示例图跑通就汇报"做完了"的,都是给自己埋坑。所以我没有用网图,而是用 PIL 自己画了面单——因为只有我自己画的,我才确切知道标准答案是什么

四张图,同一批版式,退化做成阶梯:

编号 尺寸 退化处理 标准答案
标准面单 760×1000 SF1350778829431
手持翻拍 456×600 旋转 3.2°、高斯模糊 1.7、JPEG q58 YT7512098834126
小图压缩 300×395 旋转 −6.5°、模糊 0.8、q45 JD0017283946510
极限压图 200×263 在小图基础上再模糊 1.4、q30 JD0017283946510

前三档就是仓库手机随手拍的正常水平,第四档是我故意拿来找死的。

接口长什么样

三个调用就够,没什么花活:

this.inited = await textRecognition.init();
const info: textRecognition.VisionInfo = { pixelMap: src };
const conf: textRecognition.TextRecognitionConfiguration = { isDirectionDetectionSupported: false };
const res: textRecognition.TextRecognitionResult = await textRecognition.recognizeText(info, conf);

几点实在的:

  • init() 是有状态的,我放在单例里做幂等,整个批量过程只初始化一次。别每张图 init/release 一遍,那是给自己加延迟。
  • isDirectionDetectionSupported 是给"图片可能是横着/倒着拍的"准备的。我的四张图旋转角都在 7° 以内,直接关掉。这个开关对耗时的影响我没有单独测,不拿它当优化手段。
  • 返回值是一整块文本 res.value,外加 blocks → lines → words 三层结构和每个 linecornerPoints注意 value 是把所有行拼起来的,这一点后面会害我。
  • 一次只能一张图。想批量只能自己排队串行。

结果:三张对一张崩

真机上一遍跑完四张:

样本 耗时 识别字符数 行数 单号结果 收件人 电话
标准面单 1512ms 197 22 SF1350778829431 ✓ 张伟 ✓ 138****4471 ✓
手持翻拍 1310ms 183 19 YT7512098834126 ✓ 李静 ✓ 139****2280 ✓
小图压缩 1240ms 178 20 JD0017283946510 ✓ 王强 ✓ 137***9056 少一个星号
极限压图 888ms 33 7 JO300017283946510 ✗

四张合计 5010ms。同一张标准面单我前后跑了三次,耗时 1288 / 1402 / 1512ms,波动 ±15%,所以别拿单次耗时做 SLA。

坑一:崩溃是断崖式的,不是慢慢变差

极限压图那张最有意思。它不是"识别得差一点",而是整页只剩 33 个字符、7 行——前面三档都是 178~197 字符、19~22 行。也就是说 OCR 面对糊到一定程度的图,是直接放弃,而不是勉强认几个。

更要命的是它错得很自信:

标准答案:JD0017283946510
实际输出:JO300017283946510

D 认成了形近的 O,中间还凭空多出 3000。返回值里没有任何"我不确定"的信号——没有置信度字段,没有 partial 标记。你正则一匹配,[A-Z]{2}\d{10,15} 照样命中,看起来像个合法单号,直接进表了。

所以判据应该换成输入侧的信号:字符数低于 100、或行数低于 15 的面单,直接标黄退回人工,不看它抽出来什么。这个门槛比任何正则都管用,因为它检测的是"OCR 放弃了"这个事实本身。

坑二:抢字段的不是 OCR,是你的正则

前三张单号全对,我正准备收工,回头看了一眼电话字段——标准面单那行是:

电话  1350778829431

我当时的表情大概是这样的:面单上明明印着 138****4471

原因很简单,我的正则写成了这样:

raw.match(/1[3-9]\d[\d*]{4,9}\d{3,4}/)   // 只要像手机号就行

res.value 是按行拼接的一大串,运单号在上面,SF1350778829431 里的 1350778829431 恰好以 13 开头、长度合适,先被匹配到了。手持那张侥幸对了,纯粹是因为它的运单号被识别成了 YT7512...7 打头不成手机号。

改成锚定标签就对了:

raw.match(/联系电话\s*(1[3-9][\d*\-]{6,12})/)

教训一句话:在拼平的 OCR 文本上做字段抽取,等于在没有任何版式信息的情况下猜。要么锚标签,要么用 cornerPoints 拿到行的坐标,按"同一 y 行的右侧内容"来取——后者更稳,但代码量翻三倍。我这次选了锚标签,因为面单的标签词是固定的。

坑三:* 不是字符,是玄学

小图那张的电话是 137***9056,标准答案 137****9056,少一个星号。面单上的脱敏掩码是四个 *,OCR 认成三个。

这类非文字符号(*·-、空格)的个数不可信。所以任何"拿掩码位数校验手机号"的想法都要丢掉,脱敏字段只做前缀匹配:137 开头、9056 结尾,中间不管。

那这两百单到底怎么进表

按实测 1.3s/张串行算:

  • 200 张 ≈ 260 秒 ≈ 4.4 分钟,中间不用人管。
  • 剩下的是复核时间。按我这次的样本,四张里退一张,也就是 25% 的退回率——样本太小,这个比例只能当心理预期,不能当指标。
  • 想再快只能并行多实例,但接口本身不支持批量,端侧模型吃不吃得住并发我没测,不吹。

导出这一步我没写进 demo:字段抽完之后拼成 CSV 文本、丢进剪贴板,回工位 Ctrl+V 到表格里就完事了。这一步跟鸿蒙没关系,几行字符串拼接的事,反而是仓库那边最在意的——他们那张 Excel 已经用了六年,你让他们换系统不如让他们 Ctrl+V。

真机数据

项目 实测值
单张识别耗时 888 ~ 1512ms(随内容量变化,不是随像素)
四张串行合计 5010ms
同图重复波动 1288 / 1402 / 1512ms,约 ±15%
正常面单字符数 178 ~ 197 字,19 ~ 22 行
崩图字符数 33 字,7 行
单号准确率 3/4(含一张故意压坏的)
字段准确率 收件人 3/3、电话 2/3(星号数错)

测试机 MatePad Pro(HarmonyOS 7 / API 26,序列号略)。面单为脚本合成,标准答案已知。

四张面单的批量结果,最后一行标红

字段抽取结果,电话列已经锚定标签

极限压图:单号错成 JO3000…,右侧红字给出正确答案

总结

这套东西适合"版式固定、印刷体、量又大"的单据:快递面单、发票、送货单、对账单、体检报告首页。这类单子仓库里一天几百张,人工敲纯属浪费生命。

不适合手写体、倾斜超过十度的随手拍、以及任何分辨率低于 300px 的图。最后这条是硬线,过了就断崖。真要在手机上现场拍,先把取景框和"太糊了重拍"的提示做出来,比在识别之后想办法划算得多。

💡 一句话:OCR 本身很少骗你,它放弃的时候会留下痕迹——字符数和行数突然掉一半,就先别信抽出来的内容。

参考文献

  1. 通用文字识别(开发指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-recognition
  2. textRecognition(通用文字识别)API 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-text-recognition-api
  3. Core Vision Kit 错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-core-vision
  4. Core Vision Kit 目录:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-kit-guide