Cardinfolink 讯联数据

人脸识别趟坑历程

2017-07-04
Wanny

1.人脸识别概述

人脸识别,是基于人的脸部特征信息进行身份识别的一种生物识别技术。用摄像机或摄像头采集含有人脸的图像或视频流,并自动在图像中检测和跟踪人脸,进而对检测到的人脸进行脸部的一系列相关技术。其中技术包括图像采集、特征定位、身份的确认和查找等等。简单来说,就是从照片中提取人脸中的特征,比如眉毛高度、嘴角等等,再通过特征的对比输出结果。

听着这么高大上,很高科技,然而目前很多人脸识别的落地应用还处在一个非常初级的阶段,在我国应用最多还是1:1等级,也就是人脸识别中最初级的“证明你是你”。 另外国内有很多人脸开发平台,可以做一些人脸识别的简单开发。

笔者直接采用市场上开放的人脸识别平台的API,进行人脸识别快速开发,不涉及原理、算法、模型等复杂东西,当然如果对人脸识别原理比较感兴趣,可以参考这篇博客:人脸识别(face recognition)

笔者在讯联数据从事扫码支付工作时,目前正在进行智能设备上人脸识别研究,本文就记录笔者在人脸识别这个项目的趟坑历程,希望对人脸识别感兴趣的各位作一些参考。

2. 人脸识别平台选择

目前人脸识别在国内发展迅速,各种新兴公司如雨后春笋,目前中国人脸识别的独角兽:face++ 的旷视科技,senseTime的商汤科技,还有云从科技,依图科技,他们依旧在继续发力,抢夺市场。

而国内的BAT对人脸识别这个方向也蛮重视,阿里巴巴控股旷视科技、依图科技,并且开发自己的人脸识别接口,已全面将人脸识别技术应用到自己支付宝、淘宝等平台,并联合系统旗下其他业务板块,培养人脸识别的应用场景;腾讯旗下有自己内部的优图团队,为 QQ 空间、腾讯地图、腾讯游戏、等 50 多款产品提供图像技术支持;百度人脸识别也依靠庞大的数据资源进步神速,已推出百度识图、脸优app等产品。

目前云从科技,依图,商汤等知名的人脸识别公司主要做企业用户,目前市面上能提供开放平台给开发者,免费进行调试的就只有BAT三家旗下的产品

下面是笔者调研百度、腾讯、face++这三家人脸识别开放平台对比。

公司 免费额度 收费 技术能力 知名应用
百度 <=1000次/天 公测阶段,未开始收费 99.77%(LFW) 百变魔图 百度网盘
腾讯优图 2万张/月 >=0.0005元/张,阶梯价位 99.80%(LFW) 83.29%(MegaFace) 微众银行 手机QQ
face++ 有限用量,共享的 QPS 配额,<=1000FaceSet,<=1000000faces >=0.001次 技术先进 支付宝 滴滴

上面的三家企业均可以免费接入,然后对比下来,各位选择谁家呢?face++平台开放早,业内名气大,应用多,背靠阿里,坐拥庞大的用户群体;腾讯优图平台后发先至,有庞大的社交平台作依托,技术实力强悍;百度技术先进,免费额度高。

笔者产品离大规模商业应用还是有段距离,现阶段还是技术体验阶段,所以笔者就选择免费额度比较大的百度。现阶段百度还是公测阶段,正式收费后,可以重点关注腾讯优图与百度。

3. 人脸识别流程

从2015年3月马云展示支付宝的刷脸支付,目前很多app中也加入了人脸识别功能,然而现在人脸识别应用还在很初级阶段,目前我国应用最多还是1:1等级,也就是人脸识别中最初级的“证明你是你”,一般是用户上传符合规则的照片到系统注册用户,然后用户线下拍照,于系统中的照片进行对比。其他比较复杂的1:N级和N:N级的人脸识别目前限于技术原因,还不能进行商业应用。

下图是目前人脸识别最常见的流程图: 用户拍摄自己身份信息并上传系统,系统经过公民身份信息查询获取用户信息,建立用户档案,关联用户人脸;下次扫描头像,经活体检测、人脸质量检测、人脸图像等处理后进行人脸对比,核对结果,完成“你是你”的证明。

另外目前中国公民身份证上的图片一般拍摄的比较早,与本人现在头像差距较大,如果直接用身份证上面的图片会加大识别难度,识别误差大。目前通常做法是完成身份证信息提交后,还需添加人脸,即进行活体检测、人脸质量检测、处理人脸图像,添加用户的现在头像并关联用户,下次人脸识别时与此头像进行对比。

目前市场上大部分带有人脸识别的app,都是C端产品,用户扫自己,实现在线用户身份验证。当人脸核对时,app需将用户信息及拍摄的人脸图像上传,根据用户信息获取该用户之前人脸数据,其实最终的是现在拍摄的人脸与该用户所关联之前的人脸图像进行对比,判断是否为「真人」且为「本人」,快捷的完成身份核实工作。

笔者的小项目就没有这么复杂的,而且信息少,是B端产品,商户使用。商户扫描用户,识别用户的人脸,查询人脸数据库,找到最相似的,当相似度超过80%,就可以判断该用户就是我们所需要的客户,完成身份的确认。我们项目更像是一个会员的管理系统。详细的流程图如下:

拍摄会员头像,上传头像并关联会员的id,系统存储他们关系,完成用户注册;当下次用户过来,扫描用户进行人脸检测、人脸处理、上传头像、人脸识别,查询到该头像所对应的会员id, 从而得到该用户的所有信息,从而对该用户进行后续服务。

然而落实到具体项目中,我们发现如果我们进行人脸识别过程、完成人脸捕获、质量检测、头像处理、人脸检测、活体检测、人脸识别等这么多流程,会导致我们耗时严重,体检较差,我们必须精简流程,提高人脸识别速度。另外市场上app 要求用户进行眨眼、说口令等操作,通过我们的一些微表情、微动作判断摄象头面前的人是活生生的真人,完成活体检测,验证是否真人,而非是虚假的头像。但是目前笔者还未找到免费开放的活体检测API,而百度人脸识别API中含有活体检测参数配置,可以进行静态的活体检测,笔者就采用此方案。

人脸识别方面的流程精简优化为:

当设备扫描人脸,预览头像自动完成人脸定位与检测。开启子线程处理扫描得到的图片,进行人脸检测,当人脸检测失败时继续获取图片开始新一轮的检测人脸,直至人脸检测成功。当人脸检测返回成功,停止扫描人脸检测,开始处理头像、上传头像、进行人脸识别。另外如果设备对准了人脸头像,确认头像无误,也可以手动拍摄头像、上传头像、进行人脸识别。

3. 人脸识别趟坑细节

3.1 图片压缩

目前市面的人脸识别的 API 对上传的图片都是有一定的限制的,好一点的是10M,限制比较死的是2M, 例如笔者在用的百度人脸识别API中对人脸检测的上传图片限制是小于2M, 人脸识别是小于10M;腾讯优图对整个请求包体限制为2M, 那上传的图片限制更小了;

在这里需要注意一个问题,现在很多家人脸识别开放平台对图片限制的大小是指base64编码后大小。我们知道Base64编码要求把3个8位字节转化为4个6位的字节,之后在6位的前面补两个0,形成8位一个字节的形式。如果剩下的字符不足3个字节,则用0填充,输出字符使用’=’,因此编码后输出的文本末尾可能会出现1或2个’=’。由此可见图片经base64编码后数据量会变大,简单计算大约是原来数据量的4/3倍。 (具体可以参见http://www.ruanyifeng.com/blog/2008/06/base64.html

所以最终上传图片大小限制更小了,笔者使用百度人脸识别接口,限制为10M,实际上图片限制大小为10*3/4= 7.5M。

目前的手机摄像头拍照出现的照片像素都很高,动不动就1200W像素,1600W像素,甚至是2000W也常见,拍出来的图片数据量都很大,一张图片几十M现在都很常见。然后这么大的图片直接上传给人脸识别API 肯定是不行的。我们必须要想方设法减少图片的大小。

另外笔者发现当我们上传的图片数据量比较大时,API响应比较慢,笔者曾经测试过当上传图片为10M,API 返回结果时间需要15s左右,当减到5M为10s左右,当图片大小减到2M返回结果需要7s左右(统计数据不是很准确,仅供参考)。 这个原因不难理解,当我们上传图片数据量大,我们图片上传及服务器处理肯定耗时加长,为提高API 调用速度,减少响应时间,也必须减少图片大小,进行图片压缩。

减少图片数据量的原理无非就是两条:一是缩小图片的长宽,二是降低图片的质量。应用中一般我们采用方法如下:

  1. 采用预览框,让拍摄时让头像落在预览框中,减少多余图像。
  2. 进行人脸检测,裁剪头像外多余图片
  3. 合理设置压缩比,缩小图片
  4. 降低图片质量

另外图片压缩算法Luban,号称可能是最接近微信朋友圈的图片压缩算法,各位可以借鉴一下。

3.2 相机调整

目前的人脸识别都需要相机预览头像,并拍摄头像照片,因此很多工作与相机相关,我们需要设置好相机参数。

当我们对准人脸时候发现,相机离人脸比较远时,预览的人脸比较小;当相机靠近时,预览的人脸比较大。当我们的人脸头像过小时,头像质量比较差,人脸检测及人脸识别容易出差错。另外测试发现每个手机的放大倍数不是都是相同的,这可能与各个手机的型号相关。如果直接设置为一个固定值,这可能会在某些手机上过度放大,某些手机上放大的倍数不够。索性相机的参数设定里给我们提供了最大的放大倍数值,通过取放大倍数值的N分之一作为当前的放大倍数,就完美地解决了手机的适配问题。


    private static final int TEN_DESIRED_ZOOM = 27;

      private void setZoom(Camera.Parameters parameters) {
        String zoomSupportedString = parameters.get("zoom-supported");
        if (zoomSupportedString != null && !Boolean.parseBoolean(zoomSupportedString)) {
            return;
        }

        int tenDesiredZoom = TEN_DESIRED_ZOOM;
        String maxZoomString = parameters.get("max-zoom");
        if (maxZoomString != null) {
            try {
                int tenMaxZoom = (int) (10.0 * Double.parseDouble(maxZoomString));
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad max-zoom: " + maxZoomString);
            }
        }

        String takingPictureZoomMaxString = parameters.get("taking-picture-zoom-max");
        if (takingPictureZoomMaxString != null) {
            try {
                int tenMaxZoom = Integer.parseInt(takingPictureZoomMaxString);
                if (tenDesiredZoom > tenMaxZoom) {
                    tenDesiredZoom = tenMaxZoom;
                }
            } catch (NumberFormatException nfe) {
                Log.e(TAG, "Bad taking-picture-zoom-max: " + takingPictureZoomMaxString);
            }
        }

        String motZoomValuesString = parameters.get("mot-zoom-values");
        if (motZoomValuesString != null) {
            tenDesiredZoom = findBestMotZoomValue(motZoomValuesString, tenDesiredZoom);
        }

        String motZoomStepString = parameters.get("mot-zoom-step");
        if (motZoomStepString != null) {
            try {
                double motZoomStep = Double.parseDouble(motZoomStepString.trim());
                int tenZoomStep = (int) (10.0 * motZoomStep);
                if (tenZoomStep > 1) {
                    tenDesiredZoom -= tenDesiredZoom % tenZoomStep;
                }
            } catch (NumberFormatException nfe) {
                // continue
            }
        }

        if (parameters.isZoomSupported()) {
            Log.e(TAG, "max-zoom:" + parameters.getMaxZoom());
            if (SystemUtils.isMadGlass() && parameters.getMaxZoom() >= 8) {
                parameters.setZoom(8);
            } else {
                parameters.setZoom(parameters.getMaxZoom() / tenDesiredZoom);
            }
        } else {
            Log.e(TAG, "Unsupported zoom.");
        }

        // Most devices, like the Hero, appear to expose this zoom parameter.
        // It takes on values like "27" which appears to mean 2.7x zoom
        // if (takingPictureZoomMaxString != null) {
        // parameters.set("taking-picture-zoom", tenDesiredZoom);
        // }
    }

另外拍摄头像时聚焦、预览尺寸/图片尺寸选择、扫描区域等等也需要进行考虑。笔者前段时间做了Zxing优化,涉及相机方面的优化设置,可供参考。

3.3 预览图片转换

目前笔者在拍摄预览时不停地进行人脸的检测,检测出人脸后才开始处理头像 、进行人脸识别。目前进行拍摄预览,获取预览头像数据也很简单。只需给 camera 对象设置一个 Camera.PreviewCallback,在这个回调中实现一个方法 onPreviewFrame(byte[] data, Camera camera) data 就是我们预览得到的图片数据,我们只要将其转换成bitmap即可,接下来进行人脸检测等后续工作。然而的预览得到数据不是bitmap格式,不能简单通过BitmapFactory.decodeByteArray转换的,这个因为预览数据格式问题。

Android相机预览的时候支持几种不同的格式,从图像的角度(ImageFormat)来说有NV16、NV21、YUY2、YV12、RGB_565和JPEG,从像素的角度(PixelFormat)来说,对应的是YUV422SP、YUV420SP、YUV422I、YUV420P、RGB565和JPEG。 目前大部分Android手机摄像头设置的默认格式是yuv420sp,即图像格式NV21,编码成YUV的所有像素格式里,yuv420sp占用的空间是最小的。预览得到数据可以经过YuvImage转换成bitmap格式。

  private void decode(final byte[] data, int width, int height) {
        YuvImage yuv = new YuvImage(data, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        yuv.compressToJpeg(new Rect(0, 0, width, height), 100, out);

        byte[] bytes = out.toByteArray();
        final Bitmap picture = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
      ......

3.4 Android官方人脸识别API

Android 4.1开始提供人脸识别开发API,我们可以根据其API进行简单的人脸检测,判断是否包含人脸,及确认人脸大小,眼距等参数。

     private static final int MAX_FACE_NUM = 4;//最大可以检测出的人脸数量
    
    public void faceDetect(Bitmap srcImg) {
        Bitmap bm = srcImg.copy(Bitmap.Config.RGB_565, true);
        //最关键的就是下面三句代码
        FaceDetector faceDetector = new FaceDetector(bm.getWidth(), bm.getHeight(), MAX_FACE_NUM);
        FaceDetector.Face[] faces = new FaceDetector.Face[MAX_FACE_NUM];
        int realFaceNum = faceDetector.findFaces(bm, faces);
        Log.i(tag, "检测到人脸:n = " + realFaceNum);
        for (int i = 0; i < realFaceNum; i++) {
            FaceDetector.Face f = faces[i];
            PointF midPoint = new PointF();
            float dis = f.eyesDistance();//获取人脸两眼的间距
            f.getMidPoint(midPoint); //获取人脸中心点
            int dd = (int) (dis);
            Point eyeLeft = new Point((int) (midPoint.x - dis / 2), (int) midPoint.y);
            Point eyeRight = new Point((int) (midPoint.x + dis / 2), (int) midPoint.y);
            Rect faceRect = new Rect((int) (midPoint.x - dd), (int) (midPoint.y - dd), (int) (midPoint.x + dd), (int) (midPoint.y + dd));
            Log.i(tag, "左眼坐标 x = " + eyeLeft.x + "y = " + eyeLeft.y);
            Canvas canvas = new Canvas(bm);
            Paint p = new Paint();
            p.setAntiAlias(true);
            p.setStrokeWidth(8);
            p.setStyle(Paint.Style.STROKE);
            p.setColor(Color.GREEN);
            canvas.drawCircle(eyeLeft.x, eyeLeft.y, 20, p);
            canvas.drawCircle(eyeRight.x, eyeRight.y, 20, p);
            canvas.drawRect(faceRect, p);
        }
        ImageUtil.saveJpeg(bm);
        Log.i(tag, "保存完毕");
    }

而人脸识别特性不是在所有的Android 4.1以上的设备上都支持,有网友测试了谷歌系列的设备,有的设备也不支持使用官方API方式实现人脸识别功能,具体问题还是得追踪底层是否开放这个功能!我们可以调用getMaxNumDetectedFaces()来检测设备是否支持。各位在使用官方的人脸检测 API 时需要注意这一点。

4. 总结

目前笔者按照自己的想法做个了一个人脸识别的demo, 研究下来发现人脸识别技术没有网上宣传的那么酷炫,那么高大上。

其实目前人脸识别的应用还停留在基础上,也就是在较好环境中实现1:1人脸识别,而拍照美颜更仅仅应用到了人脸特征定点提取,连识别预处理都算不上。而对于1:N级和N:N级的人脸识别,也就是单一特征对比多种特征和多种特征对比多种特征。而这两种等级的人脸识别在应用上也常常无法提供较好的环境,比如1:N级人脸识别可以应用于失踪人口搜索中,在特殊情况下拍的照片存在角度、光线的复杂性,加大了特征提取、对比的难度。可见人脸识别大规模商业应用还是有很长一条路要走,但目前技术进步神速,相信我们未来完全依靠刷脸的时代会来临。

5. 参考

  1. 百度人脸识别技术文档
  2. 人脸识别(face recognition
  3. 关于Android 使用官方API 实现人脸检测功能

6. 博客推荐

欢迎各位光顾笔者公司的博客https://cardinfolink.github.io/


分享

评论