基于安卓高仿how-old.net实现人脸识别估算年龄与性别
前几段微软推出的大数据人脸识别年龄应用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 5 android:background="#ffffff" 6 android:orientation="vertical" > 7 8 LinearLayout 9 android:layout_width10 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_height18 android:layout_weight="1" 19 android:text="请选择图片" 20 android:textColor="#000000" 21 android:textSize="14dp" /> 22 23 Button 24 ="@+id/bt_getImage" 25 26 27 28 ="选择图片" 29 ="16dp" 30 31 32 ="@+id/bt_doAction" 33 34 35 36 android:enabled="false" 37 ="识别操作" 38 39 </LinearLayout40 41 ImageView 42 ="@+id/iv_image" 43 android:layout_width44 45 android:layout_marginBottom46 android:src="@drawable/ic_launcher" 47 48 FrameLayout 49 ="@+id/fl_view" 50 51 52 android:visibility="invisible" 53 54 55 ="@+id/tv_info" 56 57 58 android:background="@drawable/hint" 59 android:drawableLeft="@drawable/female" 60 android:gravity="center" 61 ="18" 62 ="#ff0044" 63 ="22sp" 64 android:visibility65 FrameLayout66 67 >View Code ? 再来说下主程序类,关于程序的实现,基本可以分为这几步: 1、进入程序,点击按钮跳转相册选取一张图片,并在程序主界面显示。 这里要注意的一些地方: 根据开发者API的/detection/detect(http://www.faceplusplus.com.cn/detection_detect/),我们可以知道,在设定APIKEY和APISECRERT的同时,我们需要指定一张图片的URL地址或者是把图片转为二进制数据向服务端进行POST提交,这里需要注意的是图片的大小不能超过1M,而现在智能手机的像素很高,随便拍一张照片都会超出这个限制范围,所以我们在获取到图片的同时需要对图片进行压缩处理。 2、封装所需要的参数,并把图片转为二进制数据提交到服务端获取识别结果(Json数据)。 3、根据服务端所返回的数据,设置显示到图像上。 ? ? 大概实现就是这样子,下面直接上的代码吧,大部分注释都很详细,我不一个个讲了,会挑一些重点出来说。 这是一个网络请求工具类: package com.lcw.rabbit.face; 2 3 import java.io.ByteArrayOutputStream; 4 5 org.json.JSONObject; 6 7 android.graphics.Bitmap; 8 9 com.facepp.error.FaceppParseException; 10 com.facepp.http.HttpRequests; 11 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..getName(); 23 24 * 创建网络 25 * 26 * @param bitmap 27 callBack 28 29 void uploadFaces(final Bitmap bitmap,final CallBack callBack) { 30 new Thread(new Runnable() { 31 32 @Override 33 void run() { 34 try { 35 // 将Bitmap对象转换成byte数组 36 ByteArrayOutputStream stream = 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,1)">true,1)">true); 42 封装参数 43 PostParameters params = 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 64 65 66 interface CallBack { 67 68 success(JSONObject jsonObject); 69 70 error(FaceppParseException exception); 71 72 } ? 既然是网络请求,那肯定属于耗时操作,那么我们需要在子线程里去完成,然后官方给我们提供的SDK里帮我们封装了一些工具类 例如: HttpRequests类,它帮我们封装好了HTTP请求,我们可以直接设置参数去访问服务端。 打开源码我们可以发现除了无参构造,它还有2个构造方法,分别是2个参数和4个参数的,其实我们仔细看下源码便可以很轻松的发现,这些参数只不过是用来提交服务端的URL 分别是:(无疑我们是要选择CN+HTTP),所以后两个参数我们直接置为TRUE就可以了。 1 final private String WEBSITE_CN = "https://apicn.faceplusplus.com/v2/"; 2 private String DWEBSITE_CN = "http://apicn.faceplusplus.com/v2/"3 private String WEBSITE_US = "https://apius.faceplusplus.com/v2/"4 private String DWEBSITE_US = "http://apius.faceplusplus.com/v2/"; PostParameters类,用来设置参数的具体值的,这里提供了一个setImg方法,我们只需要把我们图片转为字节数组的变量直接存入即可。 为了方便主程序类方便获取到返回数据,这里采用了接口回调方法CallBack,设置了success(当数据正常返回时),error(当数据不正常返回时)。 ? 这是主程序类: 1 2 3 org.json.JSONArray; 4 org.json.JSONException; 5 6 7 android.app.Activity; 8 android.app.ProgressDialog; 9 android.content.Intent; 10 android.database.Cursor; 11 12 android.graphics.BitmapFactory; 13 android.graphics.Canvas; 14 android.graphics.Paint; 15 android.net.Uri; 16 android.os.Bundle; 17 android.os.Handler; 18 android.os.Message; 19 android.provider.MediaStore; 20 android.view.View; 21 android.view.View.MeasureSpec; 22 android.view.View.OnClickListener; 23 android.widget.Button; 24 android.widget.ImageView; 25 android.widget.TextView; 26 android.widget.Toast; 27 28 29 30 class MainActivity extends Activity implements OnClickListener { 31 32 声明控件 33 private TextView mTip; 34 Button mGetImage; 35 Button mDoAction; 36 ImageView mImageView; 37 View mView; 38 ProgressDialog mDialog; 39 40 Face++关键数据 41 final String APIKey = "你的APPKEY" 42 final String APISecret = "你的APPSERCRET" 43 44 标志变量 45 int REQUEST_CODE = 89757 46 int SUCCESS = 1 47 int ERROR = 0 48 49 图片路径 50 String mPicStr; 51 Bitmap对象 52 Bitmap mBitmap; 53 54 private Handler handler = Handler() { 55 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 ERROR: 65 失败 66 String errorInfo = (String) msg.obj; 67 if (errorInfo == null || "".equals(errorInfo)) { 68 Toast.makeText(MainActivity.this,"Error" 69 } else 70 Toast.makeText(MainActivity.this 71 72 73 74 default: 75 76 77 } 78 79 reMakeBitmap(JSONObject json) { 80 81 mDialog.dismiss(); 82 83 拷贝原Bitmap对象 84 Bitmap bitmap = Bitmap.createBitmap(mBitmap.getWidth(),mBitmap.getHeight(),mBitmap.getConfig()); 85 Canvas canvas = Canvas(bitmap); 86 canvas.drawBitmap(mBitmap,1)">null 87 88 Paint paint = Paint(); 89 paint.setColor(0xffffffff 90 paint.setStrokeWidth(3 91 92 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 *109 height = height / 100 *110 绘制矩形人脸识别框 111 canvas.drawLine(x - width / 2,y - height / 2,x - width / 2,y + height / 2112 canvas.drawLine(x - width / 2,x + width / 2,y - height / 2113 canvas.drawLine(x + width / 2,1)">114 canvas.drawLine(x - width / 2,y + height / 2,1)">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(),1)">125 126 mBitmap = bitmap; 127 128 129 130 } (JSONException e) { 131 e.printStackTrace(); 132 133 134 135 136 137 * 构建一个显示年龄,性别的Bitmap 138 * 139 * age 140 sex 141 142 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),1)">null,1)">150 } 151 男性 152 tv_info.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.male),1)">153 154 通过cache机制将View保存为Bitmap 155 tv_info.measure(MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED),MeasureSpec.makeMeasureSpec(0156 tv_info.layout(0,0157 tv_info.buildDrawingCache(); 158 tv_info.setDrawingCacheEnabled(159 Bitmap bitmap = Bitmap.createBitmap(tv_info.getDrawingCache()); 160 tv_info.destroyDrawingCache(); 161 162 163 return164 165 }; 166 167 @Override 168 protected onCreate(Bundle savedInstanceState) { 169 super.onCreate(savedInstanceState); 170 setContentView(R.layout.activity_main); 171 对控件进行初始化操作 172 initViews(); 173 对控件进行监听操作 174 initActions(); 175 176 177 initActions() { 178 mGetImage.setOnClickListener(179 mDoAction.setOnClickListener(180 181 182 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.189 mDialog.setMessage("系统检测识别中,请稍后.."190 191 192 193 194 void onActivityResult(int requestCode,1)">int resultCode,Intent intent) { 195 .onActivityResult(requestCode,resultCode,intent); 196 if (requestCode == REQUEST_CODE) { 197 if (intent != 198 Uri uri = intent.getData(); 199 Cursor cursor = getContentResolver().query(uri,1)">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 = BitmapFactory.Options(); 207 当Options的inJustDecodeBounds属性设置为true时,不会显示图片,只会返回该图片的具体数据 208 options.inJustDecodeBounds = 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 = () Math.ceil(reSize); 214 options.inJustDecodeBounds = false215 216 创建Bitmap 217 mBitmap = BitmapFactory.decodeFile(mPicStr,1)">218 mImageView.setImageBitmap(mBitmap); 219 220 mTip.setText("点击识别==》"221 mDoAction.setEnabled(222 223 224 225 226 227 228 229 230 onClick(View v) { 231 (v.getId()) { 232 R.id.bt_getImage: 233 点击获取图片按钮跳转相册选取图片 234 Intent intent = Intent(Intent.ACTION_PICK); 235 获取手机图库信息 236 intent.setType("image/*"237 startActivityForResult(intent,REQUEST_CODE); 238 239 R.id.bt_doAction: 240 点击识别按钮进行图片识别操作 241 mDialog.show(); 242 FaceHelper.uploadFaces(mBitmap,1)"> FaceHelper.CallBack() { 243 244 @Override 245 success(JSONObject result) { 246 人脸识别成功,回调获取数据 247 Message msg = Message.obtain(); 248 msg.what = SUCCESS; 249 msg.obj = result; 250 handler.sendMessage(msg); 251 252 253 254 error(FaceppParseException e) { 255 人脸识别失败,回调获取错误信息 256 Message msg =257 msg.what = ERROR; 258 msg.obj = e.getErrorMessage(); 259 260 261 }); 262 263 264 265 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去进行布局也是可以的,这里大家灵活操作,我就不多说了。 ? 到这里,文章就结束了,大家有什么疑问可以在文章评论下面给我留言。 ? 作者:李晨玮 ? (编辑:北几岛) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |