1. 程式人生 > >Android中使用Zxing整合、分析與優化

Android中使用Zxing整合、分析與優化

Zxing使用

zxingGoogle推出的用於識別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邏輯程式碼與資原始檔至專案中即可。

使用步驟

  1. 新增依賴:
compile 'com.google.zxing:core:3.2.1'
  1. 程式碼與資源拷貝
  • android中的res目錄中的資原始檔新增至專案當中(為了避免檔案覆蓋,可對檔案進行內容拷貝而非直接的檔案拷貝,同時有些檔案按需新增即可,例如:valuse及其valuse-系類)。
  1. 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捕獲的資料進行展示。

回想一下,在自定義相機時,如何將SurfaceViewCamera關聯呢?

  1. 獲取SurfaceView例項的SurfaceHolder物件
		mSurfaceHolder = getHolder();
  1. SurfaceHolder中添加回調SurfaceHolder.Callback
		mSurfaceHolder.addCallback(this);
  1. 在回撥的SurfaceHolder.Callback#surfaceCreated方法中通過Camera#setPreviewDisplay指定相機Camera的資料處理物件。
			mCamera.setPreviewDisplay(holder);
  1. 最後呼叫相機進行預覽
			mCamera.startPreview();

為了便於理解,如下是對SurfaceViewSurfaceSurfaceHolder知識的補充:
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就是我們需要的資料

幀資料的處理

通過zxingcore包提供的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中有兩個比較重要的ViewSurfaceViewViewfinderView

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()不同執行handleDecodeExternallyhandleDecodeInternally,這隻分析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預設的是橫屏掃碼,多數情況下需要改為豎屏掃描。

  1. CaptureActivity的配置,將Activity豎屏顯示:
android:screenOrientation="portrait"
  1. CameraManager類中的getFramingRectInPreview()方法,將leftrighttopbottom改變,供第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;
  1. CameraConfigurationManager類中的setDesiredCameraParameters(OpenCamera camera, boolean safeMode)方法,在setParameters之前新增,設定PreviewDisplay的方向,使SurfaceView畫面方向為豎直方向。
theCamera.setDisplayOrientation(90);
  1. 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;
}

至於相機的引數設定、掃碼的音效及震動提示、使用者偏好等,讀者可自己分析,本文就不再進行詳細分析了。
另外,需要注意的是相機有相機的自己的解析度,通常指的是它的「畫素規模」,即它能拍出含有多少個畫素的照片,螢幕也有螢幕本身的解析度。

參考