QR 码生成原理:纠错等级、模式选择与掩码
QR 码看着就是一堆黑白方块,但背后有一套精密的编码规则。说白了,你要把一个溯源标记塞进 QR 码里,得先搞清楚三个关键环节:纠错等级、模式选择 和 掩码。
我个人习惯把这三个环节看成“容错力”、“压缩器”和“干扰防护”。容错力决定你贴在曲面甚至磨损后还能扫出来;压缩器决定你的数据用什么格式存进去效率最高;干扰防护则是为了让图案均匀、避免大面积连续黑白块导致扫码器跑偏。
好,我们逐个拆开讲。
纠错等级:容错能力怎么选?
QR 码在数据位后面塞了里德-所罗门(Reed-Solomon)纠错码。什么意思呢?就是二维码破损一部分,依然能正确解码。
标准里定义了 4 个纠错等级:
| 等级 | 恢复能力 | 典型场景 |
|---|---|---|
| L(Low) | 约 7% 数据恢复 | 洁净环境,打印精度高 |
| M(Medium) | 约 15% 数据恢复 | 一般打印或屏幕显示 |
| Q(Quartile) | 约 25% 数据恢复 | 工业标签,可能轻微磨损 |
| H(High) | 约 30% 数据恢复 | 强磨损、曲面、户外 |
我的经验:做溯源标记时,我通常选 Q 或 H。比如把二维码印在易拉罐底,运输中会刮花,用 H 等级,即使罐底凹进去一块照扫不误。
注意:纠错等级越高,数据冗余越大,QR 码版本(尺寸)也会变大。你存的内容越多,就需要更大的码。别为了容错盲目选 H,结果码太大印不下。
模式选择:数据怎么塞进去效率最高?
QR 码有四种主要数据模式。不同模式对应不同的位压缩方式。你想想看,纯数字和中文的编码效率能一样吗?
| 模式 | 适用字符 | 编码效率(位/字符) | 模式指示符 |
|---|---|---|---|
| 数字模式(Numeric) | 0-9 | 10 位 / 3 字符 | 0001 |
| 字母数字模式(Alphanumeric) | 0-9, A-Z, 空格, $%*+-./: | 11 位 / 2 字符 | 0010 |
| 字节模式(Byte) | 任何 8 位字节(ISO-8859-1) | 8 位 / 字符 | 0100 |
| 汉字模式(Kanji) | Shift JIS 汉字 | 13 位 / 汉字 | 1000 |
其实还有一种扩展解释模式(ECI),但日常用不上。我遇到过最典型的坑是:用字节模式存储纯数字,结果位宽多了近 3 倍,码变大了好多。后来改成数字模式,一下子就紧凑了。
避坑指南:我曾经在一个溯源实践中,给每个产品生成包含 20 位数字 ID 的 QR 码。用字节模式硬塞,结果码尺寸飙到 Version 7;换成数字模式后直接降到 Version 4,印刷成本省一半。
模式选择还有一点要注意:模式切换。QR 码允许在一条数据里分段使用不同模式。比如开头是数字,后面是英文字母,中间可以插一个模式指示符跳转。这个在生成库(如 zxing、qrcode)里会自动处理,但你理解原理后能更精准地控制数据长度。
掩码:防止二维码变成靶心
你以为直接放编码后的模块就完事了?太天真了。
如果不做掩码,QR 码里可能出现大面积连续的黑块或白块。比如全 0 的数据会导致一大片白,扫码器很难定位。所以标准定义了 8 种掩码图案,通过 XOR 运算对数据区域进行“调色”。
8 种掩码的生成规则基于模块坐标 (i, j):
| 掩码编号 | 条件(满足任一则翻转) | 特点 |
|---|---|---|
| 0 | (i + j) % 2 == 0 | 棋盘格效果 |
| 1 | i % 2 == 0 | 水平条纹 |
| 2 | j % 3 == 0 | 垂直条纹 |
| 3 | (i + j) % 3 == 0 | 对角点阵 |
| 4 | (i//2 + j//3) % 2 == 0 | 大格子分区 |
| 5 | (i * j) % 2 + (i * j) % 3 == 0 | 复杂混合 |
| 6 | ((i * j) % 2 + (i * j) % 3) % 2 == 0 | 混合变体 |
| 7 | ((i + j) % 2 + (i * j) % 3) % 2 == 0 | 另一种混合 |
实际编码时,会分别对数据用 8 种掩码试一遍,然后选一个罚分最少的。罚分规则包括:连续同色模块、大块空白、定位图案附近样式等。这块标准里有详细的评分表,有兴趣可以翻 ISO/IEC 18004。
我的做法:写生成器时,我直接套用标准里的罚分表,但要注意排除功能图形(定位图案、分隔符、定时图案)区域。这些区域不参与掩码。
核心流程图
下面这张图把整个 QR 码数据编码的流程串起来了。纠错等级影响数据容量和冗余,模式选择决定数据打包方式,掩码最后做样式优化。三者缺一不可。
实际编码示例(伪代码)
我来给一段核心逻辑的伪代码,帮你把概念落地。假设我们有一个溯源字符串 "SN20250324",想生成版本 3 的 QR 码:
// 1. 选择纠错等级 Q(25% 恢复)
int eccLevel = QR_ECC_Q;
// 2. 分析数据类型,自动选中最佳模式
// "SN20250324" 含字母和数字 → Alphanumeric 模式
int mode = detectMode("SN20250324"); // 返回 MODE_ALPHANUM
// 3. 根据版本和纠错等级,查表得到数据容量
int maxDataBits = getCapacity(version=3, eccLevel); // 例如 480 bits
// 4. 编码数据:模式指示符(4bits) + 字符计数(9bits) + 数据(11bits/2chars)
bits = encodeAlphanumeric("SN20250324");
// 5. 填充终止符、补齐到容量
bits = padToCapacity(bits, maxDataBits);
// 6. 生成 RS 纠错码块
bits = addReedSolomon(bits, eccLevel);
// 7. 将 bits 填入模块矩阵(除功能图形区域)
bool modules[21][21]; // Version 3 = 21×21
fillDataModules(modules, bits);
// 8. 尝试 8 种掩码,计算罚分
int bestMask = 0;
int minPenalty = INT_MAX;
for (mask = 0; mask < 8; mask++) {
bool masked[21][21];
applyXorMask(modules, masked, mask);
int penalty = calculatePenalty(masked);
if (penalty < minPenalty) { minPenalty = penalty; bestMask = mask; }
}
// 9. 将最佳掩码编号写入格式信息区
writeFormatInfo(modules, eccLevel, bestMask);
// 10. 最终 modules 就是你要的 QR 码矩阵
outputQRMatrix(modules);
注意:实际生产环境别手写这套逻辑。推荐用 zxing 或 qrcode.js 这类成熟库。但搞懂流程能帮你调试问题——比如有一次我同事反馈扫码率低,我一看生成的码,掩码评分没走对,导致图案里有大片同色块。改掉就好了。
小结
纠错等级、模式选择、掩码,这三个环节决定了 QR 码的“实用品质”。我自己的经验是:先确定容错需求(H 等级无脑选),再根据数据内容选最优模式(数字最省空间,中文用汉字模式更小),掩码交给库去做,但你要知道评分逻辑才能排查“扫不出来”的坑。