Cardinfolink 讯联数据

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

2017-06-28
Wanny

1. 二维码扫码库介绍

二维码又称 QR Code ,QR 全称 Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。

二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。

目前市面上的 主流的二维码解析库主要有 ZxingZbar

Zxing是一个开源 Java 类库用于解析多种格式的1D/2D条形码,能够对QR编码、Data Matrix、UPC 的1D条形码进行解码。其提供了多种平台下的客户端包括:J2ME 、J2SE和 Android等。目前在 android 手机上应用广泛。

Zbar用C/C++实现,Zbar 可以直接扫码二维码和条码,在iphone上应用广泛。

笔者在讯联数据从事扫码支付工作时,目前正在进行智能设备上二维码的扫码优化,提高扫码的速度与效率,本文就记录些可优化的点,供各位参考。

笔者主要研究的是主要是解码库Zxing,下面文章以 Zxing 作为说明。

2. 解码 优化

2.1. 减少解码格式

zxing默认支持15种格式,支持格式有QR Code、Aztec、Code 128、Code 39、EAN-8 等等。然后我们在实际中用不到这么多解码样式,我们常见的二维码格式是 QR Code,一维码格式为 Code 128, 如果无特殊要求,这两种格式就能满足一般的条码与二维码的需求。 在解码过程中减少一种解码,就会减少解析时间,提高解码速度。

所以我们在实践过程中可以根据实际减少解码样式,提高解码速度,如果app实际只有二维码扫码,甚至可以只保留 QR Code这一种解码格式。

Zxing 我们可以修改 DecodeFormatManager 及 DecodeThread这两个类减少解码种类。

  static {
        PRODUCT_FORMATS = EnumSet.of(BarcodeFormat.UPC_A, BarcodeFormat.UPC_E, BarcodeFormat.EAN_13,
                BarcodeFormat.EAN_8, BarcodeFormat.RSS_14, BarcodeFormat.RSS_EXPANDED);
        INDUSTRIAL_FORMATS = EnumSet.of(BarcodeFormat.CODE_39, BarcodeFormat.CODE_93, BarcodeFormat
                .CODE_128, BarcodeFormat.ITF, BarcodeFormat.CODABAR);
        //注释掉一维码解码格式,减少解码耗时,提高速度
        //   ONE_D_FORMATS = EnumSet.copyOf(PRODUCT_FORMATS);
        //   ONE_D_FORMATS.addAll(INDUSTRIAL_FORMATS);

        ONE_D_FORMATS = EnumSet.of(BarcodeFormat.CODE_128);

        QR_CODE_FORMATS = EnumSet.of(BarcodeFormat.QR_CODE);
    }

    public DecodeThread(CaptureActivity activity, int decodeMode) {

        this.activity = activity;
        handlerInitLatch = new CountDownLatch(1);

        hints = new EnumMap<DecodeHintType, Object>(DecodeHintType.class);

        Collection<BarcodeFormat> decodeFormats = new ArrayList<BarcodeFormat>();
        //移除了所有与BarcodeFormat.CODE_128,二维码,不相关的格式
//        decodeFormats.addAll(EnumSet.of(BarcodeFormat.AZTEC));
//        decodeFormats.addAll(EnumSet.of(BarcodeFormat.PDF_417));
        ......
    }

2.2. 解码算法优化

二维码的解码算法主要分两部分,第一部分是二值化,第二部分是提取码值。第二部分又分为1.寻找定位符,2.寻找校正符,3.转换矩阵。我们可以对各个过程进行优化。

zxing中读取条码主要涉及4个类,分别是 LuminanceSource、Binarizer、BinaryBitmap和 MultiFormatReader。第1、2和4是抽象类,根据对象的不同有不同的具体实现。LuminanceSource类用于存放图片数据,Binarizer用于二值化图片,BinaryBitmap用于存放二值化图片,MultiFormatReader用于解码。

目前我们在Zxing我们能看到 HybridBinarizer及 GlobalHistogramBinarizer,HybridBinarizer继承自 GlobalHistogramBinarizer,在其基础上做了功能改进。这两个类都是Binarizer的实现类,都是基于二值化,将图片的色域变成黑白两个颜色,然后提取图形中的二维码矩阵。

官网上介绍 GlobalHistogramBinarizer算法适合低端设备,对手机CPU和内存要求不高。但它选择了全部的黑点来计算,因此无法处理阴影和渐变这两种情况。HybridBinarizer的算法在执行效率上要慢于 GlobalHistogramBinarizer算法,但识别相对更加有效,它专门以白色为背景的连续黑块二维码图像解析而设计,也更适合来解析更具有严重阴影和渐变的二维码图像。

zxing项目官方默认使用的是 HybridBinarizer二值化方法。然而目前的大部分二维码都是黑色二维码,白色背景的。不管是二维码扫描还是二维码图像识别,使用GlobalHistogramBinarizer算法的效果要稍微比 HybridBinarizer好一些,识别的速度更快,对低分辨的图像识别精度更高。 可以在 DecodeHandler 中更改算法

 DecodeHandler.java:
private void decode(byte[] data, int width, int height) {
    ......
    //you can use HybridBinarizer or GlobalHistogramBinarizer
    //but in most of situations HybridBinarizer is shit
    BinaryBitmap bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));
    ......
}

当然图像识别领域肯定有更牛逼的的算法存在,各位大牛可以可以持续改进。

顺便提下,微信扫码使用了自家开发的QBar引擎,并导入了预判算法,在识别条码之前会过滤无码图像,只识别有意义的内容——二维码和条形码。整个扫码预判模块位于核心识别引擎之前,不再需要对输入的视频中的每一帧图像进行检测识别,能实现快速过滤大量无码图像,减少后续不必要的定位和识别对扫码客户端造成的阻塞,使响应更加及时,增加扫码过程中的流畅度,而这就是微信扫码快速的关键原因。期望能开源代码,让我们能学习下。

2.3. 减少解码数据

现在的手机拍照的照片像素都很高,目前市场上好一点手机像素都上千万,拍摄一张照片的就十几 M, 这个大的数据量对解码很有压力,我们在开发过程有必要采取措施减少解码数据量。

官方为了减少解码的数据,提高解码效率和速度,利用扫码区域范围来裁剪裁剪无用区域,减少解码数据。我们在开发过程可以调整好扫码区域,减少解码的数据量。

2.4 Zbar 与 Zxing融合

目前Zxing官方开放全部源代码,用户可以根据自己的需求,灵活的改造优化。然后条形码扫码上就不尽人意,在某些手机上死活识别不出来,而且当条形码与扫描方向成一定角度,也很难扫出结果。而Zbar库在条形码上相对Zxing来说好得多了。所以有人结合 Zxing二维码算法和Zbar条形码算法,提高扫码速度。可参见https://github.com/heiBin/QrCodeScanner

3. 优化相机设置

二维码扫描解码除了上述因素外,还有一个重大的相关因素就是相机设置方面的。如果我们预览的图片模糊、或者二维码拉伸、图片过小、图片旋转或者扭曲等,都会导致很难定位到二维码,解析二维码困难。

3.1 预览尺寸/图片尺寸选择

如果手机摄像头生成的预览图片宽高比和手机屏幕像素宽高比(准确地说是和相机预览屏幕宽高比)不一样的话,投影的结果肯定就是图片被拉伸。现在基本上每个摄像头支持好几种不同的预览尺寸(parameters.getSupportedPreviewSizes()),我们可以根据屏幕尺寸来选择相机最适合的预览尺寸,当然如果相机支持的预览尺寸与屏幕尺寸一样更好,否则就找到宽高比相同,尺寸最为接近的。代码如下:

                 .....
     Camera.Size   mCameraResolution = findCloselySize(displayMetrics.widthPixels, displayMetrics.heightPixels,
                parameters.getSupportedPreviewSizes());
                ......
          
     Camera.Parameters parameters = camera.getParameters();
     parameters.setPreviewSize(mCameraResolution.width, mCameraResolution.height);
                
    /**
     * 通过对比得到与宽高比最接近的尺寸(如果有相同尺寸,优先选择)
     *
     * @param surfaceWidth  需要被进行对比的原宽
     * @param surfaceHeight 需要被进行对比的原高
     * @param preSizeList   需要对比的预览尺寸列表
     * @return 得到与原宽高比例最接近的尺寸
     */
    protected Camera.Size findCloselySize(int surfaceWidth, int surfaceHeight, List<Camera.Size> preSizeList) {
        Collections.sort(preSizeList, new SizeComparator(surfaceWidth, surfaceHeight));
        return preSizeList.get(0);
    }

  /**
     * 预览尺寸与给定的宽高尺寸比较器。首先比较宽高的比例,在宽高比相同的情况下,根据宽和高的最小差进行比较。
     */
    private static class SizeComparator implements Comparator<Camera.Size> {

        private final int width;
        private final int height;
        private final float ratio;

        SizeComparator(int width, int height) {
            if (width < height) {
                this.width = height;
                this.height = width;
            } else {
                this.width = width;
                this.height = height;
            }
            this.ratio = (float) this.height / this.width;
        }

        @Override
        public int compare(Camera.Size size1, Camera.Size size2) {
            int width1 = size1.width;
            int height1 = size1.height;
            int width2 = size2.width;
            int height2 = size2.height;

            float ratio1 = Math.abs((float) height1 / width1 - ratio);
            float ratio2 = Math.abs((float) height2 / width2 - ratio);
            int result = Float.compare(ratio1, ratio2);
            if (result != 0) {
                return result;
            } else {
                int minGap1 = Math.abs(width - width1) + Math.abs(height - height1);
                int minGap2 = Math.abs(width - width2) + Math.abs(height - height2);
                return minGap1 - minGap2;
            }
        }
    }

同样相机的图片尺寸参数设置类似,利用相机支持的图片尺寸parameters.getSupportedPictureSizes() 与预览尺寸,找到最接近的尺寸,然后设置相机的最适合的图片尺寸 parameters.setPictureSize()

3.2 相机放大倍数

当我们对准二维码时候发现,相机离二维码比较远时,预览的二维码比较小;当相机靠近时,预览的二维码比较大。当我们的二维码过小时,发现条码很难扫出来。另外测试发现每个手机的放大倍数不是都是相同的,这可能与各个手机的型号相关。如果直接设置为一个固定值,这可能会在某些手机上过度放大,某些手机上放大的倍数不够。索性相机的参数设定里给我们提供了最大的放大倍数值,通过取放大倍数值的N分之一作为当前的放大倍数,就完美地解决了手机的适配问题。

如果应用使用过程中,扫码二维码比较远时,有必要将放大倍数设置稍微大些,笔者app 在智能眼镜上使用,距离要扫码的二维码比较远,就是这种情况。

    private static final int TEN_DESIRED_ZOOM = 27;

    private void setZoom(Camera.Parameters parameters) {

        String zoomSupportedString = parameters.get("zoom-supported");
        if (zoomSupportedString != null && !Boolean.parseBoolean(zoomSupportedString)) {
            return;
        }

        int tenDesiredZoom = TEN_DESIRED_ZOOM;

        String maxZoomString = parameters.get("max-zoom");
        if (maxZoomString != null) {
            try {
                int tenMaxZoom = (int) (10.0 * Double.parseDouble(maxZoomString));
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad max-zoom: " + maxZoomString);
            }
        }

        String takingPictureZoomMaxString = parameters.get("taking-picture-zoom-max");
        if (takingPictureZoomMaxString != null) {
            try {
                int tenMaxZoom = Integer.parseInt(takingPictureZoomMaxString);
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad taking-picture-zoom-max: " + takingPictureZoomMaxString);
            }
        }

        String motZoomValuesString = parameters.get("mot-zoom-values");
        if (motZoomValuesString != null) {
            tenDesiredZoom = findBestMotZoomValue(motZoomValuesString, tenDesiredZoom);
        }

        String motZoomStepString = parameters.get("mot-zoom-step");
        if (motZoomStepString != null) {
            try {
                double motZoomStep = Double.parseDouble(motZoomStepString.trim());
                int tenZoomStep = (int) (10.0 * motZoomStep);
                if (tenZoomStep > 1) {
                    tenDesiredZoom -= tenDesiredZoom % tenZoomStep;
                }
            } catch (NumberFormatException nfe) {
                // continue
            }
        }

        // Set zoom. This helps encourage the user to pull back.
        // Some devices like the Behold have a zoom parameter
        // if (maxZoomString != null || motZoomValuesString != null) {
        // parameters.set("zoom", String.valueOf(tenDesiredZoom / 10.0));
        // }
        if (parameters.isZoomSupported()) {
            Log.e(TAG, "max-zoom:" + parameters.getMaxZoom());
            Log.i("0000", "tenDesiredZoom:" + tenDesiredZoom);
            parameters.setZoom(parameters.getMaxZoom() / tenDesiredZoom);
        } else {
            Log.e(TAG, "Unsupported zoom.");
        }

        // Most devices, like the Hero, appear to expose this zoom parameter.
        // It takes on values like "27" which appears to mean 2.7x zoom
        // if (takingPictureZoomMaxString != null) {
        // parameters.set("taking-picture-zoom", tenDesiredZoom);
        // }
    }

3.3 聚焦时间调整

Zxing 默认的聚焦间隔时间是2000毫秒。扫码是在每一次调用相机聚焦完成后触发回调取图解析的。在这里缩短聚焦时间会提高解析频率,扫码性能自然就提升了。当然也有不好的地方,提高了聚焦的频率,对手机电量的消耗自然增加了。我这里是把聚焦间隔修改成了1000毫秒,这个依据手机硬件的性能修改,不同厂家的手机对相机聚焦的处理是不同的,如果你设置的这个聚焦间隔时间小于了手机厂家默认设计的相机聚焦间隔就会导致程序的崩溃。这个设置请慎重使用。聚焦时间的调整也很简单,在 AutoFocusCallback 这个类里,调整 AUTO_FOCUS_INTERVAL_MS 这个值就可以了。

3.4 设置自动对焦区域

我们对焦过程发现有时经常对不准二维码,导致扫描比较慢。Android 4.0 以后我们可以通过camera.setFocusAreas设置对焦区域,提高对焦效果。

         Camera.Parameters params = camera.getParameters();
        if (parameters.getMaxNumFocusAreas() > 0) {
            List<Camera.Area> focusAreas = new ArrayList<>();
            Rect focusRect = new Rect(-100, -100, 100, 100);
            focusAreas.add(new Camera.Area(focusRect, 1000));
            parameters.setFocusAreas(focusAreas);
        }

        if (parameters.getMaxNumMeteringAreas() > 0) {
            List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();
            Rect meteringRect = new Rect(-100, -100, 100, 100);
            meteringAreas.add(new Camera.Area(meteringRect, 1000));
            parameters.setMeteringAreas(meteringAreas);
        }

3.5 调整好扫描区域

扫码时候我们经常调整手机,让扫码的二维码完全填充在扫描框里面,否则很难解析出来,而这个过程蛮花费时间的。如果我们增大扫码区域,那二维码更容易落在扫描框里面,能够缩短扫码的时间。

官方为了减少解码的数据,提高解码效率和速度,采用了裁剪无用区域的方式。这样会带来一定的问题,整个二维码数据需要完全放到聚焦框里才有可能被识别。我们可以在DecodeHandler这个类中buildLuminanceSource(byte[],int,int)这个方法中,调整裁剪区域。

 public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
        // 扫码区域大小,可以调整扫码区域大小
        Rect rect = activity.getCropRect();
        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);
    }

甚至我们可以不用裁剪数据,而是采用全幅图像的数据,当然这种做法会对解码施加压力。读者可以按照自己的需求适当的调整裁剪区域。

4. 其他优化措施

现在市场上有很多的扫码的外设,他们使用方便,而且有专门芯片,性能强悍 ,扫码效率很好。

另外二维码的复杂度与里面的内容相关的,如果app自己生成二维码图片,可以考虑缩短二维码内容长度,避免生成较复杂的二维码,导致不好解析。

项目代码 https://github.com/CardInfoLink/QRScanner

5. 参考

6. 博客推荐

欢迎各位光顾笔者公司的博客https://cardinfolink.github.io/


分享

评论