ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Android Studio和百度云的人脸识别考勤系统开发实战

基于Android Studio和百度云的人脸识别考勤系统开发实战 简介基于安卓AndroidStudio与百度云的人脸识别考勤签到系统后台采用SpringBootMySQL安卓端调用百度云人脸识别接口完成自动签到管理员可添加和维护学生人脸信息适合计算机专业学生、教师或企业员工学习也可直接用于毕设、课设项目参考。资源压缩包共328个文件、约9.39MB包含Java源码、XML布局、SQL数据库脚本、APK安装包、GIF界面演示、Markdown说明文档及SpringBoot配置文件等Java/XML支撑安卓端开发SQL初始化数据表APK可直接安装体验GIF/PNG直观展示考勤流程。已有65人浏览学习作者标注代码实测运行成功、答辩评审平均分96分。附带README.md说明可在AndroidStudio与IDEA中导入项目配合雷电模拟器运行内置管理员账号admin/123456便于快速登录体验对理解移动端调用云平台人脸识别接口、SpringBoot后端接口设计及考勤系统整体架构均有参考价值。1. 基于AndroidStudio和百度云的人脸识别考勤从课堂排队刷卡到看脸签到的完整闭环教室门口排起长队学生低头刷卡、点名册翻来翻去这是传统签到最熟悉的两个痛点。而基于 Android Studio 开发、调用百度云人脸识别 API 的学生考勤签到系统恰好把这两件事都省掉了摄像头拍到脸云端返回比对结果考勤记录落进 SQLite/MySQL学生不用停步老师课后导表即可。这套方案值得动手做是因为它不要求你有算法功底——人脸检测和特征提取全部交给百度云Android 端只负责取帧、调接口、写库。你需要搞定的是工程落地那一圈的细节鉴权、阈值、并发、权限适配。接下来按“架构 → 跑通 → 业务 → 避坑 → 验证”的顺序把每一步拆开讲目标是让新手也能在 Android Studio 里把考勤闭环跑起来。2. 学生考勤系统的整体架构Android端、百度云API与SQL文件怎么分工2.1 为什么选择“Android端 百度云”而不是本地跑ArcFace模型人脸识别在本地跑不是不行ArcFace、FaceNet 这些开源模型跑在手机上识别精度也够看。但落地到学生考勤这个具体场景本地推理的成本往往被低估模型文件动辄几十 MBAndroid 低端机内存吃紧运行几分钟温度上来识别帧率直接掉到个位数。更麻烦的是学生手中的手机型号五花八门低端机跑不动中端机各有各的相机硬件差异。老师主力机上测得好好的换台两年前的低端机就卡成幻灯片。本地推理之外还有个被忽略的坑人脸特征库的维护。考勤系统需要预先录入学生人脸如果特征向量存在本地每学期新生录入、转班、换发型你都得重新跑一遍模型更新特征库。用百度云的方案本地只存一个 face_token学生的照片和特征都托管在云端新增学生调用一次百度云的注册接口就能完成其他端的考勤机、门禁机也能共用同一套人脸库。成本账也划算。一个班 45 人、一天签到 4 次课单班一天的人脸搜索请求也就 200 次左右。百度云人脸识别 API 的免费额度足够一个小规模学校试点即便超量后按量计费单次调用的成本也远低于一个老师每天点名的时间成本。这就是标题把“AndroidStudio 和百度云”并列的原因Android 端负责交互和取帧百度云负责最核心的人脸特征提取与比对。2.2 一条考勤数据的一生从摄像头取帧到SQL落库的数据流我在项目里一般会把整个考勤流程拆成四段每一段有清晰的边界CameraX 的 ImageAnalysis 拿到预览帧转成 Bitmap。Bitmap 压缩到合适尺寸并转成 Base64 字符串。通过 Retrofit 调用百度云人脸搜索接口传入人脸图和分组 ID。解析返回的 score过了阈值就写进 attendance 表。这里的关键是分清哪些操作在 UI 线程、哪些必须在子线程。CameraX 的 analyze 回调本身就在子线程但 Base64 编码和网络请求仍然要显式丢进线程池或者用 Retrofit 的异步 enqueue否则主线程一秒内多次回调会卡顿掉帧。数据库写入也建议走异步。常见做法是直接用 Room 的 suspend 函数或者 SQLiteOpenHelper 配合单线程 Executor避免多线程写同一个连接导致 SQLiteDatabaseLockedException。还有一个容易忽略的边界本地不存特征向量只存 face_token。因为后续签到只需要用 face_token 跟百度云侧的人脸库做比对本地保存一份特征向量既占空间又没有实际用途反而会因为不同版本 SDK 的特征维度不一致导致迁移后无法比对。这一点在设计数据表时要提前想清楚。2.3 随项目交付的sql文件student、attendance、course三张表怎么设计标题里的 sql 文件通常就建这几张核心表。我一般会先给一个数据库初始化脚本包含学生表、考勤表、课程表以及必要的索引。下面是我在项目里常用的一套精简设计可以直接拿来改CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL UNIQUE, name TEXT NOT NULL, class_name TEXT NOT NULL, face_token TEXT NOT NULL, photo_url TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_id TEXT NOT NULL UNIQUE, course_name TEXT NOT NULL, teacher_name TEXT, schedule TEXT ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, course_id TEXT NOT NULL, check_in_time DATETIME DEFAULT CURRENT_TIMESTAMP, face_score REAL, status TEXT DEFAULT present, UNIQUE(student_no, course_id, date(check_in_time)) ); CREATE INDEX idx_attendance_time ON attendance(check_in_time);student 表里的 face_token 是百度云注册人脸后返回的唯一标识它代替了本地特征。attendance 表上的 UNIQUE 约束是防重复签到的最底一层保障跟你代码里怎么判断业务逻辑无关数据库层面直接拦住同一个人同一天同一门课的重复记录。标题里的 sql 文件一般会再附带几条示例 INSERT 语句和 WHERE 查询语句方便你导入后直接验证表结构和索引是否生效。重点关注唯一索引和日期函数 DATE() 的用法这两处最容易写错。3. 用Android Studio导入并跑通工程百度云鉴权与摄像头取流的代码落地3.1 导入工程前的环境准备SDK版本、Gradle配置与依赖清单Android Studio 导入别人写的工程最容易翻车的是 Gradle 版本对不上。你本地装的 Android Studio 版本如果比工程要求的版本落后Gradle Sync 会直接报错。先确认三个版本号Gradle Plugin 版本、Gradle 发行版版本、compileSdk 版本。建议统一用较新的稳定组合比如 Android Studio 的稳定版配 Gradle 8 以上compileSdk 33 或 34。老工程如果 compileSdk 太低直接升级后注意适配到 Android 12 以上的导出声明。工程里需要的最小依赖集大概是下面这样评价稳定dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.camera:camera-camera2:1.2.3 implementation androidx.camera:camera-lifecycle:1.2.3 implementation androidx.camera:camera-view:1.2.3 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.google.code.gson:gson:2.10.1 }Retrofit 和 Gson 负责调百度云接口和解析 JSONCameraX 负责摄像头预览与取帧。如果你要接数据库加上 Room 或者直接用 SQLiteOpenHelper 也行。单独把这些依赖抽出来写在 build.gradle 里Sync 后能解析成功基本说明环境没问题。Gradle 下载慢或者解析失败时先检查是否拉了官方镜像或代理别急着改依赖版本。3.2 百度云人脸识别API鉴权Access Token的获取、缓存与过期处理百度云人脸识别接口的调用凭证叫 Access Token它由一组 API Key 和 Secret Key 换来的可以理解为“临时门禁卡”。获取方式很简单在百度智能云控制台创建一个人脸识别应用拿到 Key 之后发一个 HTTP 请求换取 Token。我在本地调试时通常会用 curl 验证一下钥匙能不能用curl -i -X POST \ https://aip.baidubce.com/oauth/2.0/token?grant_typeclient_credentialsclient_id你的APIKeyclient_secret你的SecretKey返回结果里的 access_token 是后续所有请求的查询参数。这个 token 的有效期默认是 30 天但实际使用中你不能等它自然过期再去处理因为每天的请求量会累计触发配额一旦 token 失效全端功能瞬间不可用。我一般会在 Android 端封装一个 AccessTokenManager 来统一管理逻辑如下public class AccessTokenManager { private static volatile String accessToken; private static volatile long expireAt; public static synchronized String getToken() { // 提前 5 分钟刷新避免临界期过期 if (accessToken ! null System.currentTimeMillis() expireAt - 5 * 60 * 1000) { return accessToken; } accessToken fetchTokenFromApi(); expireAt System.currentTimeMillis() expiresIn * 1000L; return accessToken; } private static String fetchTokenFromApi() { // 这里用 OkHttp/Retrofit 请求 oauth/2.0/token 接口 // 解析 JSON 里的 access_token 和 expires_in return json.getString(access_token); } }这个封装里有两处值得注意一是用 static volatile 保证多线程下的可见性二是“提前 5 分钟刷新”而不是等活动校验的报错再重试。实际运行中你会发现token 一旦失效所有并发请求会同一瞬间报 401你再去刷新就出现了请求风暴。提前温和刷新是血泪经验换来的。3.3 CameraX取帧 Retrofit调用人脸搜索接口的完整代码摄像头取帧是 Android 端最核心的衔接逻辑。CameraX 的 ImageAnalysis 会把每一帧图像回调给你的 Analyzer 实现。这个类里做三件事把 YUV_420_888 格式的帧转成 Bitmap压缩高质量 JPEG 并编码成 Base64然后异步调用百度云搜索接口。public class FaceAnalyzer implements ImageAnalysis.Analyzer { Override public void analyze(NonNull ImageProxy imageProxy) { // 1. YUV 转 Bitmap Bitmap bitmap ImageUtils.imageProxyToBitmap(imageProxy); // 2. 将长边压缩到 720pxjpg 质量 85 Bitmap scaledBitmap ImageUtils.zoomBitmap(bitmap, 720); ByteArrayOutputStream baos new ByteArrayOutputStream(); scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 85, baos); String base64 Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP); // 3. 异步发起人脸搜索请求 AipFaceService service RetrofitClient.getService(); FaceSearchRequest req new FaceSearchRequest(base64, BASE64, GROUP_ID); service.search(AccessTokenManager.getToken(), req) .enqueue(new CallbackFaceSearchResponse() { Override public void onResponse(CallFaceSearchResponse call, ResponseFaceSearchResponse response) { if (response.body() ! null response.body().result ! null) { FaceMatchInfo match response.body().result.userList.get(0); handleMatchResult(match); } } Override public void onFailure(CallFaceSearchResponse call, Throwable t) { // 记录日志走待同步逻辑 } }); imageProxy.close(); } }Retrofit 的接口定义长这样从易懂和可维护的角度看大概是这种结构public interface AipFaceService { POST(rest/2.0/face/v3/search) CallFaceSearchResponse search( Query(access_token) String accessToken, Body FaceSearchRequest request ); }FaceSearchRequest 里关键字段有三个image 传 Base64 字符串image_type 固定为 BASE64group_id 传你百度云控制台里建的用户组 ID。group_id 是用来限定搜索范围的比如一个班一个组或者整个年级一个组。这里最容易踩坑的是 Base64 的换行符编码时一定要用 NO_WRAP 参数否则 JSON 体内混入换行会导致解析失败。另一个值得注意的参数是 liveness_control对于普通教室摄像头场景我建议直接设成 NONE否则活体检测误伤率会高到没法用。4. 考勤业务逻辑落地人脸比对阈值、防重复签到与SQL去重4.1 比对分数阈值怎么定score、top_n和活体控制的边界百度云人脸搜索接口返回的结果里有个 score 字段范围 0 到 100表示当前人脸与库中人脸的相似度。很多人第一次调通就拍脑袋定一个 80 分阈值实际上这是个需要拿到真实环境里校准的参数。我调过的班级里鲁迅低头看手机被拍到的侧面分数可能只有 65学生换了新发型正脸打光充足能到 90 以上戴着口罩那就直接搜不到这是人脸搜索的现实。我给的参考区间是85 分以上直接判定签到成功60 到 85 分之间标记为“待复核”界面弹窗让老师确认。这么做既避免低阈值大面积误报又不会因为阈值过高导致每天都有学生签不上。这里还涉及一个 top_n 参数默认返回前几个匹配结果。你把它调大到 1 就行除非你要做同班多人的识别优化——在考勤场景里一次只取最高分跟业务逻辑最匹配。liveness_control 这个参数也值得说清楚。百度云的活体检测对普通相机的识别要求是图像中的人脸要有明确的立体轮廓。教室里摄像头放得远学生面部小活体检测开启后经常误判静止画面为攻击。我在实践中直接设 NONE把活体检测留到手机端自己判断或者干脆不做。依赖单一参数实现安全性的做法在考勤场景里性价比太低——你没拦住代签倒是把正常签到的学生拦了一堆。4.2 防重复签到的三层机制时间窗口、唯一索引与事务写入一个学生一天同一门课只能签到一次这是考勤系统最基本的规则。实现这个规则我用了三层防线目的不是在某一层把所有问题拦住而是每层解决一类异常。第一层业务流程时间窗口。在签到逻辑里判断当前时间和课程的开始时间差超过课程时段的迟到区间就标记为 late。这个判断逻辑写在 Java 层跟业务规则耦合灵活调整方便。第二层数据库唯一约束。就是第 2 章提到的 UNIQUE 约束。不管应用层怎么并发提交数据库这一层只要出现同一天、同一 student_no、同一 course_id 的重复记录直接抛异常。这一层不能少因为 Android 端的 UI 操作不是单线程的多个回调同时触发时界面上显示没签到库里可能已经写了两次。第三层事务写入。写考勤记录的时候我先创建一个临时事务把“检查是否存在记录”和“插入新记录”包在同一个事务里而不是先查再插。先查再插在并发下是典型的竞态条件两个线程同时查到不存在然后同时插入数据库的唯一约束只能等报错而事务能直接把这种冲突吸收掉BEGIN TRANSACTION; SELECT COUNT(*) FROM attendance WHERE student_no 20240001 AND course_id C001 AND date(check_in_time) date(now); -- 如果总数是 0执行 INSERT INSERT INTO attendance(student_no, course_id, face_score) VALUES (20240001, C001, 91.5); COMMIT;这里的 SQL 用到了 SQLite 的事务和 DATE() 函数既有去重逻辑演示也有聚合查询的味道。wordwised 层面去重有个常见的误用很多人习惯用 SELECT DISTINCT 或者 GROUP BY 来“事后”去重明细记录。这在考勤表里风险很高因为明细记录被去掉了就没有考勤原始凭证了。正确的是靠唯一索引拦数据入口而不是靠查询时去重掩饰脏数据。4.3 弱网/断网兜底先存本地待同步表再补传云端教室里的网络环境没有想象中稳定尤其是教学楼老校区Wi-Fi 一挤手机端可能连百度云都连不上。如果识别请求失败就让用户重试体验会很差。我做的兜底方案是加一张 pending_attendance 表把失败请求先落到本地。CREATE TABLE pending_attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, course_id TEXT NOT NULL, capture_time DATETIME DEFAULT CURRENT_TIMESTAMP, face_photo_path TEXT, synced INTEGER DEFAULT 0 );请求失败或超时的时候先把当前帧的人脸照片存到应用私有目录再把学生信息和时间写入 pending 表界面直接提示“已记录待网络恢复后同步”。网络恢复后用 WorkManager 拉起一个后台同步任务把未同步的影子和数据库记录一条条补认到主表里。这种做法比在界面等待超时重试要稳得多也避免老师当场不知所措。这里有个重要的前提先判断比对是否成功再决定是否写入待同步表。如果网络失败发生在发送请求之前你连结果都不知道那只能把照片和“未比对”标记包起来如果网络失败发生在请求之后你可以根据重试结果来判断。最安全的策略是 pending 表只存事实数据拍了谁、几点拍的照片路径不存“判定结果”同步时重新请求接口获取 score 再做状态更新等于给“后悔药”留了一条路。5. 安卓人脸考勤系统的常见坑与排查5个真跑过才会踩到的问题5.1 Access Token 过期后全部接口返回 401现象:系统运行了大概一个月第二天上课时所有签到请求都返回 HTTP 401错误码提示 invalid_tokenAndroid 端重新登录也没用。原因:Access Token 有效期是 30 天代码里只存在内存或 SharedPreferences没有检查有效期就拿来用。更隐蔽的是多台测试手机同时在调每台都各自拿了一份 token一旦其中一台触发了刷新逻辑其他设备的旧 token 全部失效。解决:把 3.2 节的 AccessTokenManager 落到所有请求的统一拦截器里每次请求前先检查 token 是否还剩 5 分钟有效期。同时把 token 的过期时间和 token 值持久化到 SharedPreferencesApp 重启后也能继续用避免每次启动都重新申请、污染之前签发的 token。教训是token 缓存写进内存不够必须有持久化和提前刷新。5.2 Android 6.0以上动态权限没处理App 一开摄像头就闪退现象:在高版本 Android 手机上安装后打开签到页面瞬间闪退Logcat 里报 SecurityException: Permission denied (missing INTERNET permission or CAMERA permission?)。原因:Android 6.0 开始CAMERA 权限属于运行时权限只在 AndroidManifest 里声明不够必须在代码里请求用户授权。学生考勤系统的代码很多是从老项目改过来的老项目只在清单里写了权限一跑新系统就崩。解决:在进入摄像头前直接用 ActivityCompat.requestPermissions 请求 CAMERA 权限并在 onRequestPermissionsResult 回调里判断是否授权。如果用户拒绝要有个友好的引导页而不是直接闪退。如果同时要读本地照片做离线待同步还要动态申请 READ_EXTERNAL_STORAGE。高版本系统的“仅部分照片访问”也会导致读取文件路径异常统一用 getExternalFilesDir 避开公共目录的权限纠结。5.3 SQL 文件导入后中文乱码考勤名单全是问号现象:把 sql 文件通过 Navicat 或命令行导入 SQLite/MySQL 后student 表的中文名字全部显示成乱码。原因:sql 文件的编码不是 UTF-8。Windows 环境下导出 sql 文件的工具默认用了 GBK导入时没转换编码。更隐蔽的是 SQLite 本身默认 UTF-8但如果你用 sqlite3 命令行在 GBK 终端里执行脚本也会出现写入乱码。解决:先把 sql 文件用文本编辑器另存为 UTF-8 无 BOM 编码再执行导入。导入后立刻用 SELECT * FROM student 查一条记录验证中文是否正常。我在项目里还会在 SQLiteOpenHelper 的 onConfigure 里加上一句 PRAGMA encoding UTF-8从连接层面锁定编码。这个坑虽然低级但每学期换人维护时都会重新踩一次。5.4 摄像头取帧压缩过头百度云报 image too large 或识别率暴跌现象:调通接口后偶尔报 image too large更多时候是识别分数突然降到 60 以下。原因:Android 摄像头原始帧分辨率很高你把它转成 Bitmap 后直接转 Base64一张图就能到十几 MB百度云接口对 Base64 大小有限制。反过来有些人为了省流量把压缩质量调到 30图像细节丢失人脸特征提取直接失败。解决:统一压缩策略先把原图长边缩放到 720 像素再以 JPEG 质量 85 编码。这个组合在识别率和请求体积之间最均衡。压缩后的 Base64 大约在 100KB 到 300KB 之间完全安全。代码里不要做二次压缩一次到位后就编码避免内存浪费和图片失真。手机上各厂商的相机 ISP 不同同样光线同样角度识别分数可能差 5 到 10 分压缩归一化能减小这部分差异。5.5 Android 11 分区存储导致读不到已有照片照片路径一层层空白现象:离线拍照保存到 /storage/emulated/0/Android/data/ 目录下的照片处理后才发现路径正确但 File.exists() 返回 false或者相册里完全看不到新保存的照片。原因:Android 11 开始强制分区存储App 不能直接通过绝对路径访问 /storage/emulated/0/Android/data/ 下的其他文件甚至自己之前生成的文件也只能通过 MediaStore 或 App 专属目录读取。很多老代码习惯直接在 SD 卡根目录建文件夹存照片新系统下这整条路都断了。解决:把照片保存在 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 下这个目录不需要存储权限App 卸载后系统也会自动清理。数据库里存的路径改成相对于这个目录的相对路径或者直接用 content:// URI。如果你的项目里还有 content://com.baidu.searchbox.fileprovider 这类单子调用来跨应用取图也要检查 targetSdk 是否大于 30是的话必须走文件选择器而不是硬编码路径。这个坑在接手老项目时几乎必踩因为老项目的照片和数据库已经按旧路径存了一堆你只能写一个迁移脚本把旧记录搬到新目录。6. 拿到系统后先别急着上线用一节课的考勤记录做闭环自检学生考勤系统不是代码跑通就算完你必须在一个仿真的环境里把整条链路从头到尾验证一遍。我习惯的做法是准备两个测试学生账号录入两张不同光线、不同角度下的正脸照片到百度云人脸库然后在真实教室里用老师手机跑一遍完整的签到流程。第一步检查数据库有没有正常初始化学生表能查到刚才录入的人。第二步打开签到页面摄像头预览正常靠近镜头触发识别界面弹出这个学生的姓名和班级。第三步确认 attendance 表里多了一条签到记录状态是 present。第四步同一学生再次尝试签到系统提示“已签到”并拒绝重复写入。最后把 Wi-Fi 关掉再做一次签到确认进入了待同步流程。这五步都通过系统才具备上真实课堂的条件。验证过程中通常会发现一两个隐藏问题阈值不合适、摄像头角度不合适、或者背景里的人被误识别。我自己第一次部署时把摄像头放在讲台上正对全班结果后排学生的人脸太小识别率很差。后来把摄像头架在讲台侧面、向下倾斜约 15 度只覆盖前两排学生的面部区域识别率立刻上来了。这个角度和距离的调节没有捷径可走就是拿着手机在教室里反复试宁可慢一点也别直接上线。如果后续想做班级考勤统计可以在 SQL 里加一个聚合视图按班级和课程计算签到率。这里记住一个技巧统计时用 attendance 表按日期分组不要在大表上直接做复杂 JOIN先缩小时间范围再关联慢 SQL 的很多问题都是从“查全表再过滤”开始的。每次上课前花 5 分钟做一次自检确认网络、token 和摄像头这三个关键环节都没问题再让学生进场。这套习惯帮我避免过不止一次“整班签不上”的课堂翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表