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

news2024/11/17 12:46:27

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 的版本历史、使用指南、代码示例

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据分析】 JupyterNotebook安装及使用简介

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 在数据分析中&#xff0c;一般用Pycharm编辑代…

【软考】模拟考卷错题本2024-05-11

1 设计模式- 适配器模式 基本上上述的图解已经涵盖了绝大多数主流的设计模式和其特点。理解记忆下即可&#xff0c;这里对下午的考题也有帮助的。 2 计算机组成原理 cpu 访问速度 这个真的是憨憨咯~看到内存就选内存&#xff0c;题目都没审好。这里的速度比cpu内部的要比外部的…

C++入门系列-析构函数

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 析构函数 概念 析构函数&#xff0c;与构造函数功能相反&#xff0c;析构函数不是完成对对象本身的销毁&#xff0c;局部对象销毁工作是由编译器完成的&#xff0c;而对象在销…

【Linux】-网络请求和下载、端口[6]

目录 一、网络请求和下载 1、ping命令 2、wget命令 3、curl命令 二、端口 1、虚拟端口 2、查看端口占用 一、网络请求和下载 1、ping命令 可以通过ping命令&#xff0c;检查指定的网络服务器是否可联通状态 语法&#xff1a;ping [ -c num ] ip或主机名 选项&…

怎么做自己的网站

现如今&#xff0c;拥有自己的网站已经成为现代生活中的一种标志。无论是个人博客、在线商店还是企业官网&#xff0c;都可以通过拥有一个网站来展示自己的个性、产品或服务。在这篇文章中&#xff0c;我将分享如何创建和管理自己的网站。 首先&#xff0c;你需要选择一个合适的…

OGG几何内核开发-BRepAlgoAPI_Fuse与BRep_Builder.MakeCompound比较

最近在与同事讨论BRepAlgoAPI_Fuse与BRep_Builder.MakeCompound有什么区别。 一、从直觉上来说&#xff0c;BRepAlgoAPI_Fuse会对两个实体相交处理&#xff0c;相交的部分会重新的生成相关的曲面。而BRep_Builder.MakeCompound仅仅是把两个实体组合成一个新的实体&#xff0c;…

30%参数达到92%的表现,大模型稀疏化方法显神通

当我还是一位懵懂少年的时候&#xff0c;总认为“任务难度”&#xff0c;“参数规模”和“准确率”是一个不可兼顾的三角&#xff0c;比如当我想要挑战更难的任务&#xff0c;追求获得更高的准确率&#xff0c;那必然就要在更大的模型参数量上进行妥协。然而&#xff0c;真的是…

使用 Docker 部署 VS Code in The Browser

1&#xff09;介绍 GitHub&#xff1a;https://github.com/coder/code-server 在日常学习工作中&#xff0c;Vscode 已成为我们首选的代码编辑器。然而&#xff0c;其局限性在于当我们从家到公司移动时&#xff0c;难以保持连续的编码体验。针对这一痛点&#xff0c;虽然市面上…

OpenCV的视频 I/O 的标志(77)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV 下一篇 :OpenCV系列文章目录&#xff08;持续更新中......&#xff09; ​ 枚举 枚举 cv::VideoCaptureAPIs { cv::CAP_ANY 0, cv::CAP_VFW 200, cv::CAP_V4L 200, cv::CAP_V4L2 …

基于51单片机的二氧化碳检测及调节系统仿真

基于51单片机的二氧化碳检测及调节系统 &#xff08;仿真&#xff0b;程序&#xff09; 功能介绍 具体功能&#xff1a; 1.二氧化碳传感器测得二氧化碳数据后经过单片机处理。 2.LCD1602实时显示&#xff0c;第一行显示测得的浓度值&#xff0c;第二行显示报警阈值。 3.测…

【gpedit.msc】组策略编辑器的安装,针对windows家庭版,没有此功能

创建一个记事本文件然后放入以下内容 echo offpushd "%~dp0"dir /b %systemroot%\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >gp.txtdir /b %systemroot%\servicing\Packages\Microsoft-Windows-GroupPolicy-…

安装Centos7提示dracut:/# 及磁盘空闲不足的坑

前言 这个问题是因为安装程序没有找到安装文件的位置&#xff0c;一般仅出现在安装过Windows系统的电脑上。 如果出现该错误&#xff0c;两种解决办法。 解决 方法一&#xff1a; 开机时&#xff0c;进入启动选项&#xff0c;选择不带UEFI的U盘进行启动可以直接避免这个坑。 …

IIS配置SSL,根据pem和key生成pfx,openssl的版本不能太高

1、生成pfx文件 供应商给的文件是pef和key后缀的两个文件&#xff0c;在IIS里不好导入(如果有知道好导入的可以给我留言&#xff0c;谢谢。)。 1.1 下载OpenSSL工具&#xff0c;并安装。 主要用于将.pem文件转成.pfx文件。 下载OpenSSL的链接&#xff1a;http://slproweb.com/…

地下工程中测斜仪的关键应用

地下工程&#xff0c;如隧道、地铁和基坑等项目的建设&#xff0c;对于现代城市的发展至关重要。然而&#xff0c;这些工程的实施往往伴随着诸多风险&#xff0c;特别是与周围土体的稳定性有关的风险。为了确保工程的安全进行&#xff0c;实时监测技术变得尤为关键。其中&#…

【敦煌网注册/登录安全分析报告】

敦煌网注册/登录安全分析报告 前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大…

Java --- 集合(1)--- 带你了解Collection接口以及三种遍历方式

引言&#xff1a;本期博客摘选黑马程序员与Java从入门到精通&#xff0c;如果有不准确的地方还请指出&#xff0c;另外也感谢各位大佬点击进来观看。 目录 一.什么是集合&#xff1f; 二.单列集合的体系结构&#xff1a; 三.Collection接口的使用&#xff1a; 四.Collection…

C语言(指针)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

推荐全网最全的AI小白进阶指南

1. 引言 您想学习人工智能&#xff1f;但不知道如何开始&#xff0c;也不知道从哪里开始&#xff1f;互联网上的资源总是丰富多彩&#xff0c;质量参差不齐&#xff0c;往往容易看花眼而无从下手。 鉴于此&#xff0c;本文重点推荐一些个人收集的还不错的一些资源供大家学习参…

两重惊喜!奥特曼预告GPT-4和ChatGPT重大更新,Open AI要放大招

OpenAI在今天官宣13日&#xff08;下周一10点&#xff09;开启线上直播&#xff0c;将会展示全新的ChatGPT demo的演示以及GPT-4的重大更新&#xff01; OpenAI首席执行官Sam Altman在X上表示&#xff0c;这些的发布会&#xff0c;公司不会宣布下一代对话式人工智能GPT-5或人工…

日报表定时任务优化历程

报表需求背景 报表是一个很常见的需求&#xff0c;在项目中后期往往会需要加多种维度的一些统计信息&#xff0c;今天就来谈谈上线近10个月后的一次报表优化优化之路&#xff08;从一天报表跑需要五分钟&#xff0c;优化至秒级&#xff09; 需求&#xff1a;对代理商进行日统计…