写在前面

二维码是一个近几年来移动设备上超流行的一种编码方式(又称二维条码,QR Code,QR全称QuickResponse),它比传统的条形码(Bar Code)能存更多的信息,也能表示更多的数据类型。随着二维码的流行,几乎大部分手持设备和移动APP都支持二维码的扫描识别。在爱奇艺移动端,二维码主要功能是连接PC客户端,网页端,TV端的工具,可以快速引导用户账号登录,会员等付费业务的支付,运营活动的打通,小程序的入口等。为了给用户提供更快捷便利的体验,爱奇艺技术产品团队针对二维码扫码性能进行了专项优化,识别率从最初的30%提升到了75%左右,平均耗时从2.1s下降到722ms。下文将详细分享爱奇艺在扫码优化实践方面的经验。


背景

二维码因其成本低,兼容性好,存储信息更多等优势成为了线上线下主要的连接工具,并且在移动设备交互上提供了更快捷入口。对于大部分标准清晰的二维码场景,使用开源的ZXing或ZBar库就可以很好的完成二维码的识别。但是由于二维码是一种点阵式信息编码方式,任何视觉上的缺失,弯曲变形,光照,屏幕噪点等都会极大的干扰影响识别成功率和速度,影响了用户体验和转化率。碰到一些特殊的场景,ZXing/ZBar因识别率不高,难以满足业务的需求。

这些特殊场景主要包括以下几方面:

1、二维码是深色背景的,周边带有黑框,没有明显的分界线

2、二维码是灰色的,对比度很低

3、二维码拉伸变形,倾斜扭曲

4、由于光照干扰,二维码图像光照不均,局部区域过暗

原本爱奇艺APP直接集成的开源的ZXing库实现扫码功能,并没有做过多的定制和优化,这些场景下识别体验较差。因此开始了二维码扫码性能优化之路,针对各种场景进行逐一分析优化,经过2~3个版本的迭代,爱奇艺APP扫码性能指标有了质的提升,内部样本集性能测试结果显示,识别率从最初的30%提升到了75%左右,平均耗时从2.1s下降到722ms。主要从三个方面进行了针对性优化: (1) 扫描速度优化 (2) 交互体验优化 (3) 识别率优化。

由于二维码样本集的差异,测试性能数据仅供参考:


二维码扫描的原理(二维码扫描多维优化实践)(1)

扫码速度优化


1. 去除不必要的格式转换和旋转操作

在分析代码时,发现原有的逻辑在相机帧`onPreviewFrame()`回调里处理帧像素时,先把YUV数据格式转换成了RGB格式,同时由于横竖屏的关系,对像素数组进行了90°旋转操作,然后把处理好的像素数组传递给`RGBLuminanceSource`,交给ZXing解码。经过断点分析,发现YUV转RGB和旋转这两步耗费了大量时间,预览数组越大耗时越长。

ZXing本身提供了`YUVLuminanceSource`可以直接解码YUV数据,在YUV格式中Y分量就是灰度分量,对比度相比RGB更高,直接把YUV格式的data数据交给ZXing解码,转换耗时就节省下来了。

相机预览坐标系是横屏的,而移动APP一般是竖屏扫码模式,而我们分析发现ZXing解码二维码与其是水平的还是竖直的图像关系不大,都能识别,因此可以不旋转像素数组,只旋转裁剪区域的坐标系,只需要把矩形框的四个坐标点做一下变换,设置给`YUVLuminanceSource`,就避免了对所有像素进行旋转的操作,这部分耗时也节省出来了。

2. 减少执行解码格式

使用ZXing解码时,一般用的是`MultiFormatReader`,其内部维护了一个Reader数组,默认支持一维条码(UPC-A,UPC-E,EAN-8,Code 39,Code 93等)和二维条码(QR Code,Data Matrix,PDF 417,MaxiCode)等20多种格式,循环遍历解码失败耗时会拉长,影响下一帧的解码。结合爱奇艺APP扫码业务特点,实际应用,中我们裁剪ZXing解码格式只支持二维码,大大缩短了解码失败耗时(识别不了的帧),间接优化了整体的解码耗时。

3. 合理设置预览大小和裁剪扫描框

ZXing 默认提供的预览帧大小选择算法在大部分设备上获取到的是屏幕尺寸,屏幕分辨率越高图像越清晰,相应的像素点更多,解码一帧的耗时相应增加。而对于二维码扫描来说,定位二维码的回字形探测图像和采样解码并不需要很高的分辨率。因此修改预览帧大小设置,调整匹配算法,选择最接近常规的预览尺寸,然后裁剪二维码扫描框的大小给ZXing解码。为了增加扫描识别的鲁棒性,我们间隔截取一帧为屏幕宽度大小的正方形区域作为解码区域,在提高识别鲁棒性的同时,加快了解码识别速度。

4. 串行改并行化处理

ZXing 解码操作是采用一个HandlerThread消息队列处理的,是单线程模型,当一帧处理完成之后才会通过`setOneShotPreviewCallback()`请求相机准备下一帧,缺点就是如果处理一帧数据时间很长,会阻塞下一帧的处理,比如某一帧是模糊的或者没有二维码数据,等用户聚焦对准了二维码,还是需要等待一段时间才能识别。

当我们采用`setPreviewCallback()`产生连续预览帧,将串行解码修改为并行解码,每当`onPreviewFrame()`数据到达时,提交一个AsynTask到线程池,线程池大小可以动态配置。合理设置线程池大小,可以大大加快并行化处理和识别速度。

交互体验优化


1. 优化对焦模式和设置定点对焦

ZXing 默认采用的是`AUTO_FOCUS`对焦模式,间隔1.5s触发一次自动对焦,时间很长,在部分设备上体验较差,可以把对焦时间缩短,但是在`autoFocus()`瞬间图像会变模糊。通过默认设置为`FOCUS_MODE_CONTINUOUS_PICTURE`(连续拍照)模式,在第一次对焦清晰之后,后续帧都会比较清晰。

在不设置对焦区域时,相机默认是全屏幕聚焦,当二维码图片较远且周边环境较暗时,自动对焦会非常慢。可以通过使用`setFocusAreas()`和`setMeteringAreas()`设置对焦区域和测光区域为扫描框区域,大大提高远图二维码的聚焦速度和识别成功率。

2. 支持自动缩放

当二维码区域很小很远时,自动放大能大大提高用户体验和识别率。`QRCodeReader`解码二维码主要分为两步:(1) 识别二维码回字形探测位置坐标,采样得到矩阵 (2) 根据解码规则进行RS解码还原数据。在第一步解码的`DetectorResult`里包含定位点和对齐点的坐标信息。如果二维码很小,在采样过程中可能采集到不正确的像素点,第二步解码过程中还是可能会失败。在这种场景下,我们可以根据定位点的信息大致估算出二维码的尺寸,然后跟扫描框的大小做比对,根据相机的焦距做自动放大。经过几次放大操作之后,如果还是一直解码失败,而重置到默认的倍数,防止部分噪点定位错误估算二维码尺寸偏差,导致一直处于放大状态,体验较差。

3. 支持单击聚焦,双击缩放

部分相机对焦慢或者光照环境不均匀时,用户单击屏幕某点时,可以定点聚焦,提高相机预览图清晰度;用户双击屏幕时,根据当前相机的放大倍数,选择放大还是缩小。

4. 支持双指缩放调整焦距

当自动放大到不合理的区域时或者二维码太远一直无法识别时,用户可以手动使用双指缩放相机,来达到辅助识别的目的。

扫码识别率优化


在整个二维码扫描识别过程中,主要分为以下几个步骤:1) 二值化 2) 定位位置探测图形 3) 寻找对齐点 4) 透视变换校正 5) 像素采样解码。在我们的二维码测试集上,肉眼可见非常清晰的图像,ZXing却怎么都无法识别,为了进一步优化识别率,只能研究和改进ZXing的算法了。大概有以下几种场景和策略优化:

策略1:增加N:1:3:1:1的扫描模式

ZXing定位回字形位置图案时,是针对白色背景的二维码设计的,采用的状态机模式只有扫描到1:1:3:1:1时才算定位到图标。而实际对于一些黑框或者深色背景的二维码,由于回字形图案与背景融合在一起,没有明显的边界,ZXing状态机就无法定位了。

二维码扫描的原理(二维码扫描多维优化实践)(2)

图1 深色二维码示例

为了支持这种场景,需要修改ZXing定位点的相关逻辑,核心代码在`FinderPatternFinder`类,增加对边界重合场景的考虑。对比边界重合有两种case:N:1:3:1:1或者1:1:3:1:N,当状态机数组是5时,校验中央部分是否满足1:3:1,然后判断出左边界还是右边界重合,对此做一定的修正。源码中相应的对角线校验逻辑也需要同步修改,同时增加反向对角线的校验策略。

策略2:优化右下角点的估计算法

版本号为1的二维码,没有校正点,在没有平行正面90°的场景下,或者校正点存在污染或者破损的场景下,透视变换退化为仿射变换,ZXing识别效果很差。

二维码扫描的原理(二维码扫描多维优化实践)(3)

图2 矫正二维码示例

针对这种场景,我们利用左下角和右上角的回字形,通过定位下边界线和右边界线,然后利用直接相交找到正确的右下角点。在找到校正点的情况下,使用校正点做透视变换,没有校正点的场景下,则利用重新估计的右下角点做透视变换,大大增强了对倾斜版本为1的二维码识别。

策略3:调整定位点筛选阈值和排序规则

ZXing在找到潜在的定位点坐标之后,会进行一轮的筛选和过滤,找到符合条件的三个点,然后进行排序确定左下,左上,右上点的顺序。在筛选过程中,限定了定位点最大的moduleSize不能超过最小的moduleSize的40%,而对于有些二维码(图b)倾斜很严重时这个差值会大于40%,适当调整容忍阈值到60%以上可以增加容错性。对于另外一些倾斜非常严重的二维码(图a),当相邻的边长大于对角线时,由于ZXing默认是把最长的边当做对角线,这样三个点的顺序关系就会定位错误,导致错误的估计了第四个顶点的位置,从而导致识别失败。

针对这种场景,我们做了如下策略的调整。在`ResultPoint#orderBestPatterns()`时,增加图片最大宽高的限制,在第一轮以最长边作为对角线的假设下,估计第四个点的位置上,如果第四个点明显落在图片的外面,则取次长的边作为对角线再次排序,这样对于倾斜非常严重的二维码同样可以正常校正识别。

二维码扫描的原理(二维码扫描多维优化实践)(4)

图3 倾斜二维码示例

策略4:采用不同的二值化算法

二值化算法的好坏决定了后面查找定位点的准确性。所谓二值化处理,就是给定一个阈值,大于阈值的像素设定为白色,小于阈值的像素设定为黑色。对于一些灰色低对比度或者光照不均的图片,使用ZXing内置的二值化算法不能很好的还原图像,分离出回字形图案。

我们分析研究了ZXing自带二值化`HibridBinarizer`及和几种常用的局部二值化算法效果。经过综合评估,最终选用了局部均值跳帧执行的辅助策略,部分帧使用ZXing自带二值化识别,部分帧使用局部均值二值化识别,大大地提高了抗光照不均和低对比度二维码场景的识别率。

二维码扫描的原理(二维码扫描多维优化实践)(5)

图4 二值化图像

策略5:集成opencv预处理

对于部分回字形中央模糊或者有椒盐噪点的二维码,二值化之后很容易干扰回字形的定位识别,需要对图像做一些滤波降噪的预处理,比如中值滤波/高斯滤波等,能明显减少椒盐噪声的干扰。

对于部分旋转倾斜角度的二维码,由于ZXing采用水平线和竖直线扫描算法定位回字形图案,角度倾斜之后实际扫描到的是二维码的对角线,尽管对角线也会遵循1:1:3:1:1的原则,但是只会在中心点附近很小的区域内满足该规则。而且ZXing正常情况下是跳行扫描的,hard模式才是逐行扫描,很容易跳过关键的点,从而导致定位图案寻找失败。而实际上只要把二维码旋转到水平方向,则可以立即识别。

针对这部分场景,我们集成了OpenCV计算机视觉库,对相机采集到的二维码灰度图像做预处理,滤波降噪之后,截取二维码中心区域,进行仿射校正(旋转变化,拉伸变化)成固定像素大小的二值化图像,然后交给ZXing做解码,提高了部分场景下的识别率。

其他策略


除了上面提到的通用的扫码优化之后,我们还针对一些特殊场景进行了一定策略上的调度优化。

1、对于部分弯曲畸变的二维码,当存在曲面弯曲时,采用正常的屏幕透视变换很难正确采样。可以假设采样坐标系遵循一个更复杂的映射关系,比如二次函数,来拟合这种关系。当二维码版本号大于7时,存在4个以上的辅助校正点,通过采样更多的点,可以构造出这种矩阵关系,达到识别曲面场景的效果。

2、增加对反色二维码的识别能力。ZXing默认只支持白底黑色的二维码,我们在ZXing定位回字形位置探测图案过程中,增加收集潜在反色桩点的机制,当正向识别不了的情况下,fallback到反向识别策略。

未来展望


我们在扫码优化实践过程中,也查阅了一些公开的资料,从中参考了一些经验总结,我们在ZXing源码上做了大量的针对性修改,增强鲁棒性,策略上也做了调优,后续计划把优化版的ZXing贡献开源给github社区。

二维码的尺寸大小,清晰度及复杂度也是影响识别率的一个因素。扫码优化是一个长期的工作,我们还将进一步结合爱奇艺自身业务特点持续的进行优化,从二维码生成投放到客户端识别等各个环节进行探索优化,如二维码标准规范化、探索使用tensorflow图像识别检测二维码区域、尝试RenderScript利用GPU做二值化计算等。

参考资料

1. 史上最全的支付宝二维码扫码优化技术方案

https://www.infoq.cn/article/hZg1aBXkmoIsGHAYGoH8

2. Lark二维码扫描优化

https://zhuanlan.zhihu.com/p/44845942

3. 智能设备上的二维码解码优化

https://cardinfolink.github.io/2017/06/28/智能设备上的二维码解码优化/

,