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.mzph.cn/web/8185.shtml

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

相关文章

思维导图怎么画?一文掌握绘制技巧

思维导图怎么画&#xff1f;你是不是还在为不知道怎么绘制思维导图而困惑&#xff1f;别担心&#xff0c;看完这篇文章就可以掌握绘制思维导图的基础步骤了。一起来看看吧&#xff01; 一、思维导图的基本结构 思维导图通常由中心节点、分支节点和子节点组成。中心节点是思维导…

VTK数据的读写--Vttk学习记录1--《VTK图形图像开发进阶》

读和写操作是VTK可视化管线两端相关的类--Reader和Writer类 Reader:将外部数据读入可视化管线&#xff0c;主要步骤如下 s1:实例化Reader对象 s2:指定所要读取的文件名 s3:调用Update()促使管线执行 对应的Writer: s1:实例化Writer对象 s2输入要写的数据以及指定写入的文…

PyRun_SimpleString(“import cv2“); 报错解决

#include <Python.h> #include <iostream>using namespace std;int main() {Py_Initialize();if (!Py_IsInitialized()){printf("初始化失败&#xff01;");return 0;}PyRun_SimpleString("import sys");PyRun_SimpleString("sys.path.ap…

0507华为od二面

只记录自己没回答上的问题 1、ZGC的缺点&#xff1a; 1)只是适用于32位系统 2)最大只是支持4TB内存容量 3)最糟糕的情况下吞吐量会下降15%&#xff0c;这都不是事至于吞吐量&#xff0c;通过扩容分分钟解决 4)分代的原因:不同对象的生命周期不相同&#xff0c;可能会扫描整个堆…

产品推荐 | 基于Xilinx ZYNQ FPGA和ADI ADRV9009的双收双发无线电射频板卡

1、产品概述 基于XC7Z100ADRV9009的双收双发无线电射频板卡是基于Xilinx ZYNQ FPGA和ADI ADRV9009开发的专用功能板卡&#xff0c;用于5G小基站&#xff0c;无线图传&#xff0c;数据收发等领域。 2、板卡原理和功能 板卡使用XC7Z100 作为主处理器&#xff0c;包含Dual ARM C…

宏集PLC+HMI触控一体机助力构建物料自动分拣系统

一、应用背景 随着工业生产和物流领域的快速发展&#xff0c;对仓储、分拣和配送效率以及准确性的需求不断提高。传统的人工分拣已无法满足市场需求&#xff0c;为了实现智能物流&#xff0c;对高性能的物料输送分拣设备需求更为迫切。 二、物料分拣系统介绍 智能化物料分拣系…

B端系统菜单栏中使用阿里图标

B端系统菜单栏中使用阿里图标 1.需求说明 由于组件库自带的图标数量和内容有限&#xff0c;采用丰富多样的阿里图标是不错的选择 2.阿里图标使用 2.1官网 iconfont-阿里巴巴矢量图标库 2.2使用 2.2.1.先根据关键词搜索并选择对应的图标 注意&#xff1a;若只是少量的sv…

Stable Diffusion Ai绘画模型推荐:二次元Coriander_Mix v1大模型推荐

负tag嵌入式:EasyNegative,badhandv4 此模型经测试是写实偏3D的效果 画质灰暗的话请加&#xff1a;VAE840000 或者负tag&#xff1a;(watermark:2),(blurry:2),fat,paintings,sketches,(worst quality:2),(low quality:2),(normal quality:2),((monochrome)), ((grayscale))…

将 Vue、React、Angular、HTML 等一键打包成 macOS 和 Windows 平台客户端应用

应用简介 PPX 基于 pywebview 和 PyInstaller 框架&#xff0c;构建 macOS 和 Windows 平台的客户端。本应用的视图层支持 Vue、React、Angular、HTML 中的任意一种&#xff0c;业务层支持 Python 脚本。考虑到某些生物计算场景数据量大&#xff0c;数据私密&#xff0c;因此将…

计算机发展史故事【4】

继往与开来 巴贝奇巨星陨落后&#xff0c;世人已逐渐将他淡忘&#xff0c;20 世纪已经来临。计算机的历史等待着&#xff0c;等待着巴贝奇式的人物再世&#xff0c;等待着人类划时代的壮举。 大约在1936 年&#xff0c;美国青年霍德华艾肯&#xff08;H.Aiken&#xff09;来哈佛…

多个glibc库存在时如何查看ldd调用的哪个

但是发现存在多个版本的glibc版本&#xff0c;需要查看具体的库的信息&#xff0c;和相应的关键函数的信息&#xff0c;但是并不知道具体的libc.so.6的路径信息 rootalg-dev04:~/xingqiao# ldd --version ldd (GNU libc) 2.29 rootalg-dev04:/opt# which ldd /usr/local/bin/…

Exness外汇大陆投资者开户详细流程!

Exness是一家全球领先的外汇和差价合约交易平台&#xff0c;因其高效稳定的服务和多样化的交易工具&#xff0c;受到广大投资者的青睐。然而&#xff0c;由于中国大陆用户无法直接访问Exness官网&#xff0c;想要开户的大陆投资者需要通过特定方式才能完成注册。本文将详细介绍…

PDF批量编辑技巧:高效PDF转txt批量处理,轻松管理大量文档

随着信息技术的飞速发展&#xff0c;文档管理已成为日常工作中不可或缺的一部分。特别是当我们需要处理大量的PDF文件时&#xff0c;如何高效地进行编辑、转换和管理成为了一个重要的问题。本文将介绍一些PDF批量编辑的技巧&#xff0c;特别是如何将PDF批量转换为txt格式&#…

FLIR LEPTON3.5 热像仪wifi 科研实验测温采集仪

点击查看详情!点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情点击查看详情 1、描述 这是一款桌面科研实验测温热成像多功能热像记录仪&#xff0c;小巧轻便…

linux命令——软硬链接

ln可以创建软硬链接&#xff0c;类似于windows系统里的快捷方式 同时还可以创建软链接的软链接 这时cat第二个软链接&#xff0c;依然能查看原文件内容 当使用ls -l查看文件属性时能看到&#xff0c;软链接后面的指向性 同时 &#xff0c;我们可以使用readlink来查看软链接所…

【牛客】排列计算

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 如果直接涂色来计算单点权重&#xff0c;2e5*2e5必然超时。 所以用差分进行优化。 3. 代码实现 #include<bits/stdc.h> using name…

【八十二】【算法分析与设计】2421. 好路径的数目,928. 尽量减少恶意软件的传播 II,并查集的应用,元素信息绑定下标一起排序,元素通过下标进行绑定

2421. 好路径的数目 给你一棵 n 个节点的树&#xff08;连通无向无环的图&#xff09;&#xff0c;节点编号从 0 到 n - 1 且恰好有 n - 1 条边。 给你一个长度为 n 下标从 0 开始的整数数组 vals &#xff0c;分别表示每个节点的值。同时给你一个二维整数数组 edges &#xff…

【Vue】Vue packages version mismatch(vue 和 vue-template-compiler)

报错&#xff1a;Vue packages version mismatch 原因&#xff1a;vue和vue-template-compiler版本不一样解决&#xff1a;如上vue版本为 2.6.14&#xff0c;vue-template-comiler版本为2.7.16。将vue-template-comiler版本设置为和vue版本一致即可。 npm install vue-templat…

【稳定检索|EI会议】✅2024年食品科学与生物医学国际会议(ICFSBS 2024)✅

2024 International Conference on Food Science and Biomedical Sciences 一、大会信息 会议名称&#xff1a;2024年食品科学与生物医学国际会议会议简称&#xff1a;ICFSBS 2024收录检索&#xff1a;提交Ei Compendex,CPCI,CNKI,Google Scholar等会议官网&#xff1a;http:/…

Windows安装RabbitMQ教程(附安装包)

需要两个安装包 Erlang 安装包: https://download.csdn.net/download/Brevity6/89274663 (自己从官网下载也可以) RabbitMQ Windows 安装包&#xff1a; https://download.csdn.net/download/Brevity6/89274667 (自己从官网下载也可以) Erlang安装 Erlang安装傻瓜式下一…