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 |
原始碼比較簡單,這裡不做過多地講解,大部分方法都有註釋。主要分為幾大塊,
- camera
主要實現相機的配置和管理,相機自動聚焦功能,以及相機成像回撥(通過byte[]
陣列返回實際的資料)。
- decode
圖片解析相關類。通過相機掃描二維碼和解析圖片使用兩套邏輯。前者對實時性要求比較高,後者對解析結果要求較高,因此採用不同的配置。相機掃描主要在DecodeHandler
裡通過序列的方式解析,圖片識別主要通過執行緒DecodeImageThread
非同步呼叫返回回撥的結果。FinishListener
和InactivityTimer
用來控制長時間無活動時自動銷燬建立的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*240
和1200*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 |