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://xiahunao.cn/news/3019328.html

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

相关文章

二手手机店需要用专业erp软件进行管理吗?

2024年在科技发展迅速的今天&#xff0c;手机批发和零售行业正迎来前所未有的革新。为了在这个变革中抓住机遇&#xff0c;提升竞争力&#xff0c;二手手机店需要寻找一种全面、高效、智能的管理工具。基于这个原因&#xff0c;超机商城为众多二手手机店商家量身打造的一款二手…

水电站泄洪安全声光预警广播系统建设方案

一、水电站泄洪安全声光预警广播系统建设方案背景 水电站建成运行以后&#xff0c;会使河道水文情势发生改变&#xff0c;为了加强水电站工程安全管理&#xff0c;保证水库泄洪放水工作安全有序进行&#xff0c;保护下游河道沿岸人民群众生命和财产安全&#xff0c;根据《中华…

DevSecOps 是什么?你知道吗?

使用工具自动进行安全检查和扫描。这些工具包括静态应用程序安全测试 (SAST)、动态应用程序安全测试 (DAST) 和依赖性扫描。 什么是 DevSecOps&#xff1f; DevSecOps 是 DevOps 实践的自然演进&#xff0c;其重点是将安全集成到软件开发和部署流程中。 DevSecOps 一词代表了…

elk + filebeat 8.4.3 收集nginx日志(docker部署)

ELK filebeat docker部署 一、 elasticsearch部署1、运行elasticsearch临时配置容器2、拷贝文件目录到本地3、检查elasticsearch.yml4、删除之前elastic&#xff0c;运行正式容器5、docker logs记录启动日志 二、部署kibana1、运行kibana临时配置容器2、docker拷贝配置文件到本…

MySQL —— 数据类型

一、数值类型 以上表格整理了用来表示数值类型的数据类型&#xff0c;其中&#xff0c;接下来将介绍和展示其中几个类型的使用和各种细节 1.tinyint 越界测试&#xff1a;建立一个包含tinyint类型的表格&#xff0c;插入各中数据去查看结果&#xff0c;并且尝试插入边界数据和…

论文润色就用意得辑:让你的学术之作更上一层楼

在学术的海洋里&#xff0c;每一篇论文都是一艘承载智慧与探索的小船。然而&#xff0c;好的内容也需要好的包装&#xff0c;才能更好地展现其价值。在这个追求精益求精的时代&#xff0c;意得辑以其专业的论文润色服务&#xff0c;成为了众多学者们的得力助手。 意得辑&#…

极致设计!详解专业网页设计的全套步骤和流程

在当今的数字时代&#xff0c;拥有一个专业、易于使用和有吸引力的网页对任何企业或个人都至关重要。专业的网页设计是实现这一目标的关键步骤之一。本文将详细介绍专业的网页设计步骤和过程&#xff0c;以帮助您了解如何设计一个优秀的网页。 在介绍专业网页设计步骤和流程之…

2024“跨境电商+产业带”高质量发展报告

来源&#xff1a;商务部研究院 经过二十多年的发展&#xff0c;我国跨境电商出口进入了深耕国内供应链的新阶段。“跨境电商产业带”是跨境电商对各地优势产业带全面赋能&#xff0c;推动特色产品出口&#xff0c;实现产业转型升级&#xff0c;培育自主​品牌的模式&#xff0…

echars 的一些运用

基础图形 // 支架压力数据量统计 let splitLine { // 网格线设置show:true,lineStyle:{color:#E2E7EC,width:1,}, } let itemStyle { // 柱形图颜色color:#5B9BD5, } let axisLine { // 轴线样式lineStyle:{color:#E2E7EC,}, } let axisLabel { // 轴刻度字-样式fontSize:…

MYSQL8.0.20安装教程

一&#xff1a;下载mysql MySQL :: Download MySQL Installer (Archived Versions) 二&#xff1a;选中server only&#xff0c;点击next 三&#xff1a;点击server 选项&#xff0c;点击Execute 弹窗点击安装 四&#xff1a;安装项为绿色后&#xff0c;点击next 五&#xf…

uniapp——点赞、取消点赞

案例 更新点赞状态&#xff0c;而不是每次都刷新整个列表。避免页面闪烁&#xff0c;提升用户体验 代码 <view class"funcBtn zan" click"onZan(index,item.id)"><image src"/static/images/circle/zan.png" mode"aspectFill&…

【新三个数排序的自创算法,这是我厉年来很满意的一次排序算法设计,最好小于O(N)最坏O((NN/3)/2)。】2024-5-7

缘由如何用C&#xff0b;&#xff0b;解决一下问题_编程语言-CSDN问答 int a[]{1, 4, 7, 8, 5, 2, 3, 6, 9, 7}, n 10, x n, jh 0, j 0;px:if (j < n) {//缘由https://ask.csdn.net/questions/8099444if (--x < 2 j)x n - 1, j 3;if (x < n - 1 && a[x…

技术分享 | i.MX8M Mini适配MIPI转eDP芯片

1.方案概述 此方案使用HD-8MMN-CORE的核心板搭配TI公司的芯片SN65DSI86转换芯片实现。 SN65DSI86作为一款MIPI DSI转eDP的芯片&#xff0c;支持双通道DSI输入&#xff0c;最大四通道显示输出&#xff0c;最大支持4K60fps输出&#xff0c;WUXGA 1080P。本方案中将采用单通道DS…

winscp断点续传

背景 超大文件上传&#xff0c; scp可能因为网络或其他原因中断&#xff0c;并且已上传完的文件无法同步。如何断点续传呢&#xff1f;使用winscp软件 winscp安装 百度搜索&#xff0c;自行安装 开启断点续传功能 https://jingyan.baidu.com/article/fdffd1f8395529b2e98ca…

什么可以替代iframe?

网页嵌套中&#xff0c;iframe曾几何时不可一世&#xff0c;没有其他更好的选择&#xff01; iframe即内联框架&#xff0c;作为网页设计中的一种技术&#xff0c;允许在一个网页内部嵌套另一个独立的HTML文档。尽管它在某些场景下提供了便利&#xff0c;但也存在多方面的缺陷…

Linux中文乱码

背景介绍 通过unzip命令解压后中文命名的文件显示乱码 中文名称文件显示乱码 原因 这是因为Linux没有设置对中文的支持 解决办法 编辑~/.bashrc添加export LC_ALLC.UTF-8 使更改的配置生效 source ~/.bashrc查看系统字体编码格式 locale查看中文名称的文件显示是否正常…

【已解决】‘pip‘ 不是内部或外部命令问题

&#x1f60e; 作者介绍&#xff1a;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff0c;视频号&#xff1a;AI-行者Sun &#x1f388; 本文专栏&#xff1a;本文收录于《AI实战中的各种bug…

RabbitMQ - 以 MQ 为例,手写一个 RPC 框架 demo

目录 前言 一、再谈自定义应用层协议 二、再谈 BrokerServer 三、再谈 Connection、Channel 四、Demo a&#xff09;启动服务器 b&#xff09;客户端连接 前言 本篇文章来自于笔者之前写过的一个系列 —— “根据源码&#xff0c;模拟实现 RabbitMQ” 系列&#xff0c…

C语言 | Leetcode C语言题解之第75题颜色分类

题目&#xff1a; 题解&#xff1a; void swap(int *a, int *b) {int t *a;*a *b, *b t; }void sortColors(int *nums, int numsSize) {int p0 0, p2 numsSize - 1;for (int i 0; i < p2; i) {while (i < p2 && nums[i] 2) {swap(&nums[i], &num…

ERROR 1045 (28000) Access denied for user ‘root‘@‘IP‘(using password YES/NO)

查看权限 要查看MySQL用户的权限&#xff0c;您可以使用SHOW GRANTS语句。这将列出用户的权限&#xff0c;包括授予的权限和可以授予其他用户的权限。 以下是查看当前用户权限的SQL命令&#xff1a; SHOW GRANTS; 如果您想查看特定用户的权限&#xff0c;可以使用以下命令&…