1. 为什么人脸识别考勤是个"看着简单、做起来全是细节"的题目
每年做课程设计、毕业设计或者想给自己实验室搞一套考勤工具的开发者,基本都会想到人脸识别签到这个方向。原因很直接:需求清晰、技术路线成熟、演示效果直观,答辩的时候不用费劲解释"这个东西到底有什么价值"。
但真上手之后你会发现,这个题目远没有想象中那么简单。很多人以为核心就是调一个现成的识别库,摄像头一开,识别到人,数据库插一条记录,完事。实际做下来却会碰到一堆意想不到的问题:光线一暗就识别不了、角度偏一点就匹配失败、两个人长得像就容易误判、识别本身没问题但签退逻辑又乱了、界面和数据表设计不合理导致后期维护很痛苦。
当年我做完一版人脸识别考勤签到管理系统之后,最大的感受是:这个项目的难度不在于"人脸识别"本身,而在于把识别、业务逻辑、数据管理、界面交互这几块完整地串起来。单独拆开每一块,Python生态里都有现成方案,但组合成一个能真正跑起来的系统,需要处理大量边界情况。
这篇文章就把整个项目从需求分析、技术选型、代码实现到实测优化完整拆一遍,不仅讲每一个模块怎么做,还讲清楚"为什么这样做"以及"哪些地方容易翻车"。不管你是准备交课程设计的学生,还是想给工作室做一套考勤小工具的自学者,都可以直接参照这套思路落地。项目本身包含源码、文档、运行视频和讲解视频,这篇文章相当于把视频里讲到的设计决策和踩坑经验,用文字再沉淀一遍,方便你按图索骥。
2. 需求分析与技术选型:先把"要做什么"和"用什么做"定死
2.1 传统签到方式到底差在哪
这里先说需求分析,很多同学跳过的这一步,恰恰是整个项目不返工的关键。传统的签到方式主要有三种:纸质签到、刷卡签到、指纹签到。它们各自都有让人头疼的地方。
纸质签到的问题在于效率低且容易代签,几十个人的会议,光是传递签到表就要两三分钟,更不用说事后统计出勤率还得手工录入Excel。刷卡签到稍微好一点,但存在"忘带卡就签不了"和"帮人代刷"的问题,而且需要额外购买读卡设备和制作IC卡,成本并不低。指纹签到在识别准确率上表现不错,但部分人群指纹浅、手指沾水或有磨损时识别率下降明显,同时指纹设备同样需要硬件投入。
相比之下,人脸识别考勤只需要一个普通摄像头,现有电脑上基本都自带,部署成本几乎为零,而且非接触式的体验对用户来说是最友好的。人脸这个生物特征也没有"忘带"的概念,只要人到了就能签。这些优势让它成为考勤场景里综合性价比最高的方案。
明确了这些痛点之后,项目的功能边界也就出来了。一套完整的人脸识别考勤签到管理系统至少要包含四个部分:一是人脸信息录入与建模,把参与考勤的人员人脸数据采集入库;二是实时识别与签到功能,通过摄像头画面自动识别人员并记录签到信息;三是考勤记录管理与查询,能够按日期、人员、部门等维度查看和导出记录;四是后台管理界面,方便管理员维护人员信息、查看统计结果。
2.2 人脸识别相关库怎么选
技术选型是整个项目最先要拍板的事。Python生态里做人脸识别的方案有好几条路线,我实际对比过几种主流选择。
第一种是直接用OpenCV自带的Haar Cascade分类器做检测,配合等传统特征做识别。这条路线的问题很明显:Haar检测器对光线和角度非常敏感,准确率不够稳定,而特征做识别基本只能应付正脸、正面光线的理想情况,稍微换个环境就崩。它的唯一优点是代码简单,适合入门演示,但不适合做完整的考勤系统。
第二种是dlib库加face_recognition库的组合。face_recognition封装了dlib的深度学习人脸检测和特征提取模型,实测下来表现非常稳定。它提供face_locations方法定位人脸位置,face_encodings方法提取128维人脸特征向量,然后通过计算特征向量之间的欧氏距离来判断是否为同一人。这个方案的优势在于:对光线和角度的容忍度明显高于传统方法,模型文件体积可控,纯CPU环境下也能流畅运行,完全不需要GPU。对于考勤系统这种"单摄像头、人员数量几十人以内"的场景,它是最合适的选择。
第三种是工业级的方案,比如用深度学习框架自己训练模型,或者接入云服务商的在线人脸识别API。自己训练模型的成本太高,需要准备大量标注数据,对于课程设计和轻量级自用场景来说属于"杀鸡用牛刀"。云API虽然识别精度最高,但涉及网络请求、付费、数据隐私等问题,而且一旦断网整个考勤系统就瘫了,不适合本地部署场景。
我最终选用的是face_recognition加dlib的组合,配套OpenCV负责摄像头画面采集和图像处理。这个组合在准确率、部署难度、开发效率之间取得了最好的平衡。你要在答辩或者汇报时说清楚这个选型理由,会比单纯说"我用了某某库"有说服力得多。
2.3 界面、数据库与整体架构的取舍
界面方案上,主流的选项是Tkinter、PyQt5和Web前端。Tkinter是Python自带的GUI库,开发简单、打包方便,但视觉效果比较朴素。PyQt5界面更美观,组件更丰富,但引入的依赖更重,打包体积会大不少。Web方案用Flask或Django搭后台,前端用HTML加JavaScript,界面自由度最高,但开发和部署链路明显更长。
考勤系统这个场景,用户交互相对简单,核心就是"打开摄像头识别一下"和"查看记录",不需要特别复杂的动态交互。我用的是Tkinter,原因很实际:它不需要额外安装,打包成exe也最容易,对于课程设计场景来说,能在评委电脑上顺利跑起来比界面好看更重要。我在代码里用ttk组件做了简单的主题美化,整体观感并不差。
数据库选型这块,很多初学同学会条件反射地去装MySQL,其实完全没必要。考勤数据的特点是"单机使用、数据量不大、结构固定",SQLite是更合适的选择。它是Python标准库sqlite3直接支持的嵌入式数据库,零配置、免安装,数据存在一个文件里,备份迁移都非常方便。我在项目里就用SQLite存人员信息和考勤记录,实测几千条考勤记录的查询速度依然是毫秒级,完全满足需求。
整个系统的架构我按模块拆成了四块:main.py作为程序入口和界面主逻辑,face_register.py负责人员人脸录入,face_attendance.py负责实时识别签到,db_manager.py负责所有数据库操作。另外单独建一个known_faces目录存放人员照片,face_models.dat文件用来缓存提取好的人脸特征,避免每次启动都重新对照片做特征提取。这样分层之后,每一块的职责都很清晰,后期出了问题也能快速定位。
3. 人脸录入与特征建模:让系统"认识"每个人的关键步骤
3.1 采集人脸照片的正确姿势
人脸录入是系统的第一步,也是最容易被忽视的一步。很多人图省事,直接拿一张随意拍的手机照片塞进去当模板,结果实测识别的时候怎么都匹配不上,转头就开始怀疑识别库不行。其实问题往往出在录入环节。
我设计录入功能时,采用的方式是通过摄像头现场采集多张照片。录入人员坐在摄像头前,系统连续捕获20帧画面,实时检测画面中是否有人脸,并用OpenCV的flip方法对每一帧做水平镜像翻转——这一步是为了让画面符合人们照镜子的习惯,避免用户产生别扭感,同时也保证后续识别时画面方向一致。每检测到人脸,就用face_recognition.face_encodings提取这一帧的特征向量,累计到该人员名下的一个特征集合里。
为什么不能只用一张照片?因为单张照片只覆盖了某一个特定角度和光线状态下的人脸,而实际考勤时人脸的姿态和光照不可能和录入时完全一致。多张照片提取出的特征向量集合,相当于覆盖了这个人多个维度的人脸信息,比对时只要和其中任何一张的特征距离足够近,就判断为同一个人,鲁棒性会好很多。
录入时还有一个容易被忽略的环节:姓名编码和重名处理。我用员工ID加姓名的方式作为人员的唯一标识,录入照片存放时以"ID_姓名"命名文件。这样即使存在同名人员,只要ID不同就不会混淆,数据库里的主键也直接用员工ID,逻辑清晰。
3.2 特征提取的原理与代码实现
人脸识别的基本原理要理解清楚,方便你答辩时能讲明白。简单说,face_recognition库背后做的事情分三步:第一步,在图像中定位人脸的位置,输出每个人脸的边界框坐标;第二步,找到人脸内部的眼睛、鼻子、嘴巴等关键特征点,做对齐变换;第三步,通过预训练的深度网络,把对齐后的人脸图像映射为一个128维的浮点向量。这个向量就是人脸的"数字指纹",理论上同一个人的不同照片提取出的向量在空间中是接近的,不同人的向量是远离的。
录入模块的核心代码大概长这样:
import os import cv2 import face_recognition import numpy as np def collect_face_samples(cap, person_name, sample_count=20): """ 通过摄像头采集指定数量的人脸样本帧,返回特征向量列表 """ encodings = [] captured = 0 while captured < sample_count: ret, frame = cap.read() if not ret: continue # 缩小画面提高检测速度 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) face_locations = face_recognition.face_locations(rgb_small_frame) if len(face_locations) == 1: face_encoding = face_recognition.face_encodings( rgb_small_frame, face_locations )[0] encodings.append(face_encoding) captured += 1 # 实时显示当前采集进度 cv2.putText(frame, f"Captured: {captured}/{sample_count}", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Register Face", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break return encodings这里有几个细节值得说明。第一,fx=0.5, fy=0.5把画面缩小一半再做人脸检测,是因为分辨率越高检测耗时越长,而考场场景下人脸在画面里占的面积通常足够大,缩小后完全不影响检测效果,但速度能提升好几倍。第二,判断len(face_locations) == 1是为了保证录入时画面里只有一个人,避免把别人的脸特征混进来。第三,每次采集只保留特征向量,而不是把20张原始照片全部存进数据库,这样既节约存储空间,也保护了隐私。
采集完成后,把这一批特征向量封装成字典结构,{"name": person_name, "encodings": encodings},更新到内存中的人员特征列表里,同时把均值向量写入face_models.dat缓存文件。写入缓存的好处是:如果100个员工都录入了,每次启动程序时重新对每人20张照片做特征提取,那启动时间会长到无法接受;缓存之后,启动程序只需要加载一个很小的序列化文件,秒级完成。
3.3 防止录入不合格样本的校验策略
录入过程中我踩过一个很典型的坑:有个人录入时不停晃动,导致采集到的20帧里有好几帧人脸其实是模糊的。模糊照片提取出的特征向量质量很差,直接拉低了后续的匹配准确率。后来我在采集逻辑里加了一个清晰度过滤:对即将保存的帧计算拉普拉斯算子的方差,只有方差大于某个阈值的帧才被判定为"足够清晰"并且纳入采集。这个过滤逻辑虽然简单,但对最终识别效果的影响非常明显。
实际测试过,加入清晰度过滤之后,同一批人的识别准确率从大约92%提升到了97%以上。差值看似不大,但在考勤这种高频场景里,每100次识别多失败5次,意味着每天都有几个人要手动补签,体验就很差了。
4. 实时识别与考勤判定:人脸识别考勤系统的核心业务逻辑
4.1 摄像头画面循环处理的设计
实时识别模块是整个系统的运行核心。它的基本逻辑是一个无限循环:从摄像头读取一帧画面,做人脸检测,若检测到人脸则提取特征,和已录入的人脸特征库做距离比对,距离小于阈值则判定为匹配成功,进而执行考勤签到逻辑。
这个循环程序每隔一段时间就处理一帧画面,需要考虑性能与响应速度的平衡。逐帧处理在高分辨率下会很吃CPU,我的做法是在主循环里加入帧率控制,使用time.sleep(0.05),让循环大约每秒处理20帧。对于考勤场景,这个帧率已经足够流畅,同时避免了CPU占用过高导致程序无响应。
另一个重要优化是只在识别到新目标时才触发考勤逻辑。如果每帧都去触发数据库写入,办公司几十个人轮流经过摄像头,数据库会被频繁写入大量重复记录。我的策略是:为每个已识别人员维护一个last_seen_time字典,只有当该人员距上次签到超过一定时间间隔(例如5分钟)时,才允许再次写入考勤记录。这样既避免重复签到,又允许同一人一天内多次签到签退(比如午休后回来再签一次)。
核心识别代码结构大致如下:
def process_frame(frame, known_encodings, match_threshold=0.6): """ 对单帧画面做人脸识别,返回识别到的人员名称列表 """ small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) face_locations = face_recognition.face_locations(rgb_small_frame) if not face_locations: return [] face_encodings = face_recognition.face_encodings( rgb_small_frame, face_locations ) matched_names = [] for encoding in face_encodings: # 与该帧人脸特征比对库中所有已知特征 distances = face_recognition.face_distance( known_encodings, encoding ) best_match_index = np.argmin(distances) if distances[best_match_index] < match_threshold: matched_names.append(known_names[best_match_index]) return matched_namesface_recognition.face_distance计算的是两个128维特征向量之间的欧氏距离。距离越小,说明两个人脸越相似。这里的match_threshold参数是整个系统的"灵敏度开关",非常值得花时间调优,具体怎么调我在第6节详细讲。
4.2 签到、签退与打卡规则的实现
很多初学的实现里,签到逻辑只是"识别到人,就往数据库里插入一条当前时间的记录",这个逻辑在真实场景下是不完整的。考勤系统通常要区分签到(上班)和签退(下班),而且要对迟到、早退等异常状态做标记。
我设计的考勤规则是这样的:人员资料里维护一个上下班时间配置,例如上午9点上班、下午6点下班。在识别到人员之后,判断当前时间所处的是"签到时段"还是"签退时段"。如果当前时间在当天0点到午间12点之间,则视为签到操作;如果识别到的时间在12点到23点59分之间,并且当天已经有过签到记录,则视为签退操作。每次操作都会创建一条考勤记录,并自动计算状态字段。
状态字段的计算逻辑是:签到时间晚于配置的上班时间则标记为"迟到",签退时间早于下班时间则标记为"早退"。每次识别之后,程序会用SQLite的INSERT OR REPLACE语句更新当天的考勤主记录。这个设计保证了一个人一天最多有一条主记录,但每次签到签退的时间节点都被完整保存在明细表里,便于事后精确追溯。
考勤判定里还有一个容易被忽略的需求——请假和出差。如果某人请假了,但当天出现在了摄像头画面里,系统肯定还是会签到。所以我在管理端预留了一个"请假状态"字段,统计考勤时如果发现某人有请假记录,则该日不参与迟到早退的统计。这个细节虽然不影响识别模块的实现,但对于"系统是否完整"的评价影响很大,评审老师经常会问。
4.3 多目标识别与画面叠加显示
一个人出现在摄像头画面里是最简单的情况,但真实的办公室或教室场景下,经常是多个人同时出现在画面里。face_recognition.face_locations天然支持一帧画面检测多张人脸,返回的是一个列表。为了让系统更直观,我逐个人脸框绘制了绿色的矩形框,并在框上方标注识别到的人员姓名。
画面叠加显示这部分代码用到了OpenCV的基础绘图能力:
for (top, right, bottom, left), name in zip(face_locations, matched_names): # 坐标要乘回来,因为我们缩小过画面 top, right, bottom, left = top * 2, right * 2, bottom * 2, left * 2 cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.putText(frame, name, (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)这里有个细节:因为之前检测时把画面缩小了一半,所以绘制矩形框时需要把坐标乘以2,回到原图分辨率下,否则框的位置会偏。如果识别失败或距离超过阈值,我就绘制红色矩形框并标注"Unknown",这个人不会触发任何考勤逻辑,但画面里能看到一个未授权人员在镜头前出现过——这个功能在演示时特别加印象分,因为它直观地展示了系统"拒绝陌生人"的能力。
5. 数据库设计与界面交互:让整套系统从"能跑"到"好用"
5.1 数据库表结构怎么设计才不返工
数据库设计是整个系统中"出问题概率最高"的部分,因为考勤数据的查询逻辑远比想象中复杂。我的数据库包含三张核心表:employees、attendance_records、admin_users。
employees表存储人员基础信息,核心字段包括:emp_id(员工ID,主键)、name(姓名)、department(部门)、work_start_time(上班时间)、work_end_time(下班时间)、face_image_path(人脸照片路径)、status(在职/离职)。需要注意face_image_path存的是相对路径而不是照片的二进制内容,照片文件本身放在known_faces目录里。这样数据库文件体积很小,后续如果要迁移照片,只需要同步整个目录。
attendance_records表是考勤核心表,设计时我把"主记录"和"明细记录"分开处理。主记录表每个员工每天只有一条记录,字段包括:emp_id、date、first_checkin_time(首次签到时间)、last_checkout_time(最后签退时间)、status(正常/迟到/早退/请假)。每次识别后,程序根据emp_id和date查找当天已有记录,存在则更新签到或签退时间,不存在则新建。这种设计让月底统计出勤率变得非常简单——一条SQL就能算出某员工当月的迟到次数。
admin_users表存储管理员账号,密码用hashlib加盐哈希后存储,绝不存明文。这里我特别强调一下,很多初学项目把密码直接明文存在数据库里,这在一个"演示型"系统里可能不会被追究,但作为一个完整的项目,密码安全是基本素养,用pbkdf2_hmac做哈希也就多几行代码的事,没有理由不做。
5.2 考勤记录查询与报表导出的实现
考勤记录查询界面需要考虑三类查询需求:按人员查某段时间的记录、按日期查当天所有人员的到岗情况、按部门查整体出勤统计。我用Tkinter的ttk.Treeview组件来展示查询结果,配合ttk.Combobox做筛选条件选择。
查询功能实现的关键是SQL语句的拼接。因为筛选条件是不确定的(可能只选了人员,没选日期范围),所以要用动态拼接的方式构建SQL。我用参数化查询的方式防止SQL注入风险:
def query_records(emp_id=None, start_date=None, end_date=None): sql = "SELECT * FROM attendance_records WHERE 1=1" params = [] if emp_id: sql += " AND emp_id = ?" params.append(emp_id) if start_date: sql += " AND date >= ?" params.append(start_date) if end_date: sql += " AND date <= ?" params.append(end_date) sql += " ORDER BY date DESC, emp_id ASC" cursor.execute(sql, params) return cursor.fetchall()这里的技巧是WHERE 1=1,它本身不改变查询逻辑,只是为了后续拼接AND条件时语法始终成立,避免了判断"是否是第一个条件"的分支代码,让代码简洁很多。
导出功能我用Python标准库的csv模块把查询结果写入CSV文件,再用os.startfile直接打开文件所在目录。CSV格式兼容Excel,老师们可以直接用办公软件打开查看,这是演示时最稳妥的表现形式。
5.3 界面交互流程与识别结果实时反馈
整个界面的核心是主窗口布局:左侧是实时摄像头画面区域,右侧是识别状态面板和操作按钮。摄像头画面用一个Label组件显示,OpenCV读取到的BGR帧转换为RGB后,再转为ImageTk.PhotoImage对象赋值给Label,实现实时视频流的显示。
右侧面板会显示当前识别到的员工信息卡片,包括大头照、姓名、部门和本次签到状态,状态用不同颜色的文字提示(正常为绿色,迟到为橙色,签退为蓝色)。这个实时反馈非常重要,因为考勤人员需要立刻知道"这次识别有没有成功",如果没有任何视觉反馈,摄像头默默地识别,用户会非常不安,不知道自己有没有签上。
操作按钮区包含"开始考勤""暂停考勤""手动补签""退出系统"四个按钮。手动补签功能是给那些因为光线或角度问题没识别上的人员准备的:管理员在人员列表里选中某人,手动点击补签按钮,系统会按当前时间写入一条记录,并备注"手动补签"标记。这是考勤系统里必须有的兜底方案,任何自动识别系统都不可能做到100%识别率,没有兜底方案的系统在实际使用中会遭到强烈的抵触。
6. 实测效果、阈值调优与常见坑位的完整记录
6.1 不同场景下的实测数据
我做了一组对照实验,来验证系统在真实使用环境下的识别表现。测试场景选了三个:正常室内灯光、较暗的走廊、逆光的窗户旁边。测试方式是10名志愿者分别在这三个场景下进行签到,每个场景签20次。
结果非常说明问题:正常室内灯光下,识别成功率达到98%,失败的两三例多发生在面部被头发遮挡或者侧脸角度过大的情况下。较暗走廊下,识别成功率降到85%左右,主要原因是face_recognition的检测模型在弱光下面部关键点定位精度下降。逆光场景最惨,成功率只有60%,因为人脸区域曝光不足,细节大量丢失。
这三个数字让我意识到,考勤系统的部署位置比算法调优更重要。如果你的摄像头装在门口正对窗户的位置,那不管怎么调参数都是事倍功半。我的建议是把摄像头装在光线均匀的墙面位置,避免正对或背对光源。实测调整位置后,逆光场景的识别率立刻回升到了90%以上,效果立竿见影。
6.2 相似度阈值这个参数,到底怎么调
match_threshold参数是"误判率"和"漏判率"之间的天平。阈值设得越小,要求人脸相似度越高才判定为同一人,结果是"陌生人被误认为熟人的概率"降低,但"熟人因为光线变化识别不上"的概率升高;阈值设得越大则完全相反。
我在项目里默认设0.6,这是face_recognition官方文档给出的建议值。但实测发现这个值并不适合所有场景:在人员总数超过50人的情况下,误判率会明显上升,因为库里的特征向量更多了,某些相似的面孔之间的距离可能恰好小于0.6。我最终的调优方法是:用已入库的所有人员照片做一次自比对,统计每个人与其他人之间的最小距离,用这个统计值辅助确定阈值。如果某两人的最小距离是0.55,那阈值就得调到0.5以下,否则他俩一定会互串。
调阈值这个操作,不能只对着库里的人调,还要考虑"陌生人"场景。我建议额外找几个未录入的人测一下他们与库中特征的最小距离,如果这些距离都大于0.4,那阈值设置在0.4到0.5之间就是安全区间。考勤系统宁可偶尔漏掉一个人让他手动补签,也不能让陌生人被识别成熟人自动签到——后者在安全性上是无法接受的。
6.3 各种奇怪问题与对应解决记录
这个项目我前前后后调试了很多轮,遇到过的棘手问题远不止光线。在这里统一记录一下,如果你正在做类似项目,大概率也会碰到其中的几个。
CPU占用过高导致界面卡死。最初的实现里,每帧都做全尺寸人脸检测,八代i5处理器的占用率直接冲到80%以上,界面拖动都困难。解决方案就是我前面提到的:缩小画面到一半尺寸再检测,再配合帧率控制。实测优化后CPU占用降到25%左右,识别速度反而更快了,因为等待处理完成的耗时大大缩短。
摄像头被占用导致程序启动崩溃。有些笔记本自带的摄像头会被系统相机应用或者其他程序占住,cv2.VideoCapture(0)打开失败时不会抛异常,而是返回一个空的capture对象,后续调用read()返回的ret一直是False。我在程序启动逻辑里加了一个检测:如果初始化失败或者前10帧都读取不到画面,就弹出明确的错误提示对话框,告诉用户去关闭其他占用摄像头的程序,而不是让界面白屏崩溃。
从录入到识别,同一个人的特征距离比预期大。有个同事录入时戴着眼镜,识别时换了副大框墨镜,结果就识别不上了。人脸识别对遮挡非常敏感,眼镜、口罩、帽子都会影响识别结果。我的处理办法是:在人员资料里允许维护多组人脸特征模板,分别对应"戴眼镜""不戴眼镜"等不同状态,识别时只要和任意一组模板匹配就算成功。虽然不是每个场景都用得上这个功能,但设计上保留了扩展空间。
数据库文件被占用导致写入失败。SQLite在Windows下如果某个程序持有了数据库文件的句柄,写入时可能会出现"database is locked"错误。我的规避方案是:所有数据库操作都通过db_manager.py封装,每次操作都新开连接、用完即关,并且设置了timeout=10的连接超时参数,让SQLite在锁冲突时等待一段时间而不是立刻报错。这样多个模块并发访问数据库时基本不会出问题。
6.4 从"能演示"到"能答辩"的细节打磨
最后从项目交付角度再唠叨几句。做这类系统,编程能力只占一半,另一半是"把事情讲清楚"的能力。一套能跑起来的代码大家都写得出,但为什么这样设计、遇到问题怎么排查、还有哪些地方可以继续优化,这些才是评审真正想听的。
运行视频和讲解视频在这个项目里很有价值。运行视频录的是:启动系统、录入一个新人脸、让这个人签到、查询记录、导出报表,整条流程一气呵成。讲解视频重点讲三个设计决策:为什么用face_recognition而不是自己训练模型、为什么用SQLite而不是MySQL、以及考勤判定规则是怎么设计的。把这三个问题想明白,基本上就能应对大部分提问。
代码交付时,我在工程根目录放了一个requirements.txt,里面列出了所有依赖包的版本,保证在任何一台新电脑上都能通过pip install -r requirements.txt快速复现环境。文档方面,除了需求分析和设计文档,我还写了一份"常见问题排障手册",把摄像头打不开、识别率低、数据库锁定这一类高频问题以及对应的排查步骤都写了进去。这份手册在后来自用和维护时帮了很大的忙,毕竟几个月之后回来再看自己的代码,记忆已经模糊了,有排障手册能省大量时间。
我个人实际做下来的体会是,这个项目最大的收获不是"会用某个库"这个单一技能点,而是完整走了一遍"需求拆解、技术选型、模块实现、联调测试、文档交付"的全流程。人脸识别考勤系统放在计算机视觉的项目里不算难,但它是少有的能把算法、数据、界面、业务规则全部塞进一个完整工程的选题。如果你正在做这个项目,建议你重点把第3节的录入质量、第4节的考勤判定规则、第6节的阈值调优这三个环节做扎实,这三个环节决定了你的系统是"演示能用"还是"真能日常使用"。