• Android 使用OpenCV实现实时人脸识别,并绘制到SurfaceView上


    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" />
    
    • 1
    • 2
    • 3
    ActivityCompat.requestPermissions(
        this@FaceDetectionActivity,
        arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.RECORD_AUDIO
        ),
        1
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.2 新建FaceDetectionActivity

    新建FaceDetectionActivity,并将其设为默认的Activity,然后修改其XML布局

    
    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    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")
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    同时,需要在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) {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    拷贝完成后,调用init()方法,传入路径

    override fun onResume() {
            super.onResume()
    		//省略了其他代码...
    
    		//Utils类可以在本文末尾复制
            val path = Utils.getModelFile(
                this@FaceDetectionActivity,
                "haarcascade_frontalface_alt.xml"
            ).absolutePath
            
            init(path)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5. 实现init方法

    init方法也是一样的,声明tracker对象,并调用run()方法,会启动一个异步线程,后面的人脸检测会在这个异步线程进行检测了。(这个是保障实时人脸检测不卡的前提)

    //cv::Ptr 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(mainDetector, trackingDetector, DetectorParams);
        tracker= new DetectionBasedTracker(mainDetector, trackingDetector, DetectorParams);
        tracker->run();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    5. 设置Surface

    Android NDK 中,ANativeWindow 是一个C/C++接口,它提供了一种在 CC++ 代码中访问 Android Surface 的方式。通过使用ANativeWindow接口,开发者可以在NDK中直接访问和操作Android窗口系统,实现图形处理和渲染操作。
    这里我们将SurfaceView传到JNI中,方便C/C++代码后边将图像实时渲染到Android SurfaceView上。

    #include 
    
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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) {
        //待实现的代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.1 将图片转化为Mat

    参数中的jbyteArray data_,是从Android Java层中获取到的。
    然后转化为jbyte *data这个字节数组,接着将其转为一个Mat矩阵。
    Mat是是OpenCV最基本的数据结构。它用于存储图像数据。
    需要注意的是,由于传入的数据是YUV420,每个像素占1.5byte
    所以这个图像的宽度需要传入实际宽度*1.5,宽度不变。
    这里的CV_8UC1是单通道的意思,就是说无论是Y分量还是UV分量,都存储在同一个通道里。

    jbyte *data = env->GetByteArrayElements(data_, NULL);
    Mat src(h + h / 2, w, CV_8UC1, data);
    
    • 1
    • 2

    要获取Mat对象中每个通道的数据,可以使用OpenCV提供的函数和方法。
    对于一个BGR图像(即具有三个通道的图像),每个通道的数据可以分别获取并进行处理。
    以下是一些示例代码,演示如何获取每个通道的数据:

    // 假设img是包含BGR图像的Mat对象    
    // 获取B通道数据   
    Mat bChannel = img.channel(0);   
    // 获取G通道数据   
    Mat gChannel = img.channel(1);   
    //获取R通道数据   
    Mat rChannel = img.channel(2); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6.2 将YUV转为RGBA

    接着,需要将YUV格式转化为RGBA格式,方便后续的操作。
    这里的COLOR_YUV2RGBA_NV21表示原始是NV21YUV格式,将其转为RGBA格式。

    cvtColor(src, src, COLOR_YUV2RGBA_NV21);
    
    • 1

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    6.4 转为灰度图并进行直方图均衡化处理

    接着需要对图像进行灰度和直方图均衡化处理,以便提高人脸识别的准确性和可靠性,这部分和VS实现上是一样。

    Mat gray;
    //转为灰度图
    cvtColor(src, gray, COLOR_RGBA2GRAY);
    //直方图均衡化
    equalizeHist(gray, gray);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.5 进行人脸检测

    接着就可以调用tracker->process来建人脸检测了。
    检测完成后,接着调用tracker->getObjects将检测的人脸位置赋值给faces

    std::vector<Rect> faces;
    tracker->process(gray);
    tracker->getObjects(faces);
    
    • 1
    • 2
    • 3

    6.6 将人脸用红框框出来

    接着,将识别到的人脸,用红色的矩形框绘制出来,rectangle方法就是用来绘制一个矩形框的方法。

    for (Rect face : faces) {
        rectangle(src, face, Scalar(255, 0, 0));
    }
    
    • 1
    • 2
    • 3

    6.7 将图像渲染到SurfaceView上

    6.7.1 设置窗口缓冲区

    ANativeWindow_setBuffersGeometry是设置Android Native窗口的缓冲区的大小和像素格式。

    if (window) {
        ANativeWindow_setBuffersGeometry(window, src.cols, src.rows, WINDOW_FORMAT_RGBA_8888);
    	
    	//后续代码在这里编写...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    6.8 回收资源

    最后,别忘了回收资源

    src.release();
    gray.release();
    env->ReleaseByteArrayElements(data_, data, 0);
    
    • 1
    • 2
    • 3

    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博客

    ❤️ 如果觉得这篇博文写的不错,对你有所帮助,帮忙点个赞👍
    ⭐ 这是对我持续输出高质量博文的最好鼓励。😄

  • 相关阅读:
    动态添加二级表头 You may have an infinite update loop in a component rende 9 function.
    数据分享 | 全球水系流域河流湖泊污水处理河流类型矢量数据
    Python | 根据子列表中的第二个元素对列表进行排序
    C# 12 中的新增功能
    C++--简单实现定长内存池
    猿创征文|详解从0开始的嵌入式学习路线,学什么、怎么学?
    Python是“真火”还是“虚火”?
    深入浅出Python正则表达式:原理与应用
    DMSQL学习笔记
    C语言 结构体和共用体——对结构体的操作
  • 原文地址:https://blog.csdn.net/EthanCo/article/details/131980042