Google官方人脸检测介绍如下:
https://developers.google.cn/ml-kit/vision/face-detection?hl=zh-cn&authuser=4
一、ML Kit Face Detection FaceDetector 与 android.media.FaceDetector 的区别
这两者都是Android平台上的人脸检测技术,但它们在实现方式、功能特性和性能表现上有显著差异:
特性 | android.media.FaceDetector | ML Kit Face Detection |
---|---|---|
来源 | Android原生API | Google ML Kit (属于Google Play服务) |
维护状态 | 旧API,已基本停止更新 | 持续维护更新 |
最低API | API 1 (所有Android版本) | 此 API 需要 Android API 级别 21 或更高级别 |
推荐使用ML Kit Face Detection FaceDetector进行人脸检测
二、ML Kit Face Detection FaceDetector 人脸检测
借助机器学习套件的人脸检测 API,您可以检测图片中的人脸、识别主要的面部特征,并获取检测到的人脸的轮廓。请注意,该 API 用于检测人脸,不识别人脸。
主要功能
- 识别和定位面部特征:获取检测到的每个人脸的眼睛、耳朵、脸颊、鼻子和嘴巴的坐标。
- 获取面部特征的轮廓: 获取检测到的面部的轮廓及其眼睛、眉毛、嘴唇和鼻子。
- 识别面部表情: 确定人物是在微笑还是闭着眼睛。
- 跨视频帧跟踪人脸: 获取每个检测到的唯一身份人脸的标识符。 标识符在不同调用中保持一致,因此您可以对视频流中的特定人员执行图片处理。
- 实时处理视频帧:人脸检测在设备上执行,其速度足以在视频处理等实时应用中使用
三、人脸检测概念
人脸检测功能可定位数字图片或视频等视觉媒体中的人脸。当检测到人脸时,它具有关联的位置、大小和方向;并且可以搜索眼睛和鼻子等特征点。
以下是我们在机器学习套件的人脸检测功能中使用的一些术语:
-
面部跟踪将面部检测扩展到视频序列。系统可以逐帧跟踪出现在视频中任意时长的任何人脸。这意味着,在连续的视频帧中检测到的人脸可以被识别为同一个人。请注意,这并不是一种人脸识别形式;人脸跟踪仅根据人脸在视频序列中的位置和动作进行推断。
-
特征点是指人脸内的兴趣点。左眼、右眼和鼻基底部都是特征点。机器学习套件能够在检测到的面部上查找特征点。
-
轮廓是一组与面部特征形状相一致的点。机器学习套件能够找到面部的轮廓线。
-
分类决定了是否出现了某些面部特征。例如,人脸可以按照眼睛是睁开还是闭着,或者脸部是否有微笑进行分类。
四、Android中实现人脸检测
引入人脸检测库:
dependencies {
implementation 'com.google.mlkit:face-detection:16.1.7'
}
此 API 需要 Android API 级别 21 或更高级别。确保您应用的 build 文件使用的 minSdkVersion 值为 21 或更高。
配置人脸检测器
设置 | 属性 |
---|---|
setPerformanceMode | PERFORMANCE_MODE_FAST(默认) PERFORMANCE_MODE_ACCURATE 在检测人脸时更注重速度还是准确性。 |
setLandmarkMode | LANDMARK_MODE_NONE(默认) LANDMARK_MODE_ALL 是否尝试识别面部“特征点”:眼睛、耳朵、鼻子、脸颊、嘴巴等等。 |
setContourMode | CONTOUR_MODE_NONE(默认) CONTOUR_MODE_ALL是否检测面部特征的轮廓。仅检测图片中最突出的人脸的轮廓。 |
setClassificationMode | CLASSIFICATION_MODE_NONE(默认) CLASSIFICATION_MODE_ALL是否将人脸分为不同类别(例如“微笑”和“睁眼”)。 |
setMinFaceSize | float(默认值:0.1f)设置所需的最小人脸大小,表示为头部宽度与图片宽度的比率。 |
enableTracking | false(默认)true是否为人脸分配 ID,以用于跨图片跟踪人脸。 |
请注意,启用轮廓检测后,仅会检测一张人脸,因此人脸跟踪不会产生有用的结果。因此,为了加快检测速度,请勿同时启用轮廓检测和人脸跟踪。
// 配置人脸检测器
FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)//在检测人脸时更注重速度还是准确性。
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)//是否尝试识别面部“特征点”
.build();
获取 FaceDetector 实例
faceDetector = FaceDetection.getClient(options);
cameraExecutor = Executors.newSingleThreadExecutor();
在CameraX相机预览中添加图片检测
// 配置图像分析
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(cameraExecutor, new FaceAnalyzer());
/**
* 人脸检测过程
*/
public class FaceAnalyzer implements ImageAnalysis.Analyzer {
@OptIn(markerClass = ExperimentalGetImage.class)
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
// 控制人脸检测
if (!isOpenDetector) {
imageProxy.close();
return;
}
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
// 将 ImageProxy 转换为 InputImage
InputImage image = InputImage.fromMediaImage(
mediaImage,
imageProxy.getImageInfo().getRotationDegrees()
);
// 处理图像检测人脸
faceDetector.process(image)
.addOnSuccessListener(new OnSuccessListener<List<Face>>() {
@Override
public void onSuccess(List<Face> faces) {
// 更新UI显示人脸框
updateFaceOverlay(faces);
imageProxy.close();
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LogUtils.e(e.getMessage());
imageProxy.close();
}
});
}
}
}
/**
* 人脸检测结果,只能检测,不能识别
*/
private void updateFaceOverlay(List<Face> faces) {
LogUtils.d("发现人脸个数: faceCount = " + faces.size());
if (!faces.isEmpty()) {
mViewBinding.tvTip.setTextColor(getColor(R.color.white));
mViewBinding.tvTip.setText("检测到人脸个数: "+ faces.size());
}else {
mViewBinding.tvTip.setTextColor(getColor(R.color.color_FF0000));
mViewBinding.tvTip.setText(“未检测到人脸”);
}
}
五、完整检测代码
public class CameraXActivity extends BaseVBActivity<ActivityCameraXBinding> {
private VideoCapture<Recorder> videoCapture;
private ImageCapture imageCapture;
private Recording recording;
// 人脸识别控制
public boolean isOpenDetector = true;
private FaceDetector faceDetector;
private ExecutorService cameraExecutor;
@Override
protected void initView() {
// 配置人脸检测器
FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)//在检测人脸时更注重速度还是准确性。
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)//是否尝试识别面部“特征点”
.build();
//获取 FaceDetector 实例
faceDetector = FaceDetection.getClient(options);
cameraExecutor = Executors.newSingleThreadExecutor();
startCamera();
}
/**
* 开启摄像头
*/
public void startCamera() {
//创建 ProcessCameraProvider 的实例。这用于将相机的生命周期绑定到生命周期所有者。这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(mContext);
//向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。
cameraProviderFuture.addListener(new Runnable() {
@Override
public void run() {
// 在 Runnable 中,添加 ProcessCameraProvider。它用于将相机的生命周期绑定到应用进程中的 LifecycleOwner
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
// 初始化 Preview 对象,在其上调用 build,从取景器中获取 Surface 提供程序,然后在预览上进行设置。
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(mViewBinding.viewFinder.getSurfaceProvider());
//创建 ImageCapture 用例
imageCapture = new ImageCapture.Builder().build();
// 配置图像分析
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(cameraExecutor, new FaceAnalyzer());
//创建 VideoCapture 用例
Recorder recorder = new Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
.build();
videoCapture = VideoCapture.withOutput(recorder);
// 创建 CameraSelector 对象,然后选择 DEFAULT_BACK_CAMERA(后置摄像头)
// CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
// CameraSelector cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA;
// 选择后置摄像头
CameraSelector cameraSelector = new CameraSelector.Builder()
// .requireLensFacing(CameraSelector.LENS_FACING_BACK)//选择摄像头朝向
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)//选择摄像头朝向
.build();
try {
//创建一个 try 代码块。在此块内,确保没有任何内容绑定到 cameraProvider
cameraProvider.unbindAll();
// 然后将 cameraSelector 和预览对象绑定到 cameraProvider
cameraProvider.bindToLifecycle(
CameraXActivity.this, cameraSelector, preview, imageAnalysis, videoCapture);
} catch (Exception e) {
LogUtils.e("相机预览绑定失败", e.getMessage());
}
} catch (Exception e) {
LogUtils.e(e.getMessage());
}
}
}, ContextCompat.getMainExecutor(mContext));
}
/**
* 人脸检测结果,只能检测,不能识别
*/
private void updateFaceOverlay(List<Face> faces) {
LogUtils.d("发现人脸个数: faceCount = " + faces.size());
if (!faces.isEmpty()) {
mViewBinding.tvTip.setTextColor(getColor(R.color.white));
mViewBinding.tvTip.setText(getString(R.string.camerax_tip2)+" "+ faces.size());
// mViewBinding.tvTip.setText(R.string.camerax_tip2);
}else {
mViewBinding.tvTip.setTextColor(getColor(R.color.color_FF0000));
mViewBinding.tvTip.setText(R.string.no_face);
}
// 清除之前的绘制
mViewBinding. faceOverlay.removeAllViews();
// 创建自定义View绘制人脸框
FaceOverlayView overlayView = new FaceOverlayView(this, faces, mViewBinding. viewFinder.getWidth(), mViewBinding. viewFinder.getHeight());
mViewBinding. faceOverlay.addView(overlayView);
}
/**
* 设置点击事件
*/
@Override
protected void setOnClick() {
mViewBinding.imageCaptureButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ToastUtils.showShort("拍照");
takePhoto();
}
});
mViewBinding.videoCaptureButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ToastUtils.showShort("录制视频");
captureVideo();
}
});
}
/**
* 拍照
*/
private void takePhoto() {
// 首先,获取对 ImageCapture 用例的引用。如果用例为 null,请退出函数。如果在设置图片拍摄之前点按“photo”按钮,它将为 null。如果没有 return 语句,应用会在该用例为 null 时崩溃。
if (imageCapture == null) {
return;
}
//接下来,创建用于保存图片的 MediaStore 内容值。请使用时间戳,确保 MediaStore 中的显示名是唯一的。
String name = TimeUtil.getCurrentTimeName();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image");
}
// 创建一个 OutputFileOptions 对象。在该对象中,您可以指定所需的输出内容。我们希望将输出保存在 MediaStore 中,以便其他应用可以显示它,因此,请添加我们的 MediaStore 条目。
ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions
.Builder(getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build();
// 对 imageCapture 对象调用 takePicture()。传入 outputOptions、执行器和保存图片时使用的回调。
imageCapture.takePicture(outputOptions,
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
//照片拍摄成功!将照片保存到我们之前创建的文件中,显示消息框,让用户知道照片已拍摄成功,并输出日志语句。
String msg = "Photo capture succeeded: " + outputFileResults.getSavedUri();
ToastUtils.showShort(name + "\n" + msg);
LogUtils.i(msg);
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
//如果图片拍摄失败或保存图片失败,请添加错误情况以记录失败。
LogUtils.e("Photo capture failed:" + exception.getMessage(), exception);
}
});
}
/**
* 录制视频
*/
private void captureVideo() {
//检查是否已创建 VideoCapture 用例:如果尚未创建,则不执行任何操作。
if (videoCapture == null) {
return;
}
// 在 CameraX 完成请求操作之前,停用按钮;在后续步骤中,它会在我们的已注册的 VideoRecordListener 内重新启用。
mViewBinding.videoCaptureButton.setEnabled(false);
//如果有正在进行的录制操作,请将其停止并释放当前的 recording。当所捕获的视频文件可供我们的应用使用时,我们会收到通知。
Recording curRecording = recording;
if (curRecording != null) {
curRecording.stop();
recording = null;
return;
}
//为了开始录制,我们会创建一个新的录制会话。首先,我们创建预定的 MediaStore 视频内容对象,将系统时间戳作为显示名(以便我们可以捕获多个视频)。
String name = TimeUtil.getCurrentTimeName();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
//使用外部内容选项创建 MediaStoreOutputOptions.Builder。
MediaStoreOutputOptions mediaStoreOutputOptions = new MediaStoreOutputOptions
.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build();
//检测录音权限
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
return;
}
//将输出选项配置为 VideoCapture<Recorder> 的 Recorder 并启用录音:
recording = videoCapture.getOutput()
.prepareRecording(mContext, mediaStoreOutputOptions)
.withAudioEnabled()
.start(ContextCompat.getMainExecutor(this),
new Consumer<VideoRecordEvent>() {
@Override
public void accept(VideoRecordEvent videoRecordEvent) {
if (videoRecordEvent instanceof VideoRecordEvent.Start) {//当相机设备开始请求录制时
mViewBinding.videoCaptureButton.setText(R.string.stop_capture);
mViewBinding.videoCaptureButton.setEnabled(true);
} else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {//完成录制后,用消息框通知用户,并将“Stop Capture”按钮切换回“Start Capture”,然后重新启用它:
if (!((VideoRecordEvent.Finalize) videoRecordEvent).hasError()) {
String msg = "Video capture succeeded: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getOutputResults().getOutputUri();
ToastUtils.showShort(msg);
LogUtils.e(msg);
} else {
if (recording != null) {
recording.close();
recording = null;
LogUtils.e("Video capture ends with error: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getError());
}
}
mViewBinding.videoCaptureButton.setText(R.string.start_capture);
mViewBinding.videoCaptureButton.setEnabled(true);
}
}
});
}
/**
* 人脸检测过程
*/
public class FaceAnalyzer implements ImageAnalysis.Analyzer {
@OptIn(markerClass = ExperimentalGetImage.class)
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
// 控制人脸检测
if (!isOpenDetector) {
imageProxy.close();
return;
}
Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
// 将 ImageProxy 转换为 InputImage
InputImage image = InputImage.fromMediaImage(
mediaImage,
imageProxy.getImageInfo().getRotationDegrees()
);
// 处理图像检测人脸
faceDetector.process(image)
.addOnSuccessListener(new OnSuccessListener<List<Face>>() {
@Override
public void onSuccess(List<Face> faces) {
// 更新UI显示人脸框
updateFaceOverlay(faces);
imageProxy.close();
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
LogUtils.e(e.getMessage());
imageProxy.close();
}
});
}
}
}
}