热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

人脸识别考勤系统安卓APP(手把手教学手动滑稽)

由于百度API开始收费,所以这个应用暂时无法使用了,代码部分仍然有参考价值,建议大家自行更换token去验证。个人博客的下载验证码bug&

由于百度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


题目十三 人脸识别考勤 


一、目的与要求 


  1. 学会如何使用 Activity 及常见控件。 
  2. 学会使用数据库存储及网络通信。 
  3. 学会网上提供的现成 SDK。 

二、功能需求 

针对课堂上采用 APP 来进行考勤这一需求,开发一套系统。

具体的功能有:


  1. 人脸录入:通过拍摄学生照片,或者上传现有照片。
  2. 人脸识别考勤:通过人脸识别,每个学生来考勤。
  3. 考勤统计及数据查询:制定查询条件(按学生、按时间),输出统计结果。

    注:请自行研究人脸识别 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 = new HashMap<>();//用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 user_list;

        public String getFace_token() {
            return face_token;
        }

        public void setFace_token(String face_token) {
            this.face_token = face_token;
        }

        public List getUser_list() {
            return user_list;
        }

        public void setUser_list(List user_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包,已经在工程中了。


推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了求解gcdexgcd斐蜀定理的迭代法和递归法,并解释了exgcd的概念和应用。exgcd是指对于不完全为0的非负整数a和b,gcd(a,b)表示a和b的最大公约数,必然存在整数对x和y,使得gcd(a,b)=ax+by。此外,本文还给出了相应的代码示例。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
author-avatar
冠凯雅友9
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有