zxing的使用及優化

二維碼介紹

zxing項目是谷歌推出的用來識別多種格式條形碼的開源項目,項目地址爲https://github.com/zxing/zxing,zxing有多我的在維護,覆蓋主流編程語言,也是目前還在維護的較受歡迎的二維碼掃描開源項目之一。html

zxing的項目很龐大,主要的核心代碼在core文件夾裏面,也能夠單獨下載由這個文件夾打包而成的jar包,具體地址在http://mvnrepository.com/artifact/com.google.zxing/core,直接下載jar包也省去了經過maven編譯的麻煩,若是喜歡折騰的,能夠從https://github.com/zxing/zxing/wiki/Getting-Started-Developing獲取幫助文檔。
java


zxing基本使用

官方提供了zxing在Android機子上的使用例子,https://github.com/zxing/zxing/tree/master/android,做爲官方的例子,zxing-android考慮了各類各樣的狀況,包括多種解析格式、解析獲得的結果分類、長時間無活動自動銷燬機制等。有時候咱們須要根據本身的狀況定製使用需求,所以會精簡官方給的例子。在項目中,咱們僅僅用來實現掃描二維碼和識別圖片二維碼兩個功能。爲了實現高精度的二維碼識別,在zxing原有項目的基礎上,本文作了大量改進,使得二維碼識別的效率有所提高。先來看看工程的項目結構。android

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

源碼比較簡單,這裏不作過多地講解,大部分方法都有註釋。主要分爲幾大塊,git

  • camera

主要實現相機的配置和管理,相機自動聚焦功能,以及相機成像回調(經過byte[]數組返回實際的數據)。github

  • decode

圖片解析相關類。經過相機掃描二維碼和解析圖片使用兩套邏輯。前者對實時性要求比較高,後者對解析結果要求較高,所以採用不一樣的配置。相機掃描主要在DecodeHandler裏經過串行的方式解析,圖片識別主要經過線程DecodeImageThread異步調用返回回調的結果。FinishListenerInactivityTimer用來控制長時間無活動時自動銷燬建立的Activity,避免耗電。算法

掃描精度問題

使用過zxing自帶的二維碼掃描程序來識別二維碼的童鞋應該知道,zxing二維碼的掃描程序很慢,並且有可能掃不出來。zxing在配置相機參數和二維碼掃描程序參數的時候,配置都比較保守,兼顧了低端手機,而且兼顧了多種條形碼的識別。若是說僅僅是拿zxing項目來掃描和識別二維碼的話,徹底能夠對項目中的一些配置作精簡,並針對二維碼的識別作優化。express

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()裏完成。這樣便完成了亮度源的構造。canvas

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)來講有NV1六、NV2一、YUY二、YV十二、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
50
51
52
53
54
55
56
/**
 * RGB轉YUV420sp
 *
 * @param yuv420sp inputWidth * inputHeight * 3 / 2
 * @param argb inputWidth * inputHeight
 * @param width image width
 * @param height image height
 */
private static void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) {
    // 幀圖片的像素大小
    final int frameSize = width * height;
    // ---YUV數據---
    int Y, U, V;
    // Y的index從0開始
    int yIndex = 0;
    // UV的index從frameSize開始
    int uvIndex = frameSize;

    // ---顏色數據---
    int R, G, B;
    int rgbIndex = 0;

    // ---循環全部像素點,RGB轉YUV---
    for (int j = 0; j < height; j++) {
        for (int i = 0; i < width; i++) {

            R = (argb[rgbIndex] & 0xff0000) >> 16;
            G = (argb[rgbIndex] & 0xff00) >> 8;
            B = (argb[rgbIndex] & 0xff);
            //
            rgbIndex++;

            // well known RGB to YUV algorithm
            Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
            U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
            V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

            Y = Math.max(0, Math.min(Y, 255));
            U = Math.max(0, Math.min(U, 255));
            V = Math.max(0, Math.min(V, 255));

            // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
            // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
            // pixel AND every other scan line.
            // ---Y---
            yuv420sp[yIndex++] = (byte) Y;
            // ---UV---
            if ((j % 2 == 0) && (i % 2 == 0)) {
                //
                yuv420sp[uvIndex++] = (byte) V;
                //
                yuv420sp[uvIndex++] = (byte) U;
            }
        }
    }
}

Android中讀取一張圖片通常是經過BitmapFactory.decodeFile(imgPath, options)這個方法去獲得這張圖片的Bitmap數據,Bitmap是由ARGB值編碼獲得的,所以若是須要轉換成YUV,還須要作一點小小的變換。

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
private static byte[] yuvs;
/**
 * 根據Bitmap的ARGB值生成YUV420SP數據。
 *
 * @param inputWidth image width
 * @param inputHeight image height
 * @param scaled bmp
 * @return YUV420SP數組
 */
public static byte[] getYUV420sp(int inputWidth, int inputHeight, Bitmap scaled) {
    int[] argb = new int[inputWidth * inputHeight];

    scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);

    /**
 * 須要轉換成偶數的像素點,不然編碼YUV420的時候有可能致使分配的空間大小不夠而溢出。
 */
    int requiredWidth = inputWidth % 2 == 0 ? inputWidth : inputWidth + 1;
    int requiredHeight = inputHeight % 2 == 0 ? inputHeight : inputHeight + 1;

    int byteLength = requiredWidth * requiredHeight * 3 / 2;
    if (yuvs == null || yuvs.length < byteLength) {
        yuvs = new byte[byteLength];
    } else {
        Arrays.fill(yuvs, (byte) 0);
    }

    encodeYUV420SP(yuvs, argb, inputWidth, inputHeight);

    scaled.recycle();

    return yuvs;
}

這裏面有幾個坑,在方法裏已經列出來了。首先,若是每次都生成新的YUV數組,不知道在掃一掃解碼時要進行GC多少次。。。因此就採用了靜態的數組變量來存儲數據,只有噹噹前的長寬乘積超過數組大小時,才從新生成新的yuvs。其次,若是鑑於YUV的特性,長寬只能是偶數個像素點,不然可能會形成數組溢出(不信能夠嘗試)。最後,使用完了Bitmap要記得回收,那玩意吃內存不是隨便說說的。

二維碼圖像識別算法選擇

二維碼掃描精度和許多因素有關,最關鍵的因素是掃描算法。目前在圖形識別領域中,較經常使用的二維碼識別算法主要有兩種,分別是HybridBinarizerGlobalHistogramBinarizer,這兩種算法都是基於二值化,即將圖片的色域變爲黑白兩個顏色,而後提取圖形中的二維碼矩陣。實際上,zxing中的HybridBinarizer繼承自GlobalHistogramBinarizer,並在此基礎上作了功能性的改進。援引官方介紹:

This Binarizer(GlobalHistogramBinarizer) implementation uses the old ZXing global histogram approach. It is suitable for low-end mobile devices which don’t have enough CPU or memory to use a local thresholding algorithm. However, because it picks a global black point, it cannot handle difficult shadows and gradients. Faster mobile devices and all desktop applications should probably use HybridBinarizer instead.

This class(HybridBinarizer) implements a local thresholding algorithm, which while slower than the GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for high frequency images of barcodes with black data on white backgrounds. For this application, it does a much better job than a global blackpoint with severe shadows and gradients. However it tends to produce artifacts on lower frequency images and is therefore not a good general purpose binarizer for uses outside ZXing. This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers, and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already inherently local, and only fails for horizontal gradients. We can revisit that problem later, but for now it was not a win to use local blocks for 1D. ···

GlobalHistogramBinarizer算法適合於低端的設備,對手機的CPU和內存要求不高。但它選擇了所有的黑點來計算,所以沒法處理陰影和漸變這兩種狀況。HybridBinarizer算法在執行效率上要慢於GlobalHistogramBinarizer算法,但識別相對更有效。它專門爲以白色爲背景的連續黑色塊二維碼圖像解析而設計,也更適合用來解析具備嚴重陰影和漸變的二維碼圖像。

網上對這兩種算法的解析並很少,目前僅找到一篇文章詳解了GlobalHistogramBinarizer算法,詳見[http://kuangjianwei.blog.163.com/blog/static/190088953201361015055110/]()。有時間再看一下相關源碼。

zxing項目官方默認使用的是HybridBinarizer二值化方法。在實際的測試中,和官方的介紹大體同樣。然而目前的大部分二維碼都是黑色二維碼,白色背景的。無論是二維碼掃描仍是二維碼圖像識別,使用GlobalHistogramBinarizer算法的效果要稍微比HybridBinarizer好一些,識別的速度更快,對低分辨的圖像識別精度更高。

除了這兩種算法,我相信在圖像識別領域確定還有更好的算法存在,目前受限於知識水平,對二值化算法這一塊還比較陌生,期待之後可以深刻理解並改進目前的開源算法(*^__^*)……

圖像大小對識別精度的影響

這點是測試中無心發現的。如今的手機攝像頭拍照出現的照片像素都很高,動不動就1200W像素,1600W像素,甚至是2000W都不稀奇,但照片的成像質量不必定高。將一張高分辨率的圖片按原分辨率導入Android手機,很容易產生OOM。咱們來計算一下,導入一張1200W像素的圖片須要的內存,假設圖片是4000px*3000px,若是導入的圖片採用ARGB_8888編碼形式,則每一個像素須要佔用4個Bytes(分別存儲ARGB值)來存儲,則須要4000*3000*4bytes=45.776MB的內存,這在有限的移動資源裏,顯然是不能忍受的。

經過上一節對圖像算法的簡單研究,在GlobalHistogramBinarizer中,是從圖像中均勻取5行(覆蓋整個圖像高度),每行取中間五分之四做爲樣本;以灰度值爲X軸,每一個灰度值的像素個數爲Y軸創建一個直方圖,從直方圖中取點數最多的一個灰度值,而後再去給其餘的灰度值進行分數計算,按照點數乘以與最多點數灰度值的距離的平方來進行打分,選分數最高的一個灰度值。接下來在這兩個灰度值中間選取一個區分界限,取的原則是儘可能靠近中間而且要點數越少越好。界限有了之後就容易了,與整幅圖像的每一個點進行比較,若是灰度值比界限小的就是黑,在新的矩陣中將該點置1,其他的就是白,爲0。(摘自zxing源碼分析——QR碼部分

根據算法的實現,能夠知道圖像的分辨率對二維碼的取值是有影響的。並非圖像的分辨率越高就越容易取到二維碼。高分辨率的圖像對Android的內存資源佔用也很可怕。因此在測試的過程當中,我嘗試將圖片壓縮成不一樣大小分辨率,而後再進行圖片的二維碼識別。

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
/**
 * 根據給定的寬度和高度動態計算圖片壓縮比率
 * 
 * @param options Bitmap配置文件
 * @param reqWidth 須要壓縮到的寬度
 * @param reqHeight 須要壓縮到的高度
 * @return 壓縮比
 */
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

/**
 * 將圖片根據壓縮比壓縮成固定寬高的Bitmap,實際解析的圖片大小可能和#reqWidth、#reqHeight不同。
 * 
 * @param imgPath 圖片地址
 * @param reqWidth 須要壓縮到的寬度
 * @param reqHeight 須要壓縮到的高度
 * @return Bitmap
 */
public static Bitmap decodeSampledBitmapFromFile(String imgPath, int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(imgPath, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeFile(imgPath, options);
}

Android圖片優化須要經過在解析圖片的時候,設置BitmapFactory.Options.inSampleSize的值,根據比例壓縮圖片大小。在進行圖片二維碼解析的線程中,經過設置不一樣的圖片大小,來測試二維碼的識別率。這個測試過程我忘記保存了,只記得測試了壓縮成最大寬高值爲204八、102四、5十二、256和128像素的包含二維碼的圖片,但實際的測試結果是,當MAX_PICTURE_PIXEL=256的時候識別率最高。

此結論不具有理論支持,有興趣的童鞋能夠本身動手嘗試。^_^

相機預覽倍數設置及聚焦時間調整

若是使用zxing默認的相機配置,會發現須要離二維碼很近纔可以識別出來,但這樣會帶來一個問題——聚焦困難。解決辦法就是調整相機預覽倍數以及減少相機聚焦的時間。

經過測試能夠發現,每一個手機的最大放大倍數幾乎是不同的,這可能和攝像頭的型號有關。若是設置成一個固定的值,那可能會產生在某些手機上過分放大,某些手機上放大的倍數不夠。索性相機的參數設定裏給咱們提供了最大的放大倍數值,經過取放大倍數值的N分之一做爲當前的放大倍數,就完美地解決了手機的適配問題。

1
2
3
4
5
6
// 須要判斷攝像頭是否支持縮放
Parameters parameters = camera.getParameters();
if (parameters.isZoomSupported()) {
 // 設置成最大倍數的1/10,基本符合遠近需求
 parameters.setZoom(parameters.getMaxZoom() / 10);
}

zxing默認的相機聚焦時間是2s,能夠根據掃描的視覺適當調整。聚焦時間的調整也很簡單,在AutoFocusCallback這個類裏,調整AUTO_FOCUS_INTERVAL_MS這個值就能夠了。

二維碼掃描視覺調整

二維碼掃描視覺的繪製在ViewfinderView.java完成,官方是繼承了View而後在onDraw()方法中實現了視圖的繪製。

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
@Override
public void onDraw(Canvas canvas) {
    if (cameraManager == null) {
        return; // not ready yet, early draw before done configuring
    }
    Rect frame = cameraManager.getFramingRect();
    Rect previewFrame = cameraManager.getFramingRectInPreview();
    if (frame == null || previewFrame == null) {
        return;
    }
    int width = canvas.getWidth();
    int height = canvas.getHeight();

    // 繪製聚焦框外的暗色透明層
    paint.setColor(resultBitmap != null ? resultColor : maskColor);
    canvas.drawRect(0, 0, width, frame.top, paint);
    canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
    canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
    canvas.drawRect(0, frame.bottom + 1, width, height, paint);

    if (resultBitmap != null) {
        // 若是掃描結果不爲空,則把掃描的結果填充到聚焦框中
        paint.setAlpha(CURRENT_POINT_OPACITY);
        canvas.drawBitmap(resultBitmap, null, frame, paint);
    } else {

        // 畫一根紅色的激光線表示二維碼解碼正在進行
        paint.setColor(laserColor);
        paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
        scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
        int middle = frame.height() / 2 + frame.top;
        canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1, middle + 2, paint);

        float scaleX = frame.width() / (float) previewFrame.width();
        float scaleY = frame.height() / (float) previewFrame.height();

        List<ResultPoint> currentPossible = possibleResultPoints;
        List<ResultPoint> currentLast = lastPossibleResultPoints;
        int frameLeft = frame.left;
        int frameTop = frame.top;

        // 繪製解析過程當中可能掃描到的關鍵點,使用黃色小圓點表示
        if (currentPossible.isEmpty()) {
            lastPossibleResultPoints = null;
        } else {
            possibleResultPoints = new ArrayList<>(5);
            lastPossibleResultPoints = currentPossible;
            paint.setAlpha(CURRENT_POINT_OPACITY);
            paint.setColor(resultPointColor);
            synchronized (currentPossible) {
                for (ResultPoint point : currentPossible) {
                    canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
                        frameTop + (int) (point.getY() * scaleY), POINT_SIZE, paint);
                }
            }
        }
        if (currentLast != null) {
            paint.setAlpha(CURRENT_POINT_OPACITY / 2);
            paint.setColor(resultPointColor);
            synchronized (currentLast) {
                float radius = POINT_SIZE / 2.0f;
                for (ResultPoint point : currentLast) {
                    canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
                        frameTop + (int) (point.getY() * scaleY), radius, paint);
                }
            }
        }
        // 重繪聚焦框裏的內容,不須要重繪整個界面。
        postInvalidateDelayed(ANIMATION_DELAY, frame.left - POINT_SIZE, frame.top - POINT_SIZE,
            frame.right + POINT_SIZE, frame.bottom + POINT_SIZE);
    }
}

我給它作了一點小改變,效果差很少,代碼更簡潔一些。因爲代碼中我不是根據屏幕的寬高動態計算聚焦框的大小,所以這裏省去了從CameraManager獲取FramingRect和FramingRectInPreview這兩個矩形的過程。我在聚焦框外加了四個角,目前大部分二維碼產品基本都是這麼設計的吧,固然也可使用圖片來代替。總之視覺定製是因人而異,這裏不作過多介紹。

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
96
97
98
99
100
101
102
@Override
public void onDraw(Canvas canvas) {
    if (isInEditMode()) {
        return;
    }
    Rect frame = mFrameRect;
    if (frame == null) {
        return;
    }
    int width = canvas.getWidth();
    int height = canvas.getHeight();

    // 繪製焦點框外邊的暗色背景
    mPaint.setColor(mMaskColor);
    canvas.drawRect(0, 0, width, frame.top, mPaint);
    canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, mPaint);
    canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, mPaint);
    canvas.drawRect(0, frame.bottom + 1, width, height, mPaint);

    drawFocusRect(canvas, frame);
    drawAngle(canvas, frame);
    drawText(canvas, frame);
    drawLaser(canvas, frame);

    // Request another update at the animation interval, but only repaint the laser line,
    // not the entire viewfinder mask.
    postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);
}

/**
 * 畫聚焦框,白色的
 * 
 * @param canvas
 * @param rect
 */
private void drawFocusRect(Canvas canvas, Rect rect) {
    // 繪製焦點框(黑色)
    mPaint.setColor(mFrameColor);
    // 上
    canvas.drawRect(rect.left + mAngleLength, rect.top, rect.right - mAngleLength, rect.top + mFocusThick, mPaint);
    // 左
    canvas.drawRect(rect.left, rect.top + mAngleLength, rect.left + mFocusThick, rect.bottom - mAngleLength,
        mPaint);
    // 右
    canvas.drawRect(rect.right - mFocusThick, rect.top + mAngleLength, rect.right, rect.bottom - mAngleLength,
        mPaint);
    // 下
    canvas.drawRect(rect.left + mAngleLength, rect.bottom - mFocusThick, rect.right - mAngleLength, rect.bottom,
        mPaint);
}

/**
 * 畫粉色的四個角
 * 
 * @param canvas
 * @param rect
 */
private void drawAngle(Canvas canvas, Rect rect) {
    mPaint.setColor(mLaserColor);
    mPaint.setAlpha(OPAQUE);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setStrokeWidth(mAngleThick);
    int left = rect.left;
    int top = rect.top;
    int right = rect.right;
    int bottom = rect.bottom;
    // 左上角
    canvas.drawRect(left, top, left + mAngleLength, top + mAngleThick, mPaint);
    canvas.drawRect(left, top, left + mAngleThick, top + mAngleLength, mPaint);
    // 右上角
    canvas.drawRect(right - mAngleLength, top, right, top + mAngleThick, mPaint);
    canvas.drawRect(right - mAngleThick, top, right, top + mAngleLength, mPaint);
    // 左下角
    canvas.drawRect(left, bottom - mAngleLength, left + mAngleThick, bottom, mPaint);
    canvas.drawRect(left, bottom - mAngleThick, left + mAngleLength, bottom, mPaint);
    // 右下角
    canvas.drawRect(right - mAngleLength, bottom - mAngleThick, right, bottom, mPaint);
    canvas.drawRect(right - mAngleThick, bottom - mAngleLength, right, bottom, mPaint);
}

private void drawText(Canvas canvas, Rect rect) {
    int margin = 40;
    mPaint.setColor(mTextColor);
    mPaint.setTextSize(getResources().getDimension(R.dimen.text_size_13sp));
    String text = getResources().getString(R.string.qr_code_auto_scan_notification);
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    float fontTotalHeight = fontMetrics.bottom - fontMetrics.top;
    float offY = fontTotalHeight / 2 - fontMetrics.bottom;
    float newY = rect.bottom + margin + offY;
    float left = (ScreenUtils.getScreenWidth(mContext) - mPaint.getTextSize() * text.length()) / 2;
    canvas.drawText(text, left, newY, mPaint);
}

private void drawLaser(Canvas canvas, Rect rect) {
    // 繪製焦點框內固定的一條掃描線(紅色)
    mPaint.setColor(mLaserColor);
    mPaint.setAlpha(SCANNER_ALPHA[mScannerAlpha]);
    mScannerAlpha = (mScannerAlpha + 1) % SCANNER_ALPHA.length;
    int middle = rect.height() / 2 + rect.top;
    canvas.drawRect(rect.left + 2, middle - 1, rect.right - 1, middle + 2, mPaint);

}

總結

使用zxing進行二維碼的編解碼是很是方便的,zxing的API覆蓋了多種主流編程語言,具備良好的擴展性和可定製性。文中進行了二維碼基本功能介紹,zxing項目基本使用方法,zxing項目中目前存在的缺點及改進方案,以及本身在進行zxing項目二次開發的摸索過程當中總結出的提升二維碼掃描的方法。文中還有許多不足的地方,對源碼的理解還不夠深,特別是二維碼解析關鍵算法(GlobalHistogramBinarizerHybridBinarizer)。這些算法須要投入額外的時間去理解,對於目前以業務爲導向的App開發來講,還存在優化的空間,期待未來有一天能像微信的二維碼掃描同樣快速,精確。

轉自:http://iluhcm.com/2016/01/08/scan-qr-code-and-recognize-it-from-picture-fastly-using-zxing/
相關文章
相關標籤/搜索