zxing的使用及优化

Posted 在码农路上努力奋斗的小青年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了zxing的使用及优化相关的知识,希望对你有一定的参考价值。

二维码介绍

zxing项目是谷歌推出的用来识别多种格式条形码的开源项目,项目地址为https://github.com/zxing/zxing,zxing有多个人在维护,覆盖主流编程语言,也是目前还在维护的较受欢迎的二维码扫描开源项目之一。

zxing的项目很庞大,主要的核心代码在core文件夹里面,也可以单独下载由这个文件夹打包而成的jar包,具体地址在http://mvnrepository.com/artifact/com.google.zxing/core,直接下载jar包也省去了通过maven编译的麻烦,如果喜欢折腾的,可以从https://github.com/zxing/zxing/wiki/Getting-Started-Developing获取帮助文档。


zxing基本使用

官方提供了zxing在android机子上的使用例子,https://github.com/zxing/zxing/tree/master/android,作为官方的例子,zxing-android考虑了各种各样的情况,包括多种解析格式、解析得到的结果分类、长时间无活动自动销毁机制等。有时候我们需要根据自己的情况定制使用需求,因此会精简官方给的例子。在项目中,我们仅仅用来实现扫描二维码和识别图片二维码两个功能。为了实现高精度的二维码识别,在zxing原有项目的基础上,本文做了大量改进,使得二维码识别的效率有所提升。先来看看工程的项目结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── QrCodeActivity.java
├── camera
│   ├── AutoFocusCallback.java
│   ├── CameraConfigurationManager.java
│   ├── CameraManager.java
│   └── PreviewCallback.java
├── decode
│   ├── CaptureActivityHandler.java
│   ├── DecodeHandler.java
│   ├── DecodeImageCallback.java
│   ├── DecodeImageThread.java
│   ├── DecodeManager.java
│   ├── DecodeThread.java
│   ├── FinishListener.java
│   └── InactivityTimer.java
├── utils
│   ├── QrUtils.java
│   └── ScreenUtils.java
└── view
    └── QrCodeFinderView.java

源码比较简单,这里不做过多地讲解,大部分方法都有注释。主要分为几大块,

  • camera

主要实现相机的配置和管理,相机自动聚焦功能,以及相机成像回调(通过byte[]数组返回实际的数据)。

  • decode

图片解析相关类。通过相机扫描二维码和解析图片使用两套逻辑。前者对实时性要求比较高,后者对解析结果要求较高,因此采用不同的配置。相机扫描主要在DecodeHandler里通过串行的方式解析,图片识别主要通过线程DecodeImageThread异步调用返回回调的结果。FinishListenerInactivityTimer用来控制长时间无活动时自动销毁创建的Activity,避免耗电。

扫描精度问题

使用过zxing自带的二维码扫描程序来识别二维码的童鞋应该知道,zxing二维码的扫描程序很慢,而且有可能扫不出来。zxing在配置相机参数和二维码扫描程序参数的时候,配置都比较保守,兼顾了低端手机,并且兼顾了多种条形码的识别。如果说仅仅是拿zxing项目来扫描和识别二维码的话,完全可以对项目中的一些配置做精简,并针对二维码的识别做优化。

PlanarYUVLuminanceSource

官方的解码程序主要是下边这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void decode(byte[] data, int width, int height) 
    long start = System.currentTimeMillis();
    Result rawResult = null;
    // 构造基于平面的YUV亮度源,即包含二维码区域的数据源
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) 
        // 构造二值图像比特流,使用HybridBinarizer算法解析数据源
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        try 
            // 采用MultiFormatReader解析图像,可以解析多种数据格式
            rawResult = multiFormatReader.decodeWithState(bitmap);
         catch (ReaderException re) 
            // continue
         finally 
            multiFormatReader.reset();
        
    
	···
	// Hanlder处理解析失败或成功的结果
	···

再来看看YUV亮度源是怎么构造的,在CameraManager里,首先获取预览图像的聚焦框矩形getFramingRect(),这个聚焦框的矩形大小是根据屏幕的宽高值来做计算的,官方定义了最小和最大的聚焦框大小,分别是240*2401200*675,即最多的聚焦框大小为屏幕宽高的5/8。获取屏幕的聚焦框大小后,还需要做从屏幕分辨率到相机分辨率的转换才能得到预览聚焦框的大小,这个转换在getFramingRectInPreview()里完成。这样便完成了亮度源的构造。

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
84
85
86
87
88
89
90
91
92
93
94
95
private static final int MIN_FRAME_WIDTH = 240;
private static final int MIN_FRAME_HEIGHT = 240;
private static final int MAX_FRAME_WIDTH = 1200; // = 5/8 * 1920
private static final int MAX_FRAME_HEIGHT = 675; // = 5/8 * 1080

/**
 * A factory method to build the appropriate LuminanceSource object based on the format of the preview buffers, as
 * described by Camera.Parameters.
 *
 * @param data A preview frame.
 * @param width The width of the image.
 * @param height The height of the image.
 * @return A PlanarYUVLuminanceSource instance.
 */
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) 
    // 取得预览框内的矩形
    Rect rect = getFramingRectInPreview();
    if (rect == null) 
        return null;
    
    // Go ahead and assume it's YUV rather than die.
    return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top, rect.width(), rect.height(),
        false);


/**
 * Like @link #getFramingRect but coordinates are in terms of the preview frame, not UI / screen.
 *
 * @return @link Rect expressing barcode scan area in terms of the preview size
 */
public synchronized Rect getFramingRectInPreview() 
    if (framingRectInPreview == null) 
        Rect framingRect = getFramingRect();
        if (framingRect == null) 
            return null;
        
        // 获取相机分辨率和屏幕分辨率
        Rect rect = new Rect(framingRect);
        Point cameraResolution = configManager.getCameraResolution();
        Point screenResolution = configManager.getScreenResolution();
        if (cameraResolution == null || screenResolution == null) 
            // Called early, before init even finished
            return null;
        
        // 根据相机分辨率和屏幕分辨率的比例对屏幕中央聚焦框进行调整
        rect.left = rect.left * cameraResolution.x / screenResolution.x;
        rect.right = rect.right * cameraResolution.x / screenResolution.x;
        rect.top = rect.top * cameraResolution.y / screenResolution.y;
        rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
        framingRectInPreview = rect;
    
    return framingRectInPreview;


/**
 * Calculates the framing rect which the UI should draw to show the user where to place the barcode. This target
 * helps with alignment as well as forces the user to hold the device far enough away to ensure the image will be in
 * focus.
 *
 * @return The rectangle to draw on screen in window coordinates.
 */
public synchronized Rect getFramingRect() 
    if (framingRect == null) 
        if (camera == null) 
            return null;
        
        // 获取屏幕的尺寸像素
        Point screenResolution = configManager.getScreenResolution();
        if (screenResolution == null) 
            // Called early, before init even finished
            return null;
        
        // 根据屏幕的宽高找到最合适的矩形框宽高值
        int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
        int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);

        // 取屏幕中间的,宽为width,高为height的矩形框
        int leftOffset = (screenResolution.x - width) / 2;
        int topOffset = (screenResolution.y - height) / 2;
        framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
        Log.d(TAG, "Calculated framing rect: " + framingRect);
    
    return framingRect;


private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) 
    int dim = 5 * resolution / 8; // Target 5/8 of each dimension
    if (dim < hardMin) 
        return hardMin;
    
    if (dim > hardMax) 
        return hardMax;
    
    return dim;

这段代码并没有什么问题,也完全符合逻辑。但为什么在扫描的时候这么难扫到二维码呢,原因在于官方为了减少解码的数据,提高解码效率和速度,采用了裁剪无用区域的方式。这样会带来一定的问题,整个二维码数据需要完全放到聚焦框里才有可能被识别,并且在buildLuminanceSource(byte[],int,int)这个方法签名中,传入的byte数组便是图像的数据,并没有因为裁剪而使数据量减小,而是采用了取这个数组中的部分数据来达到裁剪的目的。对于目前CPU性能过剩的大多数智能手机来说,这种裁剪显得没有必要。如果把解码数据换成采用全幅图像数据,这样在识别的过程中便不再拘束于聚焦框,也使得二维码数据可以铺满整个屏幕。这样用户在使用程序来扫描二维码时,尽管不完全对准聚焦框,也可以识别出来。这属于一种策略上的让步,给用户造成了错觉,但提高了识别的精度。

解决办法很简单,就是不仅仅使用聚焦框里的图像数据,而是采用全幅图像的数据。

1
2
3
4
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) 
    // 直接返回整幅图像的数据,而不计算聚焦框大小。
    return new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false);

DecodeHintType

在使用zxing解析二维码时,允许事先进行相关配置,这个文件通过Map<DecodeHintType, ?>键值对来保存,然后使用方法public void setHints(Map<DecodeHintType,?> hints)来设置到相应的解码器中。DecodeHintType是一个枚举类,其中有几个重要的枚举值,

  • POSSIBLE_FORMATS(List.class)

用于列举支持的解析格式,一共有17种,在com.google.zxing.BarcodeFormat里定义。官方默认支持所有的格式。

  • TRY_HARDER(Void.class)

是否使用HARDER模式来解析数据,如果启用,则会花费更多的时间去解析二维码,对精度有优化,对速度则没有。

  • CHARACTER_SET(String.class)

解析的字符集。这个对解析也比较关键,最好定义需要解析数据对应的字符集。

如果项目仅仅用来解析二维码,完全没必要支持所有的格式,也没有必要使用MultiFormatReader来解析。所以在配置的过程中,我移除了所有与二维码不相关的代码。直接使用QRCodeReader类来解析,字符集采用utf-8,使用Harder模式,并且把可能的解析格式只定义为BarcodeFormat.QR_CODE,这对于直接二维码扫描解析无疑是帮助最大的。

1
2
3
4
5
6
7
8
9
private final Map<DecodeHintType, Object> mHints;
DecodeHandler(QrCodeActivity activity) 
    this.mActivity = activity;
    mQrCodeReader = new QRCodeReader();
    mHints = new Hashtable<>();
    mHints.put(DecodeHintType.CHARACTER_SET, "utf-8");
    mHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
    mHints.put(DecodeHintType.POSSIBLE_FORMATS, BarcodeFormat.QR_CODE);

二维码图像识别精度探究

图像/像素编码格式

Android相机预览的时候支持几种不同的格式,从图像的角度(ImageFormat)来说有NV16、NV21、YUY2、YV12、RGB_565和JPEG,从像素的角度(PixelFormat)来说,有YUV422SP、YUV420SP、YUV422I、YUV420P、RGB565和JPEG,它们之间的对应关系可以从Camera.Parameters.cameraFormatForPixelFormat(int)方法中得到。

1
2
3
4
5
6
7
8
9
10
11
private String cameraFormatForPixelFormat(int pixel_format) 
    switch(pixel_format) 
    case ImageFormat.NV16:      return PIXEL_FORMAT_YUV422SP;
    case ImageFormat.NV21:      return PIXEL_FORMAT_YUV420SP;
    case ImageFormat.YUY2:      return PIXEL_FORMAT_YUV422I;
    case ImageFormat.YV12:      return PIXEL_FORMAT_YUV420P;
    case ImageFormat.RGB_565:   return PIXEL_FORMAT_RGB565;
    case ImageFormat.JPEG:      return PIXEL_FORMAT_JPEG;
    default:                    return null;
    

目前大部分Android手机摄像头设置的默认格式是yuv420sp,其原理可参考文章《图文详解YUV420数据格式》。编码成YUV的所有像素格式里,yuv420sp占用的空间是最小的。既然如此,zxing当然会考虑到这种情况。因此针对YUV编码的数据,有PlanarYUVLuminanceSource这个类去处理,而针对RGB编码的数据,则使用RGBLuminanceSource去处理。在下节介绍的图像识别算法中我们可以知道,大部分二维码的识别都是基于二值化的方法,在色域的处理上,YUV的二值化效果要优于RGB,并且RGB图像在处理中不支持旋转。因此,一种优化的思路是讲所有ARGB编码的图像转换成YUV编码,再使用PlanarYUVLuminanceSource去处理生成的结果。

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以上是关于zxing的使用及优化的主要内容,如果未能解决你的问题,请参考以下文章

杂项-QRCode:ZXing

2021SC@SDUSC Zxing开源代码Zxing编码思路及代码分析

引用ZXing生成二维码

zxing开源库的基本使用

二维码的生成

用Spring Boot+ZXing生成二维码

(c)2006-2024 SYSTEM All Rights Reserved IT常识