不知道大家在用ZXing作为扫码库的时候,有没有想过“ZXing是怎么从相机捕获的每一帧图片中获取到二维码并解析的呢?”,如果你思考过并且已经从源码中知道了答案,那么这篇文章你就没必要读下去了,如果你思考过却不知道答案,那么这篇文章就是为你准备的,相信你读过后会有一个清晰的答案。
为了不那么突兀,还是先跟着源码来一步步的讲解,先来看怎样获取到相机捕获到的图片的数据的。
获取相机捕获到的数据
因为前面的文章已经分析过ZXing
解码的步骤了,这里就重点看下,相机捕获到图像的后续步骤,源码如下
public void restartPreviewAndDecode() {
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
activity.drawViewfinder();
}
}
上面的代码是在CaptureActivityHandler
构造方法中调用的,也就是在CaptureActivityHandler
实例化的时候调用。然后,调用到了cameraManager
的requestPreviewFrame
方法,代码如下
/** * 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);
}
}
现在来分析一下上面的代码,重点来看下这句
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),当然,也可以通过下面的代码来设置自己需要的格式。
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)。而色度又定义了颜色的两个方面:色调和饱和度。
上文的NV21
和YV12
是YUV存储格式。
- NV21格式属于YUV420SP类型。它也是先存储了Y分量,但接下来并不是再存储所有的U或者V分量,而是把UV 分量交替连续存储。
- YV12格式属于YUV420P类型,即先存储Y分量,再存储U、V分量,YV12是先Y再V后U。
关于YUV格式的介绍,网上有一篇比较好的文章,点击这里查看。对YUV格式有一定的了解之后,继续来分析源码,看下,是怎样从图片中识别二维码的。
识别图片中的二维码
上文已经知道,相机每获取一帧的数据都会回调PreviewCallback
类中的onPreviewFrame
方法,在此方法中,利用Handler的机制,将图片转换成的字节数组传递给了DecodeHandler
类,然后调用了decode
方法,代码如下
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
解码的核心代码了,现在一点点的来分析,先看
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
这句代码,实例化了PlanarYUVLuminanceSource
对象,主要的目的是获取扫码框中的图像的数据。在将图像进行二值化的时候会调用此对象中的方法,稍后会在源码中介绍。 再看这句代码
BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
这句代码,嗯,先看new GlobalHistogramBinarizer(source)
这句代码,GlobalHistogramBinarizer
图像的数据就是在这个类中进行二值化的,当然还有一个HybridBinarizer
类,这个类也是将图像二值化的,那主要的区别是什么呢?主要的区别就是HybridBinarizer
类处理的比GlobalHistogramBinarizer
精确,但是处理的速度较慢,推荐在性能比较好的手机上使用,而GlobalHistogramBinarizer
处理的不太精确,如有阴影的化,可能处理的图片就会有问题,但是速度较快,推荐在性能不太好的手机上使用。 这里,我们用的是GlobalHistogramBinarizer
来对图像进行二值化处理,因为,经过我测试发现,这个速度快点。
再来看整句的代码,就是实例化了BinaryBitmap
类,然后将GlobalHistogramBinarizer
对象注入。
下面的代码就是从图像中发现二维码并解析,代码如下
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
Log.e(TAG, "decode: 没有发现二维码" );
} finally {
multiFormatReader.reset();
}
跟踪下去,发现最终会调用QRCodeReader
类中的decode(BinaryBitmap image, Map<DecodeHintType,?> hints)
方法。代码如下
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;
}
来看
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)
方法,代码如下
/** * <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:1:3:1:1。如下图
- 如何找到左上角的顶点?这个顶点与其他两个顶点的夹角为90度。
通过上面几个步骤,就能识别出二维码的三个顶点,并且识别出左上角的顶点。
ZXing识别图像中的二维码
上面已经介绍了二维码的特征,也介绍了怎样发现二维码的“回”字,现在,我们来看下ZXing
是怎么识别图片中的二维码的,主要的代码如下
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就采样一行,
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去
寻找最靠近右下角的那个校正符,寻找方法与寻找定位符的方法基本相同,如果找到就返回校正符的中心坐标,如果没有找到也没关系,解码程序可以继续。
通过上面的两步就可以判断相机获取的图像帧中是否有二维码了,如果有二维码则进行二维码的解析,没有二维码就抛出异常,然后继续解析下一帧图像数据。
总结
通过上文的讲解和源码的分析,我们可以知道判断图像帧中是否有二维码需要经过以下几步:
- 获取图像帧的数据,格式为YUV;
- 将二维码扫码框中的图像数据进行灰度化处理;
- 将灰度化后的图像进行二值化处理;
- 根据二维码的特征寻找定位符;
- 寻找二维码的校正符。
如果在步骤“4”中找到了校正符,则说明这一帧图片中含有二维码,可以进行二维码的解析,否则就抛出异常,继续解析下一帧图像的数据。
结束语
没有看源码之前,我是比较迷茫的,不知道怎样才能判断图片中是否有二维码,虽然知道可以根据二维码中的“回”字来判断,但是不知道怎么找到“回”字呀!阅读源码后才知道,可以将图片进行“二值化”处理,再根据黑白像素的比例来找到“回”字,感觉学到了很多。所以呢,在我们不知道某个库的某个功能是怎样实现的时候,最好的解决办法就是阅读源码,答案都在源码中。
在研究源码的时候删除了好多与解析二维码无关的代码,最后的代码在这里。
该系列文章:
本文已由公众号“AndroidShared”首发
今天的文章ZXing源码解析四:如何识别图片中的二维码分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18721.html