前言
上一篇《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如果有新的就会直接提交更新到主分支了