Android实现人脸检测

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.FaceDetectorML Kit Face Detection
来源Android原生APIGoogle ML Kit (属于Google Play服务)
维护状态旧API,已基本停止更新持续维护更新
最低APIAPI 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 或更高。

配置人脸检测器

设置属性
setPerformanceModePERFORMANCE_MODE_FAST(默认) PERFORMANCE_MODE_ACCURATE 在检测人脸时更注重速度还是准确性。
setLandmarkModeLANDMARK_MODE_NONE(默认) LANDMARK_MODE_ALL 是否尝试识别面部“特征点”:眼睛、耳朵、鼻子、脸颊、嘴巴等等。
setContourModeCONTOUR_MODE_NONE(默认) CONTOUR_MODE_ALL是否检测面部特征的轮廓。仅检测图片中最突出的人脸的轮廓。
setClassificationModeCLASSIFICATION_MODE_NONE(默认) CLASSIFICATION_MODE_ALL是否将人脸分为不同类别(例如“微笑”和“睁眼”)。
setMinFaceSizefloat(默认值:0.1f)设置所需的最小人脸大小,表示为头部宽度与图片宽度的比率。
enableTrackingfalse(默认)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();
                            }
                        });

            }
        }
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值