27  一维码与二维码识别

到目前为止,本书的算法都在回答”目标在哪里”“尺寸是多少”“有没有缺陷”。从本章开始的识别篇要回答一个不同的问题:图像里写了什么——把像素重新变回数据。工业现场最成熟、最大规模的”图像变数据”应用就是条码:每个工件、每块电路板、每盒物料贴上或刻上一个码,产线的每一站读一次,整条供应链的追溯(traceability)体系就建立在这一次次读码之上。读码听起来简单——手机扫码人人会用——但产线上的码会被油墨扩散、镜头失焦、低分辨率和污损轮番折磨,而一次误读造成的混料事故远比读不出更昂贵。本章用一张真实的多码制一维码标样卡(图 27.1)贯穿一维码解码、退化鲁棒性边界、二维码纠错对比与打印质量分级四组实验,最后以一个”手写编码器 + SDK 解码”的闭环实验收尾。

图 27.1: 真实多码制一维码标样卡 sample/barcodes.jpg(1536×878 灰度):一张含 8 种码制、共 9 个符号的标准测试卡——上排 CODE39 与 CODE128 均编码 “SCIMV”、EAN-8 编码 12345670;中排 EAN-13 编码 1234567890128、UPC-A 编码 185020919621、UPC-E 编码 01234565;下排 CODE93 编码 “Sci-Code93”、两个 ITF(Interleaved 2 of 5)均编码 123456789013。红框与红色标签是 SciVision FindBarcode 的检测结果叠加(码制名 + 解码字符串)。

27.1 一维码原理

一维码(1D barcode)的编码思想非常朴素:用条(bar)与空(space)的宽度序列承载数据。最窄的条/空宽度称为模块(module),其余元素的宽度是模块的整数倍;一个字符由固定数目的条与空组成,宽窄组合即该字符的编码。信息只沿一个方向展开,垂直方向上每一行都是同样的内容——这既是它可以用一条扫描线解码的原因,也是它鲁棒性短板的根源,小节 27.2 会让这一点变得非常具体。

常见码制(symbology)各有分工:CODE39 用 9 个元素(5 条 4 空、其中 3 个为宽元素)编码一个字符,支持数字与大写字母,结构简单、可变长,在工业标签上极为常见——但要注意它的校验字符是可选的,标准并不强制;CODE128 密度更高、支持全 ASCII,且强制包含校验字符;EAN-13/EAN-8/UPC 是零售商品码,定长数字、末位为校验位;CODE93 是 CODE39 的高密度改良;ITF(Interleaved 2 of 5)是只编码数字的连续型码制——条与空都承载信息、字符间无间隔,密度高但校验位同样可选图 27.1 这张标样卡把这 8 种码制摆在一起,正好用来对比它们在退化下的体质差异。

CODE39 与 ITF 的每个元素自身有一定”自校验”性——单个宽窄判错通常落在非法码字上而被丢弃;但整串内容没有强制的校验字符(check character),字符级别的截断或替换无法被发现。ITF 又是连续型码制、密度最高,这个隐患将在 小节 27.2 中由它的多次误读坐实。

扫描线解码的流程分四步:先在图像中找码——条码区域有强烈的单方向纹理,梯度方向高度一致,据此可以快速定位候选区域并估计其朝向;然后沿条码方向布设一条或多条采样线(scan line),提取灰度剖面;对剖面做边缘检测,把条空交替转换成宽度序列;最后按码制的码表把宽度序列查表还原为字符,并验证起止符(CODE39 为 *)与校验字符(若有)。

图 27.1 运行 SDK 的 FindBarcode(完整工程见 code/barcodes/;因 barcodeType 无”自动”档,示例逐一尝试 8 种候选码制再归并),洁净图上 9 个符号全部一次解出,方向均近 0°(卡片水平摆放),每模块像素数(pixelPerModule)落在 2.1~4.1 px/模块之间——CODE93 最窄(约 2.1)、EAN-8 最宽(约 4.1)。同时打开 checkQuality 得到 ISO/IEC 15416 打印质量评级,差异极其显著:EAN-8 与 UPC-E 满评级 4.0,CODE93 3.0(受限于调制度 modulation),CODE39 与 EAN-13 为 2.0、CODE128 与 UPC-A 为 1.0(均受限于可解码度 decodability),而两个 ITF 评级 0.0(F 级)——可解码度为 0,宽度测量值已贴着码表判决边界、几乎没有余量。可解码度反映宽度测量离判决边界的距离,调制度反映窄元素处的局部对比度;同一张卡上印得最好的码(EAN-8)与最差的码(ITF)整整相差 4 个等级,这正是评级体系的价值所在——它度量的不是”这次读出来没有”,而是”离读不出还有多远”。

ISO/IEC 15416 把一维码打印质量按 4(A)到 0(F)分级(grading):对多条采样线分别测量解码、对比度、调制度、缺陷、可解码度等参数,每条线取最差参数为该线评级,再对各线取平均。总评级 ≥1.5 通常可稳定读取,但余量已经不大。

评级体系的价值在于:它不回答”这次读出来没有”,而是回答”这个码离读不出来还有多远”。一个今天能读的 1.0 级码,换一支镜头、脏一点就会读不出;产线验收时要求标签评级 ≥2.5 或 3.0,是比”现场试读成功”可靠得多的标准。

27.2 鲁棒性边界

解码成功只是起点,工程上更关心边界在哪里。我们对 图 27.1 的 9 个符号同时施加四类渐进退化——高斯模糊、加性噪声、降采样、遮挡——逐档记录每个码制的存活与误读情况(图 27.2 展示了四个代表性档位)。

(a) 高斯模糊 \(\sigma=2\):9 码中 6 个仍解出,CODE93 与两个 ITF 失败,且一个 ITF 被误读为 120000000013
(b) 噪声 \(\sigma=30\):仅 UPC-A 失败,其余 8 码正常(\(\sigma=10\) 时 9 码全数正常)
(c) 缩放 ×0.35:仅 CODE39/CODE128(“SCIMV”)与 EAN-8 仍可读,其余因低于约 1.5 px/模块而失败
(d) 全高白条遮挡各码宽 10%:9 码全部失败(红框标出遮挡条位置)
图 27.2: 一维码鲁棒性实验的四个代表档位(红色叠加为检测/遮挡标记)。完整边界:模糊 \(\sigma=1\) 全数正常、\(\sigma=2\) 存活 6/9、\(\sigma=3\) 全灭;噪声 \(\sigma=10\) 全数正常、\(\sigma=30\) 存活 8/9、\(\sigma=50\) 存活 6/9;缩放 ×0.5 存活 6/9、×0.35 存活 3/9、×0.25 全灭;全高白条遮挡 2% 存活 6/9、5% 存活 2/9、10% 全灭。

四组边界各有含义,而且它们逐项印证了洁净图上的评级排序——评级低的码先倒下。模糊\(\sigma=1\) 九码全数正常,\(\sigma=2\) 存活 6/9,先失守的恰是最窄模块的 CODE93(约 2.1 px)与两个 ITF(约 3.0 px),\(\sigma=3\) 全灭——模糊把窄条窄空的对比度抹平,最窄的码先死,正对应 15416 评级里 ITF/CODE93 本就垫底的预言。噪声\(\sigma=10\) 无碍,\(\sigma=30\) 仅 UPC-A 掉队,\(\sigma=50\) 仍存活 6/9——多条采样线对随机噪声有天然的平均作用,余量相当可观。分辨率:缩放 ×0.5 存活 6/9,×0.35 只剩 3 个最宽、最稳的码(两个 SCIMV 与 EAN-8,原 ppm 约 3.7~4.1、缩后≈1.3~1.4 px/模块),×0.25 全灭——1D 解码只需在采样线上分辨条空宽度,对分辨率的要求远比直觉宽松,约 1.5 px/模块是本例的实用下限,而最窄的 CODE93 一缩放就最先死(镜头与分辨率选型见 章节 3)。

最值得逐字读的,不是哪个码读不出,而是读错了。模糊 \(\sigma=2\) 下一个 ITF 没有报失败,而是解出了 120000000013(真值 123456789013);2% 与 5% 遮挡下另一个 ITF 又被误读为 123400789013;缩放 ×0.5 时甚至冒出一个本不存在的 EAN-8 误读 45252921。对追溯系统而言,这比读不出危险得多:读不出会触发人工补扫,读错却不自知会让一个工件顶着别人的身份流过整条产线。根源正是前面埋下的伏笔——ITF(与 CODE39)的校验位是可选的、本卡未带;ITF 又是连续型码制(条与空都编码、字符间无间隔),局部污损极易把一段塌缩成另一个合法的偶数位数字串,码制层面无从发现,这也解释了它在洁净图上就只有 F 级可解码度。工程对策有二:要么选用强制校验的码制(CODE128 在本实验全程零误读),要么为 ITF/CODE39 启用可选校验位并在解码端打开 codeCheck 验证;此外应用层再校验内容格式(位数、前缀)永远是值得的。

遮挡最能区分码制的体质:一条贯穿条码全高的白条,按各码宽的百分比逐档加宽——2% 时 9 码里还有 6 个能读(较宽松的 CODE39/CODE128/EAN 系存活,最密的 CODE93 与 ITF 已失守),5% 时只剩两个 SCIMV,10% 时全部失败。但”能读出”不等于”读得对”:上面两次 ITF 误读正发生在 2% 与 5% 遮挡档。原因是结构性的:一维码的冗余只存在于纵向——同一内容重复了整个条高,任何一条水平采样线都能解码,所以上下方向的污损不致命;但横向上每个字符只编码一次,没有任何纠错,全高的纵向遮挡抹掉一个字符就抹掉了它的唯一副本。要在横向损伤下生存,需要把冗余编进数据本身——这正是二维码的设计起点。

27.3 二维码与纠错

DataMatrix(ECC200)是工业件上最常用的二维码。图 27.3 (a) 是一个 16×16 符号:左侧整列与底部整行是实心的 L 形定位边(finder pattern),提供位置与朝向基准;顶行与右列是黑白交替的时钟边(clock track),标定模块网格的行列坐标;其余 14×14 的数据区按 ISO/IEC 16022 规定的折线顺序放置码字,每个码字的 8 个比特摆成一个”犹他形”模块组。

(a) 手写 ECC200 编码器生成的 16×16 符号,内容 “MVBOOK CH27”
(b) 白块遮挡符号面积 15%:超出纠错预算,解码失败(10% 时仍正确)
图 27.3: DataMatrix 符号结构与遮挡实验。(a) 左/底为 L 形定位边、顶/右为交替时钟边、内部为数据区;(b) 偏心放置的白块避开定位边、只破坏数据区,10% 面积可被 Reed-Solomon 纠错吸收,15% 失败。

二维码的鲁棒性核心是 Reed-Solomon 纠错(error correction)。直觉上可以这样理解:把 12 个数据码字看作一条多项式的系数,在 GF(256) 上额外计算 12 个校验码字附在后面——24 个码字之间由此产生强代数约束,少量码字被破坏后,解码器能从约束中反推出”哪几个错了、原值是什么”。12 个校验码字可以纠正位置未知的约 6 个错误码字——本例 24 个码字中数据只占一半,一半的符号面积都花在了冗余上

Reed-Solomon 的纠错预算:\(n_{\text{ecc}}\) 个校验码字可纠正 \(t\) 个位置未知的错误码字,需满足 \(2t \le n_{\text{ecc}}\)。本例 \(n_{\text{ecc}}=12\),故 \(t \le 6\):24 个码字中坏掉 6 个(25%)仍可完全恢复。

遮挡实验把这笔”面积换鲁棒”的交易摆上台面:用白块覆盖符号面积(偏心放置、避开定位边),10% 时解码依然正确,15% 失败图 27.3 (b))。对比一维码——最脆的 ITF/CODE93 在 2% 全高遮挡即失守、所有 1D 码到 10% 全灭,且失守前还会误读——二维码不仅容限更高,更关键的是它不会悄悄读错:超出纠错预算时 ReadDM 直接返回失败(rc=122703001),而非给出一个错值。而且 DataMatrix 在模糊 \(\sigma=1,2,3\) 与噪声 \(\sigma=30\) 下全部解码正确,一维码在这些档位早已半数阵亡(公平地说,本例 DM 模块宽约 10.3 px,比标样卡上 2~4 px/模块的一维码占了分辨率便宜,但遮挡对比不受此影响)。需要强调,纠错不是免费的:同样的数据量,带 50% 冗余的符号需要两倍面积;二维码用面积购买的,是一维码花多少面积都买不到的横向损伤生存能力

顺带一提 QR 码:它与 DataMatrix 同属”矩阵码 + Reed-Solomon”家族,差别主要在定位方式——QR 用三个角上的回字形定位图案,DataMatrix 用 L 形实边,前者更利于远距离、大符号的快速定位,后者在小尺寸直接打标(DPM)场合更紧凑。SDK 提供 ReadQR 接口,但本章测试图中没有 QR 符号,未做 QR 实验,原理结论可类推。

27.4 闭环验证方法论

上一节的 DataMatrix 图像从哪来?这里有个值得展开的方法论问题:SciVision SDK 只有解码 API,没有任何生成 API,testImage 目录里也没有二维码图像。要做二维码实验,我们在示例工程里手写了一个 ECC200 编码器(约 150 行):在 GF(256)(本原多项式 301)上实现 Reed-Solomon 编码(生成多项式根 \(\alpha^1\!\dots\!\alpha^{12}\)),按 ASCII 模式编码文本(双数字压缩为一个码字、253 态算法填充),再按 ISO/IEC 16022 附录 F 的标准放置算法把 12+12 个码字铺进 16×16 符号,加上定位边与时钟边渲染成图。

验证它的方式是闭环:编码器生成的图像交给 SDK 的 ReadDM 解码,返回字符串与原文 “MVBOOK CH27” 逐字比对。实验结果:解码成功、内容一致,符号 16×16、模块约 10.3 px,ISO/IEC 15415 总评级 4.0——满分,理想渲染的符号本就该如此。这一来一回同时检验了两端:编码器任何一个比特放错位置,解码即失败或纠错余量下降;SDK 解码器读回满评级,也证明其网格定位与质量测量工作正常。这正是 章节 5 中”黄金实验”思想的再现——用已知真值的输入检验整条链路,而且这里的真值是我们亲手编码的,每个比特都可追溯。它同时演示了 15415/15416 评级的另一种用法:以满评级符号为基准,现场符号的评级落差可逐项归因到打印与成像环节(照明引起的对比度问题参见 章节 4)。

27.5 SciVision 实现

一维码解码由 SCIMV::SciSvBarCode::FindBarcode 完成:

SCIMV::SciSvBarCode bc;
Sci1DCodeArray codes; SciROIArray rects, bad;   // 三个输出都传实体
long rc = bc.FindBarcode(img, roi, SCI_CODETYPE_CODE39,
                         1,      // polarity:深条浅底
                         0,      // sampleRate:自动采样密度
                         2,      // contrastLevel:中等对比度
                         4,      // codeNumber:期望码个数(逐码制扫描时留余量)
                         10000,  // timeOut:超时(ms)
                         0,      // codeCheck:校验字符验证开关
                         0,      // decodeDirection:双向解码
                         false,  // sendStartEnd:不输出起止符
                         true,   // checkQuality:输出 ISO 15416 评级
                         &codes, &rects, &bad);
// Sci1DCode:codeType / codeString / orientation
//            / pixelPerModule / qualityLevel_15416

三个参数值得特别交代。codeCheck 打开后按码制规则验证校验字符——对 CODE39 这要求打印端带上可选的模 43 校验字符,但它是抵御 小节 27.2 中误读事故的码制级手段;checkQuality 打开后逐码输出 15416 各分项评级,产线调试与验收阶段建议常开,稳定运行后可关闭以省时间;timeOut 为解码超时,节拍紧张的工位必须设置,否则一幅劣质图像可能拖垮整个周期。

DataMatrix 解码用 SCIMV::SciSvMatrixCode::ReadDM

SCIMV::SciSvMatrixCode mc;
SciDMCodeArray ret; SciVarArray strs; SciROIArray cand;
long rc = mc.ReadDM(img, roi, 1 /*polarity 深码浅底*/, -1 /*dmStyle 自动*/,
                    6, 20,  // minSize/maxSize:符号行列数搜索范围
                    30,     // minContrast:最小对比度
                    0,      // mirror:自动判镜像
                    1, 10000, 0,            // codeNum / timeOut / model
                    true,   // checkQuality:输出 ISO 15415 评级
                    false, false, false, false, false, false,
                    1, 1,   // 解码与输出字符串均按 UTF-8
                    &ret, &strs, &cand);
// SciDMCode:codeString / symbolRows / symbolCols
//            / moduleSize / orientation / qualityLevel_15415

两个工程坑。第一,码制必须指定FindBarcodebarcodeType 没有”自动”档,对图中不存在的码制会解码失败(返回非零错误码);码制未知时只能像本章示例那样循环尝试候选列表(本例对 8 种一维码制各调用一次再归并去重),但产线项目应在方案阶段就把码制定死——这也是与标签供应商约定规范的一部分。第二,SciSvBarCode.h 等头文件无条件按 dllexport 定义导出宏,把它的 #include 放在其余 dllimport 风格头文件之后即可正常链接。

工业案例:产线读码率从 98% 到 99.9%

某装配线 DataMatrix 读码率长期停在 98%——听起来不低,但意味着每千件有 20 件触发人工补扫,节拍与人力成本都不可接受。项目组最初反复调解码参数收效甚微,转而用评级仪测标签:调制度仅 C 级——标签纸吸墨导致油墨扩散、窄元素对比度塌陷;同时核算光学链路,发现模块宽度仅约 1.8 px/模块,贴着解码下限。两项整改:改用高对比度覆膜标签材料,评级回到 A/B;提升镜头放大率使模块达到 3 px/模块。读码率升至 99.9%,解码参数最终用的还是默认值。教训:读码率问题先查打印质量评级与每模块像素数,再去调算法参数——评级不合格时,调参数只是在边界上赌运气。

27.6 小结

  • 一维码以条空宽度编码、靠扫描线解码,纵向有整个条高的冗余、横向却没有任何纠错:本章 8 码制标样卡上,最密的 ITF/CODE93 全高遮挡 2% 即失守、所有 1D 码到 10% 全灭;分辨率下限约 1.5 px/模块(×0.35 时仅最宽的 3 个码存活)。退化下的存活顺序逐项印证洁净图的 15416 评级排序。
  • 误读比读不出更危险:本卡 ITF(可选校验、连续型码制)在模糊 \(\sigma=2\) 与 2%/5% 遮挡下数次误读(如 120000000013、123400789013),洁净图上就只有 F 级可解码度——选强制校验的码制(CODE128 全程零误读)、打开 codeCheck、应用层校验格式,三道防线至少要有一道。
  • 二维码用 Reed-Solomon 纠错以面积换鲁棒:16×16 DataMatrix 的 12 个校验码字可纠约 6 个错误码字,遮挡 10% 面积仍正确解码,且超预算时直接报失败而非误读——横向损伤容限远高于一维码。
  • SDK 只解不编时,手写编码器 + SDK 解码的闭环既补齐了实验能力,也构成对两端的黄金验证(本例闭环 ISO 15415 评级 4.0 满分)。
  • ISO/IEC 15416/15415 打印质量评级回答的是”离读不出还有多远”:验收看评级而非试读成功,读码率问题先查评级与每模块像素数。

关于条码识别与解码算法在工业视觉中的更系统讨论,可进一步阅读 Steger 等人的著作 (Steger, Ulrich, 和 Wiedemann 2018)。本章反复引用的打印质量分级体系出自两份核心标准:一维码的逐参数评级方法由 ISO/IEC 15416 规定 (International Organization for Standardization 2016),二维码(含 DataMatrix 与 QR)的符号质量评级由 ISO/IEC 15415 规定 (International Organization for Standardization 2011)。具体码制的编码、纠错与参考解码算法则各有专门标准:DataMatrix(ECC200)的码字放置与 Reed-Solomon 规则见 ISO/IEC 16022 (International Organization for Standardization 2006),QR 码见 ISO/IEC 18004 (International Organization for Standardization 2015)——产线上与标签供应商约定规范、复现本章实验时,应以这些标准的原文为准。