本篇我们来介绍在 Android 下如何实现人脸识别。
上一篇我们介绍了如何在 Windows 下通过 OpenCV 实现人脸识别,实际上,在 Android 下的实现的核心原理是非常相似的,因为 OpenCV 部分的代码改动不大,绝大部分代码可以直接移植到 Android 上。最主要的区别是,Android 摄像头采集图像的代码要复杂一些,而 Windows 下几行代码就搞定了。
目前有四种方式来使用 Android Camera:
- Camera1:虽然被 @Deprecated 了,但是很多产品中仍然在使用它,比如一些推流 SDK
- Camera2:比 Camera1 更灵活,可定制性更强,但是用起来有些麻烦
- CameraX:Jetpack 组件,封装了 Camera2,通过提供一致且易用的 API 接口来简化相机应用的开发工作
- NDKCamera:无法兼容低版本
我们会介绍 Camera1 和 CameraX 两种方式。
1、使用 Camera1 进行人脸识别
1.1 开启摄像头
我们将 Camera1 的相关操作封装到 CameraHelper 中:
class CameraHelper(
private var mCameraId: Int,
private var mHeight: Int,
private var mWidth: Int
) : Camera.PreviewCallback {
private var mCamera: Camera? = null
private lateinit var mBuffer: ByteArray
private var mPreviewCallback: Camera.PreviewCallback? = null
fun startPreview() {
// 开启摄像头,获取 Camera 对象
mCamera = Camera.open(mCameraId)
if (mCamera == null) {
Log.d(TAG, "Open camera failed.")
return
}
// 配置 Camera 参数
val cameraParams = mCamera?.parameters
// 设置预览数据格式为 NV21
cameraParams?.previewFormat = ImageFormat.NV21
// 设置摄像头宽高
cameraParams?.setPreviewSize(mWidth,mHeight)
// 更新 Camera 参数
mCamera?.parameters = cameraParams
// 摄像头采集的是 YUV NV21 格式的数据,mBuffer 承载预览数据
mBuffer = ByteArray(mWidth * mHeight * 3 / 2)
// 设置预览的回调以及缓冲区
// 将摄像头获取的数据放入 mBuffer
mCamera?.addCallbackBuffer(mBuffer)
mCamera?.setPreviewCallbackWithBuffer(this)
// 设置预览画面
mCamera?.setPreviewTexture(SurfaceTexture(11))
mCamera?.startPreview()
}
private fun stopPreview() {
mCamera?.setPreviewCallback(null)
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
if (data == null) {
Log.d(TAG, "onPreviewFrame: data 为空,直接返回")
return
}
// 注意回调给外界的图像是横向的
mPreviewCallback?.onPreviewFrame(data, camera)
mCamera?.addCallbackBuffer(mBuffer)
}
fun switchCamera() {
// 切换摄像头 ID 再重启预览
mCameraId = if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
Camera.CameraInfo.CAMERA_FACING_BACK
} else {
Camera.CameraInfo.CAMERA_FACING_FRONT
}
stopPreview()
startPreview()
}
fun setPreviewCallback(previewCallback: Camera.PreviewCallback) {
mPreviewCallback = previewCallback
}
...
}
需要特别注意 startPreview() 内设置预览画面要设置给 SurfaceTexture 而不是 SurfaceHolder。因为 SurfaceHolder 是会对 SurfaceView.SurfaceHolder.getSurface() 获取到的 Surface 对象的生命周期和渲染进行直接管理的,这就导致我们在 Native 层获取由该 Surface 创建的 ANativeWindow 的锁,即调用 ANativeWindow_lock() 会一直失败,进而无法渲染。
由于我们需要在 Native 层将 OpenCV 识别的人脸范围用矩形框画出来,所以预览就交给 SurfaceTexture。
接下来由 Activity 控制 CameraHelper 开启预览:
private lateinit var mOpenCVJNI: OpenCVJNI
private lateinit var mCameraHelper: CameraHelper
private var mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.surfaceView.holder.addCallback(this)
binding.btnSwitchCamera.setOnClickListener {
mCameraHelper.switchCamera()
mCameraId = mCameraHelper.getCameraId()
}
mOpenCVJNI = OpenCVJNI()
mCameraHelper = CameraHelper(mCameraId, 480, 640)
mCameraHelper.setPreviewCallback(this)
// 将 assets 下的 lbpcascade_frontalface.xml 拷贝到手机同名文件中
Utils.copyAssets(this, "lbpcascade_frontalface.xml")
}
override fun onResume() {
super.onResume()
// 开启摄像头预览
mCameraHelper.startPreview()
// 初始化 OpenCV
val path = File(
Environment.getExternalStorageDirectory(),
"lbpcascade_frontalface.xml"
).absolutePath
mOpenCVJNI.init(path)
}
这样我们就可以在页面中看到摄像头采集到的预览画面了。
1.2 其余初始化工作
开启摄像头的代码中,有涉及到创建以及初始化 OpenCVJNI 对象,该对象就是上层与 Native 层 OpenCV API 交互的桥梁:
class OpenCVJNI {
fun init(path: String) {
nativeInit(path)
}
fun postData(data: ByteArray, width: Int, height: Int, cameraId: Int) {
nativePostData(data, width, height, cameraId)
}
fun setSurface(surface: Surface) {
nativeSetSurface(surface)
}
private external fun nativeInit(path: String)
private external fun nativePostData(data: ByteArray, width: Int, height: Int, cameraId: Int)
private external fun nativeSetSurface(surface: Surface)
companion object {
init {
System.loadLibrary("opencv")
}
}
}
由于 Windows Demo 中我们使用的是 HAAR 级联分类器,所以 Android Demo 我们换一个,使用 LBP 级联分类器。将 OpenCV-android-sdk\sdk\etc\lbpcascades\lbpcascade_frontalface.xml 拷贝到项目的 /src/main/assets/ 目录下。并通过 copyAssets() 将文件拷贝到手机中:
class Utils {
companion object {
/**
* 将 assets 目录下的文件 path 的内容复制到手机的 path 文件中
*/
fun copyAssets(context: Context, path: String) {
val file = File(Environment.getExternalStorageDirectory(), path)
if (file.exists()) {
file.delete()
}
var fileOutputStream: FileOutputStream? = null
var inputStream: InputStream? = null
try {
fileOutputStream = FileOutputStream(file)
inputStream = context.assets.open(path)
val buffer = ByteArray(2048)
var length = inputStream.read(buffer)
while (length > 0) {
fileOutputStream.write(buffer, 0, length)
length = inputStream.read(buffer)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
fileOutputStream?.close()
inputStream?.close()
}
}
}
}
上层代码基本就这样了,接下来就是看上层如何调用 OpenCV 的 Native API 实现人脸识别了。
1.3 Native 层实现
Native 层实现主要包括三方面:
- OpenCV 的初始化
- 负责底层绘制的 ANativeWindow 初始化
- 接收上层传递的图像数据进行识别
OpenCV 的初始化是通过 OpenCVJNI 的 init() 调用 Native 方法 nativeInit() 实现的:
#include "opencv2/opencv.hpp"
#include <jni.h>
#include <android/native_window_jni.h>
using namespace cv;
DetectionBasedTracker *tracker = nullptr;
class CascadeDetectorAdapter : public DetectionBasedTracker::IDetector {
public:
CascadeDetectorAdapter(cv::Ptr<cv::CascadeClassifier> detector) :
IDetector(),
Detector(detector) {
}
// 检测人脸的函数,Mat 相当于 Android 的一张 Bitmap。一张图片有几个人脸就会调用本方法几次
void detect(const cv::Mat &Image, std::vector<cv::Rect> &objects) {
Detector->detectMultiScale(Image, objects, scaleFactor,
minNeighbours, 0, minObjSize, maxObjSize);
}
virtual ~CascadeDetectorAdapter() = default;
private:
CascadeDetectorAdapter();
cv::Ptr<cv::CascadeClassifier> Detector;
};
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativeInit(JNIEnv *env, jobject thiz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, nullptr);
// 创建检测器
Ptr<CascadeClassifier> detectorClassifier = makePtr<CascadeClassifier>(path);
Ptr<CascadeDetectorAdapter> mainDetector = makePtr<CascadeDetectorAdapter>(detectorClassifier);
// 创建跟踪器
Ptr<CascadeClassifier> trackerClassifier = makePtr<CascadeClassifier>(path);
Ptr<CascadeDetectorAdapter> trackingDetector = makePtr<CascadeDetectorAdapter>(
trackerClassifier);
// 创建 DetectionBasedTracker
DetectionBasedTracker::Parameters detectionParams;
tracker = new DetectionBasedTracker(mainDetector, trackingDetector, detectionParams);
// run() 会开启维护死循环的线程,当开启摄像头预览调用 tracker->process()
// 传入人脸数据时,线程会返回一个包含人脸结构的 face 集合给你
tracker->run();
env->ReleaseStringUTFChars(path_, path);
}
与 Windows 几乎相同,创建 DetectionBasedTracker 需要主检测器 mainDetector 和跟踪器 trackingDetector,创建两个适配器所需的 CascadeDetectorAdapter 还是来自 OpenCV 的官方 Sample 代码。
然后是底层绘制窗口 ANativeWindow 的初始化。它的初始化由 Activity 的 SurfaceView 的创建/变化触发:
class MainActivity : AppCompatActivity(), Camera.PreviewCallback, SurfaceHolder.Callback {
// SurfaceHolder.Callback start
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
mOpenCVJNI.setSurface(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
// SurfaceHolder.Callback end
}
进入到 Native 层,需要先释放原有的 ANativeWindow 对象重新分配:
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativeSetSurface(JNIEnv *env, jobject thiz, jobject surface) {
if (window) {
ANativeWindow_release(window);
window = nullptr;
}
window = ANativeWindow_fromSurface(env, surface);
}
最后就是通过 ANativeWindow 绘制了,绘制的数据来自于上层 Camera 的回调数据:
class MainActivity : AppCompatActivity(), Camera.PreviewCallback, SurfaceHolder.Callback {
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
if (data == null) {
return
}
mOpenCVJNI.postData(data, mCameraHelper.getWidth(), mCameraHelper.getHeight(), mCameraId)
}
}
Native 层拿到 data 先用 OpenCV 进行人脸识别,在识别出来的人脸区域画一个矩形:
/**
* 中间过程可以通过 imwrite(String,Mat) 将 Mat 图片输出到手机
* 指定路径查看中间效果以验证编程是否正确
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition1_OpenCVJNI_nativePostData(JNIEnv *env, jobject thiz, jbyteArray data_,
jint width, jint height, jint camera_id) {
jbyte *data = env->GetByteArrayElements(data_, nullptr);
// 创建一个 Mat 对象,Mat 相当于一张 Bitmap,由于传入的是 YUV 数据,因此高度是像素高度的 3/2
Mat src(height * 3 / 2, width, CV_8UC1, data);
// 将 src 内的 NV21 数据转换为 RGBA 数据后再赋值给 src
cvtColor(src, src, COLOR_YUV2RGBA_NV21);
// 对原始摄像头图像进行旋转调正
if (camera_id == 1) {
// 前置摄像头需要逆时针旋转 90°
rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
// 前置还需要取一个水平方向的镜像,如果传 0 就是竖直方向
flip(src, src, 1);
} else {
// 后置摄像头需要顺时针旋转 90°
rotate(src, src, ROTATE_90_CLOCKWISE);
}
// 图片调整后开始进行识别,首先要将图片转换为灰度图,可以减少杂色增加识别几率
Mat gray;
cvtColor(src, gray, COLOR_RGBA2GRAY);
// 增强对比度,目的是增强轮廓(因为识别是对轮廓进行识别)
equalizeHist(gray, gray);
// 检测人脸,结果保存到 faces 中
std::vector<Rect> faces;
tracker->process(gray);
tracker->getObjects(faces);
// 遍历检测到的人脸(一张图片内可能有多个人脸)
for (const Rect &face: faces) {
// 画个方框
rectangle(src, face, Scalar(255, 0, 255));
// 如果需要获取训练素材,就将人脸图像转换成 24 * 24 的灰度图保存到手机指定目录中
if (needTraining) {
// 拷贝人脸数据(获取正样本)
Mat m;
src(face).copyTo(m);
// 将大小调整为 24x24 的,并且设置为灰度图,然后拷贝到手机的指定目录下
resize(m, m, Size(24, 24));
cvtColor(m, m, COLOR_BGR2GRAY);
char p[100];
// 注意如果路径不存在需要手动先创建文件夹,否则不会自动生成目录
sprintf(p, "/storage/emulated/0/FaceTest/%d.jpg", index++);
imwrite(p, m);
}
}
if (window) {
ANativeWindow_setBuffersGeometry(window, src.cols, src.rows, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer window_buffer;
do {
// 如果上锁失败就直接 break
// 起初一直上锁失败,原因是 CameraHelper 中使用 SurfaceHolder 进行预览而不是 SurfaceTexture
if (ANativeWindow_lock(window, &window_buffer, nullptr)) {
ANativeWindow_release(window);
window = nullptr;
break;
}
// 画图,将 Mat 的 data 指针指向的像素数据逐行拷贝到 window_buffer.bits 中
auto dst_data = static_cast<uint8_t *>(window_buffer.bits);
int dst_line_size = window_buffer.stride * 4;
for (int i = 0; i < window_buffer.height; ++i) {
// Mat 内的数据是 RGBA,因此计算每行首地址时,要在后面乘以 4,表示 RGBA8888 各占 1 个字节
memcpy(dst_data + i * dst_line_size, src.data + i * src.cols * 4, dst_line_size);
}
// 提交刷新
ANativeWindow_unlockAndPost(window);
} while (false);
}
src.release();
gray.release();
env->ReleaseByteArrayElements(data_, data, 0);
}
主要步骤,包括获取人脸训练素材的步骤都与 Windows 基本一致,区别在于 Android 需要将摄像头采集的图像旋转 90° 调正,并且需要将图像数据拷贝到 ANativeWindow 的缓冲区以实现图像渲染。
使用 Android 后置摄像头进行人脸识别的效果如下:
2、使用 CameraX 进行人脸识别
2.1 初始化
首先引入 CameraX 的依赖,完整的引入内容如下,但是本 Demo 只用到了 core、camera2 和 lifecycle 三项:
dependencies {
def camerax_version = "1.0.0"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:${camerax_version}"
// If you want to additionally use the CameraX Extensions library
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
由于 CameraX 已经对 Camera2 进行了封装,因此我们可以直接使用,而无需像前面的例子那样自己封装一个 CameraHelper 了。
首先我们在 Activity 的 onCreate() 中进行初始化工作:
class RecognitionActivity : AppCompatActivity(), SurfaceHolder.Callback, ImageAnalysis.Analyzer {
private lateinit var mCameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private lateinit var mFaceTracker: FaceTracker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityRecognitionBinding.inflate(layoutInflater)
setContentView(binding.root)
// 权限申请
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE),
REQUEST_CODE
)
// 为 SurfaceHolder 设置回调接口
binding.surfaceView.holder.addCallback(this)
// CameraX 初始化,异步获取 CameraProvider 对象
mCameraProviderFuture = ProcessCameraProvider.getInstance(this)
mCameraProviderFuture.addListener({
try {
val cameraProvider = mCameraProviderFuture.get()
bindAnalysis(cameraProvider)
} catch (e: Exception) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
// 将识别模型拷贝到手机中
val modelPath = Utils.copyAsset2Dir(this, "lbpcascade_frontalface.xml")
// 初始化 FaceTracker 开启人脸检测
mFaceTracker = FaceTracker(modelPath)
mFaceTracker.start()
}
}
CameraX
对 CameraX 进行异步初始化,先通过 ProcessCameraProvider.getInstance() 获取到 ListenableFuture<ProcessCameraProvider>
:
/**
* Futures.transform() 的三个参数:
* CameraX.getOrCreateInstance() 会返回一个包含已经初始化的 CameraX 对象的 ListenableFuture
* cameraX -> {} 是一个函数,参数 cameraX 是第一个参数的泛型对象,即 CameraX
* CameraXExecutors.directExecutor() 会返回主调线程中缓存的会直接执行任务的 Executor
* 会在指定的 Executor 中异步执行函数
*/
public static ListenableFuture<ProcessCameraProvider> getInstance(
@NonNull Context context) {
Preconditions.checkNotNull(context);
return Futures.transform(CameraX.getOrCreateInstance(context), cameraX -> {
sAppInstance.setCameraX(cameraX);
return sAppInstance;
}, CameraXExecutors.directExecutor());
}
随后为 mCameraProviderFuture 设置监听,异步获取到 CameraProvider 对象,并将其与生命周期绑定:
private fun bindAnalysis(cameraProvider: ProcessCameraProvider?) {
if (cameraProvider == null) {
return
}
/**
* 图片分析:得到摄像头图像数据
* STRATEGY_KEEP_ONLY_LATEST:非阻塞模式,每次获得最新帧
* STRATEGY_BLOCK_PRODUCER:阻塞模式,会得到每一张图片,处理不及时会导致帧率降低
*/
val imageAnalysis = ImageAnalysis.Builder()
// CameraX 会根据传入尺寸选择最佳的预览尺寸
.setTargetResolution(Size(640, 480))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
// 设置分析器,指定回调所发生的线程(池)
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), this)
// 绑定生命周期
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_FRONT_CAMERA, imageAnalysis)
}
FaceTracker
FaceTracker 是上层与 Native 交互的类:
class FaceTracker(modelPath: String) {
// 实际上是将上层的 FaceTracker 与 Native 的 FaceTracker 绑定
// 上层以 Native 对象地址的形式持有 Native 对象,这样做的目的是
// 让上层持有 C++ 对象,当上层将地址传回给 Native 层时,C++ 可以
// 将地址强转回成一个 C++ 对象并操作该对象,这样能实现多对多的绑定
private var mFaceTracker = 0L
init {
mFaceTracker = nativeInit(modelPath)
}
fun setSurface(surface: Surface?) {
nativeSetSurface(mFaceTracker, surface)
}
fun detect(bytes: ByteArray, width: Int, height: Int, rotationDegrees: Int) {
nativeDetect(mFaceTracker, bytes, width, height, rotationDegrees)
}
fun start() {
nativeStart(mFaceTracker)
}
fun stop() {
nativeStop(mFaceTracker)
}
fun release() {
nativeRelease(mFaceTracker)
mFaceTracker = 0
}
private external fun nativeInit(modelPath: String): Long
private external fun nativeSetSurface(faceTracker: Long, surface: Surface?)
private external fun nativeDetect(
faceTracker: Long,
bytes: ByteArray,
width: Int,
height: Int,
rotationDegrees: Int
)
private external fun nativeStart(faceTracker: Long)
private external fun nativeStop(faceTracker: Long)
private external fun nativeRelease(faceTracker: Long)
}
nativeInit() 就是创建一个 Native 的 FaceTracker 对象,然后将该对象的地址返回给上层:
extern "C"
JNIEXPORT jlong JNICALL
Java_com_face_recognition_FaceTracker_nativeInit(JNIEnv *env, jobject thiz, jstring model_path) {
const char *path = env->GetStringUTFChars(model_path, 0);
// 初始化FaceTracker对象
auto *tracker = new FaceTracker(path);
env->ReleaseStringUTFChars(model_path, path);
return (jlong) tracker;
}
此外,在布局中的 SurfaceView 的 SurfaceHolder 添加 SurfaceHolder.Callback 的回调方法中,需要通过 FaceTracker 将 Surface 传给 Native 层:
// SurfaceHolder.Callback start
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
mFaceTracker.setSurface(holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
mFaceTracker.setSurface(null)
}
// SurfaceHolder.Callback end
nativeSetSurface() 会通过上层传来的 Surface 创建 Native 层的 ANativeWindow 对象:
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition_FaceTracker_nativeSetSurface(JNIEnv *env, jobject thiz,
jlong face_tracker, jobject surface) {
if (face_tracker != 0) {
auto *tracker = reinterpret_cast<FaceTracker *>(face_tracker);
if (window) {
ANativeWindow_release(window);
window = nullptr;
}
window = ANativeWindow_fromSurface(env, surface);
tracker->setNativeWindow(window);
}
}
2.2 人脸识别
初始化 CameraX 时在 bindAnalysis() 中设置了分析器:
// 设置分析器,指定回调所发生的线程(池)
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), this)
第二个参数是 ImageAnalysis.Analyzer 接口,我们在 Activity 中实现它,接收摄像头采集到的数据:
// ImageAnalysis.Analyzer
override fun analyze(image: ImageProxy) {
val bytes = Utils.getDataFromImage(image)
mFaceTracker.detect(bytes, image.width, image.height, image.imageInfo.rotationDegrees)
image.close()
}
先从 ImageProxy 中提取出图像数据的 Byte 数组:
fun getDataFromImage(image: ImageProxy): ByteArray {
// 1.获取图像的宽高以及格式,计算出图片大小字节数
val rect = image.cropRect
val imageWidth = rect.width()
val imageHeight = rect.height()
val format = image.format
val size = imageWidth * imageHeight * ImageFormat.getBitsPerPixel(format) / 8
// 2.为 data 和 rowData 分配内存
val data = ByteArray(size)
// planes 是一个数组,每个元素是一个 ImageProxy.Plane 对象,
// Y、U、V 每种像素对应一个平面,分别是 planes[0]、planes[1]、
// planes[2],每个 Plane 包含该平面图像数据的 ByteBuffer 对象
val planes = image.planes
val rowData = ByteArray(planes[0].rowStride)
// 3.将 image 图像数据拷贝到 data 中,拷贝时按照 Y、U、V
// 三个平面分开拷贝
var channelOffset: Int
for (i in planes.indices) {
channelOffset = when (i) {
// y 从 0 开始
0 -> 0
// u 从 y 之后开始
1 -> imageWidth * imageHeight
// v 从 u 之后开始,u 的数据长度为 width * height / 4
2 -> (imageWidth * imageHeight * 1.25).toInt()
else -> throw IllegalArgumentException("Unexpected number of image planes")
}
// 这一个平面的数据缓冲区
val buffer = planes[i].buffer
// 行跨度,一行的步长,即这一行有像素数据所占用的字节数
val rowStride = planes[i].rowStride
// 像素跨度,即每一个像素占用的字节数,例如 RGB 就为 3
val pixelStride = planes[i].pixelStride
// UV 只有一半,因此要右移 1 位
val shift = if (i == 0) 0 else 1
val width = imageWidth shr shift
val height = imageHeight shr shift
// 移动到每个平面在 buffer 中的起始位置,准备读取该平面的数据
buffer.position(rowStride * (rect.top shr shift) + pixelStride * (rect.left shr shift))
var length: Int
for (row in 0 until height) {
if (pixelStride == 1) {
length = width
buffer.get(data, channelOffset, length)
channelOffset += length
} else {
length = (width - 1) * pixelStride + 1
buffer.get(rowData, 0, length)
for (col in 0 until width) {
data[channelOffset++] = rowData[col * pixelStride]
}
}
if (row < height - 1) {
buffer.position(buffer.position() + rowStride - length)
}
}
}
return data
}
然后将像素数据、图片宽高和旋转角度通过 FaceTracker 传递到 Native 层进行人脸检测:
fun detect(bytes: ByteArray, width: Int, height: Int, rotationDegrees: Int) {
nativeDetect(mFaceTracker, bytes, width, height, rotationDegrees)
}
private external fun nativeDetect(
faceTracker: Long,
bytes: ByteArray,
width: Int,
height: Int,
rotationDegrees: Int
)
来到 Native 层,将检测请求转发给 FaceTracker:
extern "C"
JNIEXPORT void JNICALL
Java_com_face_recognition_FaceTracker_nativeDetect(JNIEnv *env, jobject thiz, jlong face_tracker,
jbyteArray bytes, jint width, jint height,
jint rotation_degrees) {
if (face_tracker != 0) {
jbyte *data = env->GetByteArrayElements(bytes, nullptr);
auto *tracker = (FaceTracker *) face_tracker;
// 声明时将 detect() 的 data 的 jbyte 改为 int8_t,两个类型是一回事但是 cpp 中最好不要用 JNI 类型
tracker->detect(data, width, height, rotation_degrees);
env->ReleaseByteArrayElements(bytes, data, 0);
}
}
FaceTracker 收到图像数据后,先创建 OpenCV 的图像对象 Mat,将其转换成 RGBA 格式再旋转为正向,然后开始灰度化、直方图等人脸识别过程:
void FaceTracker::detect(int8_t *data, int width, int height, int rotation_degrees) {
// src 接收的是 YUV I420 的数据,因此高度应该是 height 的 1.5 倍
Mat src(height * 3 / 2, width, CV_8UC1, data);
// 将 YUV I420 格式的 src 转换为 RGBA 格式
cvtColor(src, src, COLOR_YUV2RGBA_I420);
// 调整图像,将其旋转为正向
if (rotation_degrees == 90) {
rotate(src, src, ROTATE_90_CLOCKWISE);
} else if (rotation_degrees == 270) {
rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
// 水平翻转
flip(src, src, 1);
}
// 灰度化、增强对比度
Mat gray;
cvtColor(src, gray, COLOR_RGBA2GRAY);
equalizeHist(gray, gray);
// 检测
tracker->process(gray);
// 获取检测结果
std::vector<Rect> faces;
tracker->getObjects(faces);
// 画矩形
for (const Rect &face: faces) {
rectangle(src, face, Scalar(0, 255, 0));
}
// 绘制 src
draw(src);
// 释放
src.release();
gray.release();
}
最后在 draw() 中将画了矩形人脸框的 Mat 对象绘制到 ANativeWindow 上:
void FaceTracker::draw(const Mat &img) {
pthread_mutex_lock(&mutex);
// do-while(false) 是为了进行流程控制,在不满足条件时直接退出
// 循环执行解锁操作,否则需要写多次解锁代码
do {
if (!window) {
break;
}
// 设置 Window Buffer 的格式与大小
ANativeWindow_setBuffersGeometry(window, img.cols, img.rows, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer buffer;
// 上锁,目的是为了拿到 buffer
if (ANativeWindow_lock(window, &buffer, nullptr)) {
ANativeWindow_release(window);
window = nullptr;
break;
}
// 获取 buffer 保存实际数据的地址以及步长
auto dstData = static_cast<uint8_t *>(buffer.bits);
int dstLineSize = buffer.stride * 4;
// 获取图片数据的起始地址与步长
uint8_t *srcData = img.data;
int srcLineSize = img.cols * 4;
// 逐行拷贝图像数据到 buffer.bits
for (int i = 0; i < buffer.height; ++i) {
memcpy(dstData + i * dstLineSize, srcData + i * srcLineSize, srcLineSize);
}
ANativeWindow_unlockAndPost(window);
} while (false);
pthread_mutex_unlock(&mutex);
}
至此,Android 实现人脸识别的两个例子讲解完毕。
参考资料:
CameraX 的版本历史、使用指南、代码示例