1. 二維碼掃碼庫介紹
二維碼又稱QR Code,QR全稱Quick Response,是一個近幾年來移動設備上超流行的一種編碼方式,它比傳統的Bar Code條形碼能存更多的信息,也能表示更多的數據類型。
二維條碼/二維碼(2-dimensional bar code)是用某種特定的幾何圖形按一定規律在平面(二維方向上)分布的黑白相間的圖形記錄數據符號信息的;在代碼編制上巧妙地利用構成計算機內部邏輯基礎的“0”、“1”比特流的概念,使用若干個與二進制相對應的幾何形體來表示文字數值信息,通過圖象輸入設備或光電掃描設備自動識讀以實現信息自動處理:它具有條碼技術的一些共性:每種碼制有其特定的字符集;每個字符占有一定的寬度;具有一定的校驗功能等。同時還具有對不同行的信息自動識別功能、及處理圖形旋轉變化點。
Zxing是一個開源Java類庫用于解析多種格式的1D/2D條形碼,能夠對QR編碼、Data Matrix、UPC的1D條形碼進行解碼。其提供了多種平臺下的客戶端包括:J2ME、J2SE和Android等。目前在android 手機上應用廣泛。
Zbar用C/C++實現,Zbar 可以直接掃碼二維碼和條碼,在iphone上應用廣泛。
文本主要探討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自己生成二維碼圖片,可以考慮縮短二維碼內容長度,避免生成較復雜的二維碼,導致不好解析。
demo 見:https://github.com/jinwong001/QRScanner