Android 中使用 dlib+opencv 实现动态人脸检测
1 概述
完成 android 相机预览功能以后,在此基础上我使用 dlib 与 opencv 库做了一个关于人脸检测的 demo。该 demo 在相机预览过程中对人脸进行实时检测,并将检测到的人脸用矩形框描绘出来。具体实现原理如下:
采用双层 view,底层的 textureview 用于预览,程序从 textureview 中获取预览帧数据,然后调用 dlib 库对帧数据进行处理,最后将检测结果绘制在顶层的 surfaceview 中。
2 项目配置
由于项目中用到了 dlib 与 opencv 库,因此需要对其进行配置。主要涉及到以下几个方面:
2.1 c++支持
在项目创建过程中依次选择 include c++ support、c++11、exceptions support ( -fexceptions )以及 runtime type information support ( -frtti ) 。最后生成的 build.gradle 文件如下:
defaultconfig { applicationid "com.example.lightweh.facedetection" minsdkversion 23 targetsdkversion 28 versioncode 1 versionname "1.0" testinstrumentationrunner "android.support.test.runner.androidjunitrunner" externalnativebuild { cmake { arguments "-dcmake_build_type=release" cppflags "-std=c++11 -frtti -fexceptions" } } }
其中,arguments 参数是后添加上去的,主要用于指定 cmake 的编译模式为 release,因为在 debug 模式下 dlib 库中相关算法的运行速度非常慢。前期如果需要调试 c++ 代码,可先将 arguments 参数注释。
2.2 dlib 与 opencv 下载
到下载最新版本的源码,解压后将文件夹中的dlib目录复制到 android studio 工程的 cpp 目录下。
到 下载最新的 opencv-android 库,解压后将文件夹中的 native 目录同样复制到 android studio 工程的 cpp 目录下,并改名为 opencv。
2.3 cmakelists 配置
在 cmakelists 文件中,我们首先包含 dlib 的 cmake 文件,接下来添加 opencv 的 include 文件夹并引入 opencv 的 so 库,同时将 jni_common 目录中的文件及人脸检测相关文件添加至 native-lib 库中,最后进行链接。
# 设置native目录 set(native_dir ${cmake_source_dir}/src/main/cpp) # 设置dlib include(${native_dir}/dlib/cmake) # 设置opencv include文件夹 include_directories(${native_dir}/opencv/jni/include) # 设置opencv的so库 add_library( libopencv_java3 shared imported) set_target_properties( libopencv_java3 properties imported_location ${native_dir}/opencv/libs/${android_abi}/libopencv_java3.so) # 将jni_common目录中所有文件名,存至src_list中 aux_source_directory(${native_dir}/jni_common src_list) add_library( # sets the name of the library. native-lib # sets the library as a shared library. shared # provides a relative path to your source file(s). ${src_list} src/main/cpp/face_detector.h src/main/cpp/face_detector.cpp src/main/cpp/native-lib.cpp) find_library( # sets the name of the path variable. log-lib # specifies the name of the ndk library that # you want cmake to locate. log) target_link_libraries( # specifies the target library. native-lib dlib libopencv_java3 jnigraphics # links the target library to the log library # included in the ndk. ${log-lib}) # 指定release编译选项 set(cmake_c_flags_release "${cmake_c_flags_release} -s -o3 -wall") set(cmake_cxx_flags_release "${cmake_cxx_flags_release} -s -o3 -wall")
由于 c++ 代码中用到了头文件 "android/bitmap.h",所以链接时需要添加 jnigraphics 库。
3 jni相关 java 类定义
3.1 visiondetret 类
visiondetret 类的相关对象主要负责 c++ 与 java 之间的数据传递。
public final class visiondetret { private int mleft; private int mtop; private int mright; private int mbottom; visiondetret() {} public visiondetret(int l, int t, int r, int b) { mleft = l; mtop = t; mright = r; mbottom = b; } public int getleft() { return mleft; } public int gettop() { return mtop; } public int getright() { return mright; } public int getbottom() { return mbottom; } }
3.2 facedet 类
facedet 类为 jni 函数调用类,主要定义了一些需要 c++ 实现的 native 方法。
public class facedet { private static final string tag = "facedet"; // accessed by native methods @suppresswarnings("unused") private long mnativefacedetcontext; static { try { // 预加载native方法库 system.loadlibrary("native-lib"); jninativeclassinit(); log.d(tag, "jninativeclassinit success"); } catch (unsatisfiedlinkerror e) { log.e(tag, "library not found"); } } public facedet() { jniinit(); } @nullable @workerthread public list<visiondetret> detect(@nonnull bitmap bitmap) { visiondetret[] detrets = jnibitmapdet(bitmap); return arrays.aslist(detrets); } @override protected void finalize() throws throwable { super.finalize(); release(); } public void release() { jnideinit(); } @keep private native static void jninativeclassinit(); @keep private synchronized native int jniinit(); @keep private synchronized native int jnideinit(); @keep private synchronized native visiondetret[] jnibitmapdet(bitmap bitmap); }
4 native 方法实现
4.1 定义 visiondetret 类对应的 c++ 类
#include <jni.h> #define classname_vision_det_ret "com/lightweh/dlib/visiondetret" #define constsig_vision_det_ret "()v" #define classname_face_det "com/lightweh/dlib/facedet" class jni_visiondetret { public: jni_visiondetret(jnienv *env) { // 查找visiondetret类信息 jclass detretclass = env->findclass(classname_vision_det_ret); // 获取visiondetret类成员变量 jid_left = env->getfieldid(detretclass, "mleft", "i"); jid_top = env->getfieldid(detretclass, "mtop", "i"); jid_right = env->getfieldid(detretclass, "mright", "i"); jid_bottom = env->getfieldid(detretclass, "mbottom", "i"); } void setrect(jnienv *env, jobject &jdetret, const int &left, const int &top, const int &right, const int &bottom) { // 设置visiondetret类对象jdetret的成员变量值 env->setintfield(jdetret, jid_left, left); env->setintfield(jdetret, jid_top, top); env->setintfield(jdetret, jid_right, right); env->setintfield(jdetret, jid_bottom, bottom); } // 创建visiondetret类实例 static jobject createjobject(jnienv *env) { jclass detretclass = env->findclass(classname_vision_det_ret); jmethodid mid = env->getmethodid(detretclass, "<init>", constsig_vision_det_ret); return env->newobject(detretclass, mid); } // 创建visiondetret类对象数组 static jobjectarray createjobjectarray(jnienv *env, const int &size) { jclass detretclass = env->findclass(classname_vision_det_ret); return (jobjectarray) env->newobjectarray(size, detretclass, null); } private: jfieldid jid_left; jfieldid jid_top; jfieldid jid_right; jfieldid jid_bottom; };
4.2 定义人脸检测类
人脸检测算法需要用大小位置不同的窗口在图像中进行滑动,然后判断窗口中是否存在人脸。本文采用的是 dlib 中的是hog(histogram of oriented gradient)方法对人脸进行检测,其检测效果要好于 opencv。dlib 中同样提供了 cnn 方法来进行人脸检测,效果好于 hog,不过需要使用 gpu 加速,不然程序运行会非常慢。
class facedetector { private: dlib::frontal_face_detector face_detector; std::vector<dlib::rectangle> det_rects; public: facedetector(); // 实现人脸检测算法 int detect(const cv::mat &image); // 返回检测结果 std::vector<dlib::rectangle> getdetresultrects(); };
facedetector::facedetector() { // 定义人脸检测器 face_detector = dlib::get_frontal_face_detector(); } int facedetector::detect(const cv::mat &image) { if (image.empty()) return 0; if (image.channels() == 1) { cv::cvtcolor(image, image, cv_gray2bgr); } dlib::cv_image<dlib::bgr_pixel> dlib_image(image); det_rects.clear(); // 返回检测到的人脸矩形特征框 det_rects = face_detector(dlib_image); return det_rects.size(); } std::vector<dlib::rectangle> facedetector::getdetresultrects() { return det_rects; }
4.3 native 方法实现
jni_visiondetret *g_pjni_visiondetret; javavm *g_javavm = null; // 该函数在加载本地库时被调用 jniexport jint jni_onload(javavm *vm, void *reserved) { g_javavm = vm; jnienv *env; vm->getenv((void **) &env, jni_version_1_6); // 初始化 g_pjni_visiondetret g_pjni_visiondetret = new jni_visiondetret(env); return jni_version_1_6; } // 该函数用于执行清理操作 void jni_onunload(javavm *vm, void *reserved) { g_javavm = null; delete g_pjni_visiondetret; } namespace { #define java_null 0 using detptr = facedetector *; // 用于存放人脸检测类对象的指针,关联jave层对象与c++底层对象(相互对应) class jni_facedet { public: jni_facedet(jnienv *env) { jclass clazz = env->findclass(classname_face_det); mnativecontext = env->getfieldid(clazz, "mnativefacedetcontext", "j"); env->deletelocalref(clazz); } detptr getdetectorptrfromjava(jnienv *env, jobject thiz) { detptr const p = (detptr) env->getlongfield(thiz, mnativecontext); return p; } void setdetectorptrtojava(jnienv *env, jobject thiz, jlong ptr) { env->setlongfield(thiz, mnativecontext, ptr); } jfieldid mnativecontext; }; // protect getting/setting and creating/deleting pointer between java/native std::mutex glock; std::shared_ptr<jni_facedet> getjni_facedet(jnienv *env) { static std::once_flag sonceinitflag; static std::shared_ptr<jni_facedet> sjni_facedet; std::call_once(sonceinitflag, [env]() { sjni_facedet = std::make_shared<jni_facedet>(env); }); return sjni_facedet; } // 从java对象获取它持有的c++对象指针 detptr const getdetptr(jnienv *env, jobject thiz) { std::lock_guard<std::mutex> lock(glock); return getjni_facedet(env)->getdetectorptrfromjava(env, thiz); } // the function to set a pointer to java and delete it if newptr is empty // c++对象new以后,将指针转成long型返回给java对象持有 void setdetptr(jnienv *env, jobject thiz, detptr newptr) { std::lock_guard<std::mutex> lock(glock); detptr oldptr = getjni_facedet(env)->getdetectorptrfromjava(env, thiz); if (oldptr != java_null) { delete oldptr; } getjni_facedet(env)->setdetectorptrtojava(env, thiz, (jlong) newptr); } } // end unnamespace #ifdef __cplusplus extern "c" { #endif #define dlib_face_jni_method(method_name) java_com_lightweh_dlib_facedet_##method_name void jniexport dlib_face_jni_method(jninativeclassinit)(jnienv *env, jclass _this) {} // 生成需要返回的结果数组 jobjectarray getrecresult(jnienv *env, detptr facedetector, const int &size) { // 根据检测到的人脸数创建相应大小的jobjectarray jobjectarray jdetretarray = jni_visiondetret::createjobjectarray(env, size); for (int i = 0; i < size; i++) { // 对检测到的每一个人脸创建对应的实例对象,然后插入数组 jobject jdetret = jni_visiondetret::createjobject(env); env->setobjectarrayelement(jdetretarray, i, jdetret); dlib::rectangle rect = facedetector->getdetresultrects()[i]; // 将人脸矩形框的值赋给对应的jobject实例对象 g_pjni_visiondetret->setrect(env, jdetret, rect.left(), rect.top(), rect.right(), rect.bottom()); } return jdetretarray; } jniexport jobjectarray jnicall dlib_face_jni_method(jnibitmapdet)(jnienv *env, jobject thiz, jobject bitmap) { cv::mat rgbamat; cv::mat bgrmat; jniutils::convertbitmaptorgbamat(env, bitmap, rgbamat, true); cv::cvtcolor(rgbamat, bgrmat, cv::color_rgba2bgr); // 获取人脸检测类指针 detptr mdetptr = getdetptr(env, thiz); // 调用人脸检测算法,返回检测到的人脸数 jint size = mdetptr->detect(bgrmat); // 返回检测结果 return getrecresult(env, mdetptr, size); } jint jniexport jnicall dlib_face_jni_method(jniinit)(jnienv *env, jobject thiz) { detptr mdetptr = new facedetector(); // 设置人脸检测类指针 setdetptr(env, thiz, mdetptr); return jni_ok; } jint jniexport jnicall dlib_face_jni_method(jnideinit)(jnienv *env, jobject thiz) { // 指针置0 setdetptr(env, thiz, java_null); return jni_ok; } #ifdef __cplusplus } #endif
5 java端调用人脸检测算法
在开启人脸检测之前,需要在相机 autofittextureview 上覆盖一层自定义 boundingboxview 用于绘制检测到的人脸矩形框,该 view 的具体实现如下:
public class boundingboxview extends surfaceview implements surfaceholder.callback { protected surfaceholder msurfaceholder; private paint mpaint; private boolean miscreated; public boundingboxview(context context, attributeset attrs) { super(context, attrs); msurfaceholder = getholder(); msurfaceholder.addcallback(this); msurfaceholder.setformat(pixelformat.transparent); setzorderontop(true); mpaint = new paint(); mpaint.setantialias(true); mpaint.setcolor(color.red); mpaint.setstrokewidth(5f); mpaint.setstyle(paint.style.stroke); } @override public void surfacechanged(surfaceholder surfaceholder, int format, int width, int height) { } @override public void surfacecreated(surfaceholder surfaceholder) { miscreated = true; } @override public void surfacedestroyed(surfaceholder surfaceholder) { miscreated = false; } public void setresults(list<visiondetret> detrets) { if (!miscreated) { return; } canvas canvas = msurfaceholder.lockcanvas(); //清除掉上一次的画框。 canvas.drawcolor(color.transparent, porterduff.mode.clear); canvas.drawcolor(color.transparent); for (visiondetret detret : detrets) { rect rect = new rect(detret.getleft(), detret.gettop(), detret.getright(), detret.getbottom()); canvas.drawrect(rect, mpaint); } msurfaceholder.unlockcanvasandpost(canvas); } }
同时,需要在布局文件中添加对应的 boundingboxview 层,保证与 autofittextureview 完全重合:
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".camerafragment"> <com.lightweh.facedetection.autofittextureview android:id="@+id/textureview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centervertical="true" android:layout_centerhorizontal="true" /> <com.lightweh.facedetection.boundingboxview android:id="@+id/boundingboxview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignleft="@+id/textureview" android:layout_aligntop="@+id/textureview" android:layout_alignright="@+id/textureview" android:layout_alignbottom="@+id/textureview" /> </relativelayout>
boundingboxview 添加完成以后,即可在 camerafragment 中添加对应的人脸检测代码:
private class detectasync extends asynctask<bitmap, void, list<visiondetret>> { @override protected void onpreexecute() { misdetecting = true; super.onpreexecute(); } protected list<visiondetret> doinbackground(bitmap... bp) { list<visiondetret> results; // 返回检测结果 results = mfacedet.detect(bp[0]); return results; } protected void onpostexecute(list<visiondetret> results) { // 绘制检测到的人脸矩形框 mboundingboxview.setresults(results); misdetecting = false; } }
然后,分别在 onresume 与 onpause 函数中完成人脸检测类对象的初始化和释放:
@override public void onresume() { super.onresume(); startbackgroundthread(); mfacedet = new facedet(); if (mtextureview.isavailable()) { opencamera(mtextureview.getwidth(), mtextureview.getheight()); } else { mtextureview.setsurfacetexturelistener(msurfacetexturelistener); } } @override public void onpause() { closecamera(); stopbackgroundthread(); if (mfacedet != null) { mfacedet.release(); } super.onpause(); }
最后,在 textureview 的回调函数 onsurfacetextureupdated 完成调用:
@override public void onsurfacetextureupdated(surfacetexture texture) { if (!misdetecting) { bitmap bp = mtextureview.getbitmap(); // 保证图片方向与预览方向一致 bp = bitmap.createbitmap(bp, 0, 0, bp.getwidth(), bp.getheight(), mtextureview.gettransform(null), true ); new detectasync().execute(bp); } }
6 测试结果
经测试,960x720的 bitmap 图片在华为手机(android 6.0,8核1.2ghz,2g内存)上执行一次检测约耗时800~850ms。demo 运行效果如下:
7 demo 源码
github:facedetection
8. 参考
- https://github.com/tzutalin/dlib-android
- https://github.com/gv22ga/dlib-face-recognition-android
- https://blog.csdn.net/yanzi1225627/article/details/7934710
- https://blog.csdn.net/hjimce/article/details/64127654