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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

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