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

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? = nullprivate lateinit var mBuffer: ByteArrayprivate var mPreviewCallback: Camera.PreviewCallback? = nullfun startPreview() {// 开启摄像头,获取 Camera 对象mCamera = Camera.open(mCameraId)if (mCamera == null) {Log.d(TAG, "Open camera failed.")return}// 配置 Camera 参数val cameraParams = mCamera?.parameters// 设置预览数据格式为 NV21cameraParams?.previewFormat = ImageFormat.NV21// 设置摄像头宽高cameraParams?.setPreviewSize(mWidth,mHeight)// 更新 Camera 参数mCamera?.parameters = cameraParams// 摄像头采集的是 YUV NV21 格式的数据,mBuffer 承载预览数据mBuffer = ByteArray(mWidth * mHeight * 3 / 2)// 设置预览的回调以及缓冲区// 将摄像头获取的数据放入 mBuffermCamera?.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: OpenCVJNIprivate lateinit var mCameraHelper: CameraHelperprivate var mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONToverride 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()// 初始化 OpenCVval path = File(Environment.getExternalStorageDirectory(),"lbpcascade_frontalface.xml").absolutePathmOpenCVJNI.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? = nullvar inputStream: InputStream? = nulltry {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);// 创建 DetectionBasedTrackerDetectionBasedTracker::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 startoverride 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/2Mat src(height * 3 / 2, width, CV_8UC1, data);// 将 src 内的 NV21 数据转换为 RGBA 数据后再赋值给 srccvtColor(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 进行预览而不是 SurfaceTextureif (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-camera2implementation "androidx.camera:camera-core:${camerax_version}"implementation "androidx.camera:camera-camera2:${camerax_version}"// If you want to additionally use the CameraX Lifecycle libraryimplementation "androidx.camera:camera-lifecycle:${camerax_version}"// If you want to additionally use the CameraX View classimplementation "androidx.camera:camera-view:${camerax_version}"// If you want to additionally use the CameraX Extensions libraryimplementation "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: FaceTrackeroverride 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 = 0Linit {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): Longprivate 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 startoverride 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.Analyzeroverride 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.cropRectval imageWidth = rect.width()val imageHeight = rect.height()val format = image.formatval 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.planesval rowData = ByteArray(planes[0].rowStride)// 3.将 image 图像数据拷贝到 data 中,拷贝时按照 Y、U、V// 三个平面分开拷贝var channelOffset: Intfor (i in planes.indices) {channelOffset = when (i) {// y 从 0 开始0 -> 0// u 从 y 之后开始1 -> imageWidth * imageHeight// v 从 u 之后开始,u 的数据长度为 width * height / 42 -> (imageWidth * imageHeight * 1.25).toInt()else -> throw IllegalArgumentException("Unexpected number of image planes")}// 这一个平面的数据缓冲区val buffer = planes[i].buffer// 行跨度,一行的步长,即这一行有像素数据所占用的字节数val rowStride = planes[i].rowStride// 像素跨度,即每一个像素占用的字节数,例如 RGB 就为 3val pixelStride = planes[i].pixelStride// UV 只有一半,因此要右移 1 位val shift = if (i == 0) 0 else 1val width = imageWidth shr shiftval height = imageHeight shr shift// 移动到每个平面在 buffer 中的起始位置,准备读取该平面的数据buffer.position(rowStride * (rect.top shr shift) + pixelStride * (rect.left shr shift))var length: Intfor (row in 0 until height) {if (pixelStride == 1) {length = widthbuffer.get(data, channelOffset, length)channelOffset += length} else {length = (width - 1) * pixelStride + 1buffer.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));}// 绘制 srcdraw(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;// 上锁,目的是为了拿到 bufferif (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.bitsfor (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.rhkb.cn/news/325050.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安全继电器的使用和作用

目录 一、什么是安全继电器 二、安全继电器的接线方式 三、注意事项 四、总结 一、什么是安全继电器 安全继电器是由多个继电器与硬件电路组合而成的一种模块&#xff0c;是一种电路组成单元&#xff0c;其目的是要提高安全因素。完整点说&#xff0c;应该叫成安全继电器模…

20232906 2023-2024-2 《网络与系统攻防技术》第九次作业

20232906 2023-2024-2 《网络与系统攻防技术》第九次作业 1.实验内容 本次实践的对象是一个名为pwn1的linux可执行文件。 该程序正常执行流程是&#xff1a;main调用foo函数,foo函数会简单回显任何用户输入的字符串。 该程序同时包含另一个代码片段&#xff0c;getShell&am…

用户登录后端:登录密码解密后用PasswordEncoder验证密码是否正确

前置知识: 前端登录加密看用户登录 PasswordEncoder加密看PasswordEncoder详解 项目中因为要判断用户登录密码是否正确&#xff0c;通过输入错误次数锁住用户 1.后端配置rsa私钥 #密码加密传输&#xff0c;前端公钥加密&#xff0c;后端私钥解密 rsa:private_key: xxxx2. 读…

电影院购票管理系统

文章目录 电影院购票管理系统一、项目演示二、项目介绍三、部分功能截图四、部分代码展示五、底部获取项目源码&#xff08;9.9&#xffe5;带走&#xff09; 电影院购票管理系统 一、项目演示 电影院售票管理系统 二、项目介绍 基于springbootvue的前后端分离电影院购票管理…

【linux软件基础知识】如何使用 run_list 字段将任务放入就绪队列中

在给定的代码片段中,struct task_struct 表示内核中任务或进程的进程控制块 (PCB)。 run_list 字段的类型为 struct list_head,这表明它是链表实现的一部分。 run_list字段在Linux内核中常用来表示任务在调度队列中的位置,例如就绪队列或各种优先级队列。 init_task是一个…

《Python编程从入门到实践》day25

# 昨日知识点回顾 如何创建多行外星人 碰撞结束游戏 创建game_stats.py跟踪统计信息 # 今日知识点学习 第14章 记分 14.1 添加Play按钮 14.1.1 创建Button类 import pygame.font# button.py class Button:def __init__(self, ai_game, msg):"""初始化按钮…

Sqlite在Mybatis Plus中关于时间字段的处理

我的个人项目中&#xff0c;使用Mybatis-Plus 和 Sqlite数据库&#xff0c; 但是在存储和查询时间字段的时候&#xff0c;总是出现问题&#xff0c;记录下我解决问题的过程。 Sqlite会默认把时间字段转成时间戳存储到数据库的字段中&#xff0c;看起来不直观&#xff0c;所以我…

刷代码随想录有感(63):将有序数组转换为二叉搜索树(其实时二叉平衡搜索树)

题干&#xff1a; 代码&#xff1a; class Solution { public:TreeNode* traversal(vector<int>& nums, int left, int right){if(left > right)return NULL;int mid left (right - left)/2;TreeNode* NewRoot new TreeNode(nums[mid]);NewRoot->left tra…

2024最新从0部署Django项目(nginx+uwsgi+mysql)

云服务器 我这里用的是腾讯云免费试用的2H4Gcentos服务器&#xff08;后升级为2H8G&#xff0c;保险一点提高内存&#xff09; 因为网上很多关于django部属的教程都是宝塔啊&#xff0c;python版本控制器啊这种的&#xff0c;我也误打误撞安装了宝塔面板&#xff0c;但这里我…

【吊打面试官系列】Java高并发篇 - 同步方法和同步块,哪个是更好的选择?

大家好&#xff0c;我是锋哥。今天分享关于 【同步方法和同步块&#xff0c;哪个是更好的选择&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 同步方法和同步块&#xff0c;哪个是更好的选择&#xff1f; 同步块是更好的选择&#xff0c;因为它不会锁住整个对象…

【notepad++】使用

1 notepad 下载路径 https://notepad-plus.en.softonic.com/download 2 设置护眼模式 . 设置——语言格式设置——前景色——黑色 . 背景色——RGB &#xff1a;199 237 204 . 勾选“使用全局背景色”、“使用全局前景色” . 保存并关闭

Apache Flume概述

Apache Flume概述 1.Flume定义 ​ Flume是cloudera(CDH版本的hadoop) 开发的一个分布式、可靠、高可用的海量日志收集系统。 它将各个服务器中的数据收集起来并送到指定的地方去&#xff0c;比如说送到HDFS、Hbase&#xff0c;简单来说flume就是收集日志的。 2.Flume基础架构…

【Web后端】jsp基础知识_请求转发和重定向

1.jsp基础知识 1.1简介 java server page&#xff0c;运行在服务器端的页面java代码html代码java代码全部都放在<%%>中间 1.2jsp表达式 作用&#xff1a;将动态信息显示在页面上&#xff0c;以字符串方式&#xff0c;返回给浏览器端语法&#xff1a;<%变量或表达式…

Debian安装Redis、RabbitMQ、Nacos

安装Redis&#xff1a; 启动Redis、开机自启动 sudo systemctl start redis-server #启动sudo systemctl enable redis-server #开机自启 Redis状态(是否在运行) sudo systemctl status redis-server #查看运行状态 redis-cli ping # 客户端尝试连接 安装RabbitMQ&#xff0c;…

【回溯 网格 状态压缩】52. N 皇后 II

本文涉及知识点 回溯 网格 状态压缩 LeetCode52. N 皇后 II n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回 n 皇后问题 不同的解决方案的数量。 示例 1&#xff1a; 输入&#x…

conan2 基础入门(02)-安装

conan2 基础入门(02)-安装 文章目录 conan2 基础入门(02)-安装⭐前言⭐安装python安装安装包安装自行操作 ⭐验证配置环境变量命令行验证conan配置文件 END ⭐前言 Conan 2.0: C and C Open Source Package Manager 官方提供三种安装conan的方式。分别为&#xff1a; Recommen…

1290.二进制链表转整数

给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。 请你返回该链表所表示数字的 十进制值 。 示例 1&#xff1a; 输入&#xff1a;head [1,0,1] 输出&#xff1a;5 解释&#xff1a;二进制数 (101) 转化为十进制…

在k8s中部署Prometheus并实现对k8s集群的监控

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Prometheus&#xff1a;监控的神》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、k8s简介 2、 Prometheus概述 二、准备k8s环境 1、…

IDEA安装使用Git

IDEA安装使用Git 1 Git下载与安装 2 在IDEA中使用Git 2.1 IDEA中配置Git 在IDEA中使用Git&#xff0c;本质上还是使用本地安装的Git软件&#xff0c;所以需要在IDEA中配置Git。 2.2 在IDEA中使用Git 2.2.1 获取Git仓库 在IDEA中使用Git获取仓库有两种方式: 本地初始化仓库从…

HTTP超时时间设置

在进行超时时间设置之前我们需要了解一次http请求经历的过程 浏览器进行DNS域名解析&#xff0c;得到对应的IP地址根据这个IP&#xff0c;找到对应的服务器建立连接&#xff08;三次握手&#xff09;建立TCP连接后发起HTTP请求&#xff08;一个完整的http请求报文&#xff09;服…