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

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

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

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

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

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

1、使用 Camera1 进行人脸识别

1.1 开启摄像头

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

class CameraHelper(private var mCameraId: Int,private var mHeight: Int,private var mWidth: Int
) : Camera.PreviewCallback {private var mCamera: Camera? = 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/diannao/6644.shtml

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

相关文章

ubuntu安装LVGL/lv_img_conv并在thinkphp中进行调用生成bin文件

项目需求&#xff1a;需要处理图片成为bin文件&#xff0c;并以二进制的方式传给蓝牙设备&#xff0c;当前仅介绍如何安装&#xff0c;对lvgl功能和简介不做过多描述 项目库地址&#xff1a;https://github.com/lvgl/lv_img_conv 安装过程比较简单 一&#xff0c;确保node.j…

mall-cook本地部署运行

下载源代码 https://github.com/wangyuan389/mall-cook 下载好之后解压&#xff0c;删除.github和yarn.lock&#xff0c;因为使用pnpm 启动文档部署 切换到packages\mall-cook-document&#xff0c;删除yarn.lock&#xff0c;安装依赖包pnpm install 执行pnpm dev启动文档…

C语言/数据结构——(用双链表实现数据的增删查改)

一.前言 嗨嗨嗨&#xff0c;大家好久不见&#xff01;前面我们已经通过数组实现数据的增删查改、单链表实现数据的增删查改&#xff0c;现在让我们尝试一下使用双链表实现数据的增删查改吧&#xff01; 二.正文 如同往常一样&#xff0c;对于稍微大点的项目来说&#xff0c;…

推荐网站(2)今日热榜合集,看不同软件的热点事件

当我们想要看微博&#xff0c;今日头条&#xff0c;bilibili等等今日热点时&#xff0c;需要打开对应的app查看&#xff0c;但是有了这个网站我们可以看不同平台的热点消息&#xff0c;甚至京东&#xff0c;淘宝等购物软件&#xff0c;也能看到热销总榜。 链接直达&#xff1a;…

day-28 除自身以外数组的乘积

思路 利用两个空数组left&#xff08;left[i]表示nums下标从0到i的乘积&#xff09;和right&#xff08;right[i]表示nums下标从n-1到i的乘积&#xff09; 解题方法 返回数组nums[i]right[i1]*left[i-1],第一个元素和最后一个元素单独考虑 Code class Solution {public int[…

【计算机网络】计算机网络的定义和分类

一.定义 计算机网络并没有一个精确和统一的定义&#xff0c;在计算机网络发展的不同阶段&#xff0c;人们对计算机网络给出了不同的定义&#xff0c;这些定义反映了当时计算机网络技术的发展水平。 例如计算机网络早期的一个最简单定义&#xff1a;计算机网络是一些互连的、自…

c#Excel:2.写入Excel表 3.读取Excel表

--写入Excel表-- 该例首先从数据库aq中读取学生信息表staq(参考数据库章节)&#xff0c;然后将学生信息表中的数据写入Excel表格中 &#xff08;1&#xff09;在OfficeOperator类库项目的ExcelOperator类中定义索引器&#xff0c;用于获取Excel表格中的单元格&#xff0c;代码…

队列集使用

文章目录 前言一、功能描述二、功能实现 前言 在实际使用中我们可能要从多个队列中得到数据&#xff0c;就需要队列集。 一、功能描述 创建两个队列Queue1、Queue2。Task1往Queue1写数据、Task2往Queue2写数据。Task3使用QueueSet监测这两个队列。 二、功能实现 创建两个队列…

ArcGIS软件:地图投影的认识、投影定制

这一篇博客介绍的主要是如何在ArcGIS软件中查看投影数据&#xff0c;如何定制投影。 1.查看地图坐标系、投影数据 首先我们打开COUNTIES.shp数据&#xff08;美国行政区划图&#xff09;&#xff0c;并点击鼠标右键&#xff0c;再点击数据框属性就可以得到以下的界面。 我们从…

活动图与状态图:UML中流程图的精细化表达——专业解析系统动态性与状态变迁

流程图是一种通用的图形表示法&#xff0c;用以展示步骤、决策和循环等流程控制结构。它通常用于描述算法、程序执行流程或业务过程&#xff0c;关注于任务的顺序执行。流程图强调顺序、分支和循环&#xff0c;适用于详细说明具体的处理步骤&#xff0c;图形符号相对基础和通用…

Android NDK开发——Android Studio 3.5.2安装与配置踩坑

Android NDK开发——Android Studio 3.5.2安装与配置踩坑 一、Android Studio下载二、配置踩坑报错1&#xff1a;Failed to install the following Android SDK packages as some licences have not been accepted报错2&#xff1a;No toolchains found in the NDK toolchains …

C语言实战项目--贪吃蛇

贪吃蛇是久负盛名的游戏之一&#xff0c;它也和俄罗斯⽅块&#xff0c;扫雷等游戏位列经典游戏的行列。在编程语言的教学中&#xff0c;我们以贪吃蛇为例&#xff0c;从设计到代码实现来提升大家的编程能⼒和逻辑能⼒。 在本篇讲解中&#xff0c;我们会看到很多陌生的知识&…

Unity 性能优化之数据面板(Statistics)(一)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、unity 统计数据面板&#xff08;Statistics&#xff09;1.Audio属性2.Graphics属性 二、什么是Draw Call&#xff1f;三、Unity3D stats也可以通过代…

OpenCV的周期性噪声去除滤波器(70)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV如何通过梯度结构张量进行各向异性图像分割(69) 下一篇 :OpenCV如何为我们的应用程序添加跟踪栏(71) 目录 目标 理论 如何消除傅里叶域中的周期性噪声&#xff1f; 源代码 解释 结果 目…

设计模式之数据访问对象模式

在Java编程的浩瀚星海中&#xff0c;有一个模式低调却强大&#xff0c;它像是一位默默无闻的超级英雄&#xff0c;支撑起无数应用的数据脊梁——那就是数据访问对象&#xff08;DAO, Data Access Object&#xff09;模式&#xff01;想象一下&#xff0c;如果你能像操纵魔法一样…

Unity技术学习:RenderMesh、RenderMeshInstanced

叠甲&#xff1a;本人比较菜&#xff0c;如果哪里不对或者有认知不到的地方&#xff0c;欢迎锐评&#xff08;不玻璃心&#xff09;&#xff01; 导师留了个任务&#xff0c;渲染大量的、移动的物体。 当时找了几个解决方案&#xff1a; 静态批处理&#xff1a; 这东西只对静…

golang for经典练习 金字塔打印 示例 支持控制台输入要打印的层数

go语言中最经典的for练习程序 金字塔打印 &#xff0c;这也是其他语言中学习循环和条件算法最为经典的联系题。 其核心算法是如何控制内层循环变量j 每行打印的*号数量 j<i*2-1 和空格数量 j1 || j i*2-1 golang中实现实心金字塔 Solid Pyramid和空心金字塔 Hollow Pyram…

CSS浮动(如果想知道CSS有关浮动的知识点,那么只看这一篇就足够了!)

前言&#xff1a;在学习CSS排版的时候&#xff0c;浮动是我们必须要知道的知识点&#xff0c;浮动在设计之初是为了实现文字环绕效果的&#xff0c;但是后来被人们发现浮动在CSS排版中有着很好的实用价值&#xff0c;所以浮动便成为了CSS排版的利器之一。 ✨✨✨这里是秋刀鱼不…

pandas学习笔记12

缺失数据处理 其实在很多时候&#xff0c;人们往往不愿意过多透露自己的信息。假如您正在对用户的产品体验做调查&#xff0c;在这个过程中您会发现&#xff0c;一些用户很乐意分享自己使用产品的体验&#xff0c;但他是不愿意透露自己的姓名和联系方式&#xff1b; 还有一些用…

《尿不湿级》STM32 F103C8T6最小系统板搭建(五)BOOT

一、BOOT是什么&#xff1f; 大多数初学者第一次接触BOOT总是对这个词感到不解&#xff0c;从哪冒出一个奇奇怪怪的东西还要接跳线帽&#xff0c;为什么要配置它才能进行串口程序的下载&#xff1f;为什么不正确配置会导致单片机无法正常启动…… boot&#xff0c;及物动词&…