人脸识别(三)模拟业务场景:考生考试人脸验证的实现。

在上篇博客的结尾处,我提到了这么两个需求:

这里写图片描述

我在之前博客中讲到的face++的api可以用来实现这么这两个功能,今天这篇博客我们讲来讲一讲具体的实现。

先来看第一个需求:

  • 考生需要在考试前将自己的人脸录入到系统中(拍照或直接导入,方法二选一。)

我这里先给出拍照上传图片的需求实现,先来看实现效果。

这里写图片描述

页面如下,写的简单了点。左侧输入你的账号,下方有一个拍照上传的按钮,当点击时,会使用webrtc调用摄像头拍照,然后将拍完照的图片显示在右边的圆形内,同时将这张照片传入后台交给face++解析,然后将结果返给前台,效果如下:

这里写图片描述

这是一张上传成功的截图。我会给予弹窗提示。当然,还有不成功的情况,如下图:

这里写图片描述

这里上传不成功的其中一种情况是系统繁忙,造这个的原因是:face++对于免费用户的账号是有着并发量限制的,当face++的服务器并发过高时face++会优先解析付费用户上传的图片,对免费用户上传的图片予以搁置或者不处理,所以会出现这个错误。说到底,这个错误出现的根本原因是:我不是人民币玩家。
不过这个无关痛痒,一旦公司需要付费就行了。

此外还有一种情况,如下:

这里写图片描述

这里我在上传时用手遮住了摄像头,在后台判定这并不是一张合格的人脸头像照片后给予用户提示,必需上传正确的头像照片。

效果说完了,来看看代码,先看前台代码:

<!DOCTYPE html>    
<html>    
    <head>    
        <meta charset="utf-8">    
        <title>假装这是注册页面</title>    
        <style>  
        video,canvas{  
            border:1px solid gray;  
            width:400px;  
            height:400px;  
            border-radius:50%;  
        }     
        </style>  
    </head>    
    <body>    
       <video autoplay style="position: absolute;left: 1%;display: none;"></video>  
        <canvas id="myCanvas" style="position: absolute;left: 50%;"></canvas>
        <input type="text" id="name" style="position:  absolute;left: 30%;top: 15%" placeholder="请填入您的账号">  
        <button id="capture" style="position: absolute;left: 30%;top: 25%">拍照上传</button>
      <!--   <input type="file" name="file"  style="position: absolute;left: 30%;top: 35%" >
        <button style="position: absolute;left: 30%;top: 45%">图片上传</button>
 -->   
       <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script type="text/javascript">

        function hasUserMedia(){//判断是否支持调用设备api,因为浏览器不同所以判断方式不同哦    
            return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);    
        }    
        if(hasUserMedia()){    
            //alert(navigator.mozGetUserMedia)    
            navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;    
            var video=document.querySelector("video");    
            var canvas=document.querySelector("canvas");  
            var streaming = false;  
            navigator.getUserMedia({    
                video:true,//开启视频    
                audio:false//先关闭音频,因为会有回响,以后两台电脑通信不会有响声    
            },function(stream){//将视频流交给video    
                video.src=window.URL.createObjectURL(stream);     
                streaming = true;  
            },function(err){    
                console.log("capturing",err)    
            });    
            document.querySelector("#capture").addEventListener("click",function(event){  
                if(streaming){  
                    //alert(video.clientHeight)  
                    //canvas.width = video.clientWidth;  
                    //canvas.height= video.clientHeight;  
                    canvas.width = 800;  
                    canvas.height = 800;  
                    var context = canvas.getContext('2d');  
                    imgString = canvas.toDataURL("image/png")
                    context.drawImage(video,20,20)  

                    var info = {
                        name: $("#name").val(),
                        imgString: canvas.toDataURL("image/png")
                      }

                    $.post("/face/photograph",info,function(data){

                            alert(data.message)

                  },"json") 
                }  
            })  
        }else{    
            alert("浏览器暂不支持")    
        }    
        </script>   

    </body>    
</html>    

我对之前我的博客中拍照上传的代码修改了一些,主要去掉了一些参数,重新调用了另一个后台方法,打印出提示信息。对这部分不熟悉的童鞋可以去看我之前的博客。接下来看后台代码:

package com.avie.ltd.controller;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.avie.ltd.bean.JsonResult;
import com.avie.ltd.entity.FaceUser;
import com.avie.ltd.service.FaceUserService;
import com.avie.ltd.util.FaceUtil;

import net.sf.json.JSONObject;

@Controller
@RequestMapping(value="/face")
@RestController
public class FaceController {

    @Autowired
    private FaceUserService faceService;

    @RequestMapping(value="/photograph")
    public JsonResult getFace(String imgString,String name) throws IOException {


        String str = FaceUtil.checkFace(imgString);

         JSONObject json = JSONObject.fromObject(str);
         try {
             String faces = json.getString("faces");
             if("[]".equals(faces)) {
                 return new JsonResult("0", "对不起,您上传的不是用户头像或者照片质量不达标,请重新上传!", null);
             }
             JSONObject josnToken = JSONObject.fromObject(faces.substring(1, faces.length()-1));
             String token = josnToken.getString("face_token");
             FaceUser user = new FaceUser();
             user.setName(name);
             user.setFaceToken(token);
             faceService.add(user);
        } catch (Exception e) {
            // TODO: handle exception   
             e.printStackTrace();
             return new JsonResult("0", "系统繁忙,请稍后重试!", null);
        }
       return new JsonResult("1", "上传成功,请登录!", null);
    }

}

用java写的,进入方法,从前台传入的imgString是经过base64编码的图片的字符串,name是传入的用户的账号。进入方法后的第一句代码:String str = FaceUtil.checkFace(imgString); 这里我把整个字符串解析成二进制流然后上传给face++得到返回得字符串的过程封装在了FaceUtil这个工具类里面,来看看FaceUtil工具类的代码:

package com.avie.ltd.util;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;

import javax.net.ssl.SSLException;

import sun.misc.BASE64Decoder;

public class FaceUtil {

    public static String checkFace(String imgString) throws IOException {
        String url = "https://api-cn.faceplusplus.com/facepp/v3/detect";
        byte[] buff = getStringImage(imgString.substring(imgString.indexOf(",")+1));
        HashMap<String, String> map = new HashMap<>();
        HashMap<String, byte[]> byteMap = new HashMap<>();
        map.put("api_key", "your api key");
        map.put("api_secret", "your api secret");
        map.put("return_landmark", "1");
        map.put("return_attributes", "gender,age,smiling,headpose,facequality,blur,eyestatus,emotion,ethnicity,beauty,mouthstatus,eyegaze,skinstatus");
        byteMap.put("image_file", buff);
        String str =null;
        try{
            byte[] bacd = post(url, map, byteMap);
           str = new String(bacd);
            System.out.println(str);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return str;
    }

    /**
     * Base64字符串转 二进制流
     *
     * @param base64String Base64
     * @return base64String
     * @throws IOException 异常
     */
    @SuppressWarnings("restriction")
    public static byte[] getStringImage(String base64String) throws IOException {
        BASE64Decoder decoder = new sun.misc.BASE64Decoder();
        return base64String != null ? decoder.decodeBuffer(base64String) : null;
    }

    private final static int CONNECT_TIME_OUT = 30000;
    private final static int READ_OUT_TIME = 50000;
    private static String boundaryString = getBoundary();
    protected static byte[] post(String url, HashMap<String, String> map, HashMap<String, byte[]> fileMap) throws Exception {
        HttpURLConnection conne;
        URL url1 = new URL(url);
        conne = (HttpURLConnection) url1.openConnection();
        conne.setDoOutput(true);
        conne.setUseCaches(false);
        conne.setRequestMethod("POST");
        conne.setConnectTimeout(CONNECT_TIME_OUT);
        conne.setReadTimeout(READ_OUT_TIME);
        conne.setRequestProperty("accept", "*/*");
        conne.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundaryString);
        conne.setRequestProperty("connection", "Keep-Alive");
        conne.setRequestProperty("user-agent", "Mozilla/4.0 (compatible;MSIE 6.0;Windows NT 5.1;SV1)");
        DataOutputStream obos = new DataOutputStream(conne.getOutputStream());
        Iterator iter = map.entrySet().iterator();
        while(iter.hasNext()){
            Map.Entry<String, String> entry = (Map.Entry) iter.next();
            String key = entry.getKey();
            String value = entry.getValue();
            obos.writeBytes("--" + boundaryString + "\r\n");
            obos.writeBytes("Content-Disposition: form-data; name=\"" + key
                    + "\"\r\n");
            obos.writeBytes("\r\n");
            obos.writeBytes(value + "\r\n");
        }
        if(fileMap != null && fileMap.size() > 0){
            Iterator fileIter = fileMap.entrySet().iterator();
            while(fileIter.hasNext()){
                Map.Entry<String, byte[]> fileEntry = (Map.Entry<String, byte[]>) fileIter.next();
                obos.writeBytes("--" + boundaryString + "\r\n");
                obos.writeBytes("Content-Disposition: form-data; name=\"" + fileEntry.getKey()
                        + "\"; filename=\"" + encode(" ") + "\"\r\n");
                obos.writeBytes("\r\n");
                obos.write(fileEntry.getValue());
                obos.writeBytes("\r\n");
            }
        }
        obos.writeBytes("--" + boundaryString + "--" + "\r\n");
        obos.writeBytes("\r\n");
        obos.flush();
        obos.close();
        InputStream ins = null;
        int code = conne.getResponseCode();
        try{
            if(code == 200){
                ins = conne.getInputStream();
            }else{
                ins = conne.getErrorStream();
            }
        }catch (SSLException e){
            e.printStackTrace();
            return new byte[0];
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buff = new byte[4096];
        int len;
        while((len = ins.read(buff)) != -1){
            baos.write(buff, 0, len);
        }
        byte[] bytes = baos.toByteArray();
        ins.close();
        return bytes;
    }

    private static String getBoundary() {
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for(int i = 0; i < 32; ++i) {
            sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-".charAt(random.nextInt("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_".length())));
        }
        return sb.toString();
    }

    private static String encode(String value) throws Exception{
        return URLEncoder.encode(value, "UTF-8");
    }

    public static byte[] getBytesFromFile(File f) {
        if (f == null) {
            return null;
        }
        try {
            FileInputStream stream = new FileInputStream(f);
            ByteArrayOutputStream out = new ByteArrayOutputStream(1000);
            byte[] b = new byte[1000];
            int n;
            while ((n = stream.read(b)) != -1)
                out.write(b, 0, n);
            stream.close();
            out.close();
            return out.toByteArray();
        } catch (IOException e) {
        }
        return null;
    }
}

这个工具类的作用其实就是用java发送一个http请求,将需要传的参数及图片二进制流传过去,然后得到返回的参数。里面的参数具体都有什么含义我在之前的博客中已经讲过了,此处不再赘述,不懂的童鞋去看我之前的博客。

回到之前的方法中,继续往下走:JSONObject json = JSONObject.fromObject(str);这句代码把字符串转换为json对象。接下来的代码被包在try块里。String faces = json.getString(“faces”); 这里从json对象中解析得到faces这个参数的value值,这里有可能会抛出一个json对象中没有faces这个key的异常,如下图所示:

这里写图片描述

可以看到,抛出的异常为:JSONObject[“faces”] not found.
也就是json对象中没有faces这个参数。这里先解释一下faces参数是干什么的,之前读过我博客的细心的朋友可以知道,官方给的模拟返回参数是这个样子的

{
    "image_id": "Dd2xUw9S/7yjr0oDHHSL/Q==",
    "request_id": "1470472868,dacf2ff1-ea45-4842-9c07-6e8418cea78b",
    "time_used": 752,
    "faces": [{
        "landmark": {
            "mouth_upper_lip_left_contour2": {
                "y": 185,
                "x": 146
            },
            "contour_chin": {
                "y": 231,
                "x": 137
            },
            .............省略关键点信息 
            "right_eye_pupil": {
                "y": 146,
                "x": 205
            },
            "mouth_upper_lip_bottom": {
                "y": 195,
                "x": 159
            }
        },
        "attributes": {
            "gender": {
                "value": "Female"
            },
            "age": {
                "value": 21
            },
            "glass": {
                "value": "None"
            },
            "headpose": {
                "yaw_angle": -26.625063,
                "pitch_angle": 12.921974,
                "roll_angle": 22.814377
            },
            "smile": {
                "threshold": 30.1,
                "value": 2.566890001296997
            }
        },
        "face_rectangle": {
            "width": 140,
            "top": 89,
            "left": 104,
            "height": 141
        },
        "face_token": "ed319e807e039ae669a4d1af0922a0c8"
    }]
}

可以看到faces这个参数其实就是封装了人脸的各种分析结果,那么,为什么这里会得不到这个faces的参数喃?我在文章开头讲过,有可能出现face++因并发问题而拒绝解析我们免费玩家的图片的情况,这个时候返回的参数是这样的:

{"error_message":"CONCURRENCY_LIMIT_EXCEEDED"}

可以看到,这里面是没有faces这个参数的,所以我做得相应处理也是捕获异常然后返回系统繁忙的提示。

代码再往下走,解析成功拿到faces后我进行了一个判断if(“[]”.equals(faces)) 判断faces的值是不是为”[]”,因为假如传入的图片不是人像或者质量太低无法解析,返回的faces参数就是”[]”,所以这里如果是我就提示用户拍摄图片不和规格。再往下走就很简单了,再次解析faces这个参数,得到face_token的值,将其和账号一起存入数据库即可。

本来想一个下午将整个业务都讲完的,结果发现时间完全不够用。。。非常抱歉,由于时间关系,后面的业务实现只能留到下次博客再讲了。我现在每周至少一更,喜欢的朋友可以持续关注一下。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值