1. 程式人生 > >[043] 微信公眾平臺開發教程第19篇-應用例項之人臉檢測

[043] 微信公眾平臺開發教程第19篇-應用例項之人臉檢測

CSDN2013年度部落格之星評選活動開始,本人有幸入圍參加評選,如果部落格中的文章對你有所幫助,請為柳峰投上寶貴一票,非常感謝!


        在筆者的公眾賬號小q機器人(微訊號:xiaoqrobot)中有一個非常好玩的功能"人臉檢測",它能夠檢測出使用者傳送的圖片中有多少張人臉,並且還能分析出每張臉所對應的人種、性別和年齡。幾乎每天都有一些使用者在使用“人臉檢測”,該功能的趣味性和娛樂性在於能夠讓使用者知道自己的長相與真實年齡是否相符,是否男(女)性化偷笑本文將為讀者介紹人臉檢測應用的完整實現過程。

        人臉檢測屬於人臉識別的範疇,它是一個複雜的具有挑戰性的模式匹配問題,國內外許多組織、科研機構都在專門研究該問題。國內的Face++團隊專注於研發人臉檢測、識別、分析和重建技術,並且向廣大開發者開放了人臉識別API,本文介紹的“人臉檢測”應用正是基於Face++ API進行開發。

Face++簡介

        Face++是北京曠視科技有限公司旗下的人臉識別雲服務平臺,Face++平臺通過提供雲端API、離線SDK、以及面向使用者的自主研發產品等形式,將人臉識別技術廣泛應用到網際網路及移動應用場景中。Face++為廣大開發者提供了簡單易用的API,開發者可以輕鬆搭建屬於自己的雲端身份認證、使用者興趣挖掘、移動體感互動、社交娛樂分享等多種型別的應用。

        Face++提供的技術服務包括人臉檢測、人臉分析和人臉識別,主要說明如下:

                1)人臉檢測:可以從圖片中快速、準確的定位面部的關鍵區域位置,包括眉毛、眼睛、鼻子、嘴巴等。
                2)人臉分析:可以從圖片或實時視訊流中分析出人臉的性別(準確度達96%)、年齡、種族等多種屬性。
                3)人臉識別:可以快速判定兩張照片是否為同一個人,或者快速判定視訊中的人像是否為某一位特定的人。

        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://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
 * 
 * @author liufeng
 * @date 2013-12-18
 */
public class Face implements Comparable<Face> {
	// 被檢測出的每一張人臉都在Face++系統中的識別符號
	private String faceId;
	// 年齡估計值
	private int ageValue;
	// 年齡估計值的正負區間
	private int ageRange;
	// 性別:Male/Female
	private String genderValue;
	// 性別分析的可信度
	private double genderConfidence;
	// 人種:Asian/White/Black
	private String raceValue;
	// 人種分析的可信度
	private double raceConfidence;
	// 微笑程度
	private double smilingValue;
	// 人臉框的中心點座標
	private double centerX;
	private double centerY;

	public String getFaceId() {
		return faceId;
	}

	public void setFaceId(String faceId) {
		this.faceId = faceId;
	}

	public int getAgeValue() {
		return ageValue;
	}

	public void setAgeValue(int ageValue) {
		this.ageValue = ageValue;
	}

	public int getAgeRange() {
		return ageRange;
	}

	public void setAgeRange(int ageRange) {
		this.ageRange = ageRange;
	}

	public String getGenderValue() {
		return genderValue;
	}

	public void setGenderValue(String genderValue) {
		this.genderValue = genderValue;
	}

	public double getGenderConfidence() {
		return genderConfidence;
	}

	public void setGenderConfidence(double genderConfidence) {
		this.genderConfidence = genderConfidence;
	}

	public String getRaceValue() {
		return raceValue;
	}

	public void setRaceValue(String raceValue) {
		this.raceValue = raceValue;
	}

	public double getRaceConfidence() {
		return raceConfidence;
	}

	public void setRaceConfidence(double raceConfidence) {
		this.raceConfidence = raceConfidence;
	}

	public double getSmilingValue() {
		return smilingValue;
	}

	public void setSmilingValue(double smilingValue) {
		this.smilingValue = smilingValue;
	}

	public double getCenterX() {
		return centerX;
	}

	public void setCenterX(double centerX) {
		this.centerX = centerX;
	}

	public double getCenterY() {
		return centerY;
	}

	public void setCenterY(double centerY) {
		this.centerY = centerY;
	}

	// 根據人臉中心點座標從左至右排序
	@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中物件排序的關鍵所在。112-119行程式碼是通過比較每個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;

/**
 * 人臉檢測服務
 * 
 * @author liufeng
 * @date 2013-12-18
 */
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));
	}
}
上述程式碼雖然多,但條理很清晰,並不難理解,所以筆者只挑重點的進行講解,主要說明如下:

1)70行:引數url表示圖片的連結,由於連結中存在特殊字元,作為引數傳遞時必須進行URL編碼。請讀者記住:不管是什麼應用,呼叫什麼介面,凡是通過GET傳遞的引數中可能會包含特殊字元,都必須進行URL編碼,除了中文以外,特殊字元還包括等號“=”、與“&”、空格“ ”等。

2)76-97行:使用JSON-lib解析人臉檢測介面返回的JSON資料,並將解析結果存入List中。

3)99行:對集合中的物件進行排序,使用Collections.sort()方法排序的前提是集合中的Face物件實現了Comparable介面

4)146-203行:組裝返回給使用者的訊息內容。考慮到公眾平臺的文字訊息內容長度有限制,當一張圖片中識別出的人臉過多,則只返回一些彙總資訊給使用者。

5)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;

/**
 * 核心服務類
 * 
 * @author liufeng
 * @date 2013-12-19
 */
public class CoreService {
	/**
	 * 處理微信發來的請求
	 */
	public static String processRequest(HttpServletRequest request) {
		// 返回給微信伺服器的xml訊息
		String respXml = null;
		try {
			// xml請求解析
			Map<String, String> requestMap = MessageUtil.parseXml(request);
			// 傳送方帳號(open_id)
			String fromUserName = requestMap.get("FromUserName");
			// 公眾帳號
			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實現更多實用、有趣的功能。應用開發不是簡單的介面呼叫!

轉帖請註明本文出自柳峰的部落格(http://blog.csdn.net/lyq8479),請尊重他人的辛勤勞動成果,謝謝!