Android中使用Zxing整合、分析與優化
Zxing
使用
zxing
是Google
推出的用於識別QRCode、ISBN等圖形碼的解決方案。本文主要介紹Android
移動端對Zxing
的使用,以及官方demo
的整合。
新增依賴:
compile 'com.google.Zxing:core:3.2.1'
利用
core
包中提供的API對相機的預覽幀資料data
進行解析:PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height); BinaryBitmap bitmap = new BinaryBitmap
(new HybridBinarizer(source)) MultiFormatReader multiFormatReader = new MultiFormatReader(); Result rawResult = multiFormatReader.decodeWithState(bitmap); //獲取結果:rawResult.getText() || rawResult.getRawBytes()為了獲取相機的預覽幀資料,通常需要藉助
android.hardware.Camera
等中的API自定義一個相機,這個工作需要對Camera
的相關知識具有較好理解(這裡推薦一個連載篇Android相機開發系列)。所以,大部分人會選擇將官方的Demo--zxing
直接拷貝至專案內,然後進行適當需改即可。
官方Demo使用
官方Demo的使用比較簡單,新增依賴、拷貝java邏輯程式碼與資原始檔至專案中即可。
使用步驟
- 新增依賴:
compile 'com.google.zxing:core:3.2.1'
- 程式碼與資源拷貝
- 將
android
中的res
目錄中的資原始檔新增至專案當中(為了避免檔案覆蓋,可對檔案進行內容拷貝而非直接的檔案拷貝,同時有些檔案按需新增即可,例如:valuse
及其valuse-
系類)。
Activit
呼叫與掃描結果的獲取
- 啟動CauptureActivity進行掃描
public static final int REQUEST_SCAN_QRCODE = 0X11; private void scanQRCode() { Intent scanIntent = new Intent(this, CaptureActivity.class); scanIntent.setAction(Intents.Scan.ACTION); // scanIntent.putExtra(Intents.Scan.WIDTH, 99999); // scanIntent.putExtra(Intents.Scan.HEIGHT, 99999); startActivityForResult(scanIntent, REQUEST_SCAN_QRCODE); }
- 在
onActivityResult
獲取掃描結果case REQUEST_SCAN_QRCODE: if (resultCode == RESULT_OK){ Bundle bundle = data.getExtras(); String resultStr = bundle.getString(Intents.Scan.RESULT); if (resultStr != null){ Toast.makeText(this, resultStr, Toast.LENGTH_SHORT).show(); } } break;
最後需要在
Manifest.xml
中新增許可權等。
現在二維碼的掃描功能就基本完成了,但是依然會存在部分問題。例如:識別率敏感度不夠,掃描介面不滿足需求等。若要進行修改,還是要對官方Demo進行分析。下面分析官方Demo的工作流程。
工作流程分析
如果不關心工作流程可以直接飛至提高識別 機票~~~~
首先不考慮相機具體屬性的引數設定。整個掃描過程分為三步:相機畫面預覽→捕獲相機預覽幀資料→處理並返回幀資料
相機畫面預覽
自定義相機通常使用SurfaceView
對相機Camera
捕獲的資料進行展示。
回想一下,在自定義相機時,如何將
SurfaceView
與Camera
關聯呢?
- 獲取
SurfaceView
例項的SurfaceHolder
物件mSurfaceHolder = getHolder();
- 向
SurfaceHolder
中添加回調SurfaceHolder.Callback
mSurfaceHolder.addCallback(this);
- 在回撥的
SurfaceHolder.Callback#surfaceCreated
方法中通過Camera#setPreviewDisplay
指定相機Camera
的資料處理物件。mCamera.setPreviewDisplay(holder);
- 最後呼叫相機進行預覽
mCamera.startPreview();
為了便於理解,如下是對
SurfaceView
、Surface
和SurfaceHolder
知識的補充:
SurfaceView嵌入到Window的View結構樹中就好像在Window的Surface上強行打了個洞讓自己顯示到螢幕上,而且SurfaceView另起一個執行緒對自己的Surface進行重新整理。特別需要注意的是SurfaceHolder.Callback的所有回撥方法都是在主執行緒中回撥的
- SurfaceView是擁有獨立繪圖層的特殊View
- Surface就是指SurfaceView所擁有的那個繪圖層,其實它就是記憶體中的一段繪圖緩衝區。
- SurfaceView中具有兩個Surface,也就是我們所說的雙緩衝機制
- SurfaceHolder顧名思義就是Surface的持有者,SurfaceView就是通過過SurfaceHolder來對Surface進行管理控制的。並且SurfaceView.getHolder方法可以獲取SurfaceView相應的SurfaceHolder。
- Surface是在SurfaceView所在的Window可見的時候建立的。我們可以使用SurfaceHolder.addCallback方法來監聽Surface的建立與銷燬的事件
幀資料捕獲
相機預覽與使用者介面完成後,當然就是幀資料捕獲。
那麼如何捕獲相機的幀資料呢?
Camera
提供了setPreviewCallback
等方法,當傳入一個Camera.PreviewCallback
例項後,當Camera
產生幀時,Camera.PreviewCallback#onPreviewFrame(byte[] data, Camera camera)
的方法將被呼叫。這裡的data
就是我們需要的資料
幀資料的處理
通過zxing
的core
包提供的API便可以對上一步中的data
進行解析。
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)) MultiFormatReader multiFormatReader = new MultiFormatReader(); Result rawResult = multiFormatReader.decodeWithState(bitmap); //獲取結果:rawResult.getText() || rawResult.getRawBytes()
處理幀資料可能是一個比較耗時的過程,onPreviewFrame
在執行Camera.open()
時所在的執行緒執行。所以,可以選擇在新執行緒中開啟Camera.opne
或將data
放入新執行緒中解析,避免阻塞UI執行緒。
官方Demo分析
知道了大致的掃描流程,現在開始分析官方Demo,以便可以根據需求進行改動。
CaptureActivity
在CaptureActivity
中有兩個比較重要的View
:SurfaceView
和ViewfinderView
SurfaceView
:負責顯示Camera
捕獲到的內容;
ViewfinderView
:顯示在介面上新增的動畫或其他UI元素,例如:掃描線條、中間內容框等。可根據需要對其中的檢視進行修改;
CaptureActivity
啟動後在onResume
中執行
surfaceHolder.addCallback(this);
然後,SurfaceHolder.Callback#surfaceCreated
中執行initCamera
方法:
private void initCamera(SurfaceHolder surfaceHolder){ ...... //從CameraConfigurationManager中讀取配置資料,啟動相機掃描 cameraManager.openDriver(surfaceHolder); // Creating the handler starts the preview, which can also throw a RuntimeException. if (handler == null) { handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager); } ...... }
CameraManager
進入CameraManager#openDriver
方法,以下是CameraManager#openDriver
的部分程式碼:
//啟動相機 theCamera = OpenCameraInterface.open(requestedCameraId); //初始化相機引數 configManager.initFromCameraParameters(theCamera); //設定相機配置引數 configManager.setDesiredCameraParameters(theCamera, false); //Camera#setPreviewDisplay將SurfaceView與Camera關聯 //SurfaceHolder surfaceHolder = surfaceView.getHolder(); cameraObject.setPreviewDisplay(holder);
CaptureActivityHandler
再分析CaptureActivityHandler
:
CaptureActivityHandler(CaptureActivity activity, Collection<BarcodeFormat> decodeFormats, Map<DecodeHintType,?> baseHints, String characterSet, CameraManager cameraManager) { this.activity = activity; decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet, new ViewfinderResultPointCallback(activity.getViewfinderView())); decodeThread.start(); state = State.SUCCESS; // Start ourselves capturing previews and decoding. this.cameraManager = cameraManager; cameraManager.startPreview(); restartPreviewAndDecode(); }
在建立CaptureActivityHandler
物件時,建立並啟動了新執行緒decodeThread
(前面我們講過解析資料的過程是比較耗時的,上述邏輯表明相機的啟動其實時在主執行緒中完成的,所以這裡使用新執行緒用來處理幀資料,後面會進一步解讀到這點),然後restartPreviewAndDecode()
,至此已經完成了掃描的第一步-----相機畫面預覽,
繼續分析restartPreviewAndDecode()
:
//:CaptureActivityHandler.java private void restartPreviewAndDecode() { if (state == State.SUCCESS) { state = State.PREVIEW; cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode); activity.drawViewfinder(); } }
//:CameraManager.java private final PreviewCallback previewCallback; public synchronized void requestPreviewFrame(Handler handler, int >message) { OpenCamera theCamera = camera; if (theCamera != null && previewing) { previewCallback.setHandler(handler, message); //設定Camera.PreviewCallback回撥 theCamera.getCamera().setOneShotPreviewCallback(previewCallback); } }
setOneShotPreviewCallback
被呼叫,所以相機的幀資料將在previewCallback
例項中被處理,繼續看previewCallback
://:PreviewCallback.java void setHandler(Handler previewHandler, int previewMessage) { this.previewHandler = previewHandler; this.previewMessage = previewMessage; } @Override public void onPreviewFrame(byte[] data, Camera camera) { Point cameraResolution = configManager.getCameraResolution(); Handler thePreviewHandler = previewHandler; if (cameraResolution != null && thePreviewHandler != null) { Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x, cameraResolution.y, data); message.sendToTarget(); previewHandler = null; } else { Log.d(TAG, "Got preview callback, but no handler or resolution available"); } }
這裡的
thePreviewHandler
就是DecodeHandler
,而previewMessage
即為R.id.decode
;
DecodeHandler
當DecodeHandler
接受到訊息後,會怎麼處理呢?
@Override
public void handleMessage(Message message) {
if (message == null || !running) {
return;
}
switch (message.what) {
case R.id.decode:
Log.d(TAG, "handleMessage: ");
decode((byte[]) message.obj, message.arg1, message.arg2);
break;
case R.id.quit:
running = false;
Looper.myLooper().quit();
break;
}
}
再看DecodeHandler#decode
可以看出,實際就是使用core
包中提供的API解析資料:
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source))
MultiFormatReader multiFormatReader = new MultiFormatReader();
Result rawResult = multiFormatReader.decodeWithState(bitmap);
並將解析結果交給主執行緒:
//獲取主執行緒的CaptureActivity.java中的handler
Handler handler = activity.getHandler();
if (handler != null) {
Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
Bundle bundle = new Bundle();
bundleThumbnail(source, bundle);
message.setData(bundle);
message.sendToTarget();
}
CaptureActivityHandler
接著分析CaptureActivityHandler
得到解析資料後又將進行如何處理:
首先,handleDecode(Result rawResult, Bitmap barcode, float scaleFactor)
被呼叫,在handleDecode
中,根據source
()不同執行handleDecodeExternally
或handleDecodeInternally
,這隻分析handleDecodeExternally
。
在handlerDecodeExternally
中又將呼叫sendReplyMessage(R.id.return_scan_result, intent, resultDurationMS);
//:CaptureActivity.java
private void sendReplyMessage(int id, Object arg, long delayMS) {
Log.d(TAG, "sendReplyMessage: ");
if (handler != null) {
Message message = Message.obtain(handler, id, arg);
if (delayMS > 0L) {
handler.sendMessageDelayed(message, delayMS);
} else {
handler.sendMessage(message);
}
}
}
//:CaptureActivityHandler.java
case R.id.return_scan_result:
activity.setResult(Activity.RESULT_OK, (Intent) message.obj);
activity.finish();
break;
至此,CaptureActivityHandler.java
關閉,並將結果返回。
提高識別
Zxing
預設的是橫屏掃碼,多數情況下需要改為豎屏掃描。
CaptureActivity
的配置,將Activity
豎屏顯示:
android:screenOrientation="portrait"
CameraManager
類中的getFramingRectInPreview()
方法,將left
,right
,top
,bottom
改變,供第4步的buildLuminanceSource
內部計算使用。
//豎屏
rect.left = rect.left * cameraResolution.y / screenResolution.x;
rect.right = rect.right * cameraResolution.y / screenResolution.x;
rect.top = rect.top * cameraResolution.x / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
CameraConfigurationManager
類中的setDesiredCameraParameters(OpenCamera camera, boolean safeMode)
方法,在setParameters
之前新增,設定PreviewDisplay
的方向,使SurfaceView
畫面方向為豎直方向。
theCamera.setDisplayOrientation(90);
DecodeHandler
類中的decode(byte[] data, int width, int height)
方法,在PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height)
之前新增,將相機資料矩陣旋轉90度。
byte[] rotatedData = new byte[data.length];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++)
rotatedData[x * height + height - y - 1] = data[x + y * width];
}
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(rotatedData, height, width);
//PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
此時,豎屏掃描已經可以實現了,但是掃描複雜的圖碼時,解析度低的已經分不清紋理了,很難識別出來,所以需要優化識別率。
識別率優化
CameraConfigurationUtils
類中的findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution)
方法,將double screenAspectRatio = screenResolution.x / (double) screenResolution.y
改為:
double screenAspectRatio;
if (screenResolution.x > screenResolution.y) {
screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y;
} else {
screenAspectRatio = (double) screenResolution.y / (double) screenResolution.x;
}
至於相機的引數設定、掃碼的音效及震動提示、使用者偏好等,讀者可自己分析,本文就不再進行詳細分析了。
另外,需要注意的是相機有相機的自己的解析度,通常指的是它的「畫素規模」,即它能拍出含有多少個畫素的照片,螢幕也有螢幕本身的解析度。