项目中需要核验身份信息,所以模拟支付宝的身份证 OCR 界面,做一个类似的功能.但是又有不同的地方,我们需要拍下照片而不是不断的扫描获取图像.
由于项目最低支持 4.0 系统为了方便起见使用 Camera 而不是 Camera2 接口,因为 Camera2 是 5.0 以后加入的 api.
请求相机权限
打开相机需要<uses-permission android:name="android.permission.CAMERA" />
权限,并且相机权限也是运行时权限,需要在打开相机前动态请求.
请求存储权限
因为 sample 中使用了 cache 目录,所以不需要请求存储权限,如果使用外部存储,则需要请求运行时权限.
调用相机接口
- 创建预览界面,使用 SurfaceView 绘制实时预览图像,现在推荐使用 TextureView,关于两者的特性和区别不再赘述,文末有参考文章,
由于相机镜头是横向的,所以需要设置相机预览为竖屏camera.setDisplayOrientation(90);
,在拍照页面退出时也需要及时释放相机资源. - 调整预览尺寸、调整图片尺寸 这个是拍照关键
我们所要实现的功能是全屏预览,相机预览尺寸是从它所支持的规格里面选择,而不是我们自身决定,而不同手机又有不同屏幕的比例,所以我们需要在手机预览尺寸匹配到一个最相近的尺寸,设置为相机的预览尺寸.
匹配算法:通过这个算法我们就可以匹配到相近的相机预览尺寸,但是还必须匹配拍照尺寸,因为预览尺寸和照片尺寸是两个不同的逻辑,如果只匹配到预览尺寸,有的手机会匹配到一个过小的照片尺寸,这样就无法看清图片信息.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
private Point machBestSize(Camera.Parameters parameters, Point screenResolution, List<Camera.Size> supportedSizes) {
//降序排列
List<Camera.Size> supportedPreviewSizes = new ArrayList<Camera.Size>(supportedSizes);
Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() {
public int compare(Camera.Size a, Camera.Size b) {
int aPixels = a.height * a.width;
int bPixels = b.height * b.width;
if (bPixels < aPixels) {
return -1;
}
if (bPixels > aPixels) {
return 1;
}
return 0;
}
});
//打印镜头支持的预览尺寸
if (Log.isLoggable(TAG, Log.INFO)) {
StringBuilder previewSizesString = new StringBuilder();
for (Camera.Size supportedPreviewSize : supportedPreviewSizes) {
previewSizesString.append(supportedPreviewSize.width).append('x').append(supportedPreviewSize.height).append(' ');
}
Log.i(TAG, "Supported preview sizes: " + previewSizesString);
}
double screenAspectRatio = (double) screenResolution.x / (double) screenResolution.y;
Iterator<Camera.Size> it = supportedPreviewSizes.iterator();
while (it.hasNext()) {
Camera.Size supportedPreviewSize = it.next();
int realWidth = supportedPreviewSize.width;
int realHeight = supportedPreviewSize.height;
//MIN_PREVIEW_PIXELS = 480 * 320 移除过小的尺寸
if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {
it.remove();
continue;
}
boolean isCandidatePortrait = realWidth < realHeight;
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight;
double distortion = Math.abs(aspectRatio - screenAspectRatio);
// MAX_ASPECT_DISTORTION = 1.5 异常纵横比偏差过大的尺寸
if (distortion > MAX_ASPECT_DISTORTION) {
it.remove();
continue;
}
if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
Point exactPoint = new Point(realWidth, realHeight);
Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
return exactPoint;
}
}
//在没有匹配到最优结果情况下的兜底策略
if (!supportedPreviewSizes.isEmpty()) {
Camera.Size largestPreview = supportedPreviewSizes.get(0);
Point largestSize = new Point(largestPreview.width, largestPreview.height);
Log.i(TAG, "Using largest suitable preview size: " + largestSize);
return largestSize;
}
//在没有获取预览尺寸列表下的兜底策略
Camera.Size defaultPreview = parameters.getPictureSize();
Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
return defaultSize;
}这样就可以和手机自带的相机全屏模式下预览效果一样了1
2
3
4Point pictureSize = findBestPictureSizeValue(parameters, cameraResolution);
//设置拍照尺寸
parameters.setPictureSize(pictureSize.x, pictureSize.y);
camera.setParameters(parameters);
拍照、剪裁、压缩、保存
为了方便OCR,所以我们将图片进行剪裁至身份证大小,对于过大的尺寸压缩至复合我们要求,保存成文件方便上传至服务器.
1 | //拍照 |
总结
有的手机相机存在预览时画面稍微变形,在相机里用全屏预览时也发现有同样的问题,应该是预览比例和屏幕比例有差别导致的.如果想要预览效果完美匹配,则需要改变预览尺寸而不是固定为屏幕尺寸大小.
截图
拍摄国徽页效果图(sample 效果) 如果为人像页则有人像框 国徽页为正面 人像页为反面
更新兼容性方案
经过线上验证存在匹配照片尺寸小于身份证尺寸的问题,导致拍出的图片太小模糊问题.
所以启用备用方案,点击拍照时直接通过setOneShotPreviewCallback
实时预览回调获取图片数组,然后将数组转化为 Bitmap 供调用方使用.
这里需要注意的是 onPreviewFrame
里返回的数组是YUV420SP 格式
,不能直接通过BitmapFactory
生成图片,需要用YuvImage
进行转化才可以使用.
1 | private fun generateBitmap(camera: Camera, data: ByteArray): Bitmap? { |
其他逻辑都不用改变,这种方法通过界面上的所见即所得直接获取图片,以减少适配问题.
项目地址
参考文章
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView