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的使用比较简单,添加依赖、拷贝java逻辑代码与资源文件至项目中即可。
compile 'com.google.zxing:core:3.2.1'
- 将
android
中的逻辑包com.google.zxing
拷贝至本地项目中。- 将
android-core
中的CameraConfigurationUtils.java
添加至项目中。- 将
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的创建与销毁的事件
作者:Holmofy
原文:https://blog.csdn.net/Holmofy/article/details/66578852
相机预览与用户界面完成后,当然就是帧数据捕获。
那么如何捕获相机的帧数据呢?
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,以便可以根据需求进行改动。
在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#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(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
接受到消息后,会怎么处理呢?
@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
得到解析数据后又将进行如何处理:
首先,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;
}
至于相机的参数设置、扫码的音效及震动提示、用户偏好等,读者可自己分析,本文就不再进行详细分析了。
另外,需要注意的是相机有相机的自己的分辨率,通常指的是它的「像素规模」,即它能拍出含有多少个像素的照片,屏幕也有屏幕本身的分辨率。
Android zxing
zxing/zxing
zxing私人定制之一
Android Zxing 转换竖屏扫描且提高识别率
Android相机开发系列