由于百度API开始收费,所以这个应用暂时无法使用了,代码部分仍然有参考价值,建议大家自行更换token去验证。个人博客的下载验证码bug,已经fix。可以继续下载了。
更新于2022.02.13
————————————————————我是分割线————————————————————————————
App已经更新,现在可以永久使用。下载链接不变
百度云经常和谐我的下载,大家可以去我的个人博客下载,App和完整工程均有。https://sumtudou.cn/download.html
个人博客欢迎留言~~~ 个人博客欢迎留言~~~ 个人博客欢迎留言~~~
更新于2019.6.14
————————————————————我是分割线————————————————————————————
经私信提醒,已更新,解决了删除人脸的BUG。已经上传了新的APP和完整工程包。下载链接见上。图片见下。
码农第二痛,看自己之前写的代码~~~简直惨不忍睹。稍微优化了目录结构,至于其他操作。加滚动条,加进度条对话框等,各位自己搞定。新的目录结构见下:
还有,大家不要白嫖吧~虽然不能三连。好歹点个赞再走啊~~~
还有,大家不要白嫖吧~虽然不能三连。好歹点个赞再走啊~~~
还有,大家不要白嫖吧~虽然不能三连。好歹点个赞再走啊~~~
更新于:2019/12/13
————————————————————我是分割线————————————————————————————
目 录
题目十三 人脸识别考勤 3
一、目的与要求 3
1. 学会如何使用 Activity 及常见控件。 3
2. 学会使用数据库存储及网络通信。 3
3. 学会网上提供的现成 SDK。 3
二、功能需求 3
三、 实验内容 3
3.1百度SDK的使用 3
3.2具体实现 4
3.2.1获取手机的相机及存储权限 4
3.2.2进行拍照或者从相册选取并返回路径 5
3.2.3数据回转 6
3.2.4线程实现照片的上传 6
3.2.5使用GSON对JSON解析 9
3.2.6本地数据库的建立及操作 15
3.3部分结果展示 19
四、 实验总结 21
五、 实验源码及工程 21
针对课堂上采用 APP 来进行考勤这一需求,开发一套系统。
具体的功能有:
注:请自行研究人脸识别 API 及各种接口。
3.1百度SDK的使用
首先进入百度的人脸识别口http://ai.baidu.com/tech/face
进入立即使用
之后我们打开技术文档查看,本应用,使用的最新的V3接口,在获取了Access百度会分配一个KEY和ID,在通过这两个参数,就可以连接到人脸库。
_token之后,就可以调用百度提供的,人脸库的增删改查等功能。
本报告的重点内容不在此,具体的内容请看百度的技术文档。对于本应用需要的jar包,见后页的链接。
3.2具体实现
3.2.1获取手机的相机及存储权限
显然,我们要实现的功能是手机打卡,自然需要拍照和读取手机的照片,之后返回它的路径。再通过路径将照片传入云端。进行必要的操作。
在安卓高版本中,相机是敏感权限,必须要动态申请,且安卓版本纷杂,所以获取相机权限真是一把辛酸泪……
首先在AndroidManifest中获取一次权限
之后,在Mainactivity中调用以下语句:
/**当检测到没有权限时,一次获取读写加拍照权限**/
3.2.2进行拍照或者从相册选取并返回路径
之后,对动态读取拍照权限,且将路径用Intent回传给上一个Activity进行解析。代码加了大量注释。不太需要解释了。这儿有一个坑,就是第一个获取的照片路径并不是真实的,需要再获取他的绝对路径。
在这里,我们要实现数据的回转,将uri传回上一个界面。之后将图片传入服务器,来接收返回的信息,这里依旧以打卡功能来举例子。
3.2.3数据回转
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// 相册选择图片
if (requestCode == Photo_ALBUM) {
if (data != null) { //开启了相册,但是没有选照片
Uri uri = data.getData();
//从uri获取内容的cursor
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
cursor.moveToNext();
ImagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); //获得图片的绝对路径
cursor.close();
Log.i("图片路径", ImagePath);
runthreaad(); //开启线程,传入图片
}
} else if (requestCode == CAMERA) {
runthreaad(); //开启线程,传入图片
}
}
3.2.4线程实现照片的上传
Android靠后一点的版本,出现了一个问题。就是关于网络的POST必须用子线程来实现。答主在这里纠结了许久,才发现问题所在。这里对于上个功能的线程的具体解释。
void runthreaad() { //新建线程
new Thread(new Runnable() {
@Override
public void run() { //开启
String url = "https://aip.baidubce.com/rest/2.0/face/v3/search";//人脸查找的地址
try {
//将我们的图片以二进制读取并转码为BASE64
byte[] bytes1 = FileUtil.readFileByBytes(ImagePath);
String image1 = Base64Util.encode(bytes1);
Map
map.put("image", image1);
map.put("liveness_control", "NORMAL");
map.put("group_id_list", "face");
map.put("image_type", "BASE64");
map.put("quality_control", "LOW");
String param = GsonUtils.toJson(map);//转为json格式
// 注意这里仅为了简化编码每一次请求都去获取access_token,线上环境access_token有过期时间, 客户端可自行缓存,过期后重新获取。
String accessToken = "24.395dd6c10e2314d5886451ea8a44ff26.2592000.1548158423.282335-15236904";
result = HttpUtil.post(url, accessToken, "application/json", param);
System.out.println("hehehe:" + result);
Gson gson=new Gson();
Search_result_bean Result_bean=gson.fromJson(result,Search_result_bean.class);
System.out.println("哈哈哈哈哈哈哈哈"+ Result_bean.getError_code());
int Error_code=Result_bean.getError_code();
if(Error_code==0){
double score=Result_bean.getResult().getUser_list().get(0).getScore();
String user=Result_bean.getResult().getUser_list().get(0).getUser_id();
System.out.println("分数:"+score);
if(score>=75.0){
SQLiteDatabase db;
MyHelper ggg= new MyHelper(opt.this);
db=ggg.getWritableDatabase();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
// System.out.println(df.format(new Date()));// new Date()为获取当前系统时间
ggg.Insert_two(db,"time_id",df.format(new Date()),user);
Looper.prepare();
Toast.makeText(opt.this,"打卡成功!" , Toast.LENGTH_LONG).show();
Looper.loop();
}else{
Looper.prepare();
Toast.makeText(opt.this,"打卡失败!照片不在人脸库" , Toast.LENGTH_LONG).show();
Looper.loop();
}
}else{
String error_message="打卡失败:"+Result_bean.getError_msg();
System.out.println("xixixixixixi"+ error_message);
Looper.prepare();
Toast.makeText(opt.this,error_message , Toast.LENGTH_LONG).show();
Looper.loop();
}
} catch (Exception e) {
Log.i("错误", "hahaha");
e.printStackTrace();
}
}
}).start();
}
3.2.5使用GSON对JSON解析
首先我们来了解一下GSON,GSON是由Google开发的一款小型的解析包。可以用实现类的方式,对JSON数据逐层解析。
首先导入GSON的包。
当然,也可以在libs里加入本地拷入的包,再在depends里加入依赖。
第二步就使用GsonFormat,可以帮我们直接将Json格式的字符串自动生成实体类参数。
安装完毕之后
新建一个类:然后:Code → Generate..。 → GsonFormat ,然后将JSON字符串粘贴进去即可。
比如,我的查找JSON:
error_code : 0
error_msg : SUCCESS
log_id : 304592859128123651
timestamp : 1545912812
cached : 0
result : {"face_token":"b677eee8e44242db81a4063d0ba01efd","user_list":[{"group_id":"face","user_id":"li","user_info":"","score":95.235778808594}]}
返回的类:
package com.example.a11630.face_new;
import java.util.List;
public class Search_result_bean {
private int error_code;
private String error_msg;
private long log_id;
private int timestamp;
private int cached;
private ResultBean result;
public int getError_code() {
return error_code;
}
public void setError_code(int error_code) {
this.error_code = error_code;
}
public String getError_msg() {
return error_msg;
}
public void setError_msg(String error_msg) {
this.error_msg = error_msg;
}
public long getLog_id() {
return log_id;
}
public void setLog_id(long log_id) {
this.log_id = log_id;
}
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getCached() {
return cached;
}
public void setCached(int cached) {
this.cached = cached;
}
public ResultBean getResult() {
return result;
}
public void setResult(ResultBean result) {
this.result = result;
}
public static class ResultBean {
/**
* face_token : b677eee8e44242db81a4063d0ba01efd
* user_list : [{"group_id":"face","user_id":"li","user_info":"","score":95.235778808594}]
*/
private String face_token;
private List
public String getFace_token() {
return face_token;
}
public void setFace_token(String face_token) {
this.face_token = face_token;
}
public List
return user_list;
}
public void setUser_list(List
this.user_list = user_list;
}
public static class UserListBean {
/**
* group_id : face
* user_id : li
* user_info :
* score : 95.235778808594
*/
private String group_id;
private String user_id;
private String user_info;
private double score;
public String getGroup_id() {
return group_id;
}
public void setGroup_id(String group_id) {
this.group_id = group_id;
}
public String getUser_id() {
return user_id;
}
public void setUser_id(String user_id) {
this.user_id = user_id;
}
public String getUser_info() {
return user_info;
}
public void setUser_info(String user_info) {
this.user_info = user_info;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
}
}
}
这样我们进行层层解析,就可以轻松地获得我们需要的JSON数据了。
Gson gson = new Gson(); //新建GSON
Search_result_bean Result_bean = gson.fromJson(result, Search_result_bean.class); //GSON与我的工具类绑定
// System.out.println("哈哈哈哈哈哈哈哈" + Result_bean.getError_code());
int Error_code = Result_bean.getError_code();
if (Error_code == 0) { //返回值为零,就是打卡识别成功
double score = Result_bean.getResult().getUser_list().get(0).getScore(); //一层层进入,获取到score
String user = Result_bean.getResult().getUser_list().get(0).getUser_id(); //获取用户名
// System.out.println("分数:" + score);
if (score >= 78.0) { //分数大于78.0分,判断为同一个人,提示打卡成功
/**…………具体操作**/
}
3.2.6本地数据库的建立及操作
这个,首先我的思路是建立了两个表,一个是time_id表存储打卡信息,包括了当前时间和打卡的id号。
另一个表是name_id表,存储了人脸信息,即姓名和id号。这样两个表之前没有冲突,且不会影响。且两个表可以通过id外键连接。
表名 | name_id | time_id |
列 | Name,id(KEY) | Time,id |
3.2.6.1数据库的创建及增删操作类的实现
package com.example.a11630.face_new;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MyHelper extends SQLiteOpenHelper {
public MyHelper(Context context) {
super(context, "facedata.db", null, 2);
}
@Override
public void onCreate(SQLiteDatabase db) {
String stu_table = "create table name_id (id text primary key ,name text)"; //name----id
String stu_table1 = "create table time_id(id text ,time text)"; //time----id
//执行SQL语句
db.execSQL(stu_table);
db.execSQL(stu_table1);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
public void Insert(SQLiteDatabase db, String table, String name, String id) { ///db,table,name,id
ContentValues cValue = new ContentValues();
cValue.put("id", id);
cValue.put("name", name);
db.insert(table, null, cValue);
System.out.println("插入成功:"+id+" "+name);
}
public void Insert_two(SQLiteDatabase db, String table, String time, String id) { ///db,table,time,id
ContentValues cValue = new ContentValues();
cValue.put("id", id);
cValue.put("time", time);
db.insert(table, null, cValue);
System.out.println("插入time成功:"+id+" "+time);
}
public void Delete(SQLiteDatabase db, String table, String message) {
String sql = "delete from " + table + " where " + message;
System.out.println("sqi语句:"+sql);
db.execSQL(sql);
}
}
3.2.6.2数据库的查询操作
这个有点难办,作者这一块代码写了很久(不好找bug),因为我们还要实现条件查找,即某个人,或者某天和某人某天的打卡记录要解决。
首先我们从简单的入手,只查找某个人的打卡记录,一句SQL语句解决。
见下面代码。
String id = IDS.getText().toString().trim(); //获取id
SQLiteDatabase db;
MyHelper ggg = new MyHelper(change.this);
db = ggg.getWritableDatabase();
StringBuffer sum = new StringBuffer();
String sql = "select * from time_id where id=\"" + id + "\"";
// System.out.println("差部分:" + sql);
Cursor cursor = db.rawQuery(sql, null);
while (cursor.moveToNext()) {
String ID = cursor.getString(0); //获取第一列的值,第一列的索引从0开始
String TIME = cursor.getString(1);//获取第二列的值
sum.append(" ID:" + ID + " time:" + TIME + "\n");
}
cursor.close();
db.close();
search_sum.setText(sum.toString());
这一段并没有什么好说的,之后就是查询某天的打卡记录,由于数据库的日期已经精确到了秒,这个时候用SQL语句显然不能实现了。(我们不可能每一秒都来查询一次)
这个时候我采用的是将数据库的所有打卡信息遍历一遍,将时间与我规定的日期进行对比。筛选出某天的打卡记录。具体如下。
String TIMES = btn_choosetime.getText().toString().trim(); //日期在此.
System.out.println("截取时间在此:" + TIMES);
StringBuffer sum = new StringBuffer();
SQLiteDatabase db;
MyHelper ggg = new MyHelper(change.this);
db = ggg.getWritableDatabase();
Cursor cursor = db.query("time_id", null,
null, null, null, null, null);
sum.append("结果如下:");
if (cursor.getCount() != 0) {
cursor.moveToFirst();
String id = cursor.getString(0);
String name_time = cursor.getString(1);
if (TIMES.substring(0, TIMES.length()).equals
(name_time.substring(0, TIMES.length()))) { //截取前面的部分比较。如2018-12-31
System.out.println("查询结果:\n");
System.out.println(id + ":" + name_time);
sum.append("\n ID:" + id + " time:" + name_time + "\n");
}
while (cursor.moveToNext()) {
String id1 = cursor.getString(0);
String name_time1 = cursor.getString(1);
if (TIMES.substring(0, TIMES.length()).equals
(name_time1.substring(0, TIMES.length()))) {
System.out.println(id + ":" + name_time);
sum.append(" ID:" + id1 + " time:" + name_time1 + "\n");
}
}
}
cursor.close();
db.close();
search_sum.setText(sum.toString());
3.3部分结果展示
本报告也接近了尾声,其实本人比较烦这种总结。不知该怎么写。
在本应用制作初期,只用了很短的时间就可以用Java实现了图片上传和结果的接收。本以为改成安卓App是一件很容易的事。
然而现实狠狠给了我一耳光……安卓版本繁杂,每一个大版本对之前的改动都很大,光是一个获取拍照的权限,就可以在网上找到N个博客,且80%都是错的……
再就是安卓对于网络的控制,必须要在子线程里实现发送,这儿就又卡了小久。
最后就是数据库了,可以看到我的代码中充满了System.out.println(),因为Android studio3.1版本删除了DDMS,打不开数据库的文件夹,所以每一步调试都有点艰辛。
还好最后应用总算成形了,其实有很多细节可以去优化,比如上传图片中可以加一个进度条(这个答主实验过,要么就是开了关不了,要不就是秒开秒关)来提高用户体验。拍照时返回拍摄的图片给用户预览(这个也实验过,但是图片会自动旋转,修正后就是一片黑……)。
其他细节,在之后我会继续进行优化,总结结束。
GitHub:GitHub - Sumtudou/Face-recognition-for-android: 安卓端的人脸识别app,支持从手机上传和即时拍照打卡,并带有SQLite数据库,可以保存打卡信息。
开发工具:Android studio3.1版本
Libs内有jar包,已经在工程中了。