在上篇博客的结尾处,我提到了这么两个需求:
我在之前博客中讲到的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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
我对之前我的博客中拍照上传的代码修改了一些,主要去掉了一些参数,重新调用了另一个后台方法,打印出提示信息。对这部分不熟悉的童鞋可以去看我之前的博客。接下来看后台代码:
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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
用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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
这个工具类的作用其实就是用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"
}]
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
可以看到faces这个参数其实就是封装了人脸的各种分析结果,那么,为什么这里会得不到这个faces的参数喃?我在文章开头讲过,有可能出现face++因并发问题而拒绝解析我们免费玩家的图片的情况,这个时候返回的参数是这样的:
{"error_message":"CONCURRENCY_LIMIT_EXCEEDED"}
- 1
可以看到,这里面是没有faces这个参数的,所以我做得相应处理也是捕获异常然后返回系统繁忙的提示。
代码再往下走,解析成功拿到faces后我进行了一个判断if(“[]”.equals(faces)) 判断faces的值是不是为”[]”,因为假如传入的图片不是人像或者质量太低无法解析,返回的faces参数就是”[]”,所以这里如果是我就提示用户拍摄图片不和规格。再往下走就很简单了,再次解析faces这个参数,得到face_token的值,将其和账号一起存入数据库即可。
本来想一个下午将整个业务都讲完的,结果发现时间完全不够用。。。非常抱歉,由于时间关系,后面的业务实现只能留到下次博客再讲了。我现在每周至少一更,喜欢的朋友可以持续关注一下。