前几段微软推出的大数据人脸识别年龄应用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>
再来说下主程序类,关于程序的实现,基本可以分为这几步:
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/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
正在看本人博客的这位童鞋,我看你气度不凡,谈吐间隐隐有王者之气,日后必有一番作为!旁边有“推荐”二字,你就顺手把它点了吧,相得准,我分文不收;相不准,你也好回来找我!
来源:https://www.cnblogs.com/lichenwei/p/4616918.html
