欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 中使用 dlib+opencv 实现动态人脸检测

程序员文章站 2022-06-11 16:06:00
1 概述 完成 Android 相机预览功能以后,在此基础上我使用 dlib 与 opencv 库做了一个关于人脸检测的 demo。该 demo 在相机预览过程中对人脸进行实时检测,并将检测到的人脸用矩形框描绘出来。具体实现原理如下: 采用双层 View,底层的 TextureView 用于预览,程 ......

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 运行效果如下:

Android 中使用 dlib+opencv 实现动态人脸检测

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