拆解二维码:数据是怎么藏进黑白方块的
每天都在扫二维码,但很少停下来想过那堆黑白方块到底是怎么把一段网址塞进去的,又是怎么被手机读回来的。
二维码的物理结构
一个二维码并不全是数据。拿手机对着任意一个二维码看,最显眼的是三个角上的大方块——它们不承载数据,是给扫描器定位用的。
整个二维码由这些区域组成:
| 区域 | 位置 | 作用 |
|---|---|---|
| 定位图案 | 左上、右上、左下三个角 | 让扫描器快速找到二维码的位置、大小和旋转角度 |
| 定时图案 | 连接三个定位图案的横竖线 | 黑白交替排列,帮扫描器确定每个模块(最小方块)的坐标 |
| 对齐图案 | 数据区内(Version 2 以上才有) | 校正透视变形,手机歪着扫也能正确采样 |
| 格式信息 | 紧挨定位图案的区域 | 存纠错等级和掩码编号,扫描器最先读这里 |
| 版本信息 | 右上和左下定位图案旁(Version 7+) | 标记二维码的尺寸版本 |
| 数据 + 纠错区 | 剩余所有模块 | 实际承载的编码内容和纠错冗余 |
二维码有 40 个版本。Version 1 是 21x21 个模块,每升一级边长加 4,Version 40 是 177x177。日常见到的大多是 Version 2-7。
数据怎么编成方块
把一段数据塞进二维码,要走四步:选编码模式 → 构造比特流 → 生成纠错码 → 排进矩阵。
选编码模式
二维码定义了四种编码模式,针对不同字符集做压缩优化:
| 模式 | 可编码字符 | 每字符占位 | 典型场景 |
|---|---|---|---|
| 数字模式 | 0-9 | ~3.3 bit | 电话号码、会员卡号 |
| 字母数字模式 | 0-9、A-Z、空格及 $%*+-./: | ~5.5 bit | 大写字母 URL |
| 字节模式 | 任意字节 | 8 bit | UTF-8 文本、含小写字母的 URL |
| 汉字模式 | Shift JIS 编码的日文汉字 | 13 bit | 日文内容 |
一个二维码里可以混合使用多种模式。编码器会自动选择能让总比特数最短的组合。
构造比特流
以字母数字模式编码 HELLO 为例。
字母数字模式把 45 个合法字符各映射到一个 0-44 的数值(0-9 对应 0-9,A-Z 对应 10-35,空格及符号占 36-44)。编码时两个字符一组,用 第一个 x 45 + 第二个 压成一个 11 bit 的数;如果最后剩一个落单的,单独占 6 bit。
H=17, E=14, L=21, L=21, O=24,两两配对:
- HE: 17 x 45 + 14 = 779 → 11 bit
- LL: 21 x 45 + 21 = 966 → 11 bit
- O(落单): 24 → 6 bit
最终比特流由几部分拼接:模式指示符(4 bit,字母数字模式是 0010)→ 字符计数(这里 9 bit 表示数字 5)→ 上面的编码数据 → 终止符 0000 → 补齐到 8 bit 整数倍 → 填充字节(交替填 0xEC 和 0x11 直到塞满该版本的数据容量)。
生成纠错码
二维码用 Reed-Solomon 编码来做前向纠错,提供四个等级:
| 等级 | 可恢复的最大损坏比例 |
|---|---|
| L | ~7% |
| M | ~15% |
| Q | ~25% |
| H | ~30% |
等级越高,冗余越多,同版本能装的有效数据越少。
生成纠错码的核心操作是在 GF(256)(一个有 256 个元素的伽罗瓦域)上做多项式除法:把数据码字当作多项式系数,除以一个预定义的生成多项式,余数就是纠错码字。这些纠错码字附在数据后面,让解码端有能力定位并修复受损的码字。
数据码字和纠错码字最后按规则交错排列,形成一个最终的码字序列。
排进矩阵并加掩码
码字序列沿一条固定的路线填入矩阵的数据区:从右下角开始,每次取两列宽的条带,自下而上再自上而下蛇行前进,遇到定位图案、定时图案等非数据区域就跳过。
填完后还有最后一步——掩码。如果数据恰好形成了大面积纯黑或纯白、或者出现了类似定位图案的条纹,会干扰扫描器。规范定义了 8 种掩码图案,每种通过一个坐标公式(比如 (row + col) mod 2 == 0)决定哪些模块要翻转黑白。编码器把 8 种都试一遍,按四项惩罚规则打分,选罚分最低的。选中的掩码编号写进格式信息区。
到这一步,一个完整的二维码就生成了。
扫码时发生了什么
解码是编码的逆过程,但多了一个前置步骤:从摄像头画面里找到二维码并校正变形。
定位与透视校正
扫描器在画面中搜索三个定位图案。定位图案的结构设计很精巧:无论从哪个角度扫过去,穿越它的任意一条扫描线上黑白宽度比总是 1:1:3:1:1。这个比例在自然图像中极少出现,所以即使背景杂乱也能可靠检测。
找到三个定位图案后,扫描器就确定了二维码的位置、旋转角度和大致尺寸。有对齐图案的话再用它来修正透视变形——做一次透视变换,把倾斜拍到的梯形拉正成矩形。
逆向还原数据
校正之后,扫描器先读格式信息区的 15 bit(它自身也带 BCH 纠错保护)。这 15 bit 给出两个关键参数:纠错等级和掩码编号。拿到掩码编号,把掩码反向应用一次,数据区就恢复到掩码前的状态。
然后沿编码时相同的蛇行路线把模块值读出来,还原码字序列。按交错规则拆回各个数据块和纠错块,对每个块执行 Reed-Solomon 解码。如果有模块被污损(脏污、遮挡、印刷模糊),纠错算法会尝试定位错误位置并恢复原始值。只要损坏量没超过该纠错等级的上限,数据就能完整还原。
最后根据比特流开头的模式指示符确定编码模式,把后续比特解回原始字符串。整个过程在毫秒级内完成。
几个常见问题
中间放 logo 为什么还能扫? 本质上是故意”毁掉”了中间那片模块的数据。能扫成功靠的是纠错冗余——用 H 级时最多能恢复 30% 的损坏。logo 面积没超过这个比例就大概率没问题,但这不是规范保证的行为。
二维码能装多少数据? Version 40 + L 级纠错的理论上限是 7,089 个数字字符或 4,296 个字母数字字符。但日常几乎见不到这么大的二维码——大多数场景一个短 URL 就够了,Version 3、4 就能搞定。
为什么有些二维码扫得快有些慢? 规范层面的编解码逻辑不复杂,扫码速度主要取决于图像处理:二值化(把灰度图转成黑白)、多尺度检测(远近距离兼容)、透视校正的精度。这些是工程优化,不是编码本身的瓶颈。