2017年下半年以来,随着iPhoneX的人脸解锁功能把人脸识别这一黑科技带入大家的视野中之后,各种有关人脸识别功能的新闻和报道层出不穷。不仅是对普通群众来说,对我们程序猿来说,百度,微软,阿里等各大公司推出的可供调用的人脸识别api也如雨后春笋一般冒出来。鉴于公司以后业务发展需要,同时也是个人兴趣所致,对调用其他公司api实现人脸识别进行了一定的技术调研,于是调研成果写成几篇博客分享出来,供大家一起交流与学习。
今天主要来看face++这加公司的人脸识别api,选这家公司作为调研对象主要原因还是这家公司的在人脸识别上还是做得比较好的。支付宝,神州,世纪佳缘,联想都使用了这家公司的人脸识别技术。光从用户上来看实力还是得到承认的。这里把face++的官网贴出来:face++官网。感兴趣的童鞋可以去看一下。话不多说,先来看我用官方例子调用的代码:
package com.example.demo;
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;
public class FacePlus {
public static void main(String[] args) throws Exception{
File file = new File("C:\\Users\\lenovo\\Desktop\\4034970a304e251f535c3f9ba486c9177f3e532e.jpg");
byte[] buff = getBytesFromFile(file);
String url = "https://api-cn.faceplusplus.com/facepp/v3/detect";
HashMap map = new HashMap<>();
HashMap byteMap = new HashMap<>();
map.put("api_key", "your api key");
map.put("api_secret", "your api secrect");
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);
try{
byte[] bacd = post(url, map, byteMap);
String str = new String(bacd);
System.out.println(str);
}catch (Exception e) {
e.printStackTrace();
}
}
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 map, HashMap 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 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 fileEntry = (Map.Entry) 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写的,首先new一个file,传入你要检测的人脸的路径,然后用io流将它转换为byte数组。将你要传入的参数和图片的byte数组装入两个HashMap中。说一下这里传入的参数,api_key和api_secret这两个参数是你创建的face++账户后给你的。创建账户很简单你只需要去face++的官网用邮箱或者手机号注册一下就行,不需要花钱即可长期免费使用face++的api,只是免费的账号对调用的并发量进行了限制。对于我们初期的调研测试及一些小的需求用例来说,这并不造成任何影响。所以只管去申请一个就行了。后面的参数return_landmark 表示要检测的人脸的关键点,参数设为1表示要检测83个人脸关键点,传2表示要检测106个关键点,传0表示不检测,利用这个我们可以设置人脸检测的严格程度。再往后走,return_attributes 表示返回的检测参数,可选的有gender,age,smiling,headpose,facequality,blur,eyestatus,emotion,ethnicity,beauty,mouthstatus,eyegaze,skinstatus这些参数。分别表示性别,年龄,笑容分析结果,是否佩戴眼镜,人脸姿势,人脸模糊,眼睛状态信息,情绪识别结果,人脸质量判断结果(这个参数很关键),人种分析结果,颜值识别结果,嘴部状态信息,眼球位置与视线方向信息,面部特征识别结果。运行上面这段程序,返回json字符串如下:
{"image_id": "9nXe3gisuOcnPc9aJ7VpWw==", "request_id": "1514123739,b2e92c18-9ac3-4953-9ced-c9887e3ce533", "time_used": 303, "faces": [{"landmark": {"mouth_upper_lip_left_contour2": {"y": 304, "x": 173}, "mouth_upper_lip_left_contour3": {"y": 309, "x": 177}, "mouth_lower_lip_right_contour3": {"y": 321, "x": 209}, "mouth_upper_lip_left_contour1": {"y": 301, "x": 186}, "left_eye_upper_left_quarter": {"y": 200, "x": 140}, "left_eyebrow_lower_middle": {"y": 183, "x": 144}, "contour_chin": {"y": 378, "x": 197}, "left_eyebrow_lower_left_quarter": {"y": 185, "x": 126}, "right_eyebrow_lower_left_quarter": {"y": 187, "x": 238}, "mouth_lower_lip_right_contour1": {"y": 311, "x": 214}, "mouth_lower_lip_left_contour2": {"y": 315, "x": 170}, "left_eye_bottom": {"y": 212, "x": 151}, "mouth_lower_lip_bottom": {"y": 322, "x": 195}, "contour_left9": {"y": 374, "x": 170}, "mouth_lower_lip_top": {"y": 311, "x": 195}, "right_eyebrow_upper_middle": {"y": 176, "x": 257}, "right_eyebrow_left_corner": {"y": 185, "x": 221}, "right_eye_bottom": {"y": 213, "x": 250}, "contour_left7": {"y": 345, "x": 130}, "contour_left6": {"y": 326, "x": 115}, "contour_left5": {"y": 304, "x": 105}, "contour_left4": {"y": 280, "x": 99}, "contour_left3": {"y": 255, "x": 97}, "contour_left2": {"y": 231, "x": 95}, "contour_left1": {"y": 207, "x": 95}, "left_eye_lower_left_quarter": {"y": 210, "x": 140}, "mouth_upper_lip_top": {"y": 303, "x": 195}, "contour_right3": {"y": 260, "x": 297}, "contour_right2": {"y": 237, "x": 301}, "mouth_left_corner": {"y": 308, "x": 160}, "contour_right4": {"y": 283, "x": 293}, "contour_right7": {"y": 346, "x": 262}, "left_eyebrow_left_corner": {"y": 189, "x": 110}, "nose_right": {"y": 270, "x": 226}, "right_eye_upper_right_quarter": {"y": 205, "x": 261}, "nose_tip": {"y": 263, "x": 197}, "contour_right5": {"y": 306, "x": 286}, "nose_contour_lower_middle": {"y": 280, "x": 198}, "right_eye_top": {"y": 202, "x": 249}, "mouth_lower_lip_left_contour3": {"y": 320, "x": 181}, "right_eye_right_corner": {"y": 211, "x": 271}, "right_eye_lower_right_quarter": {"y": 213, "x": 260}, "mouth_upper_lip_right_contour2": {"y": 306, "x": 219}, "right_eyebrow_lower_right_quarter": {"y": 191, "x": 273}, "left_eye_left_corner": {"y": 207, "x": 130}, "mouth_right_corner": {"y": 312, "x": 234}, "mouth_upper_lip_right_contour3": {"y": 311, "x": 214}, "right_eye_lower_left_quarter": {"y": 212, "x": 240}, "left_eyebrow_right_corner": {"y": 184, "x": 181}, "left_eyebrow_lower_right_quarter": {"y": 184, "x": 162}, "right_eye_center": {"y": 209, "x": 250}, "left_eye_upper_right_quarter": {"y": 201, "x": 164}, "mouth_lower_lip_left_contour1": {"y": 310, "x": 178}, "contour_left8": {"y": 361, "x": 148}, "nose_left": {"y": 269, "x": 171}, "right_eyebrow_lower_middle": {"y": 188, "x": 256}, "left_eye_top": {"y": 198, "x": 152}, "left_eye_center": {"y": 206, "x": 152}, "left_eye_lower_right_quarter": {"y": 211, "x": 163}, "nose_contour_right1": {"y": 213, "x": 214}, "contour_right9": {"y": 374, "x": 223}, "right_eye_left_corner": {"y": 210, "x": 230}, "left_eyebrow_upper_left_quarter": {"y": 175, "x": 124}, "left_eye_pupil": {"y": 204, "x": 152}, "right_eyebrow_upper_left_quarter": {"y": 176, "x": 238}, "contour_right8": {"y": 362, "x": 245}, "right_eyebrow_right_corner": {"y": 195, "x": 289}, "right_eye_upper_left_quarter": {"y": 204, "x": 239}, "left_eyebrow_upper_middle": {"y": 171, "x": 144}, "right_eyebrow_upper_right_quarter": {"y": 181, "x": 275}, "nose_contour_left1": {"y": 212, "x": 185}, "nose_contour_left2": {"y": 252, "x": 177}, "mouth_upper_lip_right_contour1": {"y": 302, "x": 204}, "contour_right1": {"y": 213, "x": 302}, "nose_contour_right2": {"y": 253, "x": 220}, "mouth_lower_lip_right_contour2": {"y": 317, "x": 222}, "contour_right6": {"y": 327, "x": 276}, "nose_contour_right3": {"y": 276, "x": 212}, "nose_contour_left3": {"y": 276, "x": 183}, "left_eye_right_corner": {"y": 209, "x": 173}, "left_eyebrow_upper_right_quarter": {"y": 173, "x": 164}, "right_eye_pupil": {"y": 207, "x": 250}, "mouth_upper_lip_bottom": {"y": 310, "x": 195}}, "attributes": {"emotion": {"sadness": 0.0, "neutral": 100.0, "disgust": 0.0, "anger": 0.0, "surprise": 0.0, "fear": 0.0, "happiness": 0.0}, "beauty": {"female_score": 79.73, "male_score": 80.154}, "gender": {"value": "Male"}, "age": {"value": 26}, "mouthstatus": {"close": 100.0, "surgical_mask_or_respirator": 0.0, "open": 0.0, "other_occlusion": 0.0}, "glass": {"value": "None"}, "skinstatus": {"dark_circle": 59.871, "stain": 66.202, "acne": 92.027, "health": 5.441}, "headpose": {"yaw_angle": 5.8753686, "pitch_angle": 7.470607, "roll_angle": 1.2101119}, "blur": {"blurness": {"threshold": 50.0, "value": 0.37}, "motionblur": {"threshold": 50.0, "value": 0.37}, "gaussianblur": {"threshold": 50.0, "value": 0.37}}, "smile": {"threshold": 30.1, "value": 1.27}, "eyestatus": {"left_eye_status": {"normal_glass_eye_open": 0.001, "no_glass_eye_close": 0.0, "occlusion": 0.0, "no_glass_eye_open": 99.999, "normal_glass_eye_close": 0.0, "dark_glasses": 0.0}, "right_eye_status": {"normal_glass_eye_open": 0.012, "no_glass_eye_close": 0.0, "occlusion": 0.0, "no_glass_eye_open": 99.988, "normal_glass_eye_close": 0.0, "dark_glasses": 0.0}}, "facequality": {"threshold": 70.1, "value": 87.66}, "ethnicity": {"value": "White"}, "eyegaze": {"right_eye_gaze": {"position_x_coordinate": 0.47, "vector_z_component": 0.961, "vector_x_component": 0.031, "vector_y_component": -0.274, "position_y_coordinate": 0.451}, "left_eye_gaze": {"position_x_coordinate": 0.507, "vector_z_component": 0.906, "vector_x_component": -0.075, "vector_y_component": -0.417, "position_y_coordinate": 0.429}}}, "face_rectangle": {"width": 207, "top": 169, "left": 95, "height": 207}, "face_token": "7a1d92282a7b6b31bbc65277cba413c2"}]}
这时我随便选的一张照片放上去的分析结果,虽然有点乱,不过重要在展示一下调用结果。官网上有一个示例调用结果:
{
"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"
}]
}
实际上就是带着一大串参数的json字符串。讲完了基本的调用我们先来实现一个简单点的需求:
先来实现这个简单的demo吧,后面再讲一讲更复杂的业务。我先用springboot建个项目,如下图:
引入关键的web包:
然后运行xxxApplication.java类,因为springboot自带tomcat服务器,所以不必将它放在服务器中运行就可以直接启动项目。话说springboot真是好用,又不用写一大堆配置文件,又不需要主动去找一大堆的maven依赖,直接点点点就完了,简直不要太爽。扯远了,先来看看实现效果:
一进入页面就调用摄像头将用户的头像显示在页面上(不要在意马赛克),此时,点击拍照结果如下:
点击拍照之后,会将用户的照片显示在右侧,同时将照片传入后台解析,调用api将照片传给face++检测,得到检测结果返回给前端并将结果显示在右侧文本框中。ps:由于本人摄像头及现场光线原因,拍出的照片有些模糊造成显示的年龄信息不准确(博主TM哪有45啊!!)。
接下来把源码贴出来,先看前台网页:
face++
性别:
年龄:
微笑程度:
开心程度:
前端页面写的简单粗暴了一点,主要是此处重在实现功能,页面以后可以交给前端和UI来优化。前台我采用了webrtc技术去调用摄像头。webrtc可以理解为是一种在浏览器中用javascript调用摄像头并进行流传输的技术,感兴趣的童鞋可以自行百度一下。
将摄像头采集到的视频放入html5的video标签中,然后使用canvas.toDataURL(“image/png”)将得到某一帧的图片转换为base64编码过的字符串,然后将该字符串用ajax发送到后台,后台代码:
package com.example.demo;
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 org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import sun.misc.BASE64Decoder;
@Controller
public class TestController {
@RequestMapping(value="/face")
@ResponseBody
public String getFace(String imgString) throws IOException {
System.out.println(imgString);
System.out.println(imgString.substring(imgString.indexOf(",")+1));
byte[] buff = getStringImage(imgString.substring(imgString.indexOf(",")+1));
String url = "https://api-cn.faceplusplus.com/facepp/v3/detect";
HashMap map = new HashMap<>();
HashMap byteMap = new HashMap<>();
map.put("api_key", "1ewiKTxhRrctByKOrdrfMC8Nt3BUyi6S");
map.put("api_secret", "L10UARcTmmQzWlPHytExSgU1JQbPMnz3");
// 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 异常
*/
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 map, HashMap 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 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 fileEntry = (Map.Entry) 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;
}
}
这里用BASE64Decoder包将base64编码的图片转换为byte数组,然后后面的程序就和上面的实例一样了,将byte数组传给face++即可。
至此,一个小的调用face++实现人脸检测的demo已经实现。不过,这个demo满足不了现实中的复杂的业务场景,在后面的博客中,我会继续讲解更为复杂的业务的场景的实现,感兴趣的童鞋可以持续关注哦。
转自:https://blog.csdn.net/zoroduyu/article/details/78827627