不知道你们在用ZXing做为扫码库的时候,有没有想过“ZXing是怎么从相机捕获的每一帧图片中获取到二维码并解析的呢?”,若是你思考过而且已经从源码中知道了答案,那么这篇文章你就不必读下去了,若是你思考过殊不知道答案,那么这篇文章就是为你准备的,相信你读事后会有一个清晰的答案。java
为了避免那么突兀,仍是先跟着源码来一步步的讲解,先来看怎样获取到相机捕获到的图片的数据的。git
由于前面的文章已经分析过ZXing
解码的步骤了,这里就重点看下,相机捕获到图像的后续步骤,源码以下github
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);
}
}
复制代码
如今来分析一下上面的代码,重点来看下这句app
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),固然,也能够经过下面的代码来设置本身须要的格式。post
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
camera.setParameters(parameters);
复制代码
ZXing
库中并无设置格式,因此这里默认的是NV21
格式。那么问题来了,NV21
究竟是什么意思呢?欲知详情,请继续阅读下文性能
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)。而色度又定义了颜色的两个方面:色调和饱和度。ui
上文的NV21
和YV12
是YUV存储格式。编码
关于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
异常。如今,咱们来看怎样找到图像中的二维码的。
在介绍发现图片中二维码的方法以前,先来看下二维码的特色,以下图
二维码有三个“回“字形图案,这一点很是明显。中间的一个点位于图案的左上角,若是图像偏转,也能够根据二维码来纠正。
识别二维码,就是识别二维码的三个点,逐步分析一下这三个点的特性
经过上面几个步骤,就能识别出二维码的三个顶点,而且识别出左上角的顶点。
上面已经介绍了二维码的特征,也介绍了怎样发现二维码的“回”字,如今,咱们来看下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);
}
复制代码
上面的代码主要作了下面的事
在图像中每隔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两点。
经过ABC三点的坐标计算出校订符的可能位置,而后交给AlignmentPatternFinder去
寻找最靠近右下角的那个校订符,寻找方法与寻找定位符的方法基本相同,若是找到就返回校订符的中心坐标,若是没有找到也不要紧,解码程序能够继续。
经过上面的两步就能够判断相机获取的图像帧中是否有二维码了,若是有二维码则进行二维码的解析,没有二维码就抛出异常,而后继续解析下一帧图像数据。
经过上文的讲解和源码的分析,咱们能够知道判断图像帧中是否有二维码须要通过如下几步:
若是在步骤“4”中找到了校订符,则说明这一帧图片中含有二维码,能够进行二维码的解析,不然就抛出异常,继续解析下一帧图像的数据。
没有看源码以前,我是比较迷茫的,不知道怎样才能判断图片中是否有二维码,虽然知道能够根据二维码中的“回”字来判断,可是不知道怎么找到“回”字呀!阅读源码后才知道,能够将图片进行“二值化”处理,再根据黑白像素的比例来找到“回”字,感受学到了不少。因此呢,在咱们不知道某个库的某个功能是怎样实现的时候,最好的解决办法就是阅读源码,答案都在源码中。
在研究源码的时候删除了好多与解析二维码无关的代码,最后的代码在这里。
该系列文章:
本文已由公众号“AndroidShared”首发