天天看點

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

  前幾段微軟推出的大資料人臉識别年齡應用how-old.net在微網誌火了一把,它可以通過照片快速獲得照片上人物的年齡,系統會對瞳孔、眼角、鼻子等27個“面部地标點"展開分析,進而得出你的“顔齡"。

來看下關于這款應用的截圖:

基于安卓高仿how-old.net實作人臉識别估算年齡與性别
基于安卓高仿how-old.net實作人臉識别估算年齡與性别

 昨晚閑着沒事,在網上查閱了點資料仿寫了一款類似功能的APP,看下截圖:

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

 

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

 

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

  關于人臉識别技術本想去使用微軟給開發人員提供的SDK,但由于天朝巨坑的網絡,我連How-old.net官網都登不上,隻能繞道去找找其他地方有沒類似功能的SDK。後來想起之前在搞O2O的時候,看到過一則關于支付寶"刷臉支付"功能的新聞,查找了相關資料發現他們的"刷臉技術"是Face++提供的,也就這樣找到了個好東西。

  這是Face++的官方網站:http://www.faceplusplus.com.cn/,在網站裡可以找到它為開發者提供了一部分功能的SDK(需要注冊),其中就有人臉識别,判斷年齡性别種族這個功能。

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

  我們注冊個賬号,然後建立個應用就可以得到官方給我們提供的APIKey和APISecret,記錄下來,然後到到開發者中心(http://www.faceplusplus.com.cn/dev-tools-sdks/)就可以下載下傳到對應版本的SDK,就一個Jar包直接導入項目就可以,這是官方給我們提供的API參考文檔(http://www.faceplusplus.com.cn/api-overview/),這樣子準備工作就做好了,可以開始進入軟體編碼階段了。

先來看下布局檔案:

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

 很簡單的布局檔案,這裡就直接貼代碼了:

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、進入程式,點選按鈕跳轉相冊選取一張圖檔,并在程式主界面顯示。

這裡要注意的一些地方:

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

根據開發者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簡要使用筆記》)拿到我們所需要的資料,這裡我們需要的資料有

基于安卓高仿how-old.net實作人臉識别估算年齡與性别

具體數值各代表什麼意思,這個大家查閱下官方給定的API文檔就可以知道了,這裡就不再詳細去寫了,然後根據所給的這些數值我們會圈出所對應人臉的位置,方法可以有很多,形狀也無所謂,這裡我給出的是矩形方案。

3、再來就是對隐藏布局TextView進行顯示,由于我們可以在服務端給我們傳回的資料裡知道性别年齡等資料,這裡就很好辦了。

這裡我通過通過cache機制将View轉為Bitmap然後進行顯示,當然不止是這種方法,用自定義View去進行布局也是可以的,這裡大家靈活操作,我就不多說了。

到這裡,文章就結束了,大家有什麼疑問可以在文章評論下面給我留言。

作者:李晨玮

出處:http://www.cnblogs.com/lichenwei/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結。

正在看本人部落格的這位童鞋,我看你氣度不凡,談吐間隐隐有王者之氣,日後必有一番作為!旁邊有“推薦”二字,你就順手把它點了吧,相得準,我分文不收;相不準,你也好回來找我!