写在开头
需求方:上传试卷的时候,用户自己拍的照片有很多问题。如:不清晰、图片歪了、错误图片等。我们要是能够对拍摄照片进行识别处理就好了,能够裁切矫正就更好了,最好可以像二维码扫描一样,直接识别处理~
开发:满足你!
整体框架逻辑
试卷扫描模块,最核心的逻辑就是数据采集、解码识别、图片裁切,再加上对识别结果和裁切结果的处理,就构成了整个模块的主逻辑。整个逻辑的实现如下图所示:
在模块中,除了UI线程,还开启了一个Deocde线程,用来处理图片的解码识别和裁切。这么做的原因是因为对于图片数据的处理,是比较耗时的,如果在UI线程处理,会有ANR的风险。同时采用这种处理方式,整个模块的流畅性也更加好,且模块的结构更加清晰。
那么线程之间是如何交互的呢?这里模块中是采用了最常用的Handler消息传递机制。因为通过Handler的Message可以在线程间传递较大的图片数据(注意如果在Intent的Bundle中传递较大的数据,会崩溃报错)。请看下面这段代码:
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity);
handlerInitLatch.countDown();
Looper.loop();
}
上面这个方法是DecodeThread的run方法,在方法中,我们初始化了当前线程对应的Handler对象DecodeHandler。而DecodeHandler初始化是需要传入当前主线程的上下文activity,通过activity我们可以拿到主线程的Handler对象。这样的话主线程和解码线程就建立了联系,它们之间就可以方便得进行消息传递了。最终实现的模块采集界面如下所示:
模块开发相关实现
整个扫码拍照模块的逻辑比较琐碎,就不一一说明了。以下是整理的几个开发中比较关键的点和Camera硬件开发一些经验,在这里做记录,避免以后重复造轮子。
闪光灯设置
- 开启闪光灯
public void turnOnFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 关闭闪光灯
public void turnOffFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
预览图片分辨率选择
预览图片的分辨率选择逻辑是:有1920*1080则选之,否则选硬件支持的最大的分辨率,且满足图片比例为16:9
private static Point findBestPreviewSizeValue(List<Camera.Size> sizeList, Point screenResolution) {
int bestX = 0;
int bestY = 0;
int size = 0;
for(int i = 0; i < sizeList.size(); i ++){
// 如果有符合的分辨率,则直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default preview size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
int newX = sizeList.get(i).width;
int newY = sizeList.get(i).height;
int newSize = Math.abs(newX * newX) + Math.abs(newY * newY);
float ratio = (float)newY / (float)newX;
Log.d(TAG, newX + ":" + newY + ":" + ratio);
if (newSize >= size && ratio != 0.75) { // 确保图片是16:9的
bestX = newX;
bestY = newY;
size = newSize;
} else if (newSize < size) {
continue;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
拍照图片分辨率选择
在硬件支持的拍照图片分辨率列表中,拍照图片分辨率选择逻辑:
- 有1920*1080则选之
- 选择大于屏幕分辨率且图片比例为16:9的
- 选择图片分辨率尽可能大且图片比例为16:9的
private static Point findBestPictureSizeValue(List<Camera.Size> sizeList, Point screenResolution){
List<Camera.Size> tempList = new ArrayList<>();
for(int i = 0; i < sizeList.size(); i ++){
// 如果有符合的分辨率,则直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default picture size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
if(sizeList.get(i).width >= screenResolution.x && sizeList.get(i).height >= screenResolution.y){
tempList.add(sizeList.get(i));
}
}
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
if(tempList != null && tempList.size() > 0){
for(int i = 0; i < tempList.size(); i ++){
int newDiff = Math.abs(tempList.get(i).width - screenResolution.x) + Math.abs(tempList.get(i).height - screenResolution.y);
float ratio = (float)tempList.get(i).height / tempList.get(i).width;
Log.d(TAG, "ratio = " + ratio);
if(newDiff < diff && ratio != 0.75){ // 确保图片是16:9的
bestX = tempList.get(i).width;
bestY = tempList.get(i).height;
diff = newDiff;
}
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}else {
return findMaxPictureSizeValue(sizeList);
}
}
预览模式循环自动对焦
预览模式时,支持自动对焦。当前处理逻辑是在AutoFocusCallback的回调方法onAutoFocus中,延迟发送Message信息。这样在上一次聚焦完成后,固定时间的延迟后会发送下一次的自动聚焦消息,如此达到循环聚焦的目的。
@Override
public void onAutoFocus(boolean success, Camera camera) {
Log.d(TAG, "onAutoFocus");
PaperScanConstant.isAutoFocusSuccess = true;
if (autoFocusHandler != null) {
Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
autoFocusHandler = null;
} else {
Log.d(TAG, "Got auto-focus callback, but no handler for it");
}
}
预览画面不失真展示
如果预览图片的分辨率比例和手机画面上展示拍摄画面的区域比例不一致的话,就会出现画面拉伸或者压缩的现象。为了解决这个问题,取得更好的用户体验。模块在布局的时候,对屏幕展示区域是动态计算的,以保证预览区域比例与图片的分辨率比例是一致的。
模块开发中的那些坑
扫码模块开发,因为是跟手机硬件Camera打交道,基于目前市场中Android手机众多的型号和搭载的五花八门的ROM,没坑那是不可能的!!!下面是本模块开发过程中的相关坑。
部分机子拍摄照片分辨率不高
开发过程中碰到过这么一种情况,在部分机子上,明明已经聚焦,手机的分辨率也很高,但是拍出的照片分辨率却很小。究其原因,就是不同的手机ROM,获取的默认的照片分辨率是不同的。有的手机默认照片分辨率高,则照片就清晰;有的默认分辨率是最低的一档,则无论你手机分辨率多高,拍出来的照片还是很模糊的。解决方案就是需要显示设置拍照的图片分辨率:
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
部分机子拍摄照片发生了旋转
还是由于Android手机碎片化的问题,每个手机默认拍照的旋转角度是不一样的。刚开始模块中是按照默认旋转90度处理,在大多数机子上是没有问题的。但是在碰到Nexus 5X的时候就出问题了,图片上下导致了。查阅了相关资料,Google官方提供了下面的方法,解决了这个问题。
public void setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = BaseApplication.getInstance().getCurrentActivity().getWindowManager().getDefaultDisplay()
.getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
int result;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
// 记录本机子相机的旋转角度
PaperScanConstant.cameraRotation = result;
camera.setDisplayOrientation(result);
}
private int findFrontFacingCameraID() {
int cameraId = -1;
// Search for the back facing camera
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
Log.d(TAG, "Camera found");
cameraId = i;
break;
}
}
return cameraId;
}
频繁点击屏幕应用崩溃
因为应用支持点击屏幕自动聚焦功能,但在某些机子上,用户频繁点击屏幕进行自动聚焦,应用发生了崩溃。究其原因是因为在某些ROM上,当上一次聚焦没有完成时,就进行下一次聚焦,就会发生崩溃。解决方案是通过设置标志位,只有在上一次聚焦完成后,才能进行下一次聚焦。
第三发ROM禁止了应用的摄像头权限
有些第三方ROM会有自己的权限管理机制,当应用的摄像头权限被禁止了,进入扫码页,会发生崩溃。这样的交互体验肯定不是很好,交互要求这边权限被禁止以后,还是需要有一个温和的提示,提醒用户去设置页面重新赋予应用摄像头权限。但是系统也没有提供接口说当前应用这个权限被禁止了。因此模块中采用了一个折中的方案,监狱应用没有摄像头权限时候,开启摄像头会崩溃。因此我们捕获开启Camera的异常,在捕获异常时候弹框提醒用户去开启权限。
try {
CameraManager.get().openDriver(surfaceHolder);
} catch (Throwable tr){
showOpenCameraErrorDialog();
return;
}
Pad进入扫码页应用崩溃
实际上线时候,发现用户使用pad的话,一进入扫码页面就崩溃。因为我们应用首次进入扫码页面默认是开启设备闪光灯的。但是pad没有闪光灯,因此就崩溃了。刚开始用如下方式检测设备是否支持闪光灯:
getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
但是失败了。原因是好多pad的ROM是从手机ROM改过去的,有可能改得不是那么彻底。所以在Pad上调用如上代码进行判断时,还是会返回true。这是只能求助于try catch了。就是在开关闪光灯的时候进行异常捕获,这样在Pad上开关闪光灯崩溃问题就解决了。
部分机子拍照后闪光灯自动关闭
部分机子,在闪光灯开启的状态下,点击拍照按钮,闪光灯关闭了。目前没有找到原因,只能在模块中加了特殊处理。针对当前有此问题的手机,拍照完后主动再去开关一次闪光灯,这样拍照完成后,闪光灯还是可以亮着。只是在拍照的过程中,会出现闪光灯闪烁的情况。
部分机子拍照完后预览画面卡住了
部分机子,当点击拍照完成一张照片的拍摄后,后面就停止不动了。出现这种现象是因为在拍照的时候,Camera会停止Preview,拍照完成后,有的机子可以恢复回来重新Preview,有的则不会。因此只需在拍照完成后,手动调用一次Camera的startPreview()方法即可。
结束语
最后,大家想看代码的话,可以看下我封装的二维码扫描库,实现原理是一样的。可以看我这篇文章:一款好用的二维码扫描组件
二维码扫描库QrScan的GitHub:https://github.com/yushiwo/QrScan