CV深度学习模型Android端落地第一步:使用Android Camera2接口获得实时预览图像

这个系列的博客主要介绍如何在Android设备上移植你训练的cv神经网络模型。

主要过程如下: 1、使用Android Camera2 APIs获得摄像头实时预览的画面。 2、如果是对人脸图像进行处理,使用Android Camera2自带的Face类来对人脸检测,并完成在预览画面上画框将人脸框出、添加文字的功能。
3、使用Tensorflow Lite 将自己的训练得到的模型移植到Android上。

以上三个步骤会分为三个博客,同时也会提供示例代码。步骤二可以根据你的实际需求跳过或修改。

搭建Android Studio开发环境

在Win10或Ubuntu平台上搭建Android Stuido开发环境过程都是一样的。

1、安装JDK1.8并配置环境变量 输入java --version提示版本则成功
2、下载AndroidSDK并解压,Win10下SDK目录下启动SDK Manager.exe,Ubuntu下在tool目录下执行可执行文件android可以打开SDK manager。可以在这里面配置sdk版本、NDK版本、Cmake、LLDB等,一般装好SKD就可以了,其他的Tools按需安装即可。
3、前往官网下载安装Android Studio,在开始使用前要配置一下JDK和SDK的路径。

需要注意的是,在Ubuntu下,最好前往/opt/android/bin目录(替换成你的AS安装目录)下,执行sudo sh studio.sh,以root权限打开Android studio,这样一来会避免很多莫名其妙的问题出现。

以下是我的项目的配置情况
- Android Studio版本:

1
2
3
4
5
Android Studio 3.2.1
Build #AI-181.5540.7.32.5056338, built on October 9, 2018
JRE: 1.8.0_152-release-1136-b06 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
- JDK版本:
1
2
3
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
- 测试机型号
1
HUAWEI MATE 9(4G+64G)
- SDK版本 SDK版本一般是根据你的测试机型配置
1
2
3
Android 8.1(Oreo) 
API Level:27
Revision3
- NDK版本
1
18.1506

根据CameraDemo修改代码获得实时预览图像

项目准备

android-Camera2Basic

git clone或者下载代码解压到工作文件夹下,用AS打开,下载Gradle,Syc项目(Syc按钮在Run按钮边上),版本和插件版本是有对应关系的。

Plugin version Required Gradle version
1.0.0 - 1.1.3 2.2.1 - 2.3
1.2.0 - 1.3.1 2.2.1 - 2.9
1.5.0 2.2.1 - 2.13
2.0.0 - 2.1.2 2.10 - 2.13
2.1.3 - 2.2.3 2.14.1+
2.3.0+ 3.3+
3.0.0+ 4.1+
3.1.0+ 4.4+
3.2.0+ 4.6+

appliction的build.gradle下面写的是gradle插件版本

1
2
3
4
5
buildscript{
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}
gradle-wrapper.properties下记录gradel版本
1
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
Syc时使用android-Camera2Basic默认配置如若不行,可以按照AS提示更新到最新版本。

代码修改

完成Syc(同步)之后 ,将手机连接上,使用debug run模式将代码部署到手机上,出现的效果应该是带前置预览画面和执行聚焦的一个button。

切换摄像头(optional)

修改openCamera方法,使用前置摄像头。

原openCamera方法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Opens the camera specified by {@link Camera2BasicFragment#mCameraId}.
*/
private void openCamera(int width, int height) {
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
return;
}
setUpCameraOutputs(width, height);
configureTransform(width, height);
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
修改后的openCamera方法
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
private void openCamera(int width, int height, int isSwitch) {
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestCameraPermission();
return;
}
setUpCameraOutputs(width, height);
configureTransform(width, height);
Activity activity = getActivity();
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
// isSwitch==0 使用后置摄像头 isSwitch==1 使用前置摄像头
if(isSwitch==1){
mCameraId = "1";
}
Log.d(TAG, "openCamera is : "+mCameraId);
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
修改onSurfaceTextureAvailable方法和onResume方法选择以前置摄像头启动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
openCamera(width, height, 1);
}

@Override
public void onResume() {
super.onResume();
startBackgroundThread();

if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight(), 1);
} else {
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
### 注释掉按钮 获得真数据流的实时画面

在Android视图下编辑 Application/res/layoutxml文件 设置button 和** image button**属性 android:visibility="invisible"

Android Camera2通过建立会话(session)机制来协调程序调用相机,CameraCaptureSession.CaptureCallbac 是一个抽象类,通过对这个抽象类的实现,我们可以处理 CapterSession 的回调内容。要获得每一帧的实时画面,应该在CameraCaptureSession.CaptureCallback中写相关代码。在这个Demo中,实时预览画面是展示在一个基于Android TextureSurface Vie 控件的,我们通过调用Texture控件的getBitmap方法,就能得到当前Texture上的实时画面,想通过Camera2的API获得实时的帧数据比较麻烦,建议使用这样的方法,延迟也比较小。

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
private int frameCount = 0;

private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback() {

private void process(CaptureResult result) {
switch (mState) {
case STATE_PREVIEW: {
// ************************** 重点部分 *************************************
// 打印出当前帧
Log.d(TAG, "frameCount is " + frameCount);
frameCount = frameCount + 1;
// 预览模式下的情况 在这里获得预览图像用于输入你的神经网络中
// bitmap_get就是我们通过getBitmap方法得到的实时画面
Bitmap bitmap_get= mTextureView.getBitmap();
// *************************************************************************
break;
}
case STATE_WAITING_LOCK: {
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
captureStillPicture();
} else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
// CONTROL_AE_STATE can be null on some devices
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
} else {
runPrecaptureSequence();
}
}
break;
}
case STATE_WAITING_PRECAPTURE: {
// CONTROL_AE_STATE can be null on some devices
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
mState = STATE_WAITING_NON_PRECAPTURE;
}
break;
}
case STATE_WAITING_NON_PRECAPTURE: {
// CONTROL_AE_STATE can be null on some devices
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
}
break;
}
}
}

至此,我们就能够完成实时切换摄像头并获得了实时预览画面,后面会说明如何检测人脸并在预览画面上添加矩形框和文字以展示神经网络的处理结果。