Android 人脸检测 非人脸识别

Note:文章以Google Android API提供的人脸探测技术进行讲解,并且使用的Camera class进行开发,而不是Google推荐的camera2,如果你开发中需要camera2可移步Detecting camera features with Camera2(需要梯子)
未引用OpenCV

人脸识别说明

人脸检测:检测并定位图片中的人脸,返回人脸框坐标。
人脸比对:比对两张脸为同一个人的可信度。
人脸关键点:定位返回人脸关键部位和四官坐标(眉、眼、鼻、口)
人脸属性:通过算法获取人脸属性,如:年龄、 性别、微笑程度、眼睛状态、人种等
说这么多是不是很激动,但别忘了题目是人脸检测,我们要讲的只有一个功能,人脸检测

API

Google官方给出的有两种方法:

  1. FaceDetector:通过传递Bitmap检测图中的人脸,同时返回眼睛部位,识别人数,未详细测试;
  2. Camera.FaceDetectionListener:通过打开摄像头(不是相机),实时获取人脸定位,最大5人。

讲解

FaceDetector传图识脸
 
    /**
     * Creates a FaceDetector, configured with the size of the images to
     * be analysed and the maximum number of faces that can be detected.
     * These parameters cannot be changed once the object is constructed.
     * Note that the width of the image must be even.
     * 
     * @param width  the width of the image
     * @param height the height of the image
     * @param maxFaces the maximum number of faces to identify
     *
     */
    public FaceDetector(int width, int height, int maxFaces){}

翻译:创建一个FaceDetector,配置要分析的图像的大小以及可以检测到的最大面孔数。一旦构建对象,这些参数就不能被更改。请注意,图像的宽度必须均匀。

用法:

public Bitmap detectionFace(Bitmap b) {        
            // 检测前必须转化为RGB_565格式。文末有详述连接
            Bitmap bitmap = b.copy(Bitmap.Config.RGB_565, true);
            b.recycle();
            // 设定最大可查的人脸数量
            int MAX_FACES = 5;
            FaceDetector faceDet = new FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES);
            // 将人脸数据存储到faceArray 中
            FaceDetector.Face[] faceArray = new FaceDetector.Face[MAX_FACES];
            // 返回找到图片中人脸的数量,同时把返回的脸部位置信息放到faceArray中,过程耗时
            int findFaceCount = faceDet.findFaces(bitmap, faceArray);
            // 获取传回的脸部数组中的第一张脸的信息
            FaceDetector.Face face1 = faceArray[0];
            // 获取双眼的中心点,用一个PointF来接收其x、y坐标
            PointF point = new PointF();
            face1.getMidPoint(point);
            // 获取该部位为人脸的可信度,0~1
            float confidence = face1.confidence();
            // 获取双眼间距
            float eyesDistance = face1.eyesDistance();
            // 获取面部姿势
            // 传入X则获取到x方向上的角度,传入Y则获取到y方向上的角度,传入Z则获取到z方向上的角度
            float angle = face1.pose(FaceDetector.Face.EULER_X);

            // todo 在bitmap上绘制一个Rect框住脸,因为返回的是眼睛位置,所以还要做一些处理

            return bitmap;
}

以上就是通过传递一张Bitmap然后找出其中面部的具体方法
识别率:低

Camera.FaceDetectionListener实时检测脸部

通过调用摄像头,提供一个实时检测脸部的方案
流程:打开摄像头→给Camera类传递回调接口→从接口获取脸部位置信息

    /**
     * Callback interface for face detected in the preview frame.
     *
     * @deprecated We recommend using the new {@link android.hardware.camera2} API for new
     *             applications.
     */
    @Deprecated
    public interface FaceDetectionListener
    {
        /**
         * Notify the listener of the detected faces in the preview frame.
         *
         * @param faces The detected faces in a list
         * @param camera  The {@link Camera} service object
         */
        void onFaceDetection(Face[] faces, Camera camera);
    }

这是一个回调接口,供Camera使用。返回的数据包含脸部集合和摄像头对象。
用法:

  1. 在Activity的onCreate中提供一个SurfaceView给Camera显示图像
    private void initViews() {
        surfaceView = new SurfaceView(this);
        rectView = new DrawFacesView(this);
        addContentView(surfaceView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
        addContentView(rectView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
    }

这是Activity布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 在onCreate()中打开相机,设置监听
    /**
     * 把摄像头的图像显示到SurfaceView
     */
    private void openSurfaceView() {
        mHolder = surfaceView.getHolder();
        mHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                if (mCamera == null) {
                    mCamera = Camera.open();
                    try {
                        // 设置脸部检测监听
                        mCamera.setFaceDetectionListener(new FaceDetectorListener());
                        mCamera.setPreviewDisplay(holder);
                        // 开始脸部检测
                        startFaceDetection();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                if (mHolder.getSurface() == null) {
                    // preview surface does not exist
                    Log.e(TAG, "mHolder.getSurface() == null");
                    return;
                }

                try {
                    mCamera.stopPreview();

                } catch (Exception e) {
                    // ignore: tried to stop a non-existent preview
                    Log.e(TAG, "Error stopping camera preview: " + e.getMessage());
                }

                try {
                    mCamera.setPreviewDisplay(mHolder);
                    int measuredWidth = surfaceView.getMeasuredWidth();
                    int measuredHeight = surfaceView.getMeasuredHeight();
                    setCameraParms(mCamera, measuredWidth, measuredHeight);
                    mCamera.startPreview();

                    startFaceDetection(); // re-start face detection feature

                } catch (Exception e) {
                    // ignore: tried to stop a non-existent preview
                    Log.d(TAG, "Error starting camera preview: " + e.getMessage());
                }
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                mCamera.stopPreview();
                mCamera.release();
                mCamera = null;
                holder = null;
            }
        });
    }

    /**
     * 在摄像头启动前设置参数
     *
     * @param camera
     * @param width
     * @param height
     */
    private void setCameraParms(Camera camera, int width, int height) {
        // 获取摄像头支持的pictureSize列表
        Camera.Parameters parameters = camera.getParameters();
        // /**/注释的地方非必须,参考来源  [Android 手把手带你玩转自定义相机](http://blog.csdn.net/qq_17250009/article/details/52795530)
        /*List<Camera.Size> pictureSizeList = parameters.getSupportedPictureSizes();
        // 从列表中选择合适的分辨率
        Camera.Size pictureSize = getProperSize(pictureSizeList, (float) height / width);
        if (null == pictureSize) {
            pictureSize = parameters.getPictureSize();
        }
        // 根据选出的PictureSize重新设置SurfaceView大小
        float w = pictureSize.width;
        float h = pictureSize.height;
        parameters.setPictureSize(pictureSize.width, pictureSize.height);

        surfaceView.setLayoutParams(new FrameLayout.LayoutParams((int) (height * (h / w)), height));

        // 获取摄像头支持的PreviewSize列表
        List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes();
        Camera.Size preSize = getProperSize(previewSizeList, (float) height / width);
        if (null != preSize) {
            parameters.setPreviewSize(preSize.width, preSize.height);
        }
*/
        parameters.setJpegQuality(100);
        // 不对焦,拍摄电脑上的图片都模糊
        if (parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            // 连续对焦
            parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        }
        camera.cancelAutoFocus();
        // 摄像头图像旋转90度,此处不严谨
        camera.setDisplayOrientation(90);// 可注释该行代码看看效果
        camera.setParameters(parameters);
    }

    /**
    * 启动脸部检测,如果getMaxNumDetectedFaces()!=0说明不支持脸部检测
    */
    public void startFaceDetection() {
        // Try starting Face Detection
        Camera.Parameters params = mCamera.getParameters();
        // start face detection only *after* preview has started
        if (params.getMaxNumDetectedFaces() > 0) {
            // mCamera supports face detection, so can start it:
            mCamera.startFaceDetection();
        } else {
            Log.e("tag", "【FaceDetectorActivity】类的方法:【startFaceDetection】: " + "不支持");
        }
    }
  1. 脸部回调接口
    /**
     * 脸部检测接口
     */
    private class FaceDetectorListener implements Camera.FaceDetectionListener {
        @Override
        public void onFaceDetection(Camera.Face[] faces, Camera camera) {
            if (faces.length > 0) {
                Camera.Face face = faces[0];
                Rect rect = face.rect;
                Log.d("FaceDetection", "可信度:" + face.score + "face detected: " + faces.length +
                        " Face 1 Location X: " + rect.centerX() +
                        "Y: " + rect.centerY() + "   " + rect.left + " " + rect.top + " " + rect.right + " " + rect.bottom);
                Log.e("tag", "【FaceDetectorListener】类的方法:【onFaceDetection】: ");
                Matrix matrix = updateFaceRect();
                facesView.updateFaces(matrix, faces);
            } else {
                // 只会执行一次
                Log.e("tag", "【FaceDetectorListener】类的方法:【onFaceDetection】: " + "没有脸部");
                facesView.removeRect();
            }
        }
    }
    /**
     * 因为对摄像头进行了旋转,所以同时也旋转画板矩阵
     * 详细请查看{@link android.hardware.Camera.Face#rect}
     * @return 旋转后的矩阵
     */
    private Matrix updateFaceRect() {
        Matrix matrix = new Matrix();
        Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
        // Need mirror for front camera.
        boolean mirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);
        matrix.setScale(mirror ? -1 : 1, 1);
        // This is the value for android.hardware.Camera.setDisplayOrientation.
        // 刚才我们设置了camera的旋转参数,所以这里也要设置一下
        matrix.postRotate(90);
        // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
        // UI coordinates range from (0, 0) to (width, height).
        matrix.postScale(surfaceView.getWidth() / 2000f, surfaceView.getHeight() / 2000f);
        matrix.postTranslate(surfaceView.getWidth() / 2f, surfaceView.getHeight() / 2f);
        return matrix;
    }
  1. 现在插播一条消息:
    updateFaceRect()解释:在脸部检测时,获得的Rect并不是View的坐标,而是这样的一个坐标系,中心为原点,所以我们需要转换一下


    camera-area-coordinates.png
  2. 绘制脸部方框的View

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.view.View;
public class DrawFacesView extends View {

    private Matrix matrix;
    private Paint paint;
    private Camera.Face[] faces;
    private boolean isClear;

    public DrawFacesView(Context context) {
        this(context, null);
    }

    public DrawFacesView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DrawFacesView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(Color.GREEN);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        faces = new Camera.Face[]{};
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.setMatrix(matrix);
//        canvas.drawRect(rect, paint);
        for (Camera.Face face : faces) {
            if (face == null) break;
            canvas.drawRect(face.rect, paint);
            if (face.leftEye != null)
                canvas.drawPoint(face.leftEye.x, face.leftEye.y, paint);
            if (face.rightEye != null)
                canvas.drawPoint(face.rightEye.x, face.rightEye.y, paint);
            if (face.mouth != null)
                canvas.drawPoint(face.mouth.x, face.mouth.y, paint);
            // 因为旋转了画布矩阵,所以字体也跟着旋转
//            canvas.drawText(String.valueOf("id:" + face.id + "\n置信度:" + face.score), face.rect.left, face.rect.bottom + 10, paint);
        }
        if (isClear) {
            canvas.drawColor(Color.WHITE, PorterDuff.Mode.CLEAR);
            isClear = false;
        }
    }
    /**
     * 绘制脸部方框
     *
     * @param matrix 旋转画布的矩阵
     * @param faces 脸部信息数组
     */
    public void updateFaces(Matrix matrix, Camera.Face[] faces) {
        this.matrix = matrix;
        this.faces = faces;
        invalidate();
    }

    /**
     * 清除已经画上去的框
     */
    public void removeRect() {
        isClear = true;
        invalidate();
    }
}

权限

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

效果:

  1. 传入Bitmap识别
检测Bitmap2.jpg
  1. 摄像头实时检测
动态识别.png
动态识别多人.png

参考

Google Android Camera API
Android 手把手带你玩转自定义相机

体验

体验APK
源码Github

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 135,713评论 17 577
  • 语音识别,语义理解一站式解决之智能照相机(人脸识别,olami) 转载请注明CSDN博文地址:http://blo...
    ls0609阅读 735评论 0 1
  • 1. Outline 本文主要从以下三个大的方面来说明一下2D Graphic 绘图的一些相关函数及应用。 Col...
    lee_3do阅读 1,499评论 0 11
  • 介绍自己负责的部分,如何实现的。 框架的搭建排查问题以及结解决方式兼容性保证性能优化上线之后模块导致crash的比...
    黄海佳阅读 11,870评论 6 352
  • 《安藤忠雄论建筑/建築を語る》 建筑工业出版社“事实上,我认为安藤的作品还不能表现出他更高的理论境界。” 第一次接...
    日本建筑译介阅读 378评论 0 5