1. 前言
上篇文章 我们已经通过一个简单的例子,在android studio
中接入了opencv
。
之前我们也 在visual studio上,使用opencv实现人脸识别 中实现了人脸识别的效果。
接着,我们就可以将opencv
的人脸识别效果移植到android
中了。
1.1 环境说明
- 操作系统 :
windows 10 64
位 android studio
版本 :android studio giraffe | 2022.3.1
opencv
版本 :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
: 设置surfaceview
postdata
: 发送视频帧数据
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博客
发表评论