1. 前言
上篇文章 我们已经通过一个简单的例子,在android studio中接入了opencv。
之前我们也 在visual studio上,使用opencv实现人脸识别 中实现了人脸识别的效果。
接着,我们就可以将opencv的人脸识别效果移植到android中了。
1.1 环境说明
- 操作系统 :
windows 10 64位 android studio版本 :android studio giraffe | 2022.3.1opencv版本 :opencv-4.8.0 (2023年7月最新版)
1.2 实现效果
先来看下实现效果,识别到的人脸会用红框框出来。

接下来我们来一步步实现上述的效果。
2. 前置操作
2.1 添加权限
<uses-permission android:name="android.permission.camera" />
<uses-permission android:name="android.permission.write_external_storage" />
<uses-permission android:name="android.permission.record_audio" />
activitycompat.requestpermissions(
this@facedetectionactivity,
arrayof(
manifest.permission.camera,
manifest.permission.write_external_storage,
manifest.permission.record_audio
),
1
)
2.2 新建facedetectionactivity
新建facedetectionactivity,并将其设为默认的activity,然后修改其xml布局
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:context=".mainactivity">
<androidx.constraintlayout.widget.constraintlayout
android:background="@color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<surfaceview
android:id="@+id/surfaceview"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintbottom_tobottomof="parent"
app:layout_constraintdimensionratio="w,4:3"
app:layout_constraintleft_toleftof="parent"
app:layout_constraintright_torightof="parent"
app:layout_constrainttop_totopof="parent" />
</androidx.constraintlayout.widget.constraintlayout>
<linearlayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<button
android:text="切换摄像头"
android:onclick="switchcamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</linearlayout>
</relativelayout>
2.3 添加jni方法
然后修改facedetectionactivity为如下代码,这里增加了三个jni方法
init: 初始化opencv人脸识别setsurface: 设置surfaceviewpostdata: 发送视频帧数据
class facedetectionactivity : appcompatactivity() {
private lateinit var binding: activityfacedetectionbinding
override fun oncreate(savedinstancestate: bundle?) {
super.oncreate(savedinstancestate)
binding = activityfacedetectionbinding.inflate(layoutinflater)
setcontentview(binding.root)
//这里省略了申请权限的代码...
}
//初始化opencv
external fun init(path: string?)
//向opencv发送一帧的图像数据
external fun postdata(data: bytearray?, width: int, height: int, cameraid: int)
//设置surfaceview
external fun setsurface(surface: surface?)
companion object {
init {
system.loadlibrary("myopencvtest")
}
}
}
同时,需要在native-lib.cpp中添加这三个jni方法,这里的com_heiko_myopencvtest_facedetectionactivity需要改为你实际的包名和类名。
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_init(jnienv *env, jobject thiz, jstring path) {
}
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_postdata(jnienv *env, jobject thiz,
jbytearray data, jint width, jint height,
jint camera_id) {
}
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_setsurface(jnienv *env, jobject thiz,
jobject surface) {
}
2.4 实现相机预览功能
这里用到了camera1 api,直接使用camerahelper这个工具类接入即可,这部分详见我的另一篇博客 android 使用camera1的工具类camerahelper快速实现相机预览、拍照功能
override fun oncreate(savedinstancestate: bundle?) {
super.oncreate(savedinstancestate)
binding = activityfacedetectionbinding.inflate(layoutinflater)
setcontentview(binding.root)
//这里省略了申请权限的代码...
val surfaceview = findviewbyid<surfaceview>(r.id.surfaceview)
surfaceview.holder.addcallback(this)
camerahelper = camerahelper(cameraid)
camerahelper.setpreviewcallback(this)
}
3. 初始化opencv
3.1 配置opencv
接着,我们不要忘了配置opencv,这部分详见我的另一篇博客 : android studio 接入opencv最简单的例子 : 实现灰度图效果
3.2 赋值级联分类器文件
配置好opencv,我们要将模型,也就是人脸识别的级联分类器文件haarcascade_frontalface_alt.xml复制到asserts文件夹下。
当我们启动app的时候,需要将该文件复制到外置存储中。
override fun oncreate(savedinstancestate: bundle?) {
super.oncreate(savedinstancestate)
//省略了其他代码...
//utils类可以在本文末尾复制
utils.copyassets(this@facedetectionactivity, "haarcascade_frontalface_alt.xml")
}
拷贝完成后,调用init()方法,传入路径
override fun onresume() {
super.onresume()
//省略了其他代码...
//utils类可以在本文末尾复制
val path = utils.getmodelfile(
this@facedetectionactivity,
"haarcascade_frontalface_alt.xml"
).absolutepath
init(path)
}
4. 实现cascadedetectoradapter
这里我们需要将我的另一篇博客中的 在visual studio上,使用opencv实现人脸识别 (下面统称为vs实现) 中的代码移植过来。
这里创建了cascadedetectoradapter,实现了detectionbasedtracker::idetector接口,和vs实现上代码是一样的。
class cascadedetectoradapter : public detectionbasedtracker::idetector
{
public:
cascadedetectoradapter(cv::ptr<cv::cascadeclassifier> detector) :
idetector(),
detector(detector)
{
cv_assert(detector);
}
void detect(const cv::mat& image, std::vector<cv::rect>& objects)
{
detector->detectmultiscale(image, objects, scalefactor, minneighbours, 0, minobjsize, maxobjsize);
}
virtual ~cascadedetectoradapter()
{
}
private:
cascadedetectoradapter();
cv::ptr<cv::cascadeclassifier> detector;
};
cv::ptr<detectionbasedtracker> tracker;
5. 实现init方法
init方法也是一样的,声明tracker对象,并调用run()方法,会启动一个异步线程,后面的人脸检测会在这个异步线程进行检测了。(这个是保障实时人脸检测不卡的前提)
//cv::ptr<detectionbasedtracker> tracker;
detectionbasedtracker *tracker = 0;
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_init(jnienv *env, jobject thiz, jstring path) {
string stdfilename = env->getstringutfchars(path, 0);
//创建一个主检测适配器
cv::ptr<cascadedetectoradapter> maindetector = makeptr<cascadedetectoradapter>(
makeptr<cascadeclassifier>(stdfilename));
//创建一个跟踪检测适配器
cv::ptr<cascadedetectoradapter> trackingdetector = makeptr<cascadedetectoradapter>(
makeptr<cascadeclassifier>(stdfilename));
//创建跟踪器
detectionbasedtracker::parameters detectorparams;
//tracker = makeptr<detectionbasedtracker>(maindetector, trackingdetector, detectorparams);
tracker= new detectionbasedtracker(maindetector, trackingdetector, detectorparams);
tracker->run();
}
5. 设置surface
在 android ndk 中,anativewindow 是一个c/c++接口,它提供了一种在 c 或 c++ 代码中访问 android surface 的方式。通过使用anativewindow接口,开发者可以在ndk中直接访问和操作android窗口系统,实现图形处理和渲染操作。
这里我们将surfaceview传到jni中,方便c/c++代码后边将图像实时渲染到android surfaceview上。
#include <android/native_window_jni.h>
anativewindow *window = 0;
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_setsurface(jnienv *env, jobject thiz,
jobject surface) {
if (window) {
anativewindow_release(window);
window = 0;
}
window = anativewindow_fromsurface(env, surface);
}
6. 处理数据并实现人脸识别
处理图像数据的部分我们在postdata中实现,这部分会先对图像进行处理,然后进行人脸识别,并渲染到surfaceview上。
extern "c"
jniexport void jnicall
java_com_heiko_myopencvtest_facedetectionactivity_postdata(jnienv *env, jobject instance, jbytearray data_,
jint w, jint h, jint cameraid) {
//待实现的代码
}
6.1 将图片转化为mat
参数中的jbytearray data_,是从android java层中获取到的。
然后转化为jbyte *data这个字节数组,接着将其转为一个mat矩阵。
mat是是opencv最基本的数据结构。它用于存储图像数据。
需要注意的是,由于传入的数据是yuv420,每个像素占1.5byte,
所以这个图像的宽度需要传入实际宽度*1.5,宽度不变。
这里的cv_8uc1是单通道的意思,就是说无论是y分量还是u、v分量,都存储在同一个通道里。
jbyte *data = env->getbytearrayelements(data_, null);
mat src(h + h / 2, w, cv_8uc1, data);
6.2 将yuv转为rgba
接着,需要将yuv格式转化为rgba格式,方便后续的操作。
这里的color_yuv2rgba_nv21表示原始是nv21的yuv格式,将其转为rgba格式。
cvtcolor(src, src, color_yuv2rgba_nv21);
6.3 对图像做翻转和镜像
由于手机摄像头硬件安装在手机里时,和屏幕的方向并不是一致的,所以需要将摄像头拍摄的画面进行旋转。
- 如果是前置摄像头 : 需要将画面逆时针旋转
90度,并做左右镜像操作 - 如果是后置摄像头 : 需要将画面顺时针旋转
90度
if (cameraid == 1) {
//前置摄像头
rotate(src, src, rotate_90_counterclockwise);
//1:左右镜像
//0:上下镜像
flip(src, src, 1);
} else {
//顺时针旋转90度
rotate(src, src, rotate_90_clockwise);
}
6.4 转为灰度图并进行直方图均衡化处理
接着需要对图像进行灰度和直方图均衡化处理,以便提高人脸识别的准确性和可靠性,这部分和vs实现上是一样。
mat gray;
//转为灰度图
cvtcolor(src, gray, color_rgba2gray);
//直方图均衡化
equalizehist(gray, gray);
6.5 进行人脸检测
接着就可以调用tracker->process来建人脸检测了。
检测完成后,接着调用tracker->getobjects将检测的人脸位置赋值给faces。
std::vector<rect> faces;
tracker->process(gray);
tracker->getobjects(faces);
6.6 将人脸用红框框出来
接着,将识别到的人脸,用红色的矩形框绘制出来,rectangle方法就是用来绘制一个矩形框的方法。
for (rect face : faces) {
rectangle(src, face, scalar(255, 0, 0));
}
6.7 将图像渲染到surfaceview上
6.7.1 设置窗口缓冲区
anativewindow_setbuffersgeometry是设置android native窗口的缓冲区的大小和像素格式。
if (window) {
anativewindow_setbuffersgeometry(window, src.cols, src.rows, window_format_rgba_8888);
//后续代码在这里编写...
}
6.7.2 将图像数据填充到窗口的缓冲区
这里是个while循环,会不断地将图像数据(rgba),填充到窗口的缓冲区,最后调用anativewindow_unlockandpost提交刷新,图像就渲染到surfaceview上了。
anativewindow_buffer window_buffer;
do {
//如果 lock 失败,直接 break
if (anativewindow_lock(window, &window_buffer, 0)) {
anativewindow_release(window);
window = 0;
break;
}
//将window_buffer.bits转化为 uint8_t *
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
//stride : 一行多少个数据 (rgba) * 4
int dst_linesize = window_buffer.stride * 4;
//一行一行拷贝
for (int i = 0; i < window_buffer.height; ++i) {
memcpy(dst_data + i * dst_linesize, src.data + i * src.cols * 4, dst_linesize);
}
//提交刷新
anativewindow_unlockandpost(window);
} while (0);
6.8 回收资源
最后,别忘了回收资源
src.release();
gray.release();
env->releasebytearrayelements(data_, data, 0);
7. 运行项目
我们可以看到效果如下,至此我们就完成在android上,使用opencv实现实时的人脸识别了。

8. 本文源码下载
android和windows下,使用opencv实现人脸识别 示例 demo
9. opencv系列文章
visual studio 2022 cmake配置opencv开发环境_opencv visualstudio配置_氦客的博客-csdn博客
在visual studio上,使用opencv实现人脸识别_氦客的博客-csdn博客
android studio 接入opencv,并实现灰度图效果_氦客的博客-csdn博客
android 使用opencv实现实时人脸识别,并绘制到surfaceview上_氦客的博客-csdn博客
发表评论