Android CameraX NDK OpenCV(二)-- 实现Dnn人脸检测

2020-12-18 15:54:50 浏览数 (1)

前言

上一篇《Android CameraX NDK OpenCV(一)--实时灰度图预览》已经把Android下OpenCV的Ndk配置完成,并且实现了实时灰度图的显示,本篇来看看在Android下使用Dnn实时地进行人脸检测。Dnn的人脸检测在《实践|OpenCV4.2使用DNN进行人脸检测二(视频篇)》文章中已经实现过,不过那个是在Windows平台下的,检测的方式基本就是按那个来的,这次是我们把其的部分代码移植了过来。

实现效果

GIF动图

视频效果

划重点

  • 从上面的效果很仔细的话可以看到,我们检测人脸到画上红色矩形框时偶尔会有延时的情况,这个在《Android JetPack组件CameraX使用及修改显示图像》中说过,我们在摄像机预览中上层加入了VIEW,在VIEW中进行绘制的,其实如果不要想这个情况,可以像灰度图显示一样,把整张已经标记好的图片都传回来,然后DrawBitmap把原来的预览图盖住也可以。
  • 还要注意的一点是加载的人脸检测的模型文件,因为要在NDK中加载并初始化,所以在程序中我们要考虑怎么拷模型文件先复制到Android设备本地,然后调用JNI的方法去加载模型文件。

代码部分

微卡智享

01

模型文件处理

Demo程序还是接上一篇已经搭建好的程序实现

首先在Res下面创建一个RAW的文件夹

然后把我们已经下载好的模型文件复制进去(Demo中已经拷进去了,可以直接在里面获取到)

复制文件到本地代码

代码语言:javascript复制
    private lateinit var mFaceMdescFile: File
    private lateinit var mFaceMBinaryFile:File

    private fun copymFaceMdescFile() {
        try {
            // load cascade file from application resources
            val inputStream = resources.openRawResource(R.raw.opencv_face_detector)
            val faceDir = getDir("facedetector", MODE_PRIVATE)
            mFaceMdescFile = File(faceDir, "opencv_face_detector.pbtxt")
            if (mFaceMdescFile.exists()) return
            val os: FileOutputStream = FileOutputStream(mFaceMdescFile)
            val buffer = ByteArray(4096)
            var bytesRead: Int
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                os.write(buffer, 0, bytesRead)
            }
            inputStream.close()
            os.close()
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    private fun copymFaceMBinaryFile() {
        try {
            // load cascade file from application resources
            val inputStream = resources.openRawResource(R.raw.opencv_face_detector_uint8)
            val faceDir = getDir("facedetector", MODE_PRIVATE)
            mFaceMBinaryFile = File(faceDir, "opencv_face_detector_uint8.pb")
            if (mFaceMBinaryFile.exists()) return
            val os: FileOutputStream = FileOutputStream(mFaceMBinaryFile)
            val buffer = ByteArray(4096)
            var bytesRead: Int
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                os.write(buffer, 0, bytesRead)
            }
            inputStream.close()
            os.close()
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

上面两段代码是将两个模型文件复制到本地,如果文件存在就不用再重复了。

JNI加载模型文件

这次改造了一下代码,把所有JNI的调用都放入一个类中,加入了initFaceDetector的初始化人脸检测和facedetector的人脸检测。

NDK中调用

代码语言:javascript复制
extern "C"
JNIEXPORT jboolean JNICALL
Java_lib_vaccae_opencv_OpenCVJNI_initFaceDetector(JNIEnv *env, jobject thiz, jstring model_binary,
                                                  jstring model_desc) {
    try {
        string sbinary = env->GetStringUTFChars(model_binary, 0);
        string sdesc = env->GetStringUTFChars(model_desc, 0);
        //初始化DNN
        _faceDetect = facedetect();
        jboolean res = _faceDetect.InitDnnNet(sbinary, sdesc);
        
        return res;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
    }
}

extern "C"
JNIEXPORT jobject JNICALL
Java_lib_vaccae_opencv_OpenCVJNI_facedetector(JNIEnv *env, jobject thiz, jbyteArray bytes,
                                              jint width, jint height) {
    try {
        Mat src = byteArrayToMat(env, bytes, width, height);

        //获取ArrayList类引用
        jclass list_jcls = env->FindClass("java/util/ArrayList");
        if (list_jcls == nullptr) {
            LOGI("ArrayList没找到相关类!");
            return 0;
        }
        //获取ArrayList构造函数id
        jmethodID list_init = env->GetMethodID(list_jcls, "<init>", "()V");
        //创建一个ArrayList对象
        jobject list_obj = env->NewObject(list_jcls, list_init);
        //获取ArrayList对象的add()的methodID
        jmethodID list_add = env->GetMethodID(list_jcls, "add", "(Ljava/lang/Object;)Z");

        //人脸检测
        vector<vector<int>> outRects = _faceDetect.Detect(src);
        if(outRects.size()>0){
            jclass rect_jcls = env->FindClass("android/graphics/Rect");
            jmethodID  rect_init = env->GetMethodID(rect_jcls,"<init>","(IIII)V");
            for(int i=0;i<outRects.size();  i){
                vector<int> point = outRects[i];
                jobject tmprect = env->NewObject(rect_jcls,rect_init,
                                                 (int)point[0],
                                                 (int)point[1],
                                                 (int)point[2],
                                                 (int)point[3]);
                env->CallBooleanMethod(list_obj, list_add, tmprect);
            }
        }
        return list_obj;
    } catch (Exception e) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, e.what());
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env->ThrowNew(je, "Unknown exception in JNI code {nMatToBitmap}");
    }
}

人脸检测类

代码语言:javascript复制
//
// Created by 36574 on 2020-12-04.
//

#include "facedetect.h"

//初始化Dnn
bool facedetect::InitDnnNet(string modelbinary, string modeldesc) {
    _modelbinary = modelbinary;
    _modeldesc = modeldesc;

    //初始化置信阈值
    confidenceThreshold = 0.6;
    inScaleFactor = 0.5;
    inWidth = 300;
    inHeight = 300;
    meanVal = Scalar(104.0, 177.0, 123.0);

    _net = dnn::readNetFromTensorflow(_modelbinary, _modeldesc);
    _net.setPreferableBackend(dnn::DNN_BACKEND_OPENCV);
    _net.setPreferableTarget(dnn::DNN_TARGET_CPU);

    return !_net.empty();
}

//人脸检测
vector<Rect> facedetect::DetectToRect(Mat frame) {
    Mat tmpsrc = frame;
    vector<Rect> dsts = vector<Rect>();
    // 修改通道数
    if (tmpsrc.channels() == 4)
        cvtColor(tmpsrc, tmpsrc, COLOR_BGRA2BGR);
    // 输入数据调整
    Mat inputBlob = dnn::blobFromImage(tmpsrc, inScaleFactor,
                                       Size(inWidth, inHeight), meanVal, false, false);
    _net.setInput(inputBlob, "data");

    //人脸检测
    Mat detection = _net.forward("detection_out");

    Mat detectionMat(detection.size[2], detection.size[3],
                     CV_32F, detection.ptr<float>());

    //检测出的结果进行绘制和存放到dsts中
    for (int i = 0; i < detectionMat.rows; i  ) {
        //置值度获取
        float confidence = detectionMat.at<float>(i, 2);
        //如果大于阈值说明检测到人脸
        if (confidence > confidenceThreshold) {
            //计算矩形
            int xLeftBottom = static_cast<int>(detectionMat.at<float>(i, 3) * tmpsrc.cols);
            int yLeftBottom = static_cast<int>(detectionMat.at<float>(i, 4) * tmpsrc.rows);
            int xRightTop = static_cast<int>(detectionMat.at<float>(i, 5) * tmpsrc.cols);
            int yRightTop = static_cast<int>(detectionMat.at<float>(i, 6) * tmpsrc.rows);
            //生成矩形
            Rect rect((int)xLeftBottom, (int)yLeftBottom,
                      (int)(xRightTop - xLeftBottom),
                      (int)(yRightTop - yLeftBottom));

            //截出图矩形存放到dsts数组中
            dsts.push_back(rect);

            //在原图上用红框画出矩形
            rectangle(frame, rect, Scalar(0, 0, 255));
        }
    }

    return dsts;
}

//人脸检测返回点
vector<vector<int>> facedetect::Detect(Mat frame) {
    Mat tmpsrc = frame;
    vector<vector<int>> points = vector<vector<int>>();
    // 修改通道数
    if (tmpsrc.channels() == 4)
        cvtColor(tmpsrc, tmpsrc, COLOR_BGRA2BGR);
    // 输入数据调整
    Mat inputBlob = dnn::blobFromImage(tmpsrc, inScaleFactor,
                                       Size(inWidth, inHeight), meanVal, false, false);
    _net.setInput(inputBlob, "data");

    //人脸检测
    Mat detection = _net.forward("detection_out");

    Mat detectionMat(detection.size[2], detection.size[3],
                     CV_32F, detection.ptr<float>());

    //检测出的结果进行绘制和存放到dsts中
    for (int i = 0; i < detectionMat.rows; i  ) {
        //置值度获取
        float confidence = detectionMat.at<float>(i, 2);
        //如果大于阈值说明检测到人脸
        if (confidence > confidenceThreshold) {
            vector<int> item;
            //获取左上和右下两个点的XY坐标
            //左上X
            int xLeftTop = static_cast<int>(detectionMat.at<float>(i, 3) * tmpsrc.cols);
            item.push_back(xLeftTop);
            //左上Y
            int yLeftTop = static_cast<int>(detectionMat.at<float>(i, 4) * tmpsrc.rows);
            item.push_back(yLeftTop);
            //右下X
            int xRightBottom = static_cast<int>(detectionMat.at<float>(i, 5) * tmpsrc.cols);
            item.push_back(xRightBottom);
            //右下Y
            int yRightBottom = static_cast<int>(detectionMat.at<float>(i, 6) * tmpsrc.rows);
            item.push_back(yRightBottom);


            //截出图矩形存放到dsts数组中
            points.push_back(item);
        }
    }
    return points;
}



02

检测到的人脸标记

在ViewOverLay的类中加入一个DrawRect的方法,因为在JNI返回的是人脸检测到的矩形,所以这里加入一个画矩形的函数,后面两个参数的宽度和高度在上一篇灰度显示中提到过,我们传入的图片的大小和预览的图片大小不一致,预览时自动就缩放至设备屏幕的宽高了,所以这里传入的参数为实际处理图片的宽和高,用于计算宽和度偏移的比例

然后在OnDraw的函数中针对矩形的四个点进行比例的偏移。

03

调用相关代码

程序启动时加入复制文件及初始化DNN模型文件的调用

AnalysisCvDetector的analyze事件中加入人脸检测的调用,这样基本就完成了。

Demo地址

https://github.com/Vaccae/AndroidCameraXNDKOpenCV.git

Demo如果有新的就会直接提交更新到主分支了

0 人点赞