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

发布于:2024-05-08 ⋅ 阅读:(19) ⋅ 点赞:(0)

OpenCV 入门系列:

OpenCV 入门(一)—— OpenCV 基础
OpenCV 入门(二)—— 车牌定位
OpenCV 入门(三)—— 车牌筛选
OpenCV 入门(四)—— 车牌号识别
OpenCV 入门(五)—— 人脸识别模型训练与 Windows 下的人脸识别
OpenCV 入门(六)—— Android 下的人脸识别
OpenCV 入门(七)—— 身份证识别

本篇我们来介绍在 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 的版本历史使用指南代码示例


网站公告

今日签到

点亮在社区的每一天
去签到