03 Tengine Face Detector(20181223)

Preface: 在之前学习了Tengine在安卓上的相关配置(01_Tengine_Android)和Android Studio上使用OpenCV之后终于步入正题,进入我们这个比赛的内容,检测人脸的特征点。之前其实都没有好好看过代码,所以这篇文章就作为Tengine_FaceDetector的代码解读吧!其实东西做完了却不知从哪里说起,回想起来这几天做的很多事情,却非常杂乱,行文之间若有逻辑不清之处还望海涵。

一、整体框架

  那我们就从这个项目的最终完成版说起吧,之前说了本文主要以解读代码为主,那就先来看看整个项目工程的框架。主要是分成这么几部分,我们一一介绍:

1、Android平台的java语言,这部分主要是对安卓手机界面进行编程,看着《第一行代码》一步步走下去就行了,这部分算是顶层代码吧~,主要就是activity文件以及界面xml文件。

2、JNI接口,这是个啥玩意呢?请看词条:https://baike.so.com/doc/508314-538197.html
意思也就是说,JNI可以当做一个接口,使得Java语言能够调用底层的C++/C语言,也可以是其他语言。要使用这些接口需要首先进行函数申明,一般是在MainActivity类的最后:

1
2
3
4
public native String stringFromJNI();
public native int TengineWrapperInit();
public native float RunMobilenet(String file);
public native float TengineWrapperGetTop1();

然后呢这些函数就链接到一个叫做native-lib.cpp的文件当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
JNIEXPORT jint JNICALL
Java_com_tengine_openailab_mobilenet_MainActivity_TengineWrapperInit(
JNIEnv *env,
jobject /* this */) {
tengine_wrapper = new TengineWrapper();
if(!tengine_wrapper)
return 1;
int ret = tengine_wrapper->InitMobilenet();
if( ret!= 0) {
return ret;
}
return 0;
}

这里其实你就会发现一些东西,比如在JNI里面函数的定义与申明的不同,首先是数据类型的转换,java与JNI当中是一一对应的。JNI里面有特定的数组格式:比如jobjectArray、jbooleanArray、jbyteArray、jcharArray、jshortArray、jintArray、jlongArray、jfloatArray、jdoubleArray等,还有jclass、jstring、jthrowable等。那么其实这里就有个问题了,为什么顶层的java语言能够调用JNI接口文件呢?这就是下面这段话的功劳:

1
2
3
4
5
public class MainActivity extends Activity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}

看到了吧这里在MainActivity的一开始就load了这个名字叫做native-lib的库。所以他们能够链接起来。
  那么在做这个项目的时候呢就有一个问题出现了:顶层activity文件中如果我要输入一个图片文件如Mat格式的数据到底层去处理,数据接口应该怎么写呢?这个问题曾经困扰过,因为一开始的时候我是用Mat格式的数据直接传的,后来发现这样传递数据根本没有传到底层,而且还会导致app闪退,幸好这个已经有前人做过了,下面这篇博文就讲述了java层到JNI的Mat数据的接口处理:
https://blog.csdn.net/brcli/article/details/76407986
https://blog.csdn.net/pplxlee/article/details/52713311
  简单说来就是这样:java层新建一个mat数据,然后通过调用JNI接口函数传入mat.getNativeObjAddr()就可以将mat数据传进函数,例如:

1
if(RunMobilenet(mygraypic.getNativeObjAddr())>0)

顾名思义getNativeObjAddr()就是获取本地对象的地址,返回的是一个jlong类型的数据,所以下面我们在JNI进行调用的时候格式是这样的,
activity文件申明:

1
public native float RunMobilenet(long mat);

JNI函数定义:

1
2
3
4
5
6
7
8
9
JNIEXPORT jfloat JNICALL
Java_com_tengine_openailab_mobilenet_MainActivity_RunMobilenet(
JNIEnv *env,
jobject /* this */, jlong mat) {
cv::Mat javamat = (*(cv::Mat*)mat);
if( tengine_wrapper->RunMobilenet(javamat) > 0 )
return 1;
return 0;
}

这里稍微解释一下这句:cv::Mat javamat = (*(cv::Mat*)mat);jlong mat是一个地址,先将它转换成mat类型指针,然后取该地址的值作为JNI里面mat变量的值,这就实现了mat数据的传递。
  然后我们发现这个新的javamat又被传递到C++/C里面的一个函数叫做RunMobilenet(),下面我们就去看看C++里面是怎么写的。这个RunMobilenet()是class TengineWrapper里面的一个函数,定义在Tengine_Wrapper.h里面。具体的函数在Tengine_Wrapper.cpp里面。

1
float TengineWrapper::RunMobilenet(cv::Mat image)

这里就可以看到,它的输入就是一个mat类型的数据。至此就讲完了整个接口的调用情况。

3、底层C++/C函数
这部分就是底层代码啦,主要就在cpp文件夹里,里面是Tengine框架的调用,我们的数据图像处理就会在这里使用。在第二部分将会重点讲述这部分内容。

二、Tengine使用

大致上的调用流程如下:
调用 init_tengine_library 函数初始化
调用 load_model 函数载入训练好的模型
调用 create_runtime_graph 函数创建图
调用 get_graph_input_tensor 获取输入Tensor并用 set_tensor_shape 设置输入Tensor的shape
调用 prerun_graph 函数预启动图
调用 get_graph_output_tensor 获取输出Tensor并用 get_tensor_buffer_size 获取输出的shape
向 input_data 写入输入的数据,并调用 set_tensor_buffer 把数据转移到输入Tensor上
调用 run_graph 运行图(做一次前向传播)
调用 get_graph_output_tensor 获取输出Tensor并用 get_tensor_buffer 取得缓冲区上的数据
最后在退出程序前依次释放各个申请的动态空间

他们所对应的代码如下,基本都在Tengine_Wrapper.cpp里面:

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
int TengineWrapper::InitMobilenet()
{
init_tengine_library();
if (request_tengine_version("0.1") < 0)
return -1;
const char* mobilenet_tf_model = "/data/local/tmp/model3model.pb";
const char* format = "tensorflow";

if (load_model("mobilenet", format, mobilenet_tf_model) < 0)
return 4;

g_mobilenet = create_runtime_graph("graph0","mobilenet",NULL);
if (!check_graph_valid(g_mobilenet))
return 5;

const int img_h = 96;
const int img_w = 96;
int image_size = img_h * img_w;
g_mobilenet_input = (float *) malloc(sizeof(float) * image_size);
int dims[4] = {1, 1, img_h, img_w};

input_tensor = get_graph_input_tensor(g_mobilenet, 0, 0);
if(!check_tensor_valid(input_tensor))
return 6;

set_tensor_shape(input_tensor, dims, 4);
set_tensor_buffer(input_tensor, g_mobilenet_input, image_size * 4);

if( prerun_graph(g_mobilenet)!=0 )
return 1;
return 0;
}

下面是运行代码:

1
2
3
4
5
6
7
8
9
10
float TengineWrapper::RunMobilenet(cv::Mat image)
{
if( get_input_data(image, g_mobilenet_input, 96, 96) )
return 7;
if( !run_graph(g_mobilenet,1))
return 2;
output_tensor = get_graph_output_tensor(g_mobilenet, 0, 0);
float *data = (float *)get_tensor_buffer(output_tensor);
return 0;
}

数据返回到JNI层,随后从JNI层返回到java层使用。至此使用Tengine就可以基本完成了,但是最后能够依次释放一下各个申请的动态空间,但是好像不释放也没啥关系:

1
2
3
4
5
6
7
8
9
int TengineWrapper::ReleaseMobilenet()
{
sleep(1);
free(g_mobilenet_input);
postrun_graph(g_mobilenet);
destroy_runtime_graph(g_mobilenet);
remove_model("mobilenet");
return 0;
}

三、Tengine_FaceDetector

最后再讲述一下这个Tengine_FaceDetector的基本流程吧:
(1)Java层使用一个org.opencv.android.JavaCameraView布局来调用摄像头获取图像
(2)使用opencv自带的分类器实现人脸框图
(3)将人脸框图灰度图传给底层使用tensorflow模型(model3model.pb文件)输出人脸的15个特征点坐标,15个特征点坐标在模型输出的 1*30数组元素里面。分别是x1 y1 x2 y2 ……
(4)得到15个特征点的数据以后可以在java层画出特征点位置。