1. 程式人生 > >zxing掃描二維碼和識別圖片二維碼及其優化策略

zxing掃描二維碼和識別圖片二維碼及其優化策略

轉自:

二維碼介紹

Android中用於二維碼相關的庫比較少,並且大多數已經不再維護(具體可見https://android-arsenal.com/tag/81)。其中最常用的是zxing和zbar。

zxing專案是谷歌推出的用來識別多種格式條形碼的開源專案,專案地址為https://github.com/zxing/zxing,zxing有多個人在維護,覆蓋主流程式語言,也是目前還在維護的較受歡迎的二維碼掃描開源專案之一。zbar則是主要用C來寫的,速度極快,推出了iPhone的SDK和Android的相關呼叫方法(JNI),但這個專案已經有幾年不維護了,目前並沒有維護下去的意思,見https://github.com/ZBar/ZBar

本文不分析二維碼的生成原理和解析原理,感興趣的可以參考陳皓的部落格二維碼的生成細節和原理

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,避免耗電。

  • utils

圖片二維碼解析工具類,以及獲取螢幕寬高的工具類。

  • view

這個包裡只有一個類QrCodeFinderView,官方原本是使用這個類繪製掃描區域框,並且必須在掃描區域裡才能識別二維碼。我把這個類稍作修改,僅僅用來展示掃描區域,實際在相機掃描二維碼的時候,只要在SurfaceView區域範圍內,結果都是有效的。

  • QrCodeActivity

啟動類,包含相機掃描二維碼以及選擇圖片入口。

zxing原始碼存在的問題及解決方案

zxing專案原始碼實現了基本的二維碼掃描及圖片識別程式,但下載過原始碼並直接執行的童鞋都知道,例子存在很多的問題,包括基本的識別精準度不高、掃描區域小、部分手機存在預覽圖形拉伸、預設橫向掃描、還有自定義掃描介面困難等問題。

圖形拉伸問題

先來了解一下為什麼會產生圖形拉伸。Android手機的螢幕解析度可以說不勝列舉,不同型號的寬高比可能是不一樣的,例如Nexus 5x、小米4的解析度是1920X1080(當前主流手機的解析度都是這個級別),Nexus 6p的解析度達到2560X1440。而每臺手機使用的攝像頭型號更是千變萬化,手機攝像頭有一個成像的畫素。例如普通的卡片數碼相機,常常可以看到類似2304X1728、1600X1200、1027X768、640X480的字樣,這些數字相乘得到的結果就代表了這個相機的成像解析度。手機裡內建的攝像頭和卡片數碼相機的成像原理是一樣的,在攝像頭預覽的時候,最終都會生成連續固定畫素的圖片,這張圖片會被投影到手機的螢幕上。如果攝像頭生成的預覽圖片寬高比和手機螢幕畫素寬高比(準確地說是和相機預覽螢幕寬高比)不一樣的話,投影的結果肯定就是圖片被拉伸。

原專案其實有解決圖形拉伸的問題,並且用了很細緻的辦法,考慮了各種機型的相容性問題首先來看zxing是怎麼解決圖形拉伸問題的。在CameraConfigurationManager類裡,初始化相機配置

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
/**
 * Reads, one time, values from the camera that are needed by the app.
 */
void initFromCameraParameters(OpenCamera camera) {
    Camera.Parameters parameters = camera.getCamera().getParameters();
    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = manager.getDefaultDisplay();

    // 判斷螢幕方向,是否有需要從自然角度旋轉到顯示器角度
    int displayRotation = display.getRotation();
    int cwRotationFromNaturalToDisplay;
    switch (displayRotation) {
        case Surface.ROTATION_0:
            cwRotationFromNaturalToDisplay = 0;
            break;
        case Surface.ROTATION_90:
            cwRotationFromNaturalToDisplay = 90;
            break;
        case Surface.ROTATION_180:
            cwRotationFromNaturalToDisplay = 180;
            break;
        case Surface.ROTATION_270:
            cwRotationFromNaturalToDisplay = 270;
            break;
        default:
            // Have seen this return incorrect values like -90
            if (displayRotation % 90 == 0) {
                cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
            } else {
                throw new IllegalArgumentException("Bad rotation: " + displayRotation);
            }
    }
    Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);

    //判斷相機的方向,根據前後攝像機判斷是否有需要旋轉
    int cwRotationFromNaturalToCamera = camera.getOrientation();
    Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);

    // Still not 100% sure about this. But acts like we need to flip this:
    if (camera.getFacing() == CameraFacing.FRONT) {
        cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
        Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
    }

    //根據螢幕方向和相機方向判斷是否有需要進行旋轉
    cwRotationFromDisplayToCamera = (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
    Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
    if (camera.getFacing() == CameraFacing.FRONT) {
        Log.i(TAG, "Compensating rotation for front camera");
        cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
    } else {
        cwNeededRotation = cwRotationFromDisplayToCamera;
    }
    Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);

    Point theScreenResolution = new Point();
    display.getSize(theScreenResolution);
    screenResolution = theScreenResolution;
    Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
    // 尋找最佳的預覽寬高值
    cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    Log.i(TAG, "Camera resolution: " + cameraResolution);
    bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    Log.i(TAG, "Best available preview size: " + bestPreviewSize);

    boolean isScreenPortrait = screenResolution.x < screenResolution.y;
    boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;

    if (isScreenPortrait == isPreviewSizePortrait) {
        previewSizeOnScreen = bestPreviewSize;
    } else {
        previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
    }
    Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);
}

再來看一下CameraConfigurationUtils.findBestPreviewSizeValue(Camera.Parameters,Point)方法

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
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {

    // 獲取當前手機支援的螢幕預覽尺寸
    List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
    if (rawSupportedSizes == null) {
        Log.w(TAG, "Device returned no supported preview sizes; using default");
        Camera.Size defaultSize = parameters.getPreviewSize();
        if (defaultSize == null) {
            throw new IllegalStateException("Parameters contained no preview size!");
        }
        return new Point(defaultSize.width, defaultSize.height);
    }

    // 對這些尺寸根據畫素值(即寬乘高的值)進行重小到大排序
    List<Camera.Size> supportedPreviewSizes = new ArrayList<>(rawSupportedSizes);
    Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() {
        @Override
        public int compare(Camera.Size a, Camera.Size b) {
            int aPixels = a.height * a.width;
            int bPixels = b.height * b.width;
            if (bPixels < aPixels) {
                return -1;
            }
            if (bPixels > aPixels) {
                return 1;
            }
            return 0;
        }
    });

    double screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y;

    Iterator<Camera.Size> it = supportedPreviewSizes.iterator();
    while (it.hasNext()) {
        Camera.Size supportedPreviewSize = it.next();
        int realWidth = supportedPreviewSize.width;
        int realHeight = supportedPreviewSize.height;
        // 首先把不符合最小預覽畫素值的尺寸排除
        if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {
            it.remove();
            continue;
        }

        boolean isCandidatePortrait = realWidth < realHeight;
        int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
        int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
        double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight;
        double distortion = Math.abs(aspectRatio - screenAspectRatio);
        // 根據寬高比判斷是否滿足最大誤差要求(預設最大值為0.15,即寬高比預設不能超過給定比例的15%)
        if (distortion > MAX_ASPECT_DISTORTION) {
            it.remove();
            continue;
        }

        if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
            Point exactPoint = new Point(realWidth, realHeight);
            Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
            return exactPoint;
        }
    }

    // 如果沒有精確匹配到合適的尺寸,則使用最大的尺寸,這樣設定便是預覽影象可能產生拉伸的根本原因。
    if (!supportedPreviewSizes.isEmpty()) {
        Camera.Size largestPreview = supportedPreviewSizes.get(0);
        Point largestSize = new Point(largestPreview.width, largestPreview.height);
        Log.i(TAG, "Using largest suitable preview size: " + largestSize);
        return largestSize;
    }

    // 如果沒有找到合適的尺寸,就返回預設設定的尺寸
    Camera.Size defaultPreview = parameters.getPreviewSize();
    if (defaultPreview == null) {
        throw new IllegalStateException("Parameters contained no preview size!");
    }
    Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
    Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
    return defaultSize;
}

從註釋中已經可以清楚地看到zxing專案在尋找最佳尺寸值的方法:

  • 首先,查詢手機支援的預覽尺寸集合,如果集合為空,就返回預設的尺寸;否則,對尺寸集合根據尺寸的畫素從小到大進行排序;
  • 其次,移除不滿足最小畫素要求的所有尺寸;
  • 在者,在剩餘的尺寸集合中,剔除預覽寬高比與螢幕解析度寬高比之差的絕對值大於0.15的所有尺寸;
  • 最後,尋找能夠精確的與螢幕寬高匹配上的預覽尺寸,如果存在則返回該寬高比;如果不存在,則使用尺寸集合中最大的那個尺寸。如果說尺寸集合已經在前面的過濾中被全部排除,則返回相機預設的尺寸值。

zxing尋找最佳預覽尺寸的前三步剔除了部分不符合要求的尺寸集合,在最後一步,如果沒有精確匹配到與螢幕解析度一樣的尺寸,則使用最大的尺寸。問題的關鍵就在這裡,最大的尺寸寬高比與螢幕寬高比相差可能很大(根據剔除規則,差距可能超過15%)。

根據這個規則,我修改了尋找最佳尺寸的原始碼,將演算法的核心從最大的尺寸改為比例最接近的尺寸,這樣便能夠最原始地接近螢幕解析度的寬高比,即拉伸幾乎看不出來。首先定義一個比較器,用來對支援的預覽尺寸集合進行排序。

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
/**
 * 預覽尺寸與給定的寬高尺寸比較器。首先比較寬高的比例,在寬高比相同的情況下,根據寬和高的最小差進行比較。
 */
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;
        }
    }
}

目的就是根據寬高比來排序,然後呼叫方法取最小的那個值就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 通過對比得到與寬高比最接近的尺寸(如果有相同尺寸,優先選擇)
 *
 * @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);
}

最後在初始化相機尺寸的時候分別對預覽尺寸值和圖片尺寸值都設定為比例最接近螢幕尺寸的尺寸值就可以了。本文使用的方法精簡了zxing專案的步驟,實際上專案中使用的前三步濾除還是很有必要的,這裡為了簡短略去了。

1
2
3
4
5
6
7
8
9
void initFromCameraParameters(Camera camera) {
    Camera.Parameters parameters = camera.getParameters();
    mCameraResolution = findCloselySize(ScreenUtils.getScreenWidth(mContext), ScreenUtils.getScreenHeight(mContext),
        parameters.getSupportedPreviewSizes());
    Log.e(TAG, "Setting preview size: " + mCameraResolution.width + "-" + mCameraResolution.height);
    mPictureResolution = findCloselySize(ScreenUtils.getScreenWidth(mContext),
        ScreenUtils.getScreenHeight(mContext), parameters.getSupportedPictureSizes());
    Log.e(TAG, "Setting picture size: " + mPictureResolution.width + "-" + mPictureResolution.height);
}

掃描精度問題

使用過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

            
           

相關推薦

zxing掃描識別圖片及其優化策略

轉自: 二維碼介紹 Android中用於二維碼相關的庫比較少,並且大多數已經不再維護(具體可見https://android-arsenal.com/tag/81)。其中最常用的是zxing和zbar。 zxing專案是谷歌推出的用來識別多種格式條形碼的開源專案,專案地址為

.net c#識別圖片 圖片處理(ImgBitMap)

1.場景 承接上篇,如何對攝像頭得到的圖片或者本地圖片進行二維碼識別   2.程式   程式使用類庫zxing.dll(用來識別二維碼),根據圖片識別二維碼方法是通用的 (1)識別二維碼並得到二維碼資訊的方法。(傳入引數為BitMap物件)

Android開源庫之使用ZXing開源庫生成識別本地圖片

/** * 解析二維碼(使用解析RGB編碼資料的方式) * * @param path * @return */ public static Result decodeBarcodeRGB(String path) { if (Text

PHP生成識別

<1>PHP生成二維碼 QR Code是一個PHP二維碼生成類庫,利用它可以輕鬆生成二維碼,下載官網提供的類庫後,只需要引入phpqrcode.php就可以生成二維碼 程式碼例項如下: function setCode($url){ $data

深度學習 (一)計算機如何處理識別圖片揭祕

前言        先來一張美景圖,欣賞一下大自然,順便大家猜猜這是哪裡?        有時候真感嘆大自然的雄偉壯闊,自然形成了無數的山和風景不需要任何點綴,有

[Xcode10 實際操作]七、檔案與資料-(20)CoreML機器學習框架:檢測識別圖片中的物體

本文將演示機器學習框架的使用,實現對圖片中物體的檢測和識別。 首先訪問蘋果開發者網站關於機器學習的網址: https://developer.apple.com/cn/machine-learning/ 點選右側的滾動條,跳轉到模型知識區域。 點選頁面最下方的【Learn about working

[TensorFlow深度學習深入]實戰·使用CNN網路識別破解數字驗證

[TensorFlow深度學習深入]實戰二·使用CNN網路識別破解數字驗證碼 參考部落格。 在此基礎上做了小修改。 其中CNN網路部分仿照我們入門實戰六的內容,如果不太清楚CNN可以再去回顧一下。 本博文資料集。 程式碼部分 import os os.environ["KMP_D

指標陣列,指標陣列

在函式傳參時候,陣列名作為引數,自動轉成指標,那麼二維陣列可不可以這樣做呢。void fun(int **a,int i,int j){int m = i*j;for (int i = 0; i < m; i++){cout << *(a[0] + i)<<endl;}}int

、反補碼練習()

A:已知原碼求補碼     0b10110100 B:已知補碼求原碼     0b11101110 1 1101110(補碼) 0 0000001(-1) ------------------- 1 1101101(反碼) 1 0010010(原碼)

opencv 人臉識別)訓練識別

上一篇中我們對訓練資料做了一些預處理,檢測出人臉並儲存在\pic\color\x資料夾下(x=1,2,3,...類別號),本文做訓練和識別。為了識別,首先將人臉訓練資料 轉為灰度、對齊、歸一化,再放入分類器(EigenFaceRecognizer),最後用訓練出的model進

c#的托管代非托管代的理解

ont 線程管理 安全 本機 有一個 自己 垃圾 相關 spa 理解托管和非托管代碼的前提之下,要先了解CLR(公共語言運行庫) .Net Framework 是由彼此獨立又相關的兩部分組成:CLR 和 類庫, CLR是它為我們提供的服務,類庫是它實現的功能. .NET

托管代非托管代

機器碼 spa 中間 實現 托管 兩個 回收 lang 於平 什麽是托管代碼? 托管代碼是運行在公共語言運行庫上的(CLR)一個中間代碼,起到編譯的源代碼的作用; 源代碼運行時分為兩個階段:一是編譯源代碼為托管代碼,二 托管代碼編譯為平臺專用語言。 托管代碼運行在CLR上邊

【轉】托管代非托管代的區別

產生 沒有 匯編代碼 cati 一行 包含 虛擬機 被調用 庫類 什麽是托管代碼(managed code)? 托管代碼是一microsoft的中間語言(IL),他主要的作用是在.NET FRAMEWORK的公共語言運行庫(CLR)執行代碼前去編譯

一個神奇代一個坑爹代

神奇 python 5.1 分享圖片 caml space java bsp perl 上述代碼會運行一次生成一種語言代碼,最後運行回了ruby代碼。11種如下: ruby 1.8.7-p72、Python 2.5.2、perl v5.10.0、Lua 5.0.3、OC

區分整流極管穩壓極管

系列 mage 不包含 bsp 萬用表 src 兩種 現在 模糊 快速區分整流二極管和穩壓二極管有兩種方法,一是看二極管的型號,二是用數字萬用表二極管檔測量。下面分別介紹這兩種方法。 一、通過二極管型號區分整流二極管和穩壓二極管 現在常用的整流二極管主要有1N40XX系列1

.net組件com組件&托管代非托管代

DUID 動態鏈接庫 soft 組成 ros 重新 https 完成 內存 com組件和.net組件: COM組件是非托管對象,可以不需要.NET框架而直接運行,.NET框架組件是托管對象,必須有.NET框架的支撐才能運行。 COM組件有獨立的類型庫文件,而.NET組件是

子網掩萬用字元掩的區別

子網掩碼 子網掩碼,官方的定義是 一種用來指明一個IP地址的哪些位標識的是主機所在的子網,以及哪些位標識的是主機的掩碼。子網掩碼不能單獨存在,它必須結合IP地址一起使用。子網掩碼只有一個作用,就是將某個IP地址劃分成網路地址和主機地址兩部分。 說白了子網掩碼的工

驗證點選圖片換驗證

由於直接設定src為/commons/validator.html由於快取問題會造成圖片不更換,所以需要加一個引數,讓每次請求都不一樣,只有加入時間引數了,這樣每次重新整理都是新的,就不存在快取不更新圖片問題了 ---(? 號一定加在' '裡面) html: <di

MFC中視窗控制代控制元件控制代的獲取

1. 程式主視窗控制代碼的獲得     CWnd* p = AfxGetMainWnd();//函式AfxGetMainWnd()獲得主對話方塊的指標p     HWND h = p->m_hW

關於encodeURI兩次轉非兩次轉,以及後臺中文,前臺不顯示問題總結

一:js:如果中文亂碼,就轉碼兩次 function details(cellValue,row){ return '<a href="#" onclick=openWindow('+r