
二维码已经深入日常生活的方法面面,但你有没有好奇过二维码背后的原理呢?今天让我们来了解下二维码的原理,由于二维码的很多细节实在太精妙,今天无法一次性讲完,如果感兴趣,可以做更多期分享或自行翻阅更多资料。
二维码原理
二维码本质上是对字符串的编码规则,最终转换成二进制串。
基础介绍
QRCode:Quick Response Code;全称为快速响应矩阵图码

-
定位图案:
- 位置探测图案:用于标记二维码矩形的大小;用三个定位图案即可标识并确定一个二维码矩形的位置和方向了;
- 位置探测图形分隔符:用白边框将定位图案与其他区域区分;
- 定位图案:用于定位,二维码如果尺寸过大,扫描时容易畸变,时序图案的作用就是防止扫描时畸变的产生;
- 校正图形:只有在 Version 2 及其以上才会需要;
-
功能数据:
- 格式信息:存在于所有尺寸中,存放格式化数据;
- 版本信息:用于 Version 7 以上,需要预留两块 3×6 的区域存放部分版本信息;
-
数据内容(剩余部分):
- 数据码;
- 纠错码;
一些有意思的知识
版本信息
- 二维码一共有
40
个尺寸(也可以称为版本、Version
)。Version 1
是21 x 21
的矩阵,Version 2
是25 x 25
的矩阵。每增加一个version
,长宽就增加 4,公式是:(V - 1) * 4 + 21
- 最高版本是
40
,所以是177 x 177
的正方形,单纯存储数字的话,可以存 7089 个;只存大写字母的话,可以存大约 4k 个,大约500个汉字
常用编码模式
- 数字编码(
Numeric Mode
): 只支持数字 0~9 的编码 - 字符编码(
Alphanumeric Mode
):支持包含数字、大写 的A-Z
(不包含小写)、以及$ % * + -- . / :
和空格 - 字节编码(
Byte Mode
): 支持0x00
~0xFF
内所有的字符 - 日文编码(
Kanji Mode
): 也可以用于中文编码,只能支持0x8140
0x9FFC
、0xE040
0xEBBF
的字符
纠错码
为什么二维码有残缺也能扫出来,以及如何保证扫描结果的正确性?

- 纠错级别越高,恢复能力越强,代价是能存储的有效数据越少,因为纠错码的占比会越高。
- 纠错方法采用的是**里德所罗门码(Reed-Solomon Error correction)** ,该算法运用比较广泛,在二维码中,为了抵抗扫描错误或污点,在磁盘中,为了抵抗媒体碎片的丢失,在高级存储系统中,比如谷歌的
gfs
和bigtable
,为了抵抗数据丢失。(网络协议中的差错控制)
转换为掩码图案
- 为什么二维码黑白块看起来那么均匀?没有连续的 1/0 块吗?
- 对数据区再进行 Masking 操作,也就是做 XOR 操作


艺术二维码
zxing 库原理
数据预处理
在官方示例中,获取到 YUV 图像数据之后,进行解码的代码如下:
java
private void decode(byte[] data, int width, int height) {
Result rawResult = null;
// 构建 YUV 数据源
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
if (source != null) {
// 构造二值图像比特流,使用 HybridBinarizer 算法解析数据源
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
// 采用 MultiFormatReader 解析图像,可以解析多种数据格式
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
//解码失败
}
}
if (rawResult != null) {
// 解析到二维码结果
}
}
首先根据 YUV 图像创建亮度源,也就是通过 PlanarYUVLuminanceSource
类将数据构建为亮度源数据,该类继承于 LuminanceSource
,用来构建 YUV 图像格式的数据源,同时提供了图片裁截功能, 将多余图像去除,zxing 中还提供了构建 RGB 图像数据源的 RGBLuminanceSource
。深入到 PlanarYUVLuminanceSource
类中,该类定义了:getRow
、getMatrix
、crop
、isCropSupported
、isRotateSupported
等方法,其中getRow
与 getMatrix
都是将图像数据以像素为单位进行计算,再返回亮度值,使得之后的计算都是以 Y
分量得出的亮度数组进行计算。
YUV 是一种图像格式,"Y"表示亮度,"U" 与"V" 表示色度、浓度,图像数据还有许多其他格式,例如:JPEG、ARGB、NV21、YUY2等,这里使用 YUV 格式好处是节省带宽,对于二维码解析来说,并不需要彩色的图片数据,将 YUV 数据单独显示如下图所示:

构建完图像数据源之后,需要将源图像进行二值化,也就是将 LuminanceSource
获取到的亮度数据源矩阵构建为黑白两部分,也就是使得图像非黑即白 ,zxing 自带两种二值化解析算法分别是 HybridBinarizer
、GlobalHistogramBinarizer
,其中 GlobalHistogramBinarizer
适用于低端设备 ,对手机 CPU 与内存要求不高,它选择的全部的黑点计算,因此无法处理阴影等类似情况。而 HybridBinarizer
算法在执行效率上要慢于 GlobalHistogramBinarizer
,但识别相对更有效,它是专门以白色为背景的连续黑色块的二维码图像而设计的,适用于解析带有阴影和渐变的二维码图像。
官方例子中使用的是 HybridBinarizer
算法,简单概括下该算法流程,当获取到亮度矩阵之后,HybridBinarizer
会将矩阵分为按照 8 * 8 的像素为单位来计算像素平均值,再按照每个小矩阵与本身周边的 5 * 5 的小矩阵再计算一次平均值,得到的值作为这个矩阵块的分界点,这时将矩阵内的 8 * 8 个点都和这个分界点进行比较,小于则为黑点,这样经过运算每个像素点变为 bit 描述特征,非黑即白。这样 byte 代表的像素点变为 bit 表述,减少了很多的数据量,方便了之后解码运算。
最终构建为 BinaryBitmap
二进制位图,以上就是 zxing 对图像数据的预处理。
zxing 解码
将图像数据构建为黑白表示的 bit 矩阵之后,接下来就是对 BinaryBitmap
对象进行条码比对解析,从而出去出二维码坐标与数据,不同的条码类型对应不同的读取方式,zxing 中提供了很多这种 XXReader 类 ,它们都是实现了 Reader 接口,其中都有一个 decode 的方法,用来解析 BinaryBitmap
数据,官方示例中使用的是 MultiFormatReader
类,该类可以支持多种解码格式,需要手动设置,如果只是扫描二维码,我们只需要设置它的 hints
为 QR_CODE
格式即可,这样可以加快解码速度,二维码解码最终是使用的 QRCodeReader
类,下面分析下 QRCodeReader
解码流程。
我们来看 QRCodeReader
类中的 decode
方法,代码如下:
ini
public final Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)
throws NotFoundException, ChecksumException, FormatException {
DecoderResult decoderResult;
ResultPoint[] points;
// 判断是否是纯二维码
if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) {
BitMatrix bits = extractPureBits(image.getBlackMatrix());
decoderResult = decoder.decode(bits, hints);
points = NO_POINTS;
} else {
// 相机拍摄,首先判断是否有二维码
DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);
// 有二维码,继续解码
decoderResult = decoder.decode(detectorResult.getBits(), hints);
points = detectorResult.getPoints();
}
if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) {
((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points);
}
Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE);
List<byte[]> byteSegments = decoderResult.getByteSegments();
if (byteSegments != null) {
result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);
}
String ecLevel = decoderResult.getECLevel();
if (ecLevel != null) {
result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel);
}
if (decoderResult.hasStructuredAppend()) {
result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE,
decoderResult.getStructuredAppendSequenceNumber());
result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY,
decoderResult.getStructuredAppendParity());
}
return result;
}
首先,判断 hints
信息中是否标记了 PURE_BARCODE
纯二维码,纯二维码表示除了二维码图片外没有其他的干扰信息,类似给定一张只有二维码的图片,进行扫码时即可设置该信息,zxing 给的官方示例是使用了相机预览扫码,所以这里走到 else 中调用 new Detector(image.getBlackMatrix()).detect(hints)
方法,该 Detector
类中的 detect
方法首先检查图片中是否有二维码存在,再通过 decoder.decode()
方法进行解码,detect
方法代码如下:
ini
public final DetectorResult detect(Map<DecodeHintType,?> hints) throws NotFoundException, FormatException {
resultPointCallback = hints == null ? null :
(ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
FinderPatternFinder finder = new FinderPatternFinder(image, resultPointCallback);
FinderPatternInfo info = finder.find(hints);
return processFinderPatternInfo(info);
}
从代码可知这里使用了 FinderPatternFinder
类中的 find
方法进行检查,进入到 find
源码 ,可以知道该方法是通过扫描图片,找到规律为黑白黑,并且像素比例为 1:1:3:1:1 ,也就是二维码特征中的 Position Detection Patterns 定位图案,若未找到这 3 个定位点将直接抛出 NotFoundException
异常,当找到 3 个定位图案之后,该方法同时还会通过 ResultPoint.orderBestPatterns()
方法对图像进行旋转排序,从而将二维码图像摆正,这就是我们手机无论怎么倾斜,都会成功识别二维码的原因。
当获取到排列有序的二维码坐标信息后,zxing 就可进行下一步操作,也就是通过 processFinderPatternInfo
方法将其中的二维码构建成正确的二维码矩阵,这里构建的应该是与原图像二维码相对应的一个符号矩阵,主要是数据校验和生成最终的矩阵,从而方便之后的解码操作。
分析相关代码可知,processFinderPatternInfo
方法首先进行版本信息的读取,也就是上方二维码特征图中的 Version Information 部分,二维码版本越高可容纳的内容也就越多,版本与内容点数的计算公司为:17 + 4 * versionNumber ,读取到版本信息 之后,如果版本大于 1 则会继续获取二维码的对齐模式 ,也就是二维码特征图中的 Alignment Patterns 部分,该部分用于对二维码中信息的辅助定位。得到以上信息之后就可以重新构建与原图像中二维码相对应的符号矩阵,所做的处理就是透视转换算法,方法为 createTransform
,对图像像素进行一系列的转换修补,最终得到以 0 1 代表黑白所填充的矩阵。
此时,Detector().detect()
判断是否有二维码的操作都已完成,得到一个 DetectorResult
对象,其中包含了构建好的二维码矩阵,此时即可进行下一步 decoder.decode()
解码操作,分析相关源码,首先会将上一步得到的 bit 矩阵通过 BitMatrixParser
进行简单判断是否包含二维码,接着通过 decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)
方法,获取版本信息、格式信息、纠错码等,接着使用 DecodedBitStreamParser.decode()
方法解码内容部分,具体解码细节可以查看二维码的编码规范 ISO/IEC 16022-2006 ,最终得到 DecoderResult
对象,其中包含了二维码内容信息。
小结
- 获取图像帧的数据,格式为YUV;
- 将二维码扫码框中的图像数据进行灰度化处理;
- 将灰度化后的图像进行二值化处理;
- 根据二维码的特征寻找定位符;
- 寻找二维码的校正符;
- 解码;