目录
  1. 1. 获取相机捕获到的数据
  2. 2. YUV图片格式详解
  3. 3. 识别图片中的二维码
  4. 4. 二维码的特征
  5. 5. ZXing识别图像中的二维码
    1. 5.1. 1、寻找定位符
    2. 5.2. 2、寻找校正符
  6. 6. 总结
  7. 7. 结束语
ZXing源码解析四:如何识别图片中的二维码

不知道大家在用ZXing作为扫码库的时候,有没有想过“ZXing是怎么从相机捕获的每一帧图片中获取到二维码并解析的呢?”,如果你思考过并且已经从源码中知道了答案,那么这篇文章你就没必要读下去了,如果你思考过却不知道答案,那么这篇文章就是为你准备的,相信你读过后会有一个清晰的答案。

  为了不那么突兀,还是先跟着源码来一步步的讲解,先来看怎样获取到相机捕获到的图片的数据的。

获取相机捕获到的数据

  因为前面的文章已经分析过ZXing解码的步骤了,这里就重点看下,相机捕获到图像的后续步骤,源码如下

1
2
3
4
5
6
7
public void restartPreviewAndDecode() {
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
activity.drawViewfinder();
}
}

上面的代码是在CaptureActivityHandler构造方法中调用的,也就是在CaptureActivityHandler实例化的时候调用。然后,调用到了cameraManagerrequestPreviewFrame方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
* in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
* respectively.
*
* @param handler The handler to send the message to.
* @param message The what field of the message to be sent.
*/
public synchronized void requestPreviewFrame(Handler handler, int message) {
OpenCamera theCamera = camera;
if (theCamera != null && previewing) {
previewCallback.setHandler(handler, message);
theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
}
}

现在来分析一下上面的代码,重点来看下这句

1
theCamera.getCamera().setOneShotPreviewCallback(previewCallback);

这句代码的作用就是设置一个预览帧的回调,意思就是相机每捕获一帧数据就会调用,这里设置的previewCallback中的方法,经分析,最终调用previewCallback中的方法是public void onPreviewFrame(byte[] data, Camera camera),这里的第一个参数就是每一帧图像的数据即byte数组。Android 中Google支持的Camera Preview CallBack的YUV常用格式有两种:一种是NV21,一种是YV12,Android一般默认使用的是YCbCR_420_sp(NV21),当然,也可以通过下面的代码来设置自己需要的格式。

1
2
3
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);

ZXing库中并没有设置格式,所以这里默认的是NV21格式。那么问题来了,NV21到底是什么意思呢?欲知详情,请继续阅读下文

YUV图片格式详解

YUV是一种颜色编码方法,和它等同的还有 RGB 颜色编码方法。

  RGB 图像中,每个像素点都有红、绿、蓝三个原色,其中每种原色都占用 8 bit,也就是一个字节,那么一个像素点也就占用 24 bit,也就是三个字节。一张 1280 * 720 大小的图片,就占用 1280 * 720 * 3 / 1024 / 1024 = 2.63 MB 存储空间。
   YUV颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度(Luminance、Luma),而U和V表示色度(Chrominance、Chroma)。而色度又定义了颜色的两个方面:色调和饱和度。

  上文的NV21YV12是YUV存储格式。

  • NV21格式属于YUV420SP类型。它也是先存储了Y分量,但接下来并不是再存储所有的U或者V分量,而是把UV 分量交替连续存储。
  • YV12格式属于YUV420P类型,即先存储Y分量,再存储U、V分量,YV12是先Y再V后U。

关于YUV格式的介绍,网上有一篇比较好的文章,点击这里查看。对YUV格式有一定的了解之后,继续来分析源码,看下,是怎样从图片中识别二维码的。

识别图片中的二维码

  上文已经知道,相机每获取一帧的数据都会回调PreviewCallback类中的onPreviewFrame方法,在此方法中,利用Handler的机制,将图片转换成的字节数组传递给了DecodeHandler类,然后调用了decode方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void decode(byte[] data, int width, int height) {
long start = System.nanoTime();
//...省略部分代码

Result rawResult = null;
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
Log.e(TAG, "decode: 没有发现二维码" );
} finally {
multiFormatReader.reset();
}
}

//...省略部分代码
}

这部分代码可以说是ZXing解码的核心代码了,现在一点点的来分析,先看

1
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);

这句代码,实例化了PlanarYUVLuminanceSource对象,主要的目的是获取扫码框中的图像的数据。在将图像进行二值化的时候会调用此对象中的方法,稍后会在源码中介绍。 再看这句代码

1
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));

这句代码,嗯,先看new GlobalHistogramBinarizer(source)这句代码,GlobalHistogramBinarizer图像的数据就是在这个类中进行二值化的,当然还有一个HybridBinarizer类,这个类也是将图像二值化的,那主要的区别是什么呢?主要的区别就是HybridBinarizer类处理的比GlobalHistogramBinarizer精确,但是处理的速度较慢,推荐在性能比较好的手机上使用,而GlobalHistogramBinarizer处理的不太精确,如有阴影的化,可能处理的图片就会有问题,但是速度较快,推荐在性能不太好的手机上使用。 这里,我们用的是GlobalHistogramBinarizer来对图像进行二值化处理,因为,经过我测试发现,这个速度快点。

  再来看整句的代码,就是实例化了BinaryBitmap类,然后将GlobalHistogramBinarizer对象注入。

  下面的代码就是从图像中发现二维码并解析,代码如下

1
2
3
4
5
6
7
8
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
Log.e(TAG, "decode: 没有发现二维码" );
} finally {
multiFormatReader.reset();
}

跟踪下去,发现最终会调用QRCodeReader类中的decode(BinaryBitmap image, Map<DecodeHintType,?> hints)方法。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 the code was mirrored: swap the bottom-left and the top-right points.
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;
}

来看

1
DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints);

这句代码。image.getBlackMatrix()就是调用GlobalHistogramBinarizer类中的getBlackMatrix方法,其中的代码就不看了,getBlackMatrix方法的主要作用就是将图片进行二值化的处理,二值化的关键就是定义出黑白的界限,我们的图像已经转化为了灰度图像,每个点都是由一个灰度值来表示,就需要定义出一个灰度值,大于这个值就为白(0),低于这个值就为黑(1)。具体的处理方法如下

在 GlobalHistogramBinarizer中,是从图像中均匀取5行(覆盖整个图像高度),每行取中间五分之四作为样本;以灰度值为X轴,每个灰度值的像素个数为Y轴建立一个直方图,从直方图中取点数最多的一个灰度值,然后再去给其他的灰度值进行分数计算,按照点数乘以与最多点数灰度值的距离的平方来进行打分,选分数最高的一个灰度值。接下来在这两个灰度值中间选取一个区分界限,取的原则是尽量靠近中间并且要点数越少越好。界限有了以后就容易了,与整幅图像的每个点进行比较,如果灰度值比界限小的就是黑,在新的矩阵中将该点置1,其余的就是白,为0。

上面一句的代码,调用了Detector中的detect(Map<DecodeHintType,?> hints)方法,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* <p>Detects a QR Code in an image.</p>
*
* @param hints optional hints to detector
* @return {@link DetectorResult} encapsulating results of detecting a QR Code
* @throws NotFoundException if QR Code cannot be found
* @throws FormatException if a QR Code cannot be decoded
*/
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);
}

从这段代码的注释中可以得知,这个方法的作用就是“封装检测二维码的结果”,如果没有发现二维码就会抛出NotFoundException异常,如果不能解析二维码就会抛出FormatException异常。现在,我们来看怎样找到图像中的二维码的。

二维码的特征

  在介绍发现图片中二维码方法之前,先来看下二维码的特点,如下图


二维码在设计之初就考虑到了识别问题,所以二维码有一些特征是非常明显的。

二维码有三个“回“字形图案,这一点非常明显。中间的一个点位于图案的左上角,如果图像偏转,也可以根据二维码来纠正。

识别二维码,就是识别二维码的三个点,逐步分析一下这三个点的特性

  1. 每个点有两个轮廓。就是两个口,大“口”内部有一个小“口”,所以是两个轮廓。
  2. 如果把这个“回”放到一个白色的背景下,从左到右,或从上到下画一条线。这条线经过的图案黑白比例大约为:黑白比例为1:1:3:1:1。如下图
  3. 如何找到左上角的顶点?这个顶点与其他两个顶点的夹角为90度。

通过上面几个步骤,就能识别出二维码的三个顶点,并且识别出左上角的顶点。

ZXing识别图像中的二维码

  上面已经介绍了二维码的特征,也介绍了怎样发现二维码的“回”字,现在,我们来看下ZXing是怎么识别图片中的二维码的,主要的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
final FinderPatternInfo find(Map<DecodeHintType,?> hints) throws NotFoundException {
boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER);
int maxI = image.getHeight();
int maxJ = image.getWidth();
// 在图像中寻找黑白像素比例为1:1:3:1:1
int iSkip = (3 * maxI) / (4 * MAX_MODULES);
if (iSkip < MIN_SKIP || tryHarder) {
iSkip = MIN_SKIP;
}

boolean done = false;
int[] stateCount = new int[5];
for (int i = iSkip - 1; i < maxI && !done; i += iSkip) {
// 获取一行的黑白像素值
clearCounts(stateCount);
int currentState = 0;
for (int j = 0; j < maxJ; j++) {
if (image.get(j, i)) {
// 黑色像素
if ((currentState & 1) == 1) { // Counting white pixels
currentState++;
}
stateCount[currentState]++;
} else { // 白色像素
if ((currentState & 1) == 0) { // Counting black pixels
if (currentState == 4) { // A winner?
if (foundPatternCross(stateCount)) { // Yes 是否是二维码左上角的回字
boolean confirmed = handlePossibleCenter(stateCount, i, j);
if (confirmed) {
// Start examining every other line. Checking each line turned out to be too
// expensive and didn't improve performance.
iSkip = 2;
if (hasSkipped) {
done = haveMultiplyConfirmedCenters();
} else {
int rowSkip = findRowSkip();
if (rowSkip > stateCount[2]) {
// Skip rows between row of lower confirmed center
// and top of presumed third confirmed center
// but back up a bit to get a full chance of detecting
// it, entire width of center of finder pattern
// Skip by rowSkip, but back off by stateCount[2] (size of last center
// of pattern we saw) to be conservative, and also back off by iSkip which
// is about to be re-added
i += rowSkip - stateCount[2] - iSkip;
j = maxJ - 1;
}
}
} else {
shiftCounts2(stateCount);
currentState = 3;
continue;
}
// Clear state to start looking again
currentState = 0;
clearCounts(stateCount);
} else { // No, shift counts back by two
shiftCounts2(stateCount);
currentState = 3;
}
} else {
stateCount[++currentState]++;
}
} else { // Counting white pixels
stateCount[currentState]++;
}
}
}
if (foundPatternCross(stateCount)) {
boolean confirmed = handlePossibleCenter(stateCount, i, maxJ);
if (confirmed) {
iSkip = stateCount[0];
if (hasSkipped) {
// Found a third one
done = haveMultiplyConfirmedCenters();
}
}
}
}
FinderPattern[] patternInfo = selectBestPatterns();
ResultPoint.orderBestPatterns(patternInfo);
return new FinderPatternInfo(patternInfo);
}

上面的代码主要做了下面的事

1、寻找定位符

  在图像中每隔iSkip就采样一行,

1
int iSkip = (3 * maxI) / (4 * MAX_MODULES);

  在这一行中将连续的相同颜色的像素个数计入数组中,数组长度为5位,即去找黑\白\黑\白\黑的图像(如开始检测到黑色计入数组[0],直到检测到白色之前都将数组[0]的值+1;检测到白色了就开始在数组[1]中计数,以此类推)。填满5位后检测这5位中像素个数是否比例为1:1:3:1:1(可以有50%的误差范围),如果满足条件就说明找到了定位符的大概位置,将这个图像交给handlePossibleCenter方法去找到定位符的中心点,方法是先从垂直方向检测是否满足定位符的条件,如满足就定出Y轴的中心点坐标值,然后用这个坐标值去再次检测水平方向是否满足定位符条件,如满足就定出X轴的中心点坐标值。至此就找到了一个定位符的中心坐标。

  按照上面所说的步骤找出所有三个定位符的中心坐标,接下来开始定位三个定位符在符号中的位置,即左上(B点)、左下(A点)、右上(C点)三个位置。先通过两两之间的距离定出哪个是左上那一点(左上那点到其他两点的距离应该相差不远),然后通过计算BA、BC向量的叉乘定出A和C两点。

2、寻找校正符

  通过ABC三点的坐标计算出校正符的可能位置,然后交给AlignmentPatternFinder去寻找最靠近右下角的那个校正符,寻找方法与寻找定位符的方法基本相同,如果找到就返回校正符的中心坐标,如果没有找到也没关系,解码程序可以继续。

通过上面的两步就可以判断相机获取的图像帧中是否有二维码了,如果有二维码则进行二维码的解析,没有二维码就抛出异常,然后继续解析下一帧图像数据。

总结

  通过上文的讲解和源码的分析,我们可以知道判断图像帧中是否有二维码需要经过以下几步:

  1. 获取图像帧的数据,格式为YUV;
  2. 将二维码扫码框中的图像数据进行灰度化处理;
  3. 将灰度化后的图像进行二值化处理;
  4. 根据二维码的特征寻找定位符;
  5. 寻找二维码的校正符。

如果在步骤“4”中找到了校正符,则说明这一帧图片中含有二维码,可以进行二维码的解析,否则就抛出异常,继续解析下一帧图像的数据。

结束语

  没有看源码之前,我是比较迷茫的,不知道怎样才能判断图片中是否有二维码,虽然知道可以根据二维码中的“回”字来判断,但是不知道怎么找到“回”字呀!阅读源码后才知道,可以将图片进行“二值化”处理,再根据黑白像素的比例来找到“回”字,感觉学到了很多。所以呢,在我们不知道某个库的某个功能是怎样实现的时候,最好的解决办法就是阅读源码,答案都在源码中。

  在研究源码的时候删除了好多与解析二维码无关的代码,最后的代码在这里

  该系列文章:

ZXing源码解析一:让源码跑起来

ZXing源码解析二:掌握解码步骤

ZXing源码解析三:相机的配置与数据的处理

文章作者: wizardev
文章链接: http://pi.wizardev.com:88/blog/ZXing%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E5%9B%9B%EF%BC%9A%E5%A6%82%E4%BD%95%E8%AF%86%E5%88%AB%E5%9B%BE%E7%89%87%E4%B8%AD%E7%9A%84%E4%BA%8C%E7%BB%B4%E7%A0%81/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 wizardev的博客

评论