Android中使用Zxing集成、分析与优化

文章目录

    • `Zxing`使用
    • 官方Demo使用
      • 使用步骤
      • 工作流程分析
        • 相机画面预览
      • 帧数据捕获
      • 帧数据的处理
    • 官方Demo分析
      • CaptureActivity
      • CameraManager
      • CaptureActivityHandler
      • DecodeHandler
      • CaptureActivityHandler
      • 提高识别
      • 识别率优化
    • 参考

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中的逻辑包com.google.zxing拷贝至本地项目中。
  • android-core中的CameraConfigurationUtils.java添加至项目中。
  • 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的创建与销毁的事件

作者:Holmofy
原文:https://blog.csdn.net/Holmofy/article/details/66578852

帧数据捕获

相机预览与用户界面完成后,当然就是帧数据捕获。

那么如何捕获相机的帧数据呢?
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;
}

至于相机的参数设置、扫码的音效及震动提示、用户偏好等,读者可自己分析,本文就不再进行详细分析了。
另外,需要注意的是相机有相机的自己的分辨率,通常指的是它的「像素规模」,即它能拍出含有多少个像素的照片,屏幕也有屏幕本身的分辨率。

参考

Android zxing
zxing/zxing
zxing私人定制之一
Android Zxing 转换竖屏扫描且提高识别率
Android相机开发系列

你可能感兴趣的:(Android,Android基础)