简介:这是一套基于Android Studio与百度智能云的人脸识别学生考勤签到系统源码,面向计算机相关专业学生、教师及企业开发者,可用于毕业设计、课程设计或考勤类项目参考。项目采用SpringBoot搭建服务端,配合MySQL存储数据,管理员可维护人脸信息并同步至百度云平台,安卓端调用人脸识别接口完成自动签到,默认管理员账号为admin/123456。压缩包共328个文件,约9.39MB,包含46个Java源文件、31个XML布局、22个JS脚本、30个PNG与13个JPG图片资源,以及SQL建库脚本、APK安装包、Gradle构建文件和README说明文档,覆盖服务端、安卓端与数据库三部分。该资源为个人毕业设计成果,代码经测试运行成功,答辩评审平均分96分,已有65人学习。下载后可获得完整项目结构、人脸识别对接思路与数据库设计参考,建议先阅读README.md,仅供学习交流,勿用于商业用途。
1. 从一张签到表到一套人脸考勤系统:这套源码到底能跑通什么
如果你带过课、管过实验室门禁,或者帮某高校做过考勤工具,大概率遇到过这种场景:纸质签到表被代签、二维码截图满天飞、指纹机排队排到走廊。基于 Android Studio 和百度云的人脸识别学生考勤签到系统,本质就是把「人脸检测 + 活体判断 + 云端比对 + 签到落库」这条链路塞进一台安卓手机里,让老师拿手机对着学生扫一下,后台自动写一条带时间戳的考勤记录。它解决的不是「识别准不准」这一个点,而是「采集端、识别端、存储端怎么串起来」的工程问题。适合两类人:一是要交课程设计或毕设的学生,需要一套能编译、能演示、有 SQL 文件的完整工程;二是想快速验证人脸考勤可行性的开发者,想拿现成结构改造成自己的门禁或会议签到。这套东西的价值不在算法多先进,而在于它把安卓端调用百度云人脸接口的完整流程、数据库表结构和文档说明都摊开了,你照着改就能落地。
2. 安卓端调用百度云人脸接口:从 SDK 引入到活体检测的完整链路
2.1 为什么选百度云人脸识别而不是本地跑模型
很多人第一反应是「本地跑个 MobileFaceNet 不香吗」,但真到落地阶段,本地模型有三个绕不开的坎:一是安卓机型碎片化,不同芯片对 NNAPI 支持差异大,同一套模型在低端机上推理要两三秒,学生排队直接炸;二是活体检测本地做很容易被照片和视频攻破,而云端活体有持续更新的攻击样本库;三是本地模型更新要发版,云端接口改个参数就生效。百度云人脸识别提供检测、比对、活体、库管理一整套 HTTP 接口,安卓端只负责拍照上传和解析 JSON,逻辑轻、维护成本低。常见做法是:端上做人脸框预览和拍照质量控制,云端做特征提取和 1:N 搜索。这套源码走的就是这条路,所以你在 Android Studio 里看到的更多是网络请求和相机逻辑,而不是推理代码。
2.2 在 Android Studio 里接入人脸 SDK 的最小步骤
先把百度云控制台建好的应用拿到 API Key、Secret Key,注意人脸识别用的是独立的 Access Token 接口,不是直接拿 AK/SK 调业务接口。下面这段是获取 token 的核心代码,放在工具类里全局复用:
// 获取百度云 Access Token,有效期 30 天,建议缓存到 SharedPreferences public static String getAccessToken() { String url = "https://aip.baidubce.com/oauth/2.0/token"; OkHttpClient client = new OkHttpClient(); // grant_type 固定为 client_credentials,这是服务端应用的标准模式 RequestBody body = new FormBody.Builder() .add("grant_type", "client_credentials") .add("client_id", API_KEY) // 控制台应用的 API Key .add("client_secret", SECRET_KEY) // 控制台应用的 Secret Key .build(); Request request = new Request.Builder().url(url).post(body).build(); try (Response response = client.newCall(request).execute()) { JSONObject json = new JSONObject(response.body().string()); return json.getString("access_token"); // 拿到后缓存,别每次请求都调 } catch (Exception e) { Log.e("Token", "获取失败", e); return null; } }逻辑说明:token 接口是 POST 表单,不是 GET,参数名必须严格是grant_type、client_id、client_secret,写错一个就返回 400。参数上,client_id对应 API Key,client_secret对应 Secret Key,别和 AK/SK 混。返回的access_token默认 30 天有效,但实际建议按 25 天缓存刷新,避免边界过期导致签到失败。失败时先看返回体里的error和error_description,常见是 key 填反或应用未开通人脸识别权限。
2.3 人脸注册与签到比对的两段式设计
考勤系统的人脸数据不能每次签到都重新注册,正确做法是分两段:注册阶段把学生人脸存进百度云的人脸库,签到阶段用 1:N 搜索。注册时调/face/v3/faceset/user/add,把user_id设成学号,image传 base64 或 URL,image_type选 BASE64 时注意去掉前缀。签到调/face/v3/search,传当前照片和group_id_list,返回匹配到的user_id和score。下面是对比逻辑:
// 签到比对:返回匹配学号和相似度分数 public static JSONObject searchFace(String base64Image, String groupId) { String url = "https://aip.baidubce.com/rest/2.0/face/v3/search?access_token=" + getAccessToken(); OkHttpClient client = new OkHttpClient(); // image_type 必须与传入格式一致,BASE64 时不要带 data:image 前缀 RequestBody body = new FormBody.Builder() .add("image", base64Image) .add("image_type", "BASE64") .add("group_id_list", groupId) // 人脸库分组,如 "class_2024" .add("quality_control", "NORMAL") // 质量校验,NORMAL 会过滤模糊脸 .add("liveness_control", "NORMAL") // 活体检测,NORMAL 拒绝照片攻击 .build(); Request request = new Request.Builder().url(url).post(body).build(); try (Response response = client.newCall(request).execute()) { return new JSONObject(response.body().string()); } catch (Exception e) { Log.e("Search", "比对失败", e); return null; } }逻辑说明:group_id_list是你在人脸库里建的分组,建议按班级或课程建,避免全校一个库导致搜索变慢。quality_control设 NORMAL 会返回质量分,低于阈值直接判失败,省得拿模糊脸去比对浪费配额。liveness_control设 NORMAL 是活体检测的关键,但注意它需要额外算力,响应会慢 200 到 500 毫秒,签到高峰期要评估。返回结果里score是相似度,一般 80 以上算可信,但不同场景阈值不同,后面避坑章节会细说。
2.4 相机采集与图片压缩的参数取舍
安卓端最容易翻车的地方不是接口,是图片。百度云要求 base64 后不超过 2MB,而手机原图动辄 5MB 以上。常见做法是拍照后先按长边缩到 640 像素,再压到质量 80,这样 base64 后大约 100 到 200KB,识别率基本不掉。下面这段是压缩逻辑:
// 将 Bitmap 压缩为百度云可接受的 base64 字符串 public static String bitmapToBase64(Bitmap bitmap) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 先缩放到长边 640,人脸识别不需要原图分辨率 int maxSide = Math.max(bitmap.getWidth(), bitmap.getHeight()); float scale = 640f / maxSide; Bitmap scaled = Bitmap.createScaledBitmap(bitmap, (int) (bitmap.getWidth() * scale), (int) (bitmap.getHeight() * scale), true); // JPEG 质量 80 是识别率和体积的平衡点,再低会丢特征 scaled.compress(Bitmap.CompressFormat.JPEG, 80, baos); byte[] bytes = baos.toByteArray(); return Base64.encodeToString(bytes, Base64.NO_WRAP); // NO_WRAP 避免换行符 }逻辑说明:createScaledBitmap的 filter 参数设 true 让缩放更平滑,减少锯齿对人脸特征的干扰。质量 80 是经验值,压到 60 以下暗光环境识别率明显下降。Base64.NO_WRAP必须加,默认的换行符会让百度云解析失败,这个坑很多人踩过。如果签到环境光线差,可以在压缩前做一次直方图均衡,但会增加端上耗时,按需取舍。
3. 数据库表设计与签到落库:SQL 文件里每张表在干什么
3.1 学生表、人脸库映射表、考勤记录表的三表结构
这套源码的 SQL 文件通常包含三张核心表。学生表存学号、姓名、班级、人脸注册状态;人脸库映射表存学号和百度云user_id的对应关系,因为百度云返回的是user_id,你需要反查学号;考勤记录表存签到时间、课程 ID、匹配分数、签到状态。下面是一个可用的建表 SQL:
-- 学生基础信息表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, -- 学号,与百度云 user_id 保持一致 name VARCHAR(50) NOT NULL, class_name VARCHAR(50), face_registered TINYINT DEFAULT 0, -- 0 未注册 1 已注册 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 考勤记录表,每次签到写一条 CREATE TABLE attendance ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20), course_id VARCHAR(30), sign_time DATETIME DEFAULT CURRENT_TIMESTAMP, match_score FLOAT, -- 百度云返回的相似度分数 status TINYINT DEFAULT 1, -- 1 正常 2 迟到 3 代签嫌疑 INDEX idx_student_course (student_id, course_id) ); -- 人脸库分组映射,支持一个学生多张脸 CREATE TABLE face_mapping ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20), group_id VARCHAR(50), -- 百度云人脸库分组名 face_token VARCHAR(100), -- 注册时返回的 face_token update_time DATETIME DEFAULT CURRENT_TIMESTAMP );逻辑说明:student_id直接当百度云user_id用,省掉一层映射,但要注意百度云user_id有长度限制,学号一般够用。attendance表加match_score是为了事后审计,分数异常低的记录可以人工复核。face_mapping支持一个学生注册多张脸,比如正脸和侧脸,提高识别率。索引idx_student_course是为了查「某学生某课程是否已签到」,避免重复签到。
3.2 签到接口的幂等处理与重复签到拦截
考勤系统最怕同一节课学生连点两次签到,写两条记录。正确做法是在插入前先查当天该学生该课程是否已有记录,有则更新而不是新增。下面是对应的 SQL 逻辑:
-- 签到前查询,判断是否已签到 SELECT id, sign_time FROM attendance WHERE student_id = ? AND course_id = ? AND DATE(sign_time) = CURDATE(); -- 若已存在则更新签到时间,否则插入新记录 INSERT INTO attendance (student_id, course_id, match_score, status) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE sign_time = NOW(), match_score = VALUES(match_score);逻辑说明:ON DUPLICATE KEY UPDATE依赖唯一索引,所以attendance表最好加一个UNIQUE KEY (student_id, course_id, sign_date),其中sign_date是单独的日期字段,避免用DATE(sign_time)做唯一索引导致失效。参数上,match_score每次更新为最新值,方便看识别稳定性。如果业务允许迟到多次签到,就把唯一索引去掉,改成先查后插,但要在应用层加锁,防止并发写重。
3.3 从百度云返回 JSON 到落库的字段映射
百度云/face/v3/search返回的结构是result.user_list[0].user_id和result.user_list[0].score,你需要把user_id当学号,score存进match_score。下面是一段解析并落库的代码:
// 解析百度云返回并写入考勤记录 public static void saveAttendance(JSONObject searchResult, String courseId) { try { JSONObject result = searchResult.getJSONObject("result"); JSONArray userList = result.getJSONArray("user_list"); if (userList.length() == 0) { Log.w("Attendance", "未匹配到人脸"); return; // 没人脸匹配,不落库 } JSONObject top = userList.getJSONObject(0); String studentId = top.getString("user_id"); double score = top.getDouble("score"); // score 低于 80 标记为代签嫌疑,不直接算有效签到 int status = score >= 80 ? 1 : 3; // 调用 DAO 层执行上面的 INSERT ... ON DUPLICATE KEY UPDATE AttendanceDao.upsert(studentId, courseId, score, status); } catch (JSONException e) { Log.e("Attendance", "解析失败", e); } }逻辑说明:user_list可能为空,必须先判长度再取第 0 个,否则空指针。score阈值 80 是常见起点,但实际要按你的库大小调,库越大误匹配越多,阈值要相应提高。status设 3 表示代签嫌疑,后续可以人工复核,而不是直接拒绝,避免误伤。落库走 DAO 层,别在解析里直接拼 SQL,方便换数据库。
4. 避坑与排查:人脸考勤落地时最容易翻车的五个点
4.1 相似度阈值设 80 却频繁误识别
现象:两个长相相近的学生互相被识别成对方,签到记录张冠李戴。原因:百度云 1:N 搜索的score受库大小影响,库里人越多,最高分越容易偏高,80 分在几百人的库里不够。解决:把阈值提到 85 到 90,同时开启quality_control为 HIGH,过滤低质量脸;另外在注册时每人录两张不同角度的脸,提高区分度。如果还不行,就在应用层加二次确认,分数在 85 到 90 之间的弹窗让老师手动确认。
4.2 活体检测开了但照片依然能签到
现象:学生拿手机里存的照片对着摄像头,居然签到成功。原因:liveness_control设了 NORMAL 但没配合动作活体,静态照片在部分光线下仍可能通过。解决:改用动作活体,让百度云返回动作指令,端上提示学生转头或眨眼,完成后再调搜索接口。注意动作活体会增加 1 到 2 秒耗时,签到高峰期要分流。另外检查image_type是否传错,传 URL 时百度云拉取的是原图,可能绕过端上压缩,导致活体判断异常。
4.3 Access Token 过期导致整节课签到失败
现象:上午还能用,下午所有签到返回 401。原因:token 缓存没做刷新,30 天到期后直接失效,或者应用被杀进程后内存缓存丢失。解决:把 token 存 SharedPreferences 并记录获取时间,每次请求前检查是否超过 25 天,超过就重新获取;同时加一个失败重试,遇到 401 先刷新 token 再重试一次。别在每次请求都调 token 接口,百度云有 QPS 限制,频繁调用会被限流。
4.4 图片 base64 带前缀导致 400 错误
现象:接口返回error_code: 216101或image format error。原因:安卓端用Base64.encodeToString后手动加了data:image/jpeg;base64,前缀,百度云不认。解决:image_type传 BASE64 时,image字段只放纯 base64 字符串,不要带任何前缀;如果要用 URL,就传可公网访问的图片地址,别传本地路径。另外检查Base64.NO_WRAP是否加了,换行符也会导致解析失败。
4.5 签到高峰期接口超时
现象:上课前五分钟集中签到,大量请求超时或返回error_code: 18(QPS 超限)。原因:百度云免费版 QPS 有限,并发一高就限流。解决:端上加一个简单的队列,同一时间只发一个请求,前一个返回后再发下一个;或者错峰签到,按学号分组分批。如果班级人数多,建议升级到付费版提高 QPS,或者在端上先做本地人脸检测,只有检测到人脸才调云端搜索,减少无效请求。
5. 把识别分数用起来:从签到记录反推识别质量与调参技巧
签到记录里的match_score不是写完就完事,它是你调参的唯一依据。我一般会在系统跑一周后,把attendance表按match_score排序,看最低的那批记录。如果大量集中在 80 到 85 之间,说明阈值设低了,误识别风险高;如果普遍在 90 以上,说明可以适当降低阈值提高通过率。下面这条 SQL 能帮你快速看分布:
-- 按分数区间统计签到次数,判断阈值是否合理 SELECT CASE WHEN match_score >= 95 THEN '95+' WHEN match_score >= 90 THEN '90-95' WHEN match_score >= 85 THEN '85-90' WHEN match_score >= 80 THEN '80-85' ELSE '80以下' END AS score_range, COUNT(*) AS cnt FROM attendance WHERE sign_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY score_range ORDER BY score_range DESC;逻辑说明:DATE_SUB取最近七天,避免历史数据干扰。如果80以下占比超过 10%,说明要么阈值太低,要么注册照片质量差,需要重新录脸。85-90区间如果占比很高,可以考虑把阈值提到 88,牺牲一点通过率换准确率。这个查询建议每周跑一次,作为调参依据。
另一个技巧是给每个学生记录「历史平均分」,如果某学生某次签到分数明显低于自己的历史均值,比如低了 15 分以上,就标记为可疑。这比全局阈值更细,能抓住「本人但状态差」和「他人代签」的区别。实现上可以在student表加一个avg_score字段,每次签到后更新移动平均。
-- 更新学生历史平均分,用于个体化异常检测 UPDATE student s JOIN ( SELECT student_id, AVG(match_score) AS avg_score FROM attendance WHERE student_id = ? AND match_score > 0 ) a ON s.student_id = a.student_id SET s.avg_score = a.avg_score;逻辑说明:match_score > 0过滤掉未匹配的记录,避免拉低均值。移动平均可以用0.8 * 旧值 + 0.2 * 新值做平滑,比直接 AVG 更跟得上状态变化。这个字段不参与签到判断,只用于事后审计和预警,别在签到主流程里查,否则每次签到多一次写库,高峰期会拖慢。
最后说个血泪经验:别在签到成功后立刻弹「签到成功」就完事,要把匹配到的姓名和分数显示出来,让老师肉眼确认。我见过太多系统因为阈值问题把 A 识别成 B,老师没看直接过,期末对账才发现。多这一秒确认,能省掉后面一堆扯皮。希望帮到你。
本文还有配套的精品资源,点击获取