在本文中介绍一个非常好玩的功能"人脸检测",它能够检测出用户发送的图片中有多少张人脸,并且还能分析出每张脸所对应的人种、性别和年龄。几乎每天都有一些用户在使用“人脸检测”,该功能的趣味性和娱乐性在于能够让用户知道自己的长相与真实年龄是否相符,是否男(女)性化。本文将为读者介绍人脸检测应用的完整实现过程。
人脸检测属于人脸识别的范畴,它是一个复杂的具有挑战性的模式匹配问题,国内外许多组织、科研机构都在专门研究该问题。国内的Face++团队专注于研发人脸检测、识别、分析和重建技术,并且向广大开发者开放了人脸识别API,本文介绍的“人脸检测”应用正是基于Face++ API进行开发。除了Face++之外,近几年还有很多公共免费开放的人脸识别接口,比如百度,腾讯,阿里等都有相关接口。
Face++简介
Face++是北京旷视科技有限公司旗下的人脸识别云服务平台,Face++平台通过提供云端API、离线SDK、以及面向用户的自主研发产品等形式,将人脸识别技术广泛应用到互联网及移动应用场景中。Face++为广大开发者提供了简单易用的API,开发者可以轻松搭建属于自己的云端身份认证、用户兴趣挖掘、移动体感交互、社交娱乐分享等多种类型的应用。
Face++提供的技术服务包括人脸检测、人脸分析和人脸识别
主要说明如下:
人脸检测:可以从图片中快速、准确的定位面部的关键区域位置,包括眉毛、眼睛、鼻子、嘴巴等。
人脸分析:可以从图片或实时视频流中分析出人脸的性别(准确度达96%)、年龄、种族等多种属性。
人脸识别:可以快速判定两张照片是否为同一个人,或者快速判定视频中的人像是否为某一位特定的人。
Face++的中文网址为http://cn.faceplusplus.com/,要使用Face++ API,需要注册成为Face++开发者,也就是要注册一个Face++账号。注册完成后,先创建应用,创建应用时需要填写“应用名称”、“应用描述”、“API服务器”、“应用类型”和“应用平台”,读者可以根据实际情况填写。应用创建完成后,可以看到应用的详细信息,如下图所示。
上图中,最重要的是API KEY和API SECRET,在调用Face++提供的API时,需要传入这两个参数。
人脸检测API介绍
在Face++网站的“API文档”中,能够看到Face++提供的所有API,我们要使用的人脸检测接口是detect分类下的“/detection/detect”,它能够检测出给定图片(Image)中的所有人脸(Face)的位置和相应的面部属性,目前面部属性包括性别(gender)、年龄(age)、种族(race)、微笑程度(smiling)、眼镜(glass)和姿势(pose)。
读者可以在http://cn.faceplusplus.com/uc/doc/home?id=69 中了解到人脸检测接口的详细信息,该接口的请求地址如下:
http://apicn.faceplusplus.com/v2/detection/detect?url=URL&api_secret=API_SECRET&api_key=API_KEY
调用上述接口,必须要传入参数api_key、api_secret和待检测的图片。其中,待检测的图片可以是URL,也可以是POST方式提交的二进制数据。在微信公众账号后台,接收用户发送的图片,得到的是图片的访问路径(PicUrl),因此,在本例中,直接使用待检测图片的URL是最方便的。调用人脸检测接口返回的是JSON格式数据如下:
{ "face": [{ "attribute": { "age": { "range": 5, "value": 23 }, "gender": { "confidence": 99.9999, "value": "Female" }, "glass": { "confidence": 99.945, "value": "None" }, "pose": { "pitch_angle": { "value": 17 }, "roll_angle": { "value": 0.735735 }, "yaw_angle": { "value": -2 } }, "race": { "confidence": 99.6121, "value": "Asian" }, "smiling": { "value": 4.86501 } }, "face_id": "17233b4b1b51ac91e391e5afe130eb78", "position": { "center": { "x": 49.4, "y": 37.6 }, "eye_left": { "x": 43.3692, "y": 30.8192 }, "eye_right": { "x": 56.5606, "y": 30.9886 }, "height": 26.8, "mouth_left": { "x": 46.1326, "y": 44.9468 }, "mouth_right": { "x": 54.2592, "y": 44.6282 }, "nose": { "x": 49.9404, "y": 38.8484 }, "width": 26.8 }, "tag": "" }], "img_height": 500, "img_id": "22fd9efc64c87e00224c33dd8718eec7", "img_width": 500, "session_id": "38047ad0f0b34c7e8c6efb6ba39ed355", "url": "http://cn.faceplusplus.com/wp-content/themes/faceplusplus.zh/assets/img/demo/1.jpg?v=4" }
这里只对本文将要实现的“人脸检测”功能中主要用到的参数进行说明,
参数说明如下:
1)face是一个数组,当一张图片中包含多张人脸时,所有识别出的人脸信息都在face数组中。
2)age中的value表示估计年龄,range表示误差范围。例如,上述结果中value=23,range=5,表示人的真实年龄在18岁至28岁左右。
3)gender中的value表示性别,男性为Male,女性为Female;gender中的confidence表示检测结果的可信度。
4)race中的value表示人种,黄色人种为Asian,白色人种为White,黑色人种为Black;race中的confidence表示检测结果的可信度。
5)center表示人脸框中心点坐标,可以将x用于计算人脸的左右顺序,即x坐标的值越小,人脸的位置越靠近图片的左侧。
人脸检测API的使用方法
为了方便开发者调用人脸识别API,Face++团队提供了基于Objective-C、Java(Android)、Matlab、Ruby、C#等多种语言的开发工具包,读者可以在Face++网站的“工具下载”版块下载相关的SDK。在本例中,笔者并不打算使用官方提供的SDK进行开发,主要原因如下:1)人脸检测API的调用比较简单,自己写代码实现也并不复杂;2)如果使用SDK进行开发,笔者还要花费大量篇幅介绍SDK的使用,这些并不是本文的重点;3)自己写代码实现比较灵活。当图片中有多张人脸时,人脸检测接口返回的数据是无序的,开发者可以按照实际使用需求进行排序,例如,将图片中的人脸按照从左至右的顺序进行排序。
编程调用人脸检测API
首先,要对人脸检测接口返回的结构进行封装,建立与之对应的Java对象。由于人脸检测接口返回的参数较多,笔者只是将本例中需要用到的参数抽取出来,封装成Face对象,对应的代码如下:
package org.liufeng.course.pojo; /** * Face Model */ public class Face implements Comparable<Face> { private String faceId; // 被检测出的每一张人脸都在Face++系统中的标识符 private int ageValue; // 年龄估计值 private int ageRange; // 年龄估计值的正负区间 private String genderValue; // 性别:Male/Female private double genderConfidence; // 性别分析的可信度 private String raceValue; // 人种:Asian/White/Black private double raceConfidence; // 人种分析的可信度 private double smilingValue; // 微笑程度 private double centerX; // 人脸框的中心点坐标 private double centerY; // 省略getter和setter // 根据人脸中心点坐标从左至右排序 @Override public int compareTo(Face face) { int result = 0; if (this.getCenterX() > face.getCenterX()) result = 1; else result = -1; return result; } }
与普通Java类不同的是,Face类实现了Comparable接口,并实现了该接口的compareTo()方法,这正是Java中对象排序的关键所在。通过比较每个Face的脸部中心点的横坐标来决定对象的排序方式,这样能够实现检测出的多个Face按从左至右的先后顺序进行排序。
接下来,是人脸检测API的调用及相关处理逻辑,笔者将这些实现全部封装在FaceService类中,该类的完整实现如下:
package org.liufeng.course.service; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.liufeng.course.pojo.Face; import net.sf.json.JSONArray; import net.sf.json.JSONObject; /** * 人脸检测服务 */ public class FaceService { /** * 发送http请求 * * @param requestUrl 请求地址 * @return String */ private static String httpRequest(String requestUrl) { StringBuffer buffer = new StringBuffer(); try { URL url = new URL(requestUrl); HttpURLConnection httpUrlConn = (HttpURLConnection) url.openConnection(); httpUrlConn.setDoInput(true); httpUrlConn.setRequestMethod("GET"); httpUrlConn.connect(); // 将返回的输入流转换成字符串 InputStream inputStream = httpUrlConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } bufferedReader.close(); inputStreamReader.close(); // 释放资源 inputStream.close(); inputStream = null; httpUrlConn.disconnect(); } catch (Exception e) { e.printStackTrace(); } return buffer.toString(); } /** * 调用Face++ API实现人脸检测 * * @param picUrl 待检测图片的访问地址 * @return List<Face> 人脸列表 */ private static List<Face> faceDetect(String picUrl) { List<Face> faceList = new ArrayList<Face>(); try { // 拼接Face++人脸检测的请求地址 String queryUrl = "http://apicn.faceplusplus.com/v2/detection/detect?url=URL&api_secret=API_SECRET&api_key=API_KEY"; // 对URL进行编码 queryUrl = queryUrl.replace("URL", java.net.URLEncoder.encode(picUrl, "UTF-8")); queryUrl = queryUrl.replace("API_KEY", "替换成自己的API Key"); queryUrl = queryUrl.replace("API_SECRET", "替换成自己的API Secret"); // 调用人脸检测接口 String json = httpRequest(queryUrl); // 解析返回json中的Face列表 JSONArray jsonArray = JSONObject.fromObject(json).getJSONArray("face"); // 遍历检测到的人脸 for (int i = 0; i < jsonArray.size(); i++) { // face JSONObject faceObject = (JSONObject) jsonArray.get(i); // attribute JSONObject attrObject = faceObject.getJSONObject("attribute"); // position JSONObject posObject = faceObject.getJSONObject("position"); Face face = new Face(); face.setFaceId(faceObject.getString("face_id")); face.setAgeValue(attrObject.getJSONObject("age").getInt("value")); face.setAgeRange(attrObject.getJSONObject("age").getInt("range")); face.setGenderValue(genderConvert(attrObject.getJSONObject("gender").getString("value"))); face.setGenderConfidence(attrObject.getJSONObject("gender").getDouble("confidence")); face.setRaceValue(raceConvert(attrObject.getJSONObject("race").getString("value"))); face.setRaceConfidence(attrObject.getJSONObject("race").getDouble("confidence")); face.setSmilingValue(attrObject.getJSONObject("smiling").getDouble("value")); face.setCenterX(posObject.getJSONObject("center").getDouble("x")); face.setCenterY(posObject.getJSONObject("center").getDouble("y")); faceList.add(face); } // 将检测出的Face按从左至右的顺序排序 Collections.sort(faceList); } catch (Exception e) { faceList = null; e.printStackTrace(); } return faceList; } /** * 性别转换(英文->中文) * * @param gender * @return */ private static String genderConvert(String gender) { String result = "男性"; if ("Male".equals(gender)) result = "男性"; else if ("Female".equals(gender)) result = "女性"; return result; } /** * 人种转换(英文->中文) * * @param race * @return */ private static String raceConvert(String race) { String result = "黄色"; if ("Asian".equals(race)) result = "黄色"; else if ("White".equals(race)) result = "白色"; else if ("Black".equals(race)) result = "黑色"; return result; } /** * 根据人脸识别结果组装消息 * * @param faceList 人脸列表 * @return */ private static String makeMessage(List<Face> faceList) { StringBuffer buffer = new StringBuffer(); // 检测到1张脸 if (1 == faceList.size()) { buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸").append("\n"); for (Face face : faceList) { buffer.append(face.getRaceValue()).append("人种,"); buffer.append(face.getGenderValue()).append(","); buffer.append(face.getAgeValue()).append("岁左右").append("\n"); } } // 检测到2-10张脸 else if (faceList.size() > 1 && faceList.size() <= 10) { buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸,按脸部中心位置从左至右依次为:").append("\n"); for (Face face : faceList) { buffer.append(face.getRaceValue()).append("人种,"); buffer.append(face.getGenderValue()).append(","); buffer.append(face.getAgeValue()).append("岁左右").append("\n"); } } // 检测到10张脸以上 else if (faceList.size() > 10) { buffer.append("共检测到 ").append(faceList.size()).append(" 张人脸").append("\n"); // 统计各人种、性别的人数 int asiaMale = 0; int asiaFemale = 0; int whiteMale = 0; int whiteFemale = 0; int blackMale = 0; int blackFemale = 0; for (Face face : faceList) { if ("黄色".equals(face.getRaceValue())) if ("男性".equals(face.getGenderValue())) asiaMale++; else asiaFemale++; else if ("白色".equals(face.getRaceValue())) if ("男性".equals(face.getGenderValue())) whiteMale++; else whiteFemale++; else if ("黑色".equals(face.getRaceValue())) if ("男性".equals(face.getGenderValue())) blackMale++; else blackFemale++; } if (0 != asiaMale || 0 != asiaFemale) buffer.append("黄色人种:").append(asiaMale).append("男").append(asiaFemale).append("女").append("\n"); if (0 != whiteMale || 0 != whiteFemale) buffer.append("白色人种:").append(whiteMale).append("男").append(whiteFemale).append("女").append("\n"); if (0 != blackMale || 0 != blackFemale) buffer.append("黑色人种:").append(blackMale).append("男").append(blackFemale).append("女").append("\n"); } // 移除末尾空格 buffer = new StringBuffer(buffer.substring(0, buffer.lastIndexOf("\n"))); return buffer.toString(); } /** * 提供给外部调用的人脸检测方法 * * @param picUrl 待检测图片的访问地址 * @return String */ public static String detect(String picUrl) { // 默认回复信息 String result = "未识别到人脸,请换一张清晰的照片再试!"; List<Face> faceList = faceDetect(picUrl); if (null != faceList) { result = makeMessage(faceList); } return result; } public static void main(String[] args) { String picUrl = "http://pic11.nipic.com/20101111/6153002_002722872554_2.jpg"; System.out.println(detect(picUrl)); } }
上述代码虽然多,但条理很清晰,并不难理解,所以笔者只挑重点的进行讲解,主要说明如下:
70行:参数url表示图片的链接,由于链接中存在特殊字符,作为参数传递时必须进行URL编码。请读者记住:不管是什么应用,调用什么接口,凡是通过GET传递的参数中可能会包含特殊字符,都必须进行URL编码,除了中文以外,特殊字符还包括等号“=”、与“&”、空格“ ”等。
76-97行:使用JSON-lib解析人脸检测接口返回的JSON数据,并将解析结果存入List中。
99行:对集合中的对象进行排序,使用Collections.sort()方法排序的前提是集合中的Face对象实现了Comparable接口。
146-203行:组装返回给用户的消息内容。考虑到公众平台的文本消息内容长度有限制,当一张图片中识别出的人脸过多,则只返回一些汇总信息给用户。
211-219行:detect()方法是public的,提供给其他类调用。笔者可以在本地的开发工具中运行上面的main()方法,测试detect()方法的输出。
公众账号后台的实现
在公众账号后台的CoreService类中,需要对用户发送的消息类型进行判断,如果是图片消息,则调用人脸检测方法进行分析,如果是其他消息,则返回人脸检测的使用指南。CoreService类的完整代码如下:
package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 */ public class CoreService { /** * 处理微信发来的请求 */ public static String processRequest(HttpServletRequest request) { // 返回给微信服务器的xml消息 String respXml = null; try { Map<String, String> requestMap = MessageUtil.parseXml(request); // xml请求解析 String fromUserName = requestMap.get("FromUserName"); // 发送方帐号(open_id) String toUserName = requestMap.get("ToUserName"); // 公众帐号 String msgType = requestMap.get("MsgType"); // 消息类型 // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); // 图片消息 if (MessageUtil.REQ_MESSAGE_TYPE_IMAGE.equals(msgType)) { String picUrl = requestMap.get("PicUrl"); // 取得图片地址 String detectResult = FaceService.detect(picUrl); // 人脸检测 textMessage.setContent(detectResult); } // 其它类型的消息 else textMessage.setContent(getUsage()); respXml = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respXml; } /** * 人脸检测帮助菜单 */ public static String getUsage() { StringBuffer buffer = new StringBuffer(); buffer.append("人脸检测使用指南").append("\n\n"); buffer.append("发送一张清晰的照片,就能帮你分析出种族、年龄、性别等信息").append("\n"); buffer.append("快来试试你是不是长得太着急"); return buffer.toString(); } }
到这里,人脸检测应用就全部开发完成了,整个项目的完整结构如下:
运行结果如下:
笔者用自己的相片测试了两次,测试结果分别是26岁、30岁,这与笔者的实际年龄相差不大,可见,Face++的人脸检测准确度还是比较高的。为了增加人脸检测应用的趣味性和娱乐性,笔者忽略了年龄估计值的正负区间。读者可以充分发挥自己的想像力和创造力,使用Face++ API实现更多实用、有趣的功能。应用开发不是简单的接口调用!
作者:柳峰
未经允许请勿转载:程序喵 » 微信公众号开发教程第16篇——应用实例之人脸检测