OpenCV(六) —— Android 下的人脸识别

本篇我们来介绍在 Android 下如何实现人脸识别。

上一篇我们介绍了如何在 Windows 下通过 OpenCV 实现人脸识别,实际上,在 Android 下的实现的核心原理是非常相似的,因为 OpenCV 部分的代码改动不大,绝大部分代码可以直接移植到 Android 上。最主要的区别是,Android 摄像头采集图像的代码要复杂一些,而 Windows 下几行代码就搞定了。

目前有四种方式来使用 Android Camera:

  • Camera1:虽然被 @Deprecated 了,但是很多产品中仍然在使用它,比如一些推流 SDK
  • Camera2:比 Camera1 更灵活,可定制性更强,但是用起来有些麻烦
  • CameraX:Jetpack 组件,封装了 Camera2,通过提供一致且易用的 API 接口来简化相机应用的开发工作
  • NDKCamera:无法兼容低版本

我们会介绍 Camera1 和 CameraX 两种方式。

1、使用 Camera1 进行人脸识别

1.1 开启摄像头

我们将 Camera1 的相关操作封装到 CameraHelper 中:

class CameraHelper(
    private var mCameraId: Int,
    private var mHeight: Int,
    private var mWidth: Int
) : Camera.PreviewCallback {

    private var mCamera: Camera? = null
    private lateinit var mBuffer: ByteArray
    private var mPreviewCallback: Camera.PreviewCallback? = null

    fun startPreview() {
        // 开启摄像头,获取 Camera 对象
        mCamera = Camera.open(mCameraId)
        if (mCamera == null) {
            Log.d(TAG, "Open camera failed.")
            return
        }
        // 配置 Camera 参数
        val cameraParams = mCamera?.parameters
        // 设置预览数据格式为 NV21
        cameraParams?.previewFormat = ImageFormat.NV21
        // 设置摄像头宽高
        cameraParams?.setPreviewSize(mWidth,mHeight)
        // 更新 Camera 参数
        mCamera?.parameters = cameraParams
        // 摄像头采集的是 YUV NV21 格式的数据,mBuffer 承载预览数据
        mBuffer = ByteArray(mWidth * mHeight * 3 / 2)
        // 设置预览的回调以及缓冲区
        // 将摄像头获取的数据放入 mBuffer
        mCamera?.addCallbackBuffer(mBuffer)
        mCamera?.setPreviewCallbackWithBuffer(this)
        // 设置预览画面
        mCamera?.setPreviewTexture(SurfaceTexture(11))
        mCamera?.startPreview()
    }

    private fun stopPreview() {
        mCamera?.setPreviewCallback(null)
        mCamera?.stopPreview()
        mCamera?.release()
        mCamera = null
    }

    override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
        if (data == null) {
            Log.d(TAG, "onPreviewFrame: data 为空,直接返回")
            return
        }
        // 注意回调给外界的图像是横向的
        mPreviewCallback?.onPreviewFrame(data, camera)
        mCamera?.addCallbackBuffer(mBuffer)
    }

    fun switchCamera() {
        // 切换摄像头 ID 再重启预览
        mCameraId = if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            Camera.CameraInfo.CAMERA_FACING_BACK
        } else {
            Camera.CameraInfo.CAMERA_FACING_FRONT
        }
        stopPreview()
        startPreview()
    }

    fun setPreviewCallback(previewCallback: Camera.PreviewCallback) {
        mPreviewCallback = previewCallback
    }
	...
}

需要特别注意 startPreview() 内设置预览画面要设置给 SurfaceTexture 而不是 SurfaceHolder。因为 SurfaceHolder 是会对 SurfaceView.SurfaceHolder.getSurface() 获取到的 Surface 对象的生命周期和渲染进行直接管理的,这就导致我们在 Native 层获取由该 Surface 创建的 ANativeWindow 的锁,即调用 ANativeWindow_lock() 会一直失败,进而无法渲染。

由于我们需要在 Native 层将 OpenCV 识别的人脸范围用矩形框画出来,所以预览就交给 SurfaceTexture。

接下来由 Activity 控制 CameraHelper 开启预览:

	private lateinit var mOpenCVJNI: OpenCVJNI
    private lateinit var mCameraHelper: CameraHelper
    private var mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.surfaceView.holder.addCallback(this)
        binding.btnSwitchCamera.setOnClickListener {
            mCameraHelper.switchCamera()
            mCameraId = mCameraHelper.getCameraId()
        }

        mOpenCVJNI = OpenCVJNI()
        mCameraHelper = CameraHelper(mCameraId, 480, 640)
        mCameraHelper.setPreviewCallback(this)

        // 将 assets 下的 lbpcascade_frontalface.xml 拷贝到手机同名文件中
        Utils.copyAssets(this, "lbpcascade_frontalface.xml")
    }

	override fun onResume() {
        super.onResume()

        // 开启摄像头预览
        mCameraHelper.startPreview()

        // 初始化 OpenCV
        val path = File(
            Environment.getExternalStorageDirectory(),
            "lbpcascade_frontalface.xml"
        ).absolutePath
        mOpenCVJNI.init(path)
    }

这样我们就可以在页面中看到摄像头采集到的预览画面了。

1.2 其余初始化工作

开启摄像头的代码中,有涉及到创建以及初始化 OpenCVJNI 对象,该对象就是上层与 Native 层 OpenCV API 交互的桥梁:

class OpenCVJNI {

    fun init(path: String) {
        nativeInit(path)
    }

    fun postData(data: ByteArray, width: Int, height: Int, cameraId: Int) {
        nativePostData(data, width, height, cameraId)
    }

    fun setSurface(surface: Surface) {
        nativeSetSurface(surface)
    }

    private external fun nativeInit(path: String)
    private external fun nativePostData(data: ByteArray, width: Int, height: Int, cameraId: Int)
    private external fun nativeSetSurface(surface: Surface)

    companion object {
        init {
            System.loadLibrary("opencv")
        }
    }
}

由于 Windows Demo 中我们使用的是 HAAR 级联分类器,所以 Android Demo 我们换一个,使用 LBP 级联分类器。将 OpenCV-android-sdk\sdk\etc\lbpcascades\lbpcascade_frontalface.xml 拷贝到项目的 /src/main/assets/ 目录下。并通过 copyAssets() 将文件拷贝到手机中:

class Utils {

    companion object {

        /**
         * 将 assets 目录下的文件 path 的内容复制到手机的 path 文件中
         */
        fun copyAssets(context: Context, path: String) {
            val file = File(Environment.getExternalStorageDirectory(), path)
            if (file.exists()) {
                file.delete()
            }

            var fileOutputStream: FileOutputStream? = null
            var inputStream: InputStream? = null
            try {
                fileOutputStream = FileOutputStream(file)
                inputStream = context.assets.open(path)

                val buffer = ByteArray(2048)
                var length = inputStream.read(buffer)
                while (length > 0) {
                    fileOutputStream.write(buffer, 0, length)
                    length = inputStream.read(buffer)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                fileOutputStream?.close()
                inputStream?.close()
            }
        }
    }
}

上层代码基本就这样了,接下来就是看上层如何调用 OpenCV 的 Native API 实现人脸识别了。

1.3 Native 层实现

Native 层实现主要包括三方面:

  1. OpenCV 的初始化
  2. 负责底层绘制的 ANativeWindow 初始化
  3. 接收上层传递的图像数据进行识别

OpenCV 的初始化是通过 OpenCVJNI 的 init() 调用 Native 方法 nativeInit() 实现的:

#include "opencv2/opencv.hpp"
#include <jni.h>
#include <android/native_window_jni.h>

using namespace cv;

DetectionBasedTracker *tracker = nullptr;

class CascadeDetectorAdapter : public DetectionBasedTracker::IDetector {
public:
    CascadeDetectorAdapter(cv::Ptr<cv::CascadeClassifier> detector) :
            IDetector(),
            Detector(detector) {
    }

    // 检测人脸的函数,Mat 相当于 Android 的一张 Bitmap。一张图片有几个人脸就会调用本方法几次
    void detect(const cv::Mat &Image, std::vector<cv::Rect> &objects) {
        Detector->detectMultiScale(Image, objects, scaleFactor,
                                   minNeighbours, 0, minObjSize, maxObjSize);
    }

    virtual ~CascadeDetectorAdapter() = default;

private:
    CascadeDetectorAdapter();

    cv::Ptr<cv::CascadeClassifier> Detector;
};

extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativeInit(JNIEnv *env, jobject thiz, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, nullptr);

    // 创建检测器
    Ptr<CascadeClassifier> detectorClassifier = makePtr<CascadeClassifier>(path);
    Ptr<CascadeDetectorAdapter> mainDetector = makePtr<CascadeDetectorAdapter>(detectorClassifier);
    // 创建跟踪器
    Ptr<CascadeClassifier> trackerClassifier = makePtr<CascadeClassifier>(path);
    Ptr<CascadeDetectorAdapter> trackingDetector = makePtr<CascadeDetectorAdapter>(
            trackerClassifier);

    // 创建 DetectionBasedTracker
    DetectionBasedTracker::Parameters detectionParams;
    tracker = new DetectionBasedTracker(mainDetector, trackingDetector, detectionParams);
    // run() 会开启维护死循环的线程,当开启摄像头预览调用 tracker->process() 
    // 传入人脸数据时,线程会返回一个包含人脸结构的 face 集合给你
    tracker->run();

    env->ReleaseStringUTFChars(path_, path);
}

与 Windows 几乎相同,创建 DetectionBasedTracker 需要主检测器 mainDetector 和跟踪器 trackingDetector,创建两个适配器所需的 CascadeDetectorAdapter 还是来自 OpenCV 的官方 Sample 代码。

然后是底层绘制窗口 ANativeWindow 的初始化。它的初始化由 Activity 的 SurfaceView 的创建/变化触发:

class MainActivity : AppCompatActivity(), Camera.PreviewCallback, SurfaceHolder.Callback {
    // SurfaceHolder.Callback start
    override fun surfaceCreated(holder: SurfaceHolder) {
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        mOpenCVJNI.setSurface(holder.surface)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
    }
    // SurfaceHolder.Callback end
}

进入到 Native 层,需要先释放原有的 ANativeWindow 对象重新分配:

extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativeSetSurface(JNIEnv *env, jobject thiz, jobject surface) {
    if (window) {
        ANativeWindow_release(window);
        window = nullptr;
    }

    window = ANativeWindow_fromSurface(env, surface);
}

最后就是通过 ANativeWindow 绘制了,绘制的数据来自于上层 Camera 的回调数据:

class MainActivity : AppCompatActivity(), Camera.PreviewCallback, SurfaceHolder.Callback {
    override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
        if (data == null) {
            return
        }

        mOpenCVJNI.postData(data, mCameraHelper.getWidth(), mCameraHelper.getHeight(), mCameraId)
    }
}

Native 层拿到 data 先用 OpenCV 进行人脸识别,在识别出来的人脸区域画一个矩形:

/**
 * 中间过程可以通过 imwrite(String,Mat) 将 Mat 图片输出到手机
 * 指定路径查看中间效果以验证编程是否正确
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativePostData(JNIEnv *env, jobject thiz, jbyteArray data_,
                                                    jint width, jint height, jint camera_id) {
    jbyte *data = env->GetByteArrayElements(data_, nullptr);

    // 创建一个 Mat 对象,Mat 相当于一张 Bitmap,由于传入的是 YUV 数据,因此高度是像素高度的 3/2
    Mat src(height * 3 / 2, width, CV_8UC1, data);
    // 将 src 内的 NV21 数据转换为 RGBA 数据后再赋值给 src
    cvtColor(src, src, COLOR_YUV2RGBA_NV21);
    // 对原始摄像头图像进行旋转调正
    if (camera_id == 1) {
        // 前置摄像头需要逆时针旋转 90°
        rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
        // 前置还需要取一个水平方向的镜像,如果传 0 就是竖直方向
        flip(src, src, 1);
    } else {
        // 后置摄像头需要顺时针旋转 90°
        rotate(src, src, ROTATE_90_CLOCKWISE);
    }

    // 图片调整后开始进行识别,首先要将图片转换为灰度图,可以减少杂色增加识别几率
    Mat gray;
    cvtColor(src, gray, COLOR_RGBA2GRAY);

    // 增强对比度,目的是增强轮廓(因为识别是对轮廓进行识别)
    equalizeHist(gray, gray);

    // 检测人脸,结果保存到 faces 中
    std::vector<Rect> faces;
    tracker->process(gray);
    tracker->getObjects(faces);

    // 遍历检测到的人脸(一张图片内可能有多个人脸)
    for (const Rect &face: faces) {
        // 画个方框
        rectangle(src, face, Scalar(255, 0, 255));
        // 如果需要获取训练素材,就将人脸图像转换成 24 * 24 的灰度图保存到手机指定目录中
        if (needTraining) {
            // 拷贝人脸数据(获取正样本)
            Mat m;
            src(face).copyTo(m);
            // 将大小调整为 24x24 的,并且设置为灰度图,然后拷贝到手机的指定目录下
            resize(m, m, Size(24, 24));
            cvtColor(m, m, COLOR_BGR2GRAY);
            char p[100];
            // 注意如果路径不存在需要手动先创建文件夹,否则不会自动生成目录
            sprintf(p, "/storage/emulated/0/FaceTest/%d.jpg", index++);
            imwrite(p, m);
        }
    }

    if (window) {
        ANativeWindow_setBuffersGeometry(window, src.cols, src.rows, WINDOW_FORMAT_RGBA_8888);
        ANativeWindow_Buffer window_buffer;
        do {
            // 如果上锁失败就直接 break
            // 起初一直上锁失败,原因是 CameraHelper 中使用 SurfaceHolder 进行预览而不是 SurfaceTexture
            if (ANativeWindow_lock(window, &window_buffer, nullptr)) {
                ANativeWindow_release(window);
                window = nullptr;
                break;
            }

            // 画图,将 Mat 的 data 指针指向的像素数据逐行拷贝到 window_buffer.bits 中
            auto dst_data = static_cast<uint8_t *>(window_buffer.bits);
            int dst_line_size = window_buffer.stride * 4;
            for (int i = 0; i < window_buffer.height; ++i) {
                // Mat 内的数据是 RGBA,因此计算每行首地址时,要在后面乘以 4,表示 RGBA8888 各占 1 个字节
                memcpy(dst_data + i * dst_line_size, src.data + i * src.cols * 4, dst_line_size);
            }

            // 提交刷新
            ANativeWindow_unlockAndPost(window);
        } while (false);
    }

    src.release();
    gray.release();

    env->ReleaseByteArrayElements(data_, data, 0);
}

主要步骤,包括获取人脸训练素材的步骤都与 Windows 基本一致,区别在于 Android 需要将摄像头采集的图像旋转 90° 调正,并且需要将图像数据拷贝到 ANativeWindow 的缓冲区以实现图像渲染。

使用 Android 后置摄像头进行人脸识别的效果如下:

在这里插入图片描述

2、使用 CameraX 进行人脸识别

2.1 初始化

首先引入 CameraX 的依赖,完整的引入内容如下,但是本 Demo 只用到了 core、camera2 和 lifecycle 三项:

dependencies {
  def camerax_version = "1.0.0"
  // The following line is optional, as the core library is included indirectly by camera-camera2
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  // If you want to additionally use the CameraX Lifecycle library
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  // If you want to additionally use the CameraX View class
  implementation "androidx.camera:camera-view:${camerax_version}"
  // If you want to additionally use the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

由于 CameraX 已经对 Camera2 进行了封装,因此我们可以直接使用,而无需像前面的例子那样自己封装一个 CameraHelper 了。

首先我们在 Activity 的 onCreate() 中进行初始化工作:

class RecognitionActivity : AppCompatActivity(), SurfaceHolder.Callback, ImageAnalysis.Analyzer {
    
	private lateinit var mCameraProviderFuture: ListenableFuture<ProcessCameraProvider>
    private lateinit var mFaceTracker: FaceTracker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityRecognitionBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 权限申请
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE),
            REQUEST_CODE
        )

        // 为 SurfaceHolder 设置回调接口
        binding.surfaceView.holder.addCallback(this)

        // CameraX 初始化,异步获取 CameraProvider 对象
        mCameraProviderFuture = ProcessCameraProvider.getInstance(this)
        mCameraProviderFuture.addListener({
            try {
                val cameraProvider = mCameraProviderFuture.get()
                bindAnalysis(cameraProvider)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))

        // 将识别模型拷贝到手机中
        val modelPath = Utils.copyAsset2Dir(this, "lbpcascade_frontalface.xml")

        // 初始化 FaceTracker 开启人脸检测
        mFaceTracker = FaceTracker(modelPath)
        mFaceTracker.start()
    }
}

CameraX

对 CameraX 进行异步初始化,先通过 ProcessCameraProvider.getInstance() 获取到 ListenableFuture<ProcessCameraProvider>

	/**
	* Futures.transform() 的三个参数:
	* CameraX.getOrCreateInstance() 会返回一个包含已经初始化的 CameraX 对象的 ListenableFuture
	* cameraX -> {} 是一个函数,参数 cameraX 是第一个参数的泛型对象,即 CameraX
	* CameraXExecutors.directExecutor() 会返回主调线程中缓存的会直接执行任务的 Executor
	* 会在指定的 Executor 中异步执行函数
	*/
	public static ListenableFuture<ProcessCameraProvider> getInstance(
            @NonNull Context context) {
        Preconditions.checkNotNull(context);
        return Futures.transform(CameraX.getOrCreateInstance(context), cameraX ->  {
            sAppInstance.setCameraX(cameraX);
            return sAppInstance;
        }, CameraXExecutors.directExecutor());
    }

随后为 mCameraProviderFuture 设置监听,异步获取到 CameraProvider 对象,并将其与生命周期绑定:

	private fun bindAnalysis(cameraProvider: ProcessCameraProvider?) {
        if (cameraProvider == null) {
            return
        }

        /**
         * 图片分析:得到摄像头图像数据
         * STRATEGY_KEEP_ONLY_LATEST:非阻塞模式,每次获得最新帧
         * STRATEGY_BLOCK_PRODUCER:阻塞模式,会得到每一张图片,处理不及时会导致帧率降低
         */
        val imageAnalysis = ImageAnalysis.Builder()
            // CameraX 会根据传入尺寸选择最佳的预览尺寸
            .setTargetResolution(Size(640, 480))
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build()

        // 设置分析器,指定回调所发生的线程(池)
        imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), this)

        // 绑定生命周期
        cameraProvider.unbindAll()
        cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_FRONT_CAMERA, imageAnalysis)
    }

FaceTracker

FaceTracker 是上层与 Native 交互的类:

class FaceTracker(modelPath: String) {

    // 实际上是将上层的 FaceTracker 与 Native 的 FaceTracker 绑定
    // 上层以 Native 对象地址的形式持有 Native 对象,这样做的目的是
    // 让上层持有 C++ 对象,当上层将地址传回给 Native 层时,C++ 可以
    // 将地址强转回成一个 C++ 对象并操作该对象,这样能实现多对多的绑定
    private var mFaceTracker = 0L

    init {
        mFaceTracker = nativeInit(modelPath)
    }

    fun setSurface(surface: Surface?) {
        nativeSetSurface(mFaceTracker, surface)
    }

    fun detect(bytes: ByteArray, width: Int, height: Int, rotationDegrees: Int) {
        nativeDetect(mFaceTracker, bytes, width, height, rotationDegrees)
    }

    fun start() {
        nativeStart(mFaceTracker)
    }

    fun stop() {
        nativeStop(mFaceTracker)
    }

    fun release() {
        nativeRelease(mFaceTracker)
        mFaceTracker = 0
    }

    private external fun nativeInit(modelPath: String): Long

    private external fun nativeSetSurface(faceTracker: Long, surface: Surface?)

    private external fun nativeDetect(
        faceTracker: Long,
        bytes: ByteArray,
        width: Int,
        height: Int,
        rotationDegrees: Int
    )

    private external fun nativeStart(faceTracker: Long)

    private external fun nativeStop(faceTracker: Long)

    private external fun nativeRelease(faceTracker: Long)
}

nativeInit() 就是创建一个 Native 的 FaceTracker 对象,然后将该对象的地址返回给上层:

extern "C"
JNIEXPORT jlong JNICALL
Java_com_face_recognition_FaceTracker_nativeInit(JNIEnv *env, jobject thiz, jstring model_path) {

    const char *path = env->GetStringUTFChars(model_path, 0);

    // 初始化FaceTracker对象
    auto *tracker = new FaceTracker(path);

    env->ReleaseStringUTFChars(model_path, path);

    return (jlong) tracker;
}

此外,在布局中的 SurfaceView 的 SurfaceHolder 添加 SurfaceHolder.Callback 的回调方法中,需要通过 FaceTracker 将 Surface 传给 Native 层:

	// SurfaceHolder.Callback start
    override fun surfaceCreated(holder: SurfaceHolder) {
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        mFaceTracker.setSurface(holder.surface)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        mFaceTracker.setSurface(null)
    }
    // SurfaceHolder.Callback end

nativeSetSurface() 会通过上层传来的 Surface 创建 Native 层的 ANativeWindow 对象:

extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition_FaceTracker_nativeSetSurface(JNIEnv *env, jobject thiz,
                                                       jlong face_tracker, jobject surface) {

    if (face_tracker != 0) {
        auto *tracker = reinterpret_cast<FaceTracker *>(face_tracker);
        if (window) {
            ANativeWindow_release(window);
            window = nullptr;
        }
        window = ANativeWindow_fromSurface(env, surface);
        tracker->setNativeWindow(window);
    }
}

2.2 人脸识别

初始化 CameraX 时在 bindAnalysis() 中设置了分析器:

		// 设置分析器,指定回调所发生的线程(池)
        imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), this)

第二个参数是 ImageAnalysis.Analyzer 接口,我们在 Activity 中实现它,接收摄像头采集到的数据:

	// ImageAnalysis.Analyzer
    override fun analyze(image: ImageProxy) {
        val bytes = Utils.getDataFromImage(image)
        mFaceTracker.detect(bytes, image.width, image.height, image.imageInfo.rotationDegrees)
        image.close()
    }

先从 ImageProxy 中提取出图像数据的 Byte 数组:

		fun getDataFromImage(image: ImageProxy): ByteArray {
            // 1.获取图像的宽高以及格式,计算出图片大小字节数
            val rect = image.cropRect
            val imageWidth = rect.width()
            val imageHeight = rect.height()
            val format = image.format
            val size = imageWidth * imageHeight * ImageFormat.getBitsPerPixel(format) / 8

            // 2.为 data 和 rowData 分配内存
            val data = ByteArray(size)

            // planes 是一个数组,每个元素是一个 ImageProxy.Plane 对象,
            // Y、U、V 每种像素对应一个平面,分别是 planes[0]、planes[1]、
            // planes[2],每个 Plane 包含该平面图像数据的 ByteBuffer 对象
            val planes = image.planes
            val rowData = ByteArray(planes[0].rowStride)

            // 3.将 image 图像数据拷贝到 data 中,拷贝时按照 Y、U、V
            // 三个平面分开拷贝
            var channelOffset: Int
            for (i in planes.indices) {
                channelOffset = when (i) {
                    // y 从 0 开始
                    0 -> 0
                    // u 从 y 之后开始
                    1 -> imageWidth * imageHeight
                    // v 从 u 之后开始,u 的数据长度为 width * height / 4
                    2 -> (imageWidth * imageHeight * 1.25).toInt()
                    else -> throw IllegalArgumentException("Unexpected number of image planes")
                }

                // 这一个平面的数据缓冲区
                val buffer = planes[i].buffer
                // 行跨度,一行的步长,即这一行有像素数据所占用的字节数
                val rowStride = planes[i].rowStride
                // 像素跨度,即每一个像素占用的字节数,例如 RGB 就为 3
                val pixelStride = planes[i].pixelStride

                // UV 只有一半,因此要右移 1 位
                val shift = if (i == 0) 0 else 1
                val width = imageWidth shr shift
                val height = imageHeight shr shift

                // 移动到每个平面在 buffer 中的起始位置,准备读取该平面的数据
                buffer.position(rowStride * (rect.top shr shift) + pixelStride * (rect.left shr shift))

                var length: Int
                for (row in 0 until height) {
                    if (pixelStride == 1) {
                        length = width
                        buffer.get(data, channelOffset, length)
                        channelOffset += length
                    } else {
                        length = (width - 1) * pixelStride + 1
                        buffer.get(rowData, 0, length)
                        for (col in 0 until width) {
                            data[channelOffset++] = rowData[col * pixelStride]
                        }
                    }
                    if (row < height - 1) {
                        buffer.position(buffer.position() + rowStride - length)
                    }
                }
            }
            return data
        }

然后将像素数据、图片宽高和旋转角度通过 FaceTracker 传递到 Native 层进行人脸检测:

	fun detect(bytes: ByteArray, width: Int, height: Int, rotationDegrees: Int) {
        nativeDetect(mFaceTracker, bytes, width, height, rotationDegrees)
    }

	private external fun nativeDetect(
        faceTracker: Long,
        bytes: ByteArray,
        width: Int,
        height: Int,
        rotationDegrees: Int
    )

来到 Native 层,将检测请求转发给 FaceTracker:

extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition_FaceTracker_nativeDetect(JNIEnv *env, jobject thiz, jlong face_tracker,
                                                   jbyteArray bytes, jint width, jint height,
                                                   jint rotation_degrees) {
    if (face_tracker != 0) {
        jbyte *data = env->GetByteArrayElements(bytes, nullptr);
        auto *tracker = (FaceTracker *) face_tracker;
        // 声明时将 detect() 的 data 的 jbyte 改为 int8_t,两个类型是一回事但是 cpp 中最好不要用 JNI 类型
        tracker->detect(data, width, height, rotation_degrees);
        env->ReleaseByteArrayElements(bytes, data, 0);
    }
}

FaceTracker 收到图像数据后,先创建 OpenCV 的图像对象 Mat,将其转换成 RGBA 格式再旋转为正向,然后开始灰度化、直方图等人脸识别过程:

void FaceTracker::detect(int8_t *data, int width, int height, int rotation_degrees) {
    // src 接收的是 YUV I420 的数据,因此高度应该是 height 的 1.5 倍
    Mat src(height * 3 / 2, width, CV_8UC1, data);
    // 将 YUV I420 格式的 src 转换为 RGBA 格式
    cvtColor(src, src, COLOR_YUV2RGBA_I420);
    // 调整图像,将其旋转为正向
    if (rotation_degrees == 90) {
        rotate(src, src, ROTATE_90_CLOCKWISE);
    } else if (rotation_degrees == 270) {
        rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
        // 水平翻转
        flip(src, src, 1);
    }

    // 灰度化、增强对比度
    Mat gray;
    cvtColor(src, gray, COLOR_RGBA2GRAY);
    equalizeHist(gray, gray);

    // 检测
    tracker->process(gray);

    // 获取检测结果
    std::vector<Rect> faces;
    tracker->getObjects(faces);

    // 画矩形
    for (const Rect &face: faces) {
        rectangle(src, face, Scalar(0, 255, 0));
    }

    // 绘制 src
    draw(src);

    // 释放
    src.release();
    gray.release();
}

最后在 draw() 中将画了矩形人脸框的 Mat 对象绘制到 ANativeWindow 上:

void FaceTracker::draw(const Mat &img) {
    pthread_mutex_lock(&mutex);

    // do-while(false) 是为了进行流程控制,在不满足条件时直接退出
    // 循环执行解锁操作,否则需要写多次解锁代码
    do {
        if (!window) {
            break;
        }

        // 设置 Window Buffer 的格式与大小
        ANativeWindow_setBuffersGeometry(window, img.cols, img.rows, WINDOW_FORMAT_RGBA_8888);
        ANativeWindow_Buffer buffer;

        // 上锁,目的是为了拿到 buffer
        if (ANativeWindow_lock(window, &buffer, nullptr)) {
            ANativeWindow_release(window);
            window = nullptr;
            break;
        }

        // 获取 buffer 保存实际数据的地址以及步长
        auto dstData = static_cast<uint8_t *>(buffer.bits);
        int dstLineSize = buffer.stride * 4;

        // 获取图片数据的起始地址与步长
        uint8_t *srcData = img.data;
        int srcLineSize = img.cols * 4;

        // 逐行拷贝图像数据到 buffer.bits
        for (int i = 0; i < buffer.height; ++i) {
            memcpy(dstData + i * dstLineSize, srcData + i * srcLineSize, srcLineSize);
        }

        ANativeWindow_unlockAndPost(window);
    } while (false);

    pthread_mutex_unlock(&mutex);
}

至此,Android 实现人脸识别的两个例子讲解完毕。

参考资料:

CameraX 的版本历史、使用指南、代码示例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/592604.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Pytorch: nn.Embedding

文章目录 1. 本质2. 用Embedding产生一个10 x 5 的随机词典3. 用这个词典编码两个简单单词4. Embedding的词典是可以学习的5. 例子完整代码 1. 本质 P y t o r c h \mathrm{Pytorch} Pytorch 的 E m b e d d i n g \mathrm{Embedding} Embedding 模块是一个简单的查找表&#…

【多变量控制系统 Multivariable Control System】(3)系统的状态空间模型至转换方程模型(使用Python)【新加坡南洋理工大学】

一、转换式 二、系统的状态空间模型 由矩阵A, B, C, D给出&#xff1a; 三、由状态空间模型转化为转换方程模型 函数原型&#xff08;版权所有&#xff1a;scipy&#xff09;&#xff1a; def ss2tf(A, B, C, D, input0):r"""State-space to transfer functi…

【netty系列-03】深入理解NIO的基本原理和底层实现(详解)

Netty系列整体栏目 内容链接地址【一】深入理解网络通信基本原理和tcp/ip协议https://zhenghuisheng.blog.csdn.net/article/details/136359640【二】深入理解Socket本质和BIOhttps://zhenghuisheng.blog.csdn.net/article/details/136549478【三】深入理解NIO的基本原理和底层…

SpringCloud Alibaba Nacos简单应用(三)

文章目录 SpringCloud Alibaba Nacos创建Nacos 的服务消费者需求说明/图解创建member-service-nacos-consumer-80 并注册到NacosServer8848创建member-service-nacos-consumer-80修改pom.xml创建application.yml创建主启动类业务类测试 SpringCloud Alibaba Nacos 创建Nacos 的…

鸿蒙通用组件Image简介

鸿蒙通用组件Image简介 图片----Image图片支持三种引用方式设置图片宽高设置图片缩放模式设置图片占位图设置图片重复样式设置图片插值效果 图片----Image Image主要用于在应用中展示图片 Image($r(app.media.app_icon)).width(150) // 设置宽.height(150) // 设置高.objectF…

使用docker-compose编排lnmp(dockerfile)完成wordpress

文章目录 使用docker-compose编排lnmp&#xff08;dockerfile&#xff09;完成wordpress1、服务器环境2、Docker、Docker-Compose环境安装2.1 安装Docker环境2.2 安装Docker-Compose 3、nginx3.1 新建目录&#xff0c;上传安装包3.2 编辑Dockerfile脚本3.3 准备nginx.conf配置文…

redis集群-主从机连接过程

首先从机需要发送自身携带的replid和offset向主机请求连接 replid&#xff1a;replid是所有主机在启动时会生成的一个固定标识&#xff0c;它表示当前复制流的id&#xff0c;当从机第一次请求连接时&#xff0c;主机会将自己的replid发送给从机&#xff0c;从机在接下来的请求…

docker部署nginx并配置https

1.准备SSL证书&#xff1a; 生成私钥&#xff1a;运行以下命令生成一个私钥文件。 生成证书请求&#xff08;CSR&#xff09;&#xff1a;运行以下命令生成证书请求文件。 生成自签名证书&#xff1a;使用以下命令生成自签名证书。 openssl genrsa -out example.com.key 2048 …

【Java探索之旅】内部类 静态、实例、局部、匿名内部类全面解析

文章目录 &#x1f4d1;前言一、内部类1.1 概念1.2 静态内部类1.3 实例内部类1.4 局部内部类1.5 匿名内部类 &#x1f324;️全篇总结 &#x1f4d1;前言 在Java编程中&#xff0c;内部类是一种强大的特性&#xff0c;允许在一个类的内部定义另一个类&#xff0c;从而实现更好的…

Vue3-element-plus表格

一、element-plus 1.用组件属性实现跳转路由 <el-menu active-text-color"#ffd04b" background-color"#232323" :default-active"$route.path" //高亮 text-color"#fff"router><el-menu-item index"/article/channe…

第十篇:深入文件夹:Python中的文件管理和自动化技术

深入文件夹&#xff1a;Python中的文件管理和自动化技术 1 文件系统基础操作 在今天的技术博客中&#xff0c;我们将深入探讨Python中的文件系统基础操作。文件系统对于任何操作系统都是不可或缺的组成部分&#xff0c;它管理着数据的存储、检索以及维护。Python通过其标准库中…

节能洗车房车牌识别项目实战

项目背景 学电子信息的你加入了一家节能环保企业&#xff0c;公司的主营产品是节能型洗车房。由于节水节电而且可自动洗车&#xff0c;产品迅速得到了市场和资本的认可。公司决定继续投入研发新一代产品&#xff1a;在节能洗车房的基础上实现无人值守的功能。新产品需要通过图…

Java高阶私房菜:JVM性能优化案例及讲解

目录 核心思想 优化思考方向 压测环境准备 堆大小配置调优 调优前 调优后 分析结论 垃圾收集器配置调优 调优前 调优后 分析结论 JVM性能优化是一项复杂且耗时的工作&#xff0c;该环节没办法一蹴而就&#xff0c;它需要耐心雕琢&#xff0c;逐步优化至理想状态。“…

Qt服务器端与客户端交互

Qt做客户端与服务器端交互第一步引入network 第一步引入network后继续编程首先界面设计 创建server和socket 引入QTcpServer&#xff0c;QTcpSocket MainWindow.h代码如下 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QTcpServer&…

EPAI手绘建模APP演示板、材质编辑器、样式编辑器

(11) 更多 图 74 更多工具栏 ① 演示板&#xff1a;打开关闭演示板。演示板用来显示从设备导入的模型图纸图片或者打开模型建模教程网页&#xff0c;是建模过程中一个辅助功能。有些设备有小窗口功能有些没有&#xff0c;对于没有小窗口功能的设备&#xff0c;通过演示板能够在…

智慧旅游引领旅游行业创新发展:借助智能科技的力量,实现旅游资源的优化配置和高效利用,推动旅游行业的转型升级和可持续发展

目录 一、引言 二、智慧旅游的定义与特点 1、信息化程度高 2、智能化服务丰富 3、互动性强 4、个性化服务突出 5、可持续性发展 三、智慧旅游在旅游行业创新发展中的作用 &#xff08;一&#xff09;优化旅游资源配置 &#xff08;二&#xff09;提升旅游服务质量 &…

【吃透Java手写】- Spring(上)-启动-扫描-依赖注入-初始化-后置处理器

【吃透Java手写】Spring&#xff08;上&#xff09;启动-扫描-依赖注入-初始化-后置处理器 1 准备工作1.1 创建自己的Spring容器类1.2 创建自己的配置类 ComponentScan1.3 ComponentScan1.3.1 Retention1.3.2 Target 1.4 用户类UserService Component1.5 Component1.6 测试类 2…

HCIA-题目解析1

0x00 前言 遇到这样一道题,这种题目对于我来说还是比较复杂的,所以记录一下。主要还是和熟练度有关系。 0x01 题目 路由器RouterID邻居关系如下,下列说法正确的是 A:本路由器和Router-lD为10.0.3.3的路由器不能直接交换链路状态信息 B:DR路由器的Router-lD为10.0.1.2 C:…

机器学习:基于K-近邻(KNN)、高斯贝叶斯(GaussianNB)、SVC、随机森林(RF)、梯度提升树(GBDT)对葡萄酒质量进行预测

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

Finder Windows for Mac:双系统窗口,一键切换!

Finder Windows for Mac是一款专为Mac用户设计的实用工具&#xff0c;它模拟了Windows系统的窗口管理功能&#xff0c;让Mac用户也能享受到类似Windows的窗口操作体验。这款软件的主要功能是提供一个浮动面板&#xff0c;帮助用户随时即时访问打开的Finder窗口列表&#xff0c;…
最新文章