1. 为什么要做这个库?
相信大家在平常的生活中,如果遇到扫码的场景第一个想到的应该就是微信了,可以说微信使用二维码打开了移动互联网的另一扇大门,并且在扫码体验上及其优秀,本该有一定要求的扫码过程,在经过微信的优化之后,让用户在使用时拥有了一种『随意性』,像拍一张照片一样简单,像发一句消息随意,像摆弄一件玩具一样有趣。
有了这样的『标杆』存在,大家在潜意识里面也都有了标准,你们的扫一扫为什么不好用?为什么要识别这么久?甚至我对准了也识别不出来?摆在我们面前的是各种用户的不满,解决这些问题就成了我们必须要面对的情形。
2. 选型
二维码处理,绕不开的就是ZXing和ZBar了,ZXing作为老牌的识别库已经"孵化"出了包括js、Python、C++、PHP等各个语言的lib,同时Android版本也一直在更新,但是ZBar作为C的处理者,上次的更新已经是7年前了。为了让二维码的识别尽量的快,并且对图像处理有更多的可能性,考量之后我们选用了更具活力的zxing-cpp,选用了它来作为我们的底层处理库。
3. 相机的处理
原始图像的获取至关重要,倘若这一步走不好,其他的处理再好也于事无补,对于从来没有接触这一领域的自己来说,踏遍Android相机的坑不知要花多少时间,好在已经有优秀的开源库,这里特别感谢BGAQRCode-Android的开源库,操纵摄像头的一些重要功能,比如自动对焦、触摸对焦、放大缩小等都已经具备,自己也只是在其之上做了一些小改进,比如GroupView的改进、加入传感器对焦、线程池处理等等。有了这些之后,我们就可以开始处理数据了。
4. 相机数据处理
Android的相机获取到的数据并非我们平常认为的RGB数据,而是视频采集中的经常使用的NV21格式即YUV,所以在获取到这些数据之后是无法直接使用的。
- 格式转换
要转格式首先我们先要了解NV21在内存中是什么样子的。
不同于我们平常的图片格式,比如png的图片,图片由一个一个像素点构成,400 * 800的图片就有320000个像素,每一个像素对应一个ARGB,即4个字节,分别表示(透明度,红色色值,绿色色值,蓝色色值),就是我们平常见到的(0,255,255,255)一个像素的内存是连在一起的,但是YUV不同于我们『认知』上的格式,这3个数值分别代表的是(明亮度,色度,浓度),一个很有意思的知识是:YUV的发明是由于彩色电视与黑白电视的过渡时期。
有了一个大概的了解之后,我们就可以把摄像头的数据转化为我们需要的数据,其实只要根据公式来推倒就可以了,但是了解原理能让我们更好的理解。
-
算法优化
对于二维码来说,它是一个个黑白的点组成的,其实并不需要多么色彩斑斓的装饰,一张灰度图或许是更好的选择,一般的图像处理,都是轮询所有像素数据,对于一个或者一组数据进行处理。一个YUV转化为RGB的算法就是要拿出所有像素,然后各种转换,这无疑是一种浪费。一个更好的选择是把原图像直接转化为灰度图。
for (int i = top; i < height + top; ++i) {
srcIndex += left;
for (int j = left; j < left + width; ++j, ++desIndex, ++srcIndex) {
p = data[srcIndex] & 0xFF;
pixels[desIndex] = 0xff000000 | p << 16 | p << 8 | p;
}
srcIndex += margin;
}
-
图片裁剪
我们知道,通常二维码识别的界面都有一个『框』,这个框并不是可有可无的,它不仅能告诉用户我们正在扫描,二维码应该放在这里面,更能在我们处理时成倍的提高处理效率,在测试的过程中,裁剪和没有经过裁剪的图片处理一般要相差4-5倍的时间。一张600 * 600的图片识别要50-80ms;而一张完整照片,比如1920 * 1080的图片,通常要经过200ms以上的时间处理,如果所有的图片都不经过截取,那么想要提升整体的识别效率是很困难的。图片裁剪是一个非常必要的操作,加上我们上面的灰度转换,两个操作合二为一,得到灰度图的同时也裁剪了图像。经过简单的处理之后,这个图像的“质量”是很高的。
截取后的图像只有原图像的1/5,更利于我们去处理数据,至此我们的图像已经准备好了。
void ImageUtil::convertNV21ToGrayAndScale(int left, int top, int width, int height, int rowWidth,
const jbyte *data, int *pixels) {
int p;
int desIndex = 0;
int bottom = top + height;
int right = left + width;
int srcIndex = top * rowWidth;
int marginRight = rowWidth - right;
for (int i = top; i < bottom; ++i) {
srcIndex += left;
for (int j = left; j < right; ++j, ++desIndex, ++srcIndex) {
p = data[srcIndex] & 0xFF;
pixels[desIndex] = 0xff000000u | p << 16u | p << 8u | p;
}
srcIndex += marginRight;
}
}
5. 二维码识别
解析二维码
有了充足的准备,二维码的识别已经是水到渠成的事情了,根据转化好的数据,生成HybridBinarizer对象,通过MultiFormatReader即可解析。-
识别流程优化
在一些Demo中,二维码处理流程通常是使用setOneShotPreviewCallback作为相机数据的处理,即一帧画面处理完再处理下一帧(两帧不一定是相连的),这样的处理会造成两个问题,首先:相机获取的画面不一定是完全对焦好的,一般我们拿出手机都有一个对焦的动作,中间可能只有50%的画面是可用的,这种情况下可能会丢失清晰的图像而处理了模糊的图像;其次这种串行的处理也是对机能的浪费,现在的手机处理连续的图像是绰绰有余的;最后,这样的处理流程是不受我们控制的,只能来一张处理一张。
在流程改进中我使用了setPreviewCallback的回调,并统一加入线程池处理。这里我可以控制一秒之内处理多少帧图像,在测试中是300ms处理一帧(不同机型处理的速度不尽相同,为了避免线程池队列过长,选择了较低的处理速度,后期可以根据机型来动态设置处理间隔),为了加速处理,这4帧是识别框内的数据。同时,为了能快速识别简单的二维码,每4帧处理完之后加入一帧全屏处理,这一帧可以作为识别图像明亮度的主帧,也可以在二维码超出识别框时,继续识别数据。有了这个改动,就可以做到点击扫一扫,抬手就能得到结果。但是这个扫码的距离实在不能让人满意,我们常用的扫一扫通常都会有一个放大的操作,而这个操作是扫码优化中也是非常关键的一步。
-
放大优化
想要进一步的优化我们就得更进一步的研究二维码了,二维码的生成细节和原理和二维码(QR code)基本结构及生成原理有详细的解释,这里我们发现左上、左下、右上三个位置探测图形,在二维码的解码过程中,其实是分几个步骤的,首先就是要定位这个二维码确认其位置,然后才能取出里面的数据,而这个定位的点就是这三个。在距离二维码较远时,可能无法解析出完整的数据,但是却能定位这个二维码,通过定位点的信息,我们可以进行放大的操作,从而获取到更加精确的图像数据,也更有利于我们解析。
/**
* 没有查询到二维码结果,但是能基本能定位到二维码,根据返回的数据集,检验是否要放大
*
* @param result 二维码定位信息
*/
void tryZoom(BarcodeReader.Result result) {
int len = 0;
float[] points = result.getPoints();
if (points.length > 3) {
float point1X = points[0];
float point1Y = points[1];
float point2X = points[2];
float point2Y = points[3];
float xLen = Math.abs(point1X - point2X);
float yLen = Math.abs(point1Y - point2Y);
len = (int) Math.sqrt(xLen * xLen + yLen * yLen);
}
handleAutoZoom(len);
}
- 与微信的对比
微信的扫一扫可以说是秒级的处理,特别是在iOS的设备上,更不可思议的是它好像没有距离的限制。经过我们的优化之后,我们的二维码可以在50cm内解析出来,但是与微信相差的还是太远,我们需要更好的处理图像数据,来定位二维码。
6. OpenCV
二维码的识别中,距离 是一个非常关键的制约条件,通常在30cm-40cm内是一定可以识别出来的,但是超过这个距离获取到的图像就会比较模糊,如果摄像头的分辨率不高识别率也会下降,如果超过这个阈值,识别算法就只能定位数据而无法解析数据,比如上图中的B点,这里我们加入自动放大就可以解决,但是超过这个距离呢?我们就需要手机移动了。如果有一种方案,可以像在B点时一样,虽然无法获取到数据但是可以得到二维码的位置、大小呢?要做到这个,OpenCV是一个不二之选。
说到图像处理,我们大致有两套方案,方案一:处理图像数据,获取图像轮廓,算法检测二维码位置。方案二:机器学习,直接定位二维码。
两者其实都是可行的,只是在难易度方面的差异,我们首先尝试了机器学习的方案,奈何自己学的还比较浅,收集到的样本数据也不够,训练出来的模型也不太理想,比如一个没有二维码的画面会检查出好几个,又比如有的时候又要离的特别近才能识别出来,这又违背了我们的本意。所以我选择了方案一,虽然听起来没那么高大上了,但是在实际的测试中也完全能达到预期水平。
当图像即无法解析出数据也无法定位到二维码时,我们采用OpenCV去处理图像。因为之前已经进行过灰度处理了,这里可以直接进行Canny化,然后执行findContours方法获取轮廓信息,之后过滤轮廓信息,判断点与点之间的距离,得到二维码的位置信息。(以上的过程看似简单,其实进行了很多尝试,包括二值化,毛边去除、调节亮度、对比度处理,直接获取点信息等等,这里感谢https://blog.csdn.net/jia20003/article/details/77348170
和https://blog.csdn.net/zwx1995zwx/article/details/79171979
的图像过滤算法,作为一个图像处理的门外汉真的学到很多)
拿到这些信息之后,我们就可以遵循在B点时的处理逻辑,直接放大图像获取数据。(你可能会想为什么不直接截取图像,这样就不用费时费力再进行一轮识别,其实这里也想到过,但是得到的数据精度丢失实在太多,我尝试用微信去识别截取得到的二维码,微信也无法检测出来,这样的处理对于简单的二维码或许可行,但是对于稍微复杂的二维码或者我们所要解决的问题来说是远远不够的)
7. 成果
在加入OpenCV之后,我们的识别距离扩大了一倍,得到的效果比预期的还要好。
识别效果展示
8. 待完善的功能
- aar过大,因为有OpenCV的加入,aar文件有7.7M。
- 扩展性、可定制性不够,这个可以慢慢加入。
注: 不同手机识别效率其实不尽相同,摄像头越好,识别效率越高。
源码地址
欢迎Star,欢迎Fork,欢迎和我一起开发。
9. 感谢
在不到一个月的时间里完成了主要功能(主要是自己在业余时间完成的),感谢那些无私奉献的博主之余也感受到开源的便利和伟大,我能做的也只是用开源来回馈各位。最后以牛顿的一句话来结尾吧,“我之所以站得高,是因为我站在巨人的肩膀上。”