前幾段微軟推出的大資料人臉識别年齡應用how-old.net在微網誌火了一把,它可以通過照片快速獲得照片上人物的年齡,系統會對瞳孔、眼角、鼻子等27個“面部地标點"展開分析,進而得出你的“顔齡"。
來看下關于這款應用的截圖:
昨晚閑着沒事,在網上查閱了點資料仿寫了一款類似功能的APP,看下截圖:
關于人臉識别技術本想去使用微軟給開發人員提供的SDK,但由于天朝巨坑的網絡,我連How-old.net官網都登不上,隻能繞道去找找其他地方有沒類似功能的SDK。後來想起之前在搞O2O的時候,看到過一則關于支付寶"刷臉支付"功能的新聞,查找了相關資料發現他們的"刷臉技術"是Face++提供的,也就這樣找到了個好東西。
這是Face++的官方網站:http://www.faceplusplus.com.cn/,在網站裡可以找到它為開發者提供了一部分功能的SDK(需要注冊),其中就有人臉識别,判斷年齡性别種族這個功能。
我們注冊個賬号,然後建立個應用就可以得到官方給我們提供的APIKey和APISecret,記錄下來,然後到到開發者中心(http://www.faceplusplus.com.cn/dev-tools-sdks/)就可以下載下傳到對應版本的SDK,就一個Jar包直接導入項目就可以,這是官方給我們提供的API參考文檔(http://www.faceplusplus.com.cn/api-overview/),這樣子準備工作就做好了,可以開始進入軟體編碼階段了。
先來看下布局檔案:
很簡單的布局檔案,這裡就直接貼代碼了:
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:tools="http://schemas.android.com/tools"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:background="#ffffff"
6 android:orientation="vertical" >
7
8 <LinearLayout
9 android:layout_width="match_parent"
10 android:layout_height="wrap_content"
11 android:layout_margin="5dp"
12 android:orientation="horizontal" >
13
14 <TextView
15 android:id="@+id/tv_tip"
16 android:layout_width="0dp"
17 android:layout_height="wrap_content"
18 android:layout_weight="1"
19 android:text="請選擇圖檔"
20 android:textColor="#000000"
21 android:textSize="14dp" />
22
23 <Button
24 android:id="@+id/bt_getImage"
25 android:layout_width="0dp"
26 android:layout_height="wrap_content"
27 android:layout_weight="1"
28 android:text="選擇圖檔"
29 android:textSize="16dp" />
30
31 <Button
32 android:id="@+id/bt_doAction"
33 android:layout_width="0dp"
34 android:layout_height="wrap_content"
35 android:layout_weight="1"
36 android:enabled="false"
37 android:text="識别操作"
38 android:textSize="16dp" />
39 </LinearLayout>
40
41 <ImageView
42 android:id="@+id/iv_image"
43 android:layout_width="match_parent"
44 android:layout_height="match_parent"
45 android:layout_marginBottom="5dp"
46 android:src="@drawable/ic_launcher" />
47
48 <FrameLayout
49 android:id="@+id/fl_view"
50 android:layout_width="wrap_content"
51 android:layout_height="wrap_content"
52 android:visibility="invisible" >
53
54 <TextView
55 android:id="@+id/tv_info"
56 android:layout_width="wrap_content"
57 android:layout_height="wrap_content"
58 android:background="@drawable/hint"
59 android:drawableLeft="@drawable/female"
60 android:gravity="center"
61 android:text="18"
62 android:textColor="#ff0044"
63 android:textSize="22sp"
64 android:visibility="invisible" />
65 </FrameLayout>
66
67 </LinearLayout>
View Code
再來說下主程式類,關于程式的實作,基本可以分為這幾步:
1、進入程式,點選按鈕跳轉相冊選取一張圖檔,并在程式主界面顯示。
這裡要注意的一些地方:
根據開發者API的/detection/detect(http://www.faceplusplus.com.cn/detection_detect/),我們可以知道,在設定APIKEY和APISECRERT的同時,我們需要指定一張圖檔的Url位址或者是把圖檔轉為二進制資料向服務端進行POST送出,這裡需要注意的是圖檔的大小不能超過1M,而現在智能手機的像素很高,随便拍一張照片都會超出這個限制範圍,是以我們在擷取到圖檔的同時需要對圖檔進行壓縮處理。
2、封裝所需要的參數,并把圖檔轉為二進制資料送出到服務端擷取識别結果(Json資料)。
3、根據服務端所傳回的資料,設定顯示到圖像上。
大概實作就是這樣子,下面直接上的代碼吧,大部分注釋都很詳細,我不一個個講了,會挑一些重點出來說。
這是一個網絡請求工具類:
1 package com.lcw.rabbit.face;
2
3 import java.io.ByteArrayOutputStream;
4
5 import org.json.JSONObject;
6
7 import android.graphics.Bitmap;
8
9 import com.facepp.error.FaceppParseException;
10 import com.facepp.http.HttpRequests;
11 import com.facepp.http.PostParameters;
12
13 /**
14 * Face++ 幫助類,執行網絡請求耗時操作
15 *
16 * @author Rabbit_Lee
17 *
18 */
19 public class FaceHelper {
20
21 private static final String TAG = FaceHelper.class.getName();
22
23 /**
24 * 建立網絡
25 *
26 * @param bitmap
27 * @param callBack
28 */
29 public static void uploadFaces(final Bitmap bitmap, final CallBack callBack) {
30 new Thread(new Runnable() {
31
32 @Override
33 public void run() {
34 try {
35 // 将Bitmap對象轉換成byte數組
36 ByteArrayOutputStream stream = new ByteArrayOutputStream();
37 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
38 byte[] data = stream.toByteArray();
39
40 // 建立連接配接請求
41 HttpRequests requests = new HttpRequests(MainActivity.APIKey, MainActivity.APISecret, true, true);
42 // 封裝參數
43 PostParameters params = new PostParameters();
44 params.setImg(data);
45 // 送出網絡請求
46 JSONObject jsonObject = requests.detectionDetect(params);
47 // 設定回調資料
48 callBack.success(jsonObject);
49 } catch (FaceppParseException e) {
50 // 設定回調資料
51 callBack.error(e);
52 e.printStackTrace();
53 }
54
55 }
56 }).start();
57
58 }
59
60 /**
61 * 資料回調接口,友善主類擷取資料
62 *
63 * @author Rabbit_Lee
64 *
65 */
66 public interface CallBack {
67
68 void success(JSONObject jsonObject);
69
70 void error(FaceppParseException exception);
71 }
72 }
既然是網絡請求,那肯定屬于耗時操作,那麼我們需要在子線程裡去完成,然後官方給我們提供的SDK裡幫我們封裝了一些工具類
例如:
HttpRequests類,它幫我們封裝好了HTTP請求,我們可以直接設定參數去通路服務端。
打開源碼我們可以發現除了無參構造,它還有2個構造方法,分别是2個參數和4個參數的,其實我們仔細看下源碼便可以很輕松的發現,這些參數隻不過是用來送出服務端的URL
分别是:(無疑我們是要選擇CN+HTTP),是以後兩個參數我們直接置為TRUE就可以了。
1 static final private String WEBSITE_CN = "https://apicn.faceplusplus.com/v2/";
2 static final private String DWEBSITE_CN = "http://apicn.faceplusplus.com/v2/";
3 static final private String WEBSITE_US = "https://apius.faceplusplus.com/v2/";
4 static final private String DWEBSITE_US = "http://apius.faceplusplus.com/v2/";
PostParameters類,用來設定參數的具體值的,這裡提供了一個setImg方法,我們隻需要把我們圖檔轉為位元組數組的變量直接存入即可。
為了友善主程式類友善擷取到傳回資料,這裡采用了接口回調方法CallBack,設定了success(當資料正常傳回時),error(當資料不正常傳回時)。
這是主程式類:
1 package com.lcw.rabbit.face;
2
3 import org.json.JSONArray;
4 import org.json.JSONException;
5 import org.json.JSONObject;
6
7 import android.app.Activity;
8 import android.app.ProgressDialog;
9 import android.content.Intent;
10 import android.database.Cursor;
11 import android.graphics.Bitmap;
12 import android.graphics.BitmapFactory;
13 import android.graphics.Canvas;
14 import android.graphics.Paint;
15 import android.net.Uri;
16 import android.os.Bundle;
17 import android.os.Handler;
18 import android.os.Message;
19 import android.provider.MediaStore;
20 import android.view.View;
21 import android.view.View.MeasureSpec;
22 import android.view.View.OnClickListener;
23 import android.widget.Button;
24 import android.widget.ImageView;
25 import android.widget.TextView;
26 import android.widget.Toast;
27
28 import com.facepp.error.FaceppParseException;
29
30 public class MainActivity extends Activity implements OnClickListener {
31
32 // 聲明控件
33 private TextView mTip;
34 private Button mGetImage;
35 private Button mDoAction;
36 private ImageView mImageView;
37 private View mView;
38 private ProgressDialog mDialog;
39
40 // Face++關鍵資料
41 public static final String APIKey = "你的APPKEY";
42 public static final String APISecret = "你的APPSERCRET";
43
44 // 标志變量
45 private static final int REQUEST_CODE = 89757;
46 private static final int SUCCESS = 1;
47 private static final int ERROR = 0;
48
49 // 圖檔路徑
50 private String mPicStr;
51 // Bitmap對象
52 private Bitmap mBitmap;
53
54 private Handler handler = new Handler() {
55 public void handleMessage(android.os.Message msg) {
56 switch (msg.what) {
57 case SUCCESS:
58 // 成功
59 JSONObject object = (JSONObject) msg.obj;
60 // 解析Json資料,重構Bitmap對象
61 reMakeBitmap(object);
62 mImageView.setImageBitmap(mBitmap);
63 break;
64 case ERROR:
65 // 失敗
66 String errorInfo = (String) msg.obj;
67 if (errorInfo == null || "".equals(errorInfo)) {
68 Toast.makeText(MainActivity.this, "Error", Toast.LENGTH_LONG).show();
69 } else {
70 Toast.makeText(MainActivity.this, errorInfo, Toast.LENGTH_LONG).show();
71 }
72 break;
73
74 default:
75 break;
76 }
77 }
78
79 private void reMakeBitmap(JSONObject json) {
80
81 mDialog.dismiss();
82
83 // 拷貝原Bitmap對象
84 Bitmap bitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), mBitmap.getConfig());
85 Canvas canvas = new Canvas(bitmap);
86 canvas.drawBitmap(mBitmap, 0, 0, null);
87
88 Paint paint = new Paint();
89 paint.setColor(0xffffffff);
90 paint.setStrokeWidth(3);
91
92 try {
93 JSONArray faces = json.getJSONArray("face");
94 // 檢測照片有多少張人臉
95 int facesCount = faces.length();
96 mTip.setText("識别到"+facesCount+"張人臉");
97 for (int i = 0; i < facesCount; i++) {
98 JSONObject position = faces.getJSONObject(i).getJSONObject("position");
99 // position屬性下的所需資料,定位人臉位置
100 Float x = (float) position.getJSONObject("center").getDouble("x");
101 Float y = (float) position.getJSONObject("center").getDouble("y");
102 Float width = (float) position.getDouble("width");
103 Float height = (float) position.getDouble("height");
104
105 // 把百分比轉化為實際像素值
106 x = x / 100 * bitmap.getWidth();
107 y = y / 100 * bitmap.getHeight();
108 width = width / 100 * bitmap.getWidth();
109 height = height / 100 * bitmap.getHeight();
110 // 繪制矩形人臉識别框
111 canvas.drawLine(x - width / 2, y - height / 2, x - width / 2, y + height / 2, paint);
112 canvas.drawLine(x - width / 2, y - height / 2, x + width / 2, y - height / 2, paint);
113 canvas.drawLine(x + width / 2, y - height / 2, x + width / 2, y + height / 2, paint);
114 canvas.drawLine(x - width / 2, y + height / 2, x + width / 2, y + height / 2, paint);
115
116 // 擷取年齡,性别
117 JSONObject attribute = faces.getJSONObject(i).getJSONObject("attribute");
118 Integer age = attribute.getJSONObject("age").getInt("value");
119 String sex = attribute.getJSONObject("gender").getString("value");
120
121 // 擷取顯示年齡性别的Bitmap對象
122 Bitmap infoBm = makeView(age, sex);
123
124 canvas.drawBitmap(infoBm, x - infoBm.getWidth() / 2, y - height / 2-infoBm.getHeight(), null);
125
126 mBitmap = bitmap;
127
128 }
129
130 } catch (JSONException e) {
131 e.printStackTrace();
132 }
133
134 }
135
136 /**
137 * 建構一個顯示年齡,性别的Bitmap
138 *
139 * @param age
140 * @param sex
141 */
142 private Bitmap makeView(Integer age, String sex) {
143 mView.setVisibility(View.VISIBLE);
144 TextView tv_info = (TextView) mView.findViewById(R.id.tv_info);
145 tv_info.setText(age.toString());
146
147 if (sex.equals("Female")) {
148 //女性
149 tv_info.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.female), null, null, null);
150 } else {
151 //男性
152 tv_info.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.male), null, null, null);
153 }
154 // 通過cache機制将View儲存為Bitmap
155 tv_info.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
156 tv_info.layout(0, 0, tv_info.getMeasuredWidth(), tv_info.getMeasuredHeight());
157 tv_info.buildDrawingCache();
158 tv_info.setDrawingCacheEnabled(true);
159 Bitmap bitmap = Bitmap.createBitmap(tv_info.getDrawingCache());
160 tv_info.destroyDrawingCache();
161
162
163 return bitmap;
164 }
165 };
166
167 @Override
168 protected void onCreate(Bundle savedInstanceState) {
169 super.onCreate(savedInstanceState);
170 setContentView(R.layout.activity_main);
171 // 對控件進行初始化操作
172 initViews();
173 // 對控件進行監聽操作
174 initActions();
175 }
176
177 private void initActions() {
178 mGetImage.setOnClickListener(this);
179 mDoAction.setOnClickListener(this);
180 }
181
182 private void initViews() {
183 mTip = (TextView) findViewById(R.id.tv_tip);
184 mGetImage = (Button) findViewById(R.id.bt_getImage);
185 mDoAction = (Button) findViewById(R.id.bt_doAction);
186 mImageView = (ImageView) findViewById(R.id.iv_image);
187 mView=findViewById(R.id.fl_view);
188 mDialog = new ProgressDialog(MainActivity.this);
189 mDialog.setMessage("系統檢測識别中,請稍後..");
190
191 }
192
193 @Override
194 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
195 super.onActivityResult(requestCode, resultCode, intent);
196 if (requestCode == REQUEST_CODE) {
197 if (intent != null) {
198 Uri uri = intent.getData();
199 Cursor cursor = getContentResolver().query(uri, null, null, null, null);
200 if (cursor.moveToFirst()) {
201 int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
202 mPicStr = cursor.getString(index);
203 cursor.close();
204
205 // 根據擷取到的圖檔路徑,擷取圖檔并進行圖檔壓縮
206 BitmapFactory.Options options = new BitmapFactory.Options();
207 // 當Options的inJustDecodeBounds屬性設定為true時,不會顯示圖檔,隻會傳回該圖檔的具體資料
208 options.inJustDecodeBounds = true;
209
210 // 根據所選實際的寬高計算壓縮比例并将圖檔壓縮
211 BitmapFactory.decodeFile(mPicStr, options);
212 Double reSize = Math.max(options.outWidth * 1.0 / 1024f, options.outHeight * 1.0 / 1024f);
213 options.inSampleSize = (int) Math.ceil(reSize);
214 options.inJustDecodeBounds = false;
215
216 // 建立Bitmap
217 mBitmap = BitmapFactory.decodeFile(mPicStr, options);
218 mImageView.setImageBitmap(mBitmap);
219
220 mTip.setText("點選識别==》");
221 mDoAction.setEnabled(true);
222
223 }
224 }
225
226 }
227 }
228
229 @Override
230 public void onClick(View v) {
231 switch (v.getId()) {
232 case R.id.bt_getImage:
233 // 點選擷取圖檔按鈕跳轉相冊選取圖檔
234 Intent intent = new Intent(Intent.ACTION_PICK);
235 // 擷取手機圖庫資訊
236 intent.setType("image/*");
237 startActivityForResult(intent, REQUEST_CODE);
238 break;
239 case R.id.bt_doAction:
240 // 點選識别按鈕進行圖檔識别操作
241 mDialog.show();
242 FaceHelper.uploadFaces(mBitmap, new FaceHelper.CallBack() {
243
244 @Override
245 public void success(JSONObject result) {
246 // 人臉識别成功,回調擷取資料
247 Message msg = Message.obtain();
248 msg.what = SUCCESS;
249 msg.obj = result;
250 handler.sendMessage(msg);
251 }
252
253 @Override
254 public void error(FaceppParseException e) {
255 // 人臉識别失敗,回調擷取錯誤資訊
256 Message msg = Message.obtain();
257 msg.what = ERROR;
258 msg.obj = e.getErrorMessage();
259 handler.sendMessage(msg);
260 }
261 });
262 break;
263
264 default:
265 break;
266 }
267
268 }
269
270 }
由于注釋很全,這裡我就挑幾個地方出來講,有其他不清楚的朋友可以在文章評論裡留言,我會答複的。
1、當點選選擇圖檔按鈕時,通過Intent去通路系統圖檔,并根據傳回的圖檔進行圖檔壓縮,因為官方文檔很明确的告訴我們圖檔大小不能超過1MB。
壓縮圖檔有2種方式,一種是通過壓縮圖檔的品質大小,另一種則是根據比例來壓縮圖檔大小(這裡我選擇第二種壓縮方式)。
根據得到的Bitmap對象,我們可以先将inJustDecodeBounds先設定成true,這樣我們就不會得到具體的圖檔,隻會得到該圖檔的擷取到真實圖檔的寬和高,然後我們讓其去除以1024,取較大的一個數作為壓縮比例,取整利用inSampleSize去對Bitmap進行壓縮,然後再把inJustDecodeBounds設定為false。
2、當我們送出圖檔并且服務端向我們傳回資料時,由于我們是在子線程中去執行網絡請求的,是以這邊我們通過Handler機制來傳輸資料,使得主線程可以拿到資料并更新UI。
服務端給我們傳回的是一個Json的資料,是以我們需要在這裡将它進行解析(關于Json解析,我這裡用到的是安卓官方自帶的Json幫助類,想用谷歌提供的Gson工具類也是可以的,這邊是工具類使用方法:《Gson簡要使用筆記》)拿到我們所需要的資料,這裡我們需要的資料有
具體數值各代表什麼意思,這個大家查閱下官方給定的API文檔就可以知道了,這裡就不再詳細去寫了,然後根據所給的這些數值我們會圈出所對應人臉的位置,方法可以有很多,形狀也無所謂,這裡我給出的是矩形方案。
3、再來就是對隐藏布局TextView進行顯示,由于我們可以在服務端給我們傳回的資料裡知道性别年齡等資料,這裡就很好辦了。
這裡我通過通過cache機制将View轉為Bitmap然後進行顯示,當然不止是這種方法,用自定義View去進行布局也是可以的,這裡大家靈活操作,我就不多說了。
到這裡,文章就結束了,大家有什麼疑問可以在文章評論下面給我留言。
作者:李晨玮
出處:http://www.cnblogs.com/lichenwei/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。
正在看本人部落格的這位童鞋,我看你氣度不凡,談吐間隐隐有王者之氣,日後必有一番作為!旁邊有“推薦”二字,你就順手把它點了吧,相得準,我分文不收;相不準,你也好回來找我!