帮朋友做扫码追溯时,本以为最难的是硬件对接,结果真正卡住的是解码率。同样的二维码,在光照好的工位扫得出来,换个角度就识别不了。后来不得不把纠错级别从 M 调到 H,二维码变密了,但读取率从 87% 提到了 99% 以上。
那之后我开始认真研究 QR 码的结构——这东西远比表面上复杂。
二维码不是二进制点阵
很多人以为二维码就是 01 矩阵:黑色格子是 1,白色是 0,扫码器逐行扫描就能读出数据。
实际差得远。
QR 码的结构分好几层:功能图形、数据区域、格式信息、版本信息。扫码器拿到图像后的第一步是定位,第二步是校正畸变,第三步才到数据提取。每一步都可能出错。
功能图形包括三个角上的回字形位置探测图形。这三个图案长得一模一样,分别位于二维码的左上、右上、左下三个角。扫码器找到这三个图案就能确定二维码的位置、方向和倾斜角度。之所以放在三个角而不是四个角,是因为三点确定一个平面,右下角省出来放数据。
位置探测图形之间连着时序图形——一条黑白交替的线。时序图形的作用是告诉扫码器单个模块的实际尺寸。二维码在印刷或显示时可能被缩放,扫码器通过时序图形计算每个格子对应多少个像素,才能正确采样。
二维码里还有格式信息区域,存放纠错级别和掩码编号。扫码器最先读取的就是这部分——它需要先知道纠错级别和掩码,才能正确解码后面的数据。
我犯过一个低级错误:生成二维码时为了多塞点数据,选了更低的版本号。版本越低模块越少,定位图案之间的间距也变小了。扫码器在远距离时分辨不出三个定位点。这是一个典型的取舍——数据容量和识别距离是反比关系。
四种数据编码模式
QR 码支持四种数据编码模式,每种对特定类型的数据有不同的压缩效率。
数字模式(Numeric):只编码 0-9。每 10 个比特存 3 个数字,压缩率最高。适合电话号码、订单号这种纯数字场景。
字母数字模式(Alphanumeric):编码大写字母 A-Z、数字 0-9 和少数标点符号($%*+-./: 和空格)。每 11 个比特存 2 个字符。
字节模式(Byte):编码任意 8 位字节数据。UTF-8 文本或者二进制文件都行。这是最通用的模式,也是大多数生成库的默认选择。
汉字模式(Kanji):针对汉字优化的模式,每个汉字用 13 个比特编码。对比 UTF-8 编码(一个汉字 3 字节 = 24 比特),节省近一半空间。
做个实际对比。要编码"船长工具箱"这五个字:
- 用 UTF-8 字节模式:每个汉字 24 比特,5 个字 = 120 比特
- 用汉字模式:每个汉字 13 比特,5 个字 = 65 比特
省了 55 个比特,接近一半。
当时做工厂追溯系统,二维码里要存产品批次号和中文产地信息。生成库默认用字节模式,二维码版本被推到 6 以上,模块密密麻麻。手动切换到汉字模式后版本直接降到 4,识别率明显提升。
遗憾的地方是,很多开源二维码生成库不会自动检测内容是否以中文为主。它们默认走字节模式,哪怕全是中文。如果在生成二维码之前能检测字符范围再选择编码模式,二维码能小一圈。
Reed-Solomon 纠错码的工作机制
QR 码的纠错能力来自 Reed-Solomon 码(里德-所罗门码)。这个算法最早用在通信领域——CD、DVD、卫星通信里都在用。核心思想是:在原始数据后面追加冗余数据,即使部分数据被遮挡或损坏,也能从剩余数据中恢复原内容。
QR 码定义了四个纠错级别:
| 级别 | 最大恢复比例 | 适用场景 |
|---|---|---|
| L | 约 7% | 识别环境好,二维码无遮挡,扫码设备质量高 |
| M | 约 15% | 一般场景,日常使用默认级别 |
| Q | 约 25% | 二维码可能有部分遮挡或污损 |
| H | 约 30% | 极端环境,二维码可能严重污损或贴在产品曲面 |
纠错级别越高,附加的纠错码越多,数据区的有效容量就越少。H 级别下大约 30% 的模块存的是纠错码,而不是原始数据。
用个简化的类比理解纠错原理:假设要传 4 个数字 [1, 2, 3, 4]。额外传两个校验值——比如它们的和 10,以及某种加权组合。如果其中一个数字丢失了,从剩下的数字和校验值就能反推出来。Reed-Solomon 在数学上更复杂(基于伽罗瓦域上的多项式运算),但"用冗余数据恢复原始数据"的思路是一样的。
工厂项目里遇到过这种情况:二维码贴在金属曲面上,扫码时有一半在阴影里。用的 H 级别纠错,扫码枪还是能读出来,只是比正常情况慢了一两秒。如果当初用 L 级别,这种场景基本读不出来。
纠错码还有一个巧妙的设计:数据在编码时被打散分布在整张码上。即使某个区域完全被遮挡(比如中间贴了标签纸),只要损坏面积不超过纠错上限,数据仍然可以恢复。这与 Reed-Solomon 配合使用,效果很好。
掩码的作用与原理
仔细观察就会发现,即使编码内容相同、版本相同,不同库生成的二维码图案也不一样。差异来自掩码操作。
掩码(Masking)的目的是防止出现大面积连续黑白区域。如果数据里恰好有很多连续的 0 或 1,二维码会出现大块空白或全黑。这种情况会让扫码器难以定位模块边界,尤其是在光照不均匀的时候。
QR 规范定义了 8 种掩码模式,每种是一个固定的数学图案。编码器按顺序把数据和每种掩码做 XOR 运算,然后评估结果的质量——黑白模块分布是否均匀,有没有长条连续区域。选最好的那个用。
掩码信息本身被编码在格式信息区域。扫码器先读格式信息,从里面解析出纠错级别和掩码编号,再用同样的掩码反向操作还原原始数据。
调试过一个扫码异常:同一台打印机出来的二维码,有的秒扫,有的死活扫不出来。排查了三天,最后发现是打印机墨盒快没墨了。墨量不足导致深浅变化,正好影响了掩码区域的对比度。换墨盒后一切正常。
这是一个很小的细节,但影响很大。打印二维码的时候最好定期检查打印质量,尤其是大批量生产时。
版本与容量
QR 码有 40 个版本。版本 1 是 21×21 模块,版本 40 是 177×177 模块。每升一个版本,每边增加 4 个模块。
版本 1 能存多少数据?取决于纠错级别和数据模式的组合:
- 数字模式 + L 纠错:最多 41 个数字
- 字节模式 + H 纠错:最多 17 个字节
版本 40 是上限。字节模式 + L 纠错下最多存 2953 字节,约 3 KB。
所以不要想着在二维码里塞 PDF 或图片。二维码的定位是短数据的入口,不是数据传输通道。绝大部分应用的做法是二维码里放 URL,实际数据存在服务器上。
生成二维码的常见坑
踩过的坑做个记录。
颜色反转:有些设计为了美观把二维码反白(黑色背景上放白色模块),很多扫码器认不出来。规范里允许反转,但市面上一半以上的扫码器没有实现这个功能。别冒这个险。
边距不足:QR 规范要求二维码外侧有至少 4 个模块宽的空白边距(quiet zone)。很多设计为了做排版切齐把边距去掉了。扫码器需要这个空白区域来区分二维码和周围内容,去掉边距直接导致扫码失败。
圆角模块:有些设计把二维码的模块做成圆角矩形。扫码器的定位算法依赖清晰的角点来识别模块边界,圆角模糊了这些关键点。要做圆角的话半径不能太大。
版本自动选择:生成库通常会根据数据长度自动选最小版本。如果你的数据量接近版本切换边界,最好选大一号的版本,给纠错数据留余量。
参考标准问题:微信的扫码能力在所有消费级 App 里算顶尖的——它能在很低的光照和很大角度下识别。但微信对图像做了很多预处理,包括自动补全缺失的定位图案边缘。如果你做工业级扫码系统,不要用微信做参考标准。工业扫码枪没有这些预处理逻辑,反而在一些微信能扫的场景下扫不出来。用标准的测试卡和工业扫码枪来验证才是正确的做法。
有时候需要自己生成和测试二维码,网上很多条码生成器类似的工具,现在AI也可以直接生成,对于不懂编程的朋友的话还是找个在线工具会更好。