☰
基于Python+OpenCV的人脸识别考勤系统实现与避坑指南
2026/10/1 3:03:32 网站建设 项目流程

简介:这是一套基于Python与OpenCV的人脸识别员工考勤系统完整毕业设计项目,面向计算机相关专业的学生,适用于毕业设计、课程设计及期末大作业等场景。项目已获导师指导并通过审核,下载解压后即可直接运行,无需额外修改,确保环境完整可用。资源包共669个文件,主体为501个Python源码文件,覆盖人脸检测、特征提取、考勤记录等核心模块;另含44个pyd扩展库、22个exe辅助程序、4个pth模型权重文件,以及XML配置、PNG/JPG图像与TXT说明文档,整体约197.57MB,目录结构清晰,便于按模块学习和二次开发。目前已吸引521人学习浏览。对于正在筹备毕业设计的学生,这套项目提供了完整可运行的工程方案、文档说明与模型文件,既能帮助理解OpenCV人脸识别技术的落地流程,也能直接作为答辩演示的高分参考。

1. 基于python+opencv人脸识别的员工考勤系统:毕业答辩真正卡你的三件事

每年到这个时间点,都会有一批人拿到同一个题目:基于python+opencv人脸识别的员工考勤系统源码+文档说明。我见过太多人把精力花在“跑通人脸识别”上,结果素材库只有三张正脸照,摄像头一开就翻车,最后卡在“考勤记录怎么才算有效”这种业务逻辑上。其实这个系统的技术栈很固定:opencv负责检测和识别,sqlite或mysql管考勤流水,界面层用tkinter或pyqt把摄像头预览和打卡结果拼起来。难点从来不在某一个环节,而在“识别准”和“业务闭环”之间那条缝。这套东西适合两类人:一类是急着交差的毕设党,另一类是想把图像处理做成真实管理工具、以后往门禁考勤方向走的从业者。下面按我自己的落地顺序,从选型讲到排错,把能抄的代码和参数一次给全。

2. 人脸识别方案怎么选:LBPH、face_recognition与云端API的取舍

2.1 为什么毕业设计首选opencv自带方案

人脸识别这条路上,可选的方案比大多数人想的多:opencv自带的LBPH人脸识别器、face_recognition库、dlib、以及百度阿里腾讯的云端API。作为毕业设计,我几乎每次都推荐LBPH,原因很实际:答辩老师问“算法原理”时,LBPH能讲清楚一个完整的因果链,而不是一句“调用了第三方接口”带过。云端API识别率虽高,但“把图片传到服务器”这个动作本身就容易引发追问:隐私怎么保护?离线能不能跑?网络断了是不是全瘫?这几个问题在答辩现场很难圆回来。face_recognition识别效果好很多,但它要编译dlib,Windows下装个cmake和VS工具链就能劝退一半人,在毕设这种时间预算下不值得冒险。LBPH只需要opencv-contrib-python一个包,采集、训练、识别都能在本地闭环,演示时把网线拔了照样刷脸打卡,这个“离线可用”在答辩里是硬加分项。

2.2 LBPH识别的核心原理与参数全解

LBPH的全程是Local Binary Pattern Histogram,局部二值模式直方图。它的思路分三步:先把人脸图片切成小块,对每个像素和周围邻域像素比较灰度大小,得到一串二进制编码;然后统计每个小块里这些编码出现的直方图;最后把整张脸的所有小块直方图拼成一个特征向量。识别时用卡方距离比较当前人脸和已注册人脸的直方图,距离越小越像。这里不需要人肉眼能看懂,但要理解两个影响识别效果的关键设计:一是分块数量,opencv里对应grid_x和grid_y,分得越细越能保留空间位置信息,但分太细会把光照噪声也学进去;二是局部二值模式本身对光照变化相对不敏感,所以它比裸的灰度像素匹配更适合人脸这种纹理稳定的场景。

这段代码是把LBPH识别器创建出来的标准写法,注意cv2.face所在的包问题,后面避坑章节会专门说:

import cv2 # 创建LBPH人脸识别器 recognizer = cv2.face.LBPHFaceRecognizer_create() # 关键参数:radius是LBP算子的邻域半径,neighbors是采样点个数 recognizer.setRadius(1) # 半径越大,覆盖的纹理范围越宽 recognizer.setNeighbors(8) # 采样点越多,编码越细,但计算量越大 recognizer.setGridX(8) # 横向分块数,8x8是经验值 recognizer.setGridY(8) # 纵向分块数

这里的setRadius和setNeighbors很多人不调,直接用默认值,倒是也能跑。但如果你想在识别率上做出差异化,radius和neighbors是优先调整对象:radius=1对大多数室内摄像头距离的人脸都合适,调大到2以后,人脸表面的大块纹理会被放大,小细节比如眼角皱纹反而不敏感;neighbors默认8,减到4会加快速度但特征粗糙,增加到12对一些光照杂乱的环境更稳,但会拖慢训练。grid_x和grid_y我一般不动,8x8已经能平衡空间信息和计算量,除非你发现“两张脸交叉误判”频繁,才考虑把分块加到12x12,代价是生成的模型文件变大。记住,这些参数没有绝对最优,要在你自己的训练集上测,下面第4章会给一套量化验证的办法。

2.3 备选方案:face_recognition与云端接口的适配场景

如果非要提高识别率,face_recognition是LBPH之后最顺手的升级路径。它对外封装了dlib的人脸检测和特征提取,一张脸变成128维特征向量,识别时比较欧氏距离,阈值设到0.45到0.5之间就比较稳。但它有个被反复提起的坑:依赖dlib,而dllib在Windows上需要先装好Visual Studio的C++构建工具,再装cmake,整个过程对没碰过编译的学生来说很容易翻车。云端接口则是另一个极端,百度、阿里的人脸识别REST接口,用一张base64图换一个相似度分数,准确率在现有人脸库里最高,但你需要考虑:所有员工人脸照片都要上传到第三方服务器,这在真实的考勤场景里有合规压力,在毕业设计里也容易让答辩老师把问题从“你怎么做的”变成“你敢不敢这么用”。我的结论是:如果你做的是课程设计,目标只是把整个系统跑通,LBPH最合适;如果题目明确写了“高精度”或“误识率低”,再考虑face_recognition,并且提前把dlib编译好,不要临到联调才装。

3. 把人脸变成训练集:采集、对齐与预处理的完整流程

3.1 用opencv摄像头批量采集人脸:脚本与参数

训练集质量直接决定模型上限,这是整个系统里唯一没法用代码弥补的部分。采集的关键是“多样”,同一个人的脸至少要有20到30张,覆盖正脸、左右倾斜、抬头低头、戴不戴眼镜、室内光照和背光。很多人图省事,按住摄像头连拍50张,结果50张全是同一个姿态,训练出来的模型换个角度就认不出。正确做法是让采集程序自动检测人脸、裁出人脸区域、隔几帧才保存一张。下面是我常用的采集脚本:

import cv2 import os # 加载opencv自带的人脸检测器 cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_default.xml" face_cascade = cv2.CascadeClassifier(cascade_path) # 为每个员工建一个独立文件夹 person_name = "zhangsan" save_dir = f"dataset/{person_name}" os.makedirs(save_dir, exist_ok=True) cap = cv2.VideoCapture(0) count = 0 while True: ret, frame = cap.read() if not ret: continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale参数:scaleFactor控制每层缩放,minNeighbors控制误检 faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(100, 100) ) # 每隔5帧保存一张,避免连续帧几乎一模一样 if count % 5 == 0: for (x, y, w, h) in faces: face = gray[y:y+h, x:x+w] # 统一缩放到200x200,减少后续训练时的尺寸干扰 face_resized = cv2.resize(face, (200, 200)) img_name = f"{save_dir}/{len(os.listdir(save_dir))}.jpg" cv2.imwrite(img_name, face_resized) count += 1 # 实时画框,提示用户转头 for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

脚本里两个参数最值得注意:detectMultiScale的scaleFactor=1.1意味着检测窗口每次缩小10%然后再扫一遍,数值越小,检测越精细但越慢;minNeighbors=5表示一个候选区域要被附近至少5个窗口确认才算人脸,这个值调小会更容易框到非人脸区域,调大则可能漏掉侧脸。存图之前先用cvtColor转灰度,因为LBPH的输入是灰度图,训练时也要保持一致,否则识别时会因为颜色空间不一致导致特征分布错位。还有一个细节:保存的文件名直接用序号,不要用时间戳+中文名,因为opencv的imread对中文路径在Windows上经常读不出文件,返回None,这个问题会在训练时以“找不到图片”的形式爆出来。

3.2 直方图均衡化与归一化:预处理两个关键操作

采集到的原始人脸图不能直接扔给训练器,至少要做两步预处理:直方图均衡化和尺寸归一化。直方图均衡化的意义是消除一部分光照差异。同一个人的脸,在窗户边拍和背光拍,灰度分布完全不同,LBPH虽然对光照有一定鲁棒性,但极端情况下特征直方图会被环境光“带偏”。cv2的equalizeHist是全局均衡,简单但对整张脸都有效;进阶做法是CLAHE,也就是对比度受限的自适应直方图均衡化,它在局部块内做均衡,同时限制对比度放大倍数,对侧光脸的效果更好:

import cv2 # CLAHE比equalizeHist更适合人脸这种局部光照复杂的场景 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) def preprocess_face(gray_face): # 统一尺寸,LBPH训练和识别必须使用同样的尺寸 resized = cv2.resize(gray_face, (200, 200)) # CLAHE增强局部对比度 enhanced = clahe.apply(resized) return enhanced

clipLimit=2.0表示限制对比度增益不超过2倍,值太大会把噪声也放大,脸上会出现一块块斑痕;tileGridSize=(8,8)把图片分成64个小块做局部均衡,这个值和LBPH的grid参数相互独立,不需要保持相同。我在实际项目中对比过,原始图直接训练的模型,换一个窗帘拉开后的环境,识别率能从90%掉到70%左右;加了CLAHE以后,同样换环境,波动能控制在5个百分点以内。所以预处理这段代码别省,它是性价比最高的一个环节。尺寸统一用200x200是因为这个分辨率在保留特征和计算速度之间比较平衡,再大像300x300会让训练和预测都明显变慢,再小像100x100会丢失纹理细节,导致不同人的脸区分度下降。

3.3 训练集该建多大:每人多少张图、光照和角度怎么覆盖

训练集规模没有固定公式,但有一条底线经验:每人至少20张,30张以上效果才会出现明显稳定。这里的“稳定”指的是识别结果不会因为多了一个人而剧烈波动。有一种常见误区是只采集正脸,以为正脸就够,结果戴个口罩就认不出来,或者稍微侧身就被判成另一个人。我一般会让人采集时做完两套动作:第一套是头部朝向,正对镜头、向左转约30度、向右转约30度、稍微低头抬头,每个方向保持2秒让程序抓几帧;第二套是环境变化,在靠窗亮处和背光暗处各采一轮。如果你发现训练集里有不少照片是模糊的,那多半是采集时快门速度跟不上头部转动,建议让被采集的人动慢一点,而不是把摄像头帧率调高,因为VideoCapture在低光照下会自动降低帧率,高帧率反而会存进大量拖影图。

关于样本均衡性,这是容易被忽略的点:团队里如果张三拍了50张、李四拍了12张,训练出的模型会偏向张三,识别李四时误判率明显偏高。所以采集阶段就要控制每人的数量接近,最好在脚本里加一个提示,当某个文件夹超过目标数量就停止采集。一个额外的建议是,如果项目时间紧,不要追求“全班全部识别成功”,而是优先保证主要演示的那3到5个人精度高,其余人只要能稳定拒绝即可。这个“拒绝”能力也很重要,否则随便一个路人经过摄像头都会被识别成某个员工,考勤记录就会乱掉。

4. 训练与识别:LBPH参数调优和置信度阈值的取舍

4.1 训练核心代码:从dataset到yml模型

训练阶段就是把上一章准备的dataset文件夹读进来,给每张图片打上标签,然后调用train方法生成模型文件。这里有一个容易暗坑的地方:图片路径乱序,标签和人对不上。训练脚本里必须保证标签的一致性,我最常看到的翻车就是有人把标签写死在循环里,结果张三的图被标成了李四的id。稳妥做法是遍历文件夹时按员工名字排序,再建立name_to_id映射,并且最后把这个映射表保存成一个json,识别时还要用同一张表反查名字:

import cv2 import os import json import numpy as np dataset_dir = "dataset" faces = [] labels = [] name_to_id = {} id_to_name = {} # 给每个员工分配固定id,注意不要用0,opencv某些版本里0有特殊含义 for idx, person_name in enumerate(sorted(os.listdir(dataset_dir)), start=1): person_dir = os.path.join(dataset_dir, person_name) if not os.path.isdir(person_dir): continue name_to_id[person_name] = idx id_to_name[idx] = person_name for img_name in os.listdir(person_dir): img_path = os.path.join(person_dir, img_name) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: print(f"读取失败: {img_path}") continue faces.append(img) labels.append(idx) # 训练并保存模型 recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.train(faces, np.array(labels)) recognizer.save("employee_face_model.yml") # 保存id映射表,识别端要用 with open("id_map.json", "w", encoding="utf-8") as f: json.dump({"id_to_name": id_to_name}, f, ensure_ascii=False, indent=2)

train方法接受两个参数:一个图片列表和一个标签numpy数组,opencv要求图片列表里的每张图尺寸一致,所以采集时已经统一成200x200的话这里就不用处理;如果采集时没统一,训练前要在循环里加一行resize。id从1开始而不是0,是因为LBPH在opencv某些版本的实现里认为标签0是“未识别”状态,训练后predict返回的标签如果本来就是0,会干扰后续的业务判断。保存模型用recognizer.save生成yml文件,这个文件就是个普通文本格式,里面存储了每个类的直方图数据,不需要加密码或自定义格式,换台机器拷过去就能直接加载。json映射表必须和yml模型放在一起,因为后面识别返回的是标签数字,没有映射表你只能看到“识别出标签3”,却不知道对应哪个员工。

4.2 三个必调参数:radius、neighbors、threshold

LBPH识别器训练完成后,真正影响线上识别效果的参数就剩下三个:radius、neighbors、threshold。前两个是模型训练时的结构参数,后者是预测时的判定阈值。radius和neighbors决定LBP算子怎么提取纹理特征,前面2.2节已经给了默认值,这里说怎么调:新建一个测试脚本,把radius分别设成1、2、3,neighbors设成8、10、12,训练3到4个版本的模型,用同样的验证集去测,选准确率最高的组合。这个工作看起来笨,但它是避免“调参靠玄学”的唯一路径。我自己遇到过一个案例:训练集里人脸普遍偏小,radius=2的组合反而比radius=1高出5个百分点,因为小脸上2个像素的半径正好覆盖到眼睛和眉毛之间的纹理跨度。

threshold的取值系统里默认是123.0,但这个默认值几乎不可能对你的项目恰好合适。threshold的本质是卡方距离的容忍上限,距离大于这个值就判“未知”。要确定自己项目的threshold,最直接的办法是用一批未参与训练的测试图批量predict,把所有正确识别的confidence值和错误识别的confidence值分别列出来,找一个能把两类完全分开或者重叠最少的数值。常见范围在50到100之间,但这不是万能区间,有些人脸库光照统一,正确识别的confidence只有30多;有些人脸库采集得杂,正确识别都到90了,这时候threshold设80会把所有合法员工全部拒之门外。我建议的初始策略是“宁拒勿错”:threshold先设小一点,比如60,识别出来一个人就一定是这个人;之后如果发现拒识太多,再逐步往上加。这个方向错了顶多是打不上卡,方向反了就是A刷脸打了B的卡,性质完全不同。

4.3 识别翻车现场:置信度低不代表识别正确

调用识别器的方式非常简单,predict返回两个值:标签和confidence。但很多人把confidence理解成“相似度百分数”,形成了两个很深的误解。第一个误解是“confidence越低越像”,实际上LBPH的confidence是卡方距离,越小说明当前人脸和这个标签的直方图越接近,所以是“距离”不是“相似度”。第二个误解是“识别成功就说明人是对的”,这个只在训练集质量高、阈值调准的情况下才成立。毕业设计演示时最容易翻车的一幕:A站在摄像头前,系统弹出一个“已打卡:B员工”。这不是模型坏了,往往是A和B的脸型接近,或者训练样本太少导致类间区分不够。

为了把误识率压下来,一个实用做法是加入“双通道校验”:除了LBPH的标签距离,再对同一张人脸计算和已注册样本的平均像素差异,或者更简单地,在打卡界面把识别到的标签名称显示出来,同时显示confidence值,给考勤管理员留一个人工确认的窗口。更合理的工程化方案是把身份识别分成“识别到人”和“确认是这人”两步:第一步置信度小于threshold才认为系统认识这个人;第二步再用一个更严格的阈值(比如confidence小于threshold的一半)才允许自动打卡,中间区间弹提示要求手动确认。这会让你在答辩时有一个可以讲的业务闭环设计,而不是一个“识别到就放行”的裸循环。

5. 考勤系统业务落地:数据库设计与打卡逻辑的避坑实操

5.1 数据库两张表:员工表和考勤表怎么建

人脸识别模型只是整个考勤系统的“眼睛”,真正决定这个系统能不能叫“考勤系统”的是数据库和业务逻辑。我用sqlite做演示级项目,因为它是Python标准库自带的,不需要额外装数据库服务,交作业时一个.db文件拷走就行。员工表存姓名、人脸注册编号、部门,考勤表存打卡记录。这里最关键的设计是考勤表的唯一键,要防止同一个人同一天被插入几十条记录。

-- 员工表:face_id要和训练标签保持一致 CREATE TABLE employee ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, face_id INTEGER UNIQUE NOT NULL, department TEXT DEFAULT '未分配' ); -- 考勤表:employee_id + work_date 唯一,保证一天最多一条记录 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, employee_id INTEGER NOT NULL, work_date TEXT NOT NULL, check_in TEXT, check_out TEXT, FOREIGN KEY (employee_id) REFERENCES employee(id), UNIQUE(employee_id, work_date) );

employee表里的face_id就是你在训练脚本里分配给每个员工的标签数字,两者必须对齐,否则识别出的标签在数据库里查不到对应员工。attendance表用UNIQUE(employee_id, work_date)的复合唯一约束,这是防止重复打卡最关键的一道防线。很多毕设没有这个约束,结果就是相同的人同一天重复记录,最后统计迟到早退时数据全是脏的。work_date用YYYY-MM-DD格式字符串,check_in和check_out用HH:MM:SS格式字符串,因为打卡逻辑里只需要比较先后顺序,不需要做真正的日期运算,字符串比较就能完成,省掉datetime转换的麻烦。

5.2 上下班打卡逻辑:同一张脸怎么区分上班和下班

同一个员工一天要打两次卡,但系统怎么知道这次是上班还是下班?最直接的做法是把“第一次识别记为上班,当天已存在check_in就记为下班”这个规则用代码写清楚。代码如下:

import sqlite3 from datetime import datetime def handle_check(face_id): now = datetime.now() today = now.strftime("%Y-%m-%d") current_time = now.strftime("%H:%M:%S") conn = sqlite3.connect("attendance.db") cur = conn.cursor() # 先查员工是否注册 cur.execute("SELECT id, name FROM employee WHERE face_id = ?", (face_id,)) emp = cur.fetchone() if emp is None: conn.close() return "未注册员工,打卡失败" emp_id, emp_name = emp # 查当天是否已有记录,决定是上班还是下班 cur.execute( "SELECT check_in, check_out FROM attendance WHERE employee_id = ? AND work_date = ?", (emp_id, today) ) record = cur.fetchone() if record is None: # 没有记录 -> 视为上班打卡 cur.execute( "INSERT INTO attendance (employee_id, work_date, check_in) VALUES (?, ?, ?)", (emp_id, today, current_time) ) conn.commit() conn.close() return f"{emp_name} 上班打卡成功:{current_time}" check_in, check_out = record if check_out is not None: conn.close() return f"{emp_name} 今天已完成上下班打卡,请勿重复刷卡" # 已有check_in但没有check_out -> 下班打卡 cur.execute( "UPDATE attendance SET check_out = ? WHERE employee_id = ? AND work_date = ?", (current_time, emp_id, today) ) conn.commit() conn.close() return f"{emp_name} 下班打卡成功:{current_time}"

这段逻辑的顺序是:查员工存在性 -> 查当天记录 -> 没有记录则插入上班时间;有记录但没有下班时间则更新下班时间;两者都有则拒绝重复打卡。这个流程里有个容易被忽视的边界:如果员工早上打了上班卡,下午识别时被模型误识别成另一个人,就会在另一个人的记录里插入或更新数据。所以业务上还是要依赖前面第4章说的“宁拒勿错”阈值策略。如果要在验收时展示更专业的做法,可以在插入记录前加一个时间窗口判断:例如公司规定9点到12点之间识别算上班,12点到23点之间识别算下班,但这样做会让演示时的时间控制很被动,建议把这套规则放到UI层做成可配置,而不是写死在逻辑里。

5.3 避坑记录:5个让考勤系统翻车的真实场景

第一条:“opencv报No module named 'cv2.face'”。现象是import cv2能成功,但访问cv2.face时直接报AttributeError。原因是pip install opencv-python只装了基础模块,不包含人脸识别等contrib扩展功能。解决:先卸载opencv-python和opencv-contrib-python,然后只装opencv-contrib-python,注意两个包不能同时存在,否则模块冲突。命令是pip install opencv-contrib-python,装完验证cv2.face能正常import。

第二条:“训练时imread返回None,程序直接崩”。现象是训练脚本遍历图片列表时,打印出“读取失败: dataset/张三/12.jpg”。原因有两个:一是图片路径里带中文,Windows下opencv的imread对中文路径支持不稳定;二是图片文件真实格式是bmp或png但扩展名被写成了jpg。解决:采集脚本里统一用英文文件夹名和纯数字文件名,或者用cv2.imdecode配合np.fromfile读中文路径,但最省事的是路径里别放中文。

第三条:“换一台电脑运行,加载yml模型报格式错误”。现象是程序在自己机器上跑得好好的,拷到同学电脑上加载employee_face_model.yml时报cv2.error。原因多半是两台机器opencv版本不一致,低版本读高版本保存的模型文件会解析失败。解决:记录好训练时的opencv版本号,在README里写明pip install opencv-contrib-python==你用的版本,或者干脆在代码里启动时打印cv2.version,让对方知道自己版本是否匹配。

第四条:“识别结果稳定地把A误判成B”。现象是A的脸一出现,系统返回的标签总是B。原因通常是两个人脸型相似,且训练样本数量都不足,导致两个类的直方图距离很近。解决:先给每人补到30张以上多角度图片,重训一次;如果误判仍然存在,把threshold调低10个点,让系统倾向于拒识而不是乱认;最后才考虑换face_recognition方案。

第五条:“打卡记录一天出现十几条”。现象是attendance表里同一员工同一日期有多行。原因是打卡逻辑里没有按日期查找已有记录,或者数据库表没有加UNIQUE约束。解决:按5.1的建表语句执行,UNIQUE(employee_id, work_date)会直接从数据库层拒绝重复插入;同时代码里也要用“先查后写”的逻辑,双保险防止程序把异常当正常。

6. 让系统经得起答辩:批量验证准确率的两套脚本和文档技巧

6.1 批量测试脚本:把识别准确率量化出来

答辩老师最爱问的一句话是“你的识别准确率是多少”。口头说“感觉挺准”基本等于送命,正确做法是用一份不参与训练的测试集跑批量预测,输出识别准确率和具体错误样本。测试集最好在训练集采集完隔一天再拍,模拟真实使用时的环境变化,单独放到test_dataset文件夹下,每张图片命名为“员工标签_序号.jpg”方便统计。

import cv2 import os import json # 加载训练好的模型和标签映射 recognizer = cv2.face.LBPHFaceRecognizer_create() recognizer.read("employee_face_model.yml") with open("id_map.json", "r", encoding="utf-8") as f: id_to_name = json.load(f)["id_to_name"] test_dir = "test_dataset" total = 0 correct = 0 wrong_list = [] for img_name in os.listdir(test_dir): # 文件名规则:标签_序号.jpg,例如 1_001.jpg true_label = int(img_name.split("_")[0]) img_path = os.path.join(test_dir, img_name) img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue img = cv2.resize(img, (200, 200)) pred_label, confidence = recognizer.predict(img) total += 1 if pred_label == true_label: correct += 1 else: wrong_list.append((img_name, pred_label, confidence)) accuracy = correct / total * 100 if total > 0 else 0 print(f"准确率: {accuracy:.2f}%") print(f"总测试样本: {total}, 正确: {correct}, 错误: {len(wrong_list)}") for item in wrong_list: print(f"误判: {item[0]}, 预测标签: {item[1]}, confidence: {item[2]:.1f}")

这个脚本输出的数字可以直接写进毕业论文的测试章节。如果准确率低于90%,先用误判列表回看图片,确认是不是采集时照片模糊或角度过大,再决定补数据还是调threshold。补数据后要重新训练模型、重新跑测试,整个过程保持同一个数据划分标准,这样在文档里写“测试集包含XX张,准确率XX%”才经得起追问。如果老师要求更细,可以再加一项:非注册人脸测试,随便找几张陌生脸图跑predict,统计有多少张被误认为员工,这个比例应该越低越好,它衡量的是系统的拒识能力。

6.2 误识别的处理策略:迟到早退和代打卡怎么约束

批量测试能提高识别准确率,但阻挡不了一种真实世界里的恶意场景:A用B的照片对着摄像头,系统识别成B并打了上班卡。这种照片攻击连很多商业门禁系统都头疼,毕业设计里不需要彻底解决,但可以在逻辑上增加一道约束:同一个摄像头在连续5分钟内只允许标签相同的人成功打卡,且打卡界面显示的照片和数据库中注册照片做一次像素相似度比对,相似度低于阈值就判为异常。具体做法是在打卡流程里加一个最近记录表,记录员工ID、识别时间、人脸图片路径,如果相邻两次完全相同的打卡间隔过短,就拒绝并给出提示“疑似代打卡”。

另一个约束是迟到和早退的计算。数据库里只存了打卡时间,迟到判断可以在业务代码里做:公司规定9点上班,check_in晚于09:00:00就标记为迟到;下班时间早于18:00:00但不在当天记录里体现,就标记早退。这些标记建议直接存在考勤表旁边,增加一个status字段,不要每次查询时现算。原因是毕业答辩的演示场景里,老师可能要求看一张考勤统计表,如果你能快速导出一份“本月迟到X次、早退X次”的汇总,比现场跑SQL语句靠谱得多。

6.3 文档撰写的三个硬技巧

文档说明是这个题目里和源码同等分量的交付物,因为评分标准里“文档规范”通常占30%左右。第一个技巧是需求分析章节不要只写功能列表,要画出业务流程:人脸注册流程、打卡流程、考勤查询流程,每个流程用文字描述出一个闭环,让读者看出你思考过“识别失败怎么办”“重复打卡怎么处理”这类边界问题。第二个技巧是数据库设计章节把建表SQL贴全,并附上字段说明表,解释为什么employee_id和work_date要设联合唯一约束,这比大篇幅复述代码更能体现数据库设计能力。第三个技巧是测试章节把上面的批量测试脚本输出结果做成表格,列出每个员工的测试样本数、正确数、误判数,再附两三张截图。这张表是答辩时最有说服力的一页,因为它是可复现的实测数据,而不是“系统测试稳定”这种形容词。

另外一个很实际的经验:把环境配置步骤写进README。很多毕设源码老师拿到手第一件事就是跑一遍,跑不通就会被认为“完成度不高”。README里写清楚python版本、opencv-contrib-python安装命令、摄像头编号设置、训练集目录结构、启动命令顺序,这五样能极大降低老师复现时的挫败感。我自己吃过这个亏:第一次交的项目自己电脑上跑得好好的,老师那台机器只装了opencv-python,启动就报错,被质疑系统不是自己做的。从那以后,我都在README开头加一句“本项目需要安装opencv-contrib-python,不能用opencv-python替代”,这个提示用一句话避开了最大的一个坑。答辩被追问的时候,主动说“我在测试中发现换机器后模型加载会失败,所以用固定版本号固定了依赖”,这种回答比背任何概念都加分,因为它证明你踩过坑并且找到了补救方法。希望这篇从头到尾的落地笔记,能让你少走几段我说过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询