多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

OpenCV人脸识别考勤系统实战:从LBPH训练到活体检测避坑指南

OpenCV人脸识别考勤系统实战:从LBPH训练到活体检测避坑指南 简介这是一份面向计算机相关专业学生与项目实战学习者的OpenCV人脸识别考勤系统完整资料可直接用于毕业设计、课程设计或期末大作业。项目围绕图像采集、人脸检测、特征提取与人脸匹配四个核心环节展开涉及灰度化、直方图均衡化等预处理手段以及Eigenfaces、Fisherfaces、LBPH等经典特征提取算法并配有详细文档说明各功能模块的设计原理与实现思路。压缩包共123个文件以100张jpg人脸样本图像为主另含6个xml分类器配置、5个py源码脚本、2个exe可执行程序及bat批处理、yml、cfg等环境与运行配置文件整体约1.88MB结构清晰便于按模块查阅。目前已有48人学习关注。项目经过严格调试可直接运行读者既能获得完整源码与文档也能借此理解人脸识别算法的落地流程并针对光照变化、姿态表情差异及性能优化等问题积累排错与调优经验。1. 从一台打卡机说起OpenCV 人脸识别考勤系统到底难在哪很多公司前台那台打卡机员工走过去刷脸屏幕上跳出名字和工号后台自动记一条时间戳。看起来简单但真让你用 OpenCV 从零搭一套人脸识别考勤系统你会发现难点根本不在「识别」两个字上。识别本身有现成的人脸检测器和特征提取器几行代码就能跑通。真正让人翻车的是同一个人早上戴眼镜、中午摘了、下午换了发型系统还认不认得出光线从窗户斜射进来半边脸过曝检测框直接飘走两个人长得像阈值设松了互相冒名设紧了本人打不上卡。这套系统的本质是一条从摄像头取帧、人脸检测、特征提取、比对识别、到考勤记录落库的完整流水线。OpenCV 在其中承担图像采集、预处理、检测和部分特征计算的工作识别环节通常搭配 face_recognition、dlib 或 OpenCV 自带的 LBPH 算法。适合谁做适合有 Python 基础、想拿一个完整项目练手图像处理和后端逻辑的开发者也适合需要给小型团队搭一套低成本考勤方案的技术负责人。源码和详细文档的价值在于它把这条流水线上每个环节的参数和边界都固定下来了你不需要从论文开始啃照着调就能跑。2. 人脸识别考勤系统的技术选型为什么是 OpenCV 加 LBPH 而不是深度学习2.1 三条技术路线的真实成本对比做考勤系统的人脸识别市面上常见三条路OpenCV 自带的 LBPH 算法、dlib 的 ResNet 特征提取、以及基于深度学习的人脸识别模型。三条路我都跑过说下真实感受。LBPH 是 OpenCV contrib 模块里的局部二值模式直方图算法。它的原理是把人脸图像分成若干小区域在每个区域内计算像素的局部二值模式然后统计直方图作为特征。优点是训练极快新增人员只需要把新的人脸图片加进去重新训练几秒钟的事。缺点是对光照和姿态变化比较敏感同一个人不同角度拍的照片特征差异可能比不同人还大。dlib 的 ResNet 模型输出 128 维特征向量通过计算欧氏距离判断是否同一个人。这个方案精度明显高于 LBPH对光照和角度有一定鲁棒性。代价是需要编译 dlib在 Windows 上编译经常卡在 CMake 和 Visual Studio 版本匹配上很多人在这里就放弃了。深度学习方案精度最高但需要 GPU 推理部署成本直接上一个台阶。对于几十人到几百人规模的公司考勤说实话用不上。我一般会推荐 LBPH 起步原因是考勤场景有个天然优势同一个人每天在同一个摄像头前打卡角度和光照相对固定。LBPH 在这个约束下表现完全够用而且部署零依赖pip install opencv-contrib-python 就能跑。方案精度训练速度部署难度适合规模OpenCV LBPH中等秒级极低50 人以内dlib ResNet较高分钟级中等500 人以内深度学习模型高小时级高不限2.2 环境搭建与依赖安装的完整命令先把环境跑通。Python 版本建议 3.8 到 3.10太新的版本有些包还没跟上。# 创建虚拟环境避免污染系统 Python python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install opencv-contrib-python4.8.1.78 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install Pillow10.0.0 pip install pandas2.0.3 # 如果要用 dlib 方案额外安装 # pip install dlib19.24.2 # pip install face-recognition1.3.0这里有个坑要注意opencv-contrib-python 和 opencv-python 不能同时装不同版本否则会互相覆盖。我一般只装 opencv-contrib-python它包含了主模块和 contrib 模块LBPH 就在 contrib 里。如果你之前装过 opencv-python先 pip uninstall 掉再装 contrib 版本。安装完之后验证一下import cv2 import numpy as np # 检查 OpenCV 版本 print(fOpenCV version: {cv2.__version__}) # 检查 LBPH 识别器是否可用 try: recognizer cv2.face.LBPHFaceRecognizer_create() print(LBPH recognizer: OK) except AttributeError: print(LBPH recognizer: NOT FOUND - 请安装 opencv-contrib-python) # 检查人脸检测器 cascade_path cv2.data.haarcascades haarcascade_frontalface_default.xml detector cv2.CascadeClassifier(cascade_path) print(fHaar cascade loaded: {not detector.empty()})这段代码做了三件事打印 OpenCV 版本确认安装成功尝试创建 LBPH 识别器确认 contrib 模块可用加载 Haar 级联检测器确认人脸检测功能正常。如果 LBPH 那行报 AttributeError说明你装的是 opencv-python 而不是 opencv-contrib-python卸载重装即可。2.3 人脸检测器的选择与参数调优OpenCV 自带的人脸检测器有几种Haar 级联、LBP 级联、DNN 模块。考勤场景我一般用 Haar 级联因为它在正面人脸检测上召回率不错速度也快。DNN 检测器精度更高但需要额外下载模型文件部署时多一个依赖。Haar 检测器的关键参数有三个import cv2 detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # detectMultiScale 的三个核心参数 faces detector.detectMultiScale( gray, scaleFactor1.1, # 每次图像缩小的比例 minNeighbors5, # 每个候选框需要被检测多少次才保留 minSize(80, 80) # 最小人脸尺寸 ) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(Face Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()scaleFactor 控制图像金字塔的缩放步长1.1 表示每次缩小 10%。设小了检测更细致但速度慢设大了速度快但可能漏掉小脸。minNeighbors 是投票机制值越高误检越少但漏检越多考勤场景建议 5 到 6。minSize 根据摄像头到人的距离调一般 80x80 起步如果人站得远就调小到 50x50。3. 从拍照到训练人脸数据采集与 LBPH 模型训练全流程3.1 人脸样本采集脚本与质量把控数据采集是整套系统里最容易被忽视但最影响效果的环节。我见过太多人随便拍几张就训练结果识别率惨不忍睹。采集时要注意每人至少 30 张不同角度左转、右转、抬头、低头各来几张不同表情正常、微笑、严肃光照尽量均匀。import cv2 import os def collect_faces(person_id, person_name, save_dirdataset, num_samples50): 采集指定人员的人脸样本 person_id: 人员编号用于文件命名 person_name: 人员姓名 save_dir: 样本保存目录 num_samples: 采集数量 # 创建保存目录 person_dir os.path.join(save_dir, f{person_id}_{person_name}) os.makedirs(person_dir, exist_okTrue) detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) count 0 print(f开始采集 {person_name} 的人脸样本按空格键拍照按 q 退出) while count num_samples: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) # 在框上方显示已采集数量 cv2.putText(frame, fCount: {count}/{num_samples}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(Collect Faces, frame) key cv2.waitKey(1) 0xFF if key ord( ) and len(faces) 1: # 只采集一张人脸的情况避免多人入镜 (x, y, w, h) faces[0] # 保存灰度人脸区域统一缩放到 200x200 face_img cv2.resize(gray[y:yh, x:xw], (200, 200)) file_path os.path.join(person_dir, f{count}.jpg) cv2.imwrite(file_path, face_img) count 1 print(f已采集 {count}/{num_samples}) elif key ord(q): break cap.release() cv2.destroyAllWindows() print(f{person_name} 采集完成共 {count} 张) # 使用示例 collect_faces(person_id1, person_namezhangsan, num_samples50)这个脚本的逻辑是打开摄像头实时检测人脸并画框按空格键保存当前人脸区域。保存时统一缩放到 200x200 灰度图这是 LBPH 训练的标准输入尺寸。注意只采集单张人脸的情况如果画面里有多人就不保存避免样本污染。采集完成后dataset 目录下会按人员分文件夹存放每个文件夹里是编号的 jpg 文件。这个结构方便后续训练脚本遍历。3.2 LBPH 模型训练与参数含义样本采集完就可以训练了。LBPH 的训练过程很快核心是把每张人脸图片转换成 LBP 直方图特征然后存成一个模型文件。import cv2 import numpy as np import os def train_lbph_model(dataset_dirdataset, model_pathtrainer.yml): 训练 LBPH 人脸识别模型 dataset_dir: 样本目录结构为 dataset/人员ID_姓名/编号.jpg model_path: 模型保存路径 recognizer cv2.face.LBPHFaceRecognizer_create( radius1, # LBP 圆形邻域半径 neighbors8, # 邻域像素点数量 grid_x8, # 水平方向划分的网格数 grid_y8, # 垂直方向划分的网格数 threshold100.0 # 识别阈值距离大于此值判为未知 ) faces [] labels [] # 遍历数据集目录 for person_dir in os.listdir(dataset_dir): person_path os.path.join(dataset_dir, person_dir) if not os.path.isdir(person_path): continue # 从文件夹名解析人员 ID格式为 1_zhangsan person_id int(person_dir.split(_)[0]) for img_name in os.listdir(person_path): img_path os.path.join(person_path, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue # 统一尺寸 img cv2.resize(img, (200, 200)) faces.append(img) labels.append(person_id) print(f共加载 {len(faces)} 张人脸样本{len(set(labels))} 个人员) # 训练 recognizer.train(faces, np.array(labels)) recognizer.save(model_path) print(f模型已保存到 {model_path}) return recognizer # 执行训练 train_lbph_model()LBPH 的五个参数需要理解一下。radius 和 neighbors 控制 LBP 算子的感受野radius1、neighbors8 是经典配置对应 3x3 邻域。grid_x 和 grid_y 把人脸图分成 8x8 的网格每个网格单独计算直方图这样保留了空间信息比整图一个直方图效果好很多。threshold 是识别阈值预测时返回的距离大于这个值就判定为未知人员设太小会导致本人被拒设太大会导致冒名通过。训练完成后会生成 trainer.yml 文件里面存的是每个人脸的特征直方图和对应标签。这个文件通常几百 KB 到几 MB取决于人员数量和样本数量。3.3 识别与考勤记录写入的联动逻辑训练好模型后识别脚本要做三件事检测人脸、预测身份、写考勤记录。import cv2 import numpy as np import csv from datetime import datetime import os class AttendanceSystem: def __init__(self, model_pathtrainer.yml, names_filenames.csv): self.recognizer cv2.face.LBPHFaceRecognizer_create() self.recognizer.read(model_path) self.detector cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) # 加载人员 ID 到姓名的映射 self.id_to_name {} if os.path.exists(names_file): with open(names_file, r, encodingutf-8) as f: reader csv.reader(f) for row in reader: if len(row) 2: self.id_to_name[int(row[0])] row[1] # 考勤记录文件 self.attendance_file attendance.csv if not os.path.exists(self.attendance_file): with open(self.attendance_file, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([姓名, 日期, 时间, 状态]) # 防止同一人短时间内重复打卡 self.last_seen {} def mark_attendance(self, name): 写入考勤记录5 分钟内不重复 now datetime.now() today now.strftime(%Y-%m-%d) time_str now.strftime(%H:%M:%S) # 检查是否重复打卡 key f{name}_{today} if key in self.last_seen: last_time self.last_seen[key] if (now - last_time).total_seconds() 300: return False self.last_seen[key] now # 判断迟到9 点之后算迟到 status 正常 if now.hour 9 else 迟到 with open(self.attendance_file, a, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([name, today, time_str, status]) print(f考勤记录{name} {today} {time_str} {status}) return True def run(self): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces self.detector.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: face_roi cv2.resize(gray[y:yh, x:xw], (200, 200)) # 预测 label, confidence self.recognizer.predict(face_roi) if confidence 100: name self.id_to_name.get(label, f未知_{label}) color (0, 255, 0) self.mark_attendance(name) else: name 未知 color (0, 0, 255) # 画框和标签 cv2.rectangle(frame, (x, y), (xw, yh), color, 2) cv2.putText(frame, f{name} ({confidence:.1f}), (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow(Attendance System, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() # 运行 system AttendanceSystem() system.run()这段代码的核心逻辑predict 方法返回两个值label 是预测的人员 IDconfidence 是距离值。注意 LBPH 的 confidence 是距离越小越可信和一般分类器的置信度含义相反。我设的阈值是 100低于 100 认为是已知人员高于 100 判为未知。这个阈值需要根据实际数据调后面会讲怎么调。mark_attendance 方法做了防重复打卡同一人 5 分钟内只记一次。考勤状态简单按 9 点判断迟到实际项目里可以配置多个时间段。4. 识别率上不去人脸识别考勤系统避坑与排查清单4.1 识别率忽高忽低先查光照和摄像头现象同一个人有时候秒识别有时候反复打不上卡confidence 值波动很大。原因LBPH 对光照变化敏感。如果摄像头正对窗户上午和下午的光线方向不同人脸阴影分布变化LBP 特征跟着变。另外摄像头自动曝光如果频繁调整也会导致图像亮度不稳定。解决固定摄像头位置和角度避免逆光。如果无法避免在检测前做直方图均衡化# 在灰度转换后加一步 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) # 直方图均衡化缓解光照不均另外把摄像头的自动曝光关掉手动设一个固定值。OpenCV 里可以用 cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) 尝试关闭自动曝光具体值取决于摄像头驱动。4.2 训练时样本不够新增人员后旧人员识别变差现象新加了一个人重新训练后原来能识别的人开始频繁报未知。原因LBPH 训练时所有样本一起参与如果新人的样本数量远多于旧人或者新人样本质量差模糊、侧脸多会影响整体模型的直方图分布。另外如果训练时没有固定随机种子每次训练结果会有微小差异。解决保证每个人样本数量均衡建议每人 30 到 50 张。新增人员时不要只加新人样本把旧人样本一起重新训练。训练前检查每张样本的质量模糊的、检测框偏移的删掉。我一般会写个脚本先过一遍样本把尺寸不对的、全黑的、全白的筛掉。4.3 confidence 阈值设多少合适怎么科学地调现象阈值设 80 时本人经常被拒设 120 时偶尔有两个人互相认错。原因LBPH 的 confidence 是距离值不同数据集的距离分布不同没有一个通用阈值。样本质量好、人员之间差异大距离整体偏低样本模糊、人员之间长得像距离整体偏高。解决用验证集来调。把每个人的样本留出 5 张不参与训练训练完后用这 5 张做预测记录本人预测的距离分布再拿其他人的样本做预测记录非本人的距离分布。找一个能分开这两个分布的值作为阈值。# 阈值调优脚本 import cv2 import numpy as np import os def evaluate_threshold(model_path, test_dir): recognizer cv2.face.LBPHFaceRecognizer_create() recognizer.read(model_path) genuine_distances [] # 本人预测距离 impostor_distances [] # 非本人预测距离 for person_dir in os.listdir(test_dir): person_path os.path.join(test_dir, person_dir) if not os.path.isdir(person_path): continue true_id int(person_dir.split(_)[0]) for img_name in os.listdir(person_path): img cv2.imread(os.path.join(person_path, img_name), 0) if img is None: continue img cv2.resize(img, (200, 200)) label, confidence recognizer.predict(img) if label true_id: genuine_distances.append(confidence) else: impostor_distances.append(confidence) print(f本人距离均值 {np.mean(genuine_distances):.1f} f最大 {np.max(genuine_distances):.1f}) print(f非本人距离均值 {np.mean(impostor_distances):.1f} f最小 {np.min(impostor_distances):.1f}) # 建议阈值取本人最大值和非本人最小值的中点 suggested (np.max(genuine_distances) np.min(impostor_distances)) / 2 print(f建议阈值{suggested:.1f}) evaluate_threshold(trainer.yml, test_dataset)这个脚本分别统计本人和非本人的预测距离分布取本人最大值和非本人最小值的中点作为建议阈值。如果两个分布有重叠说明样本区分度不够需要增加样本或改善采集条件。4.4 摄像头读帧卡顿导致识别延迟现象画面明显卡顿人站在摄像头前要等两三秒才出识别结果。原因detectMultiScale 是计算密集型操作如果每帧都做全图检测分辨率高的时候帧率会掉到个位数。另外如果识别和显示在同一个线程里识别耗时直接拖慢画面刷新。解决降低检测频率比如每 3 帧检测一次中间帧沿用上一次的检测框。或者先把图像缩小再检测检测到人脸后再在原图上裁剪。frame_count 0 last_faces [] while True: ret, frame cap.read() if not ret: break frame_count 1 # 每 3 帧做一次检测 if frame_count % 3 0: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 缩小到一半再检测速度提升明显 small_gray cv2.resize(gray, (0, 0), fx0.5, fy0.5) faces_small detector.detectMultiScale(small_gray, 1.1, 5, minSize(40, 40)) # 坐标还原到原图尺寸 last_faces [(x*2, y*2, w*2, h*2) for (x, y, w, h) in faces_small] for (x, y, w, h) in last_faces: cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.imshow(Optimized, frame) if cv2.waitKey(1) 0xFF ord(q): break缩小到一半再检测计算量降到四分之一检测框坐标乘以 2 还原即可。配合每 3 帧检测一次帧率能从个位数拉到 20 以上。4.5 打包部署后摄像头打不开现象开发环境跑得好好的打包成 exe 或者部署到另一台机器后VideoCapture(0) 返回空。原因常见三种情况。一是目标机器没有摄像头或者摄像头被其他程序占用。二是 OpenCV 的摄像头后端在 Windows 上默认用 MSMF某些摄像头驱动不兼容需要切到 DSHOW。三是打包工具没有把 OpenCV 的依赖 dll 打进去。解决先换后端试试# Windows 上优先尝试 DSHOW 后端 cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # 如果还不行列出可用后端逐个试 for backend in [cv2.CAP_DSHOW, cv2.CAP_MSMF, cv2.CAP_ANY]: cap cv2.VideoCapture(0, backend) if cap.isOpened(): print(f可用后端{backend}) break打包用 PyInstaller 时加 --add-data 把 cv2.data.haarcascades 目录带上否则 Haar 模型文件找不到。5. 让考勤系统真正能用活体检测与多线程的进阶技巧5.1 用眨眼检测挡住照片冒名LBPH 方案有个致命问题拿一张照片对着摄像头也能识别通过。考勤场景必须加活体检测。最简单的活体检测是眨眼检测用 OpenCV 的级联分类器检测眼睛连续帧里眼睛从有到无再到有判定为眨眼。import cv2 eye_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_eye.xml ) def detect_blink(eye_history, current_eyes): eye_history: 最近几帧的眼睛检测结果列表 current_eyes: 当前帧检测到的眼睛数量 eye_history.append(current_eyes) if len(eye_history) 10: eye_history.pop(0) # 检测到眼睛数量从 2 变成 0 再变回 2 if len(eye_history) 6: mid len(eye_history) // 2 before eye_history[:mid] after eye_history[mid:] had_eyes any(e 2 for e in before) lost_eyes any(e 0 for e in before[mid-2:mid2] if mid-2 0) has_eyes_again any(e 2 for e in after) if had_eyes and lost_eyes and has_eyes_again: return True return False这个逻辑是维护一个最近 10 帧的眼睛数量队列检测「有眼睛 → 没眼睛 → 有眼睛」的模式。实际用的时候识别到人脸后先要求用户眨眼眨眼确认后才写入考勤记录。这样照片就过不了。5.2 多线程架构让识别和界面互不阻塞单线程方案里识别一帧要几十毫秒界面就卡几十毫秒。人多的时候体验很差。改成生产者-消费者模式一个线程专门读摄像头帧放进队列一个线程从队列取帧做识别主线程负责显示。import threading import queue import cv2 import time class VideoStream: 独立线程读取摄像头帧 def __init__(self, src0): self.cap cv2.VideoCapture(src, cv2.CAP_DSHOW) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.q queue.Queue(maxsize2) # 只保留最新两帧 self.running True self.thread threading.Thread(targetself._reader, daemonTrue) self.thread.start() def _reader(self): while self.running: ret, frame self.cap.read() if not ret: break # 队列满了就丢掉旧帧保证实时性 if self.q.full(): try: self.q.get_nowait() except queue.Empty: pass self.q.put(frame) def read(self): return self.q.get() def release(self): self.running False self.cap.release() # 使用 stream VideoStream(0) while True: frame stream.read() # 识别逻辑放在这里不阻塞读帧 cv2.imshow(Multi-thread, frame) if cv2.waitKey(1) 0xFF ord(q): break stream.release()队列 maxsize 设为 2保证永远处理最新帧。如果识别速度跟不上旧帧自动丢弃不会累积延迟。这个架构下界面始终流畅识别结果晚一两帧出来对考勤没影响。5.3 考勤数据落库与导出CSV 适合小规模人多了还是上 SQLite。建一张考勤表加唯一索引防止重复。import sqlite3 from datetime import datetime def init_db(db_pathattendance.db): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, person_id INTEGER NOT NULL, person_name TEXT NOT NULL, check_date TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT DEFAULT 正常, UNIQUE(person_id, check_date) ) ) conn.commit() return conn def record_attendance(conn, person_id, person_name): now datetime.now() date_str now.strftime(%Y-%m-%d) time_str now.strftime(%H:%M:%S) status 正常 if now.hour 9 else 迟到 cursor conn.cursor() try: cursor.execute( INSERT INTO attendance (person_id, person_name, check_date, check_time, status) VALUES (?, ?, ?, ?, ?) , (person_id, person_name, date_str, time_str, status)) conn.commit() return True except sqlite3.IntegrityError: # 唯一索引冲突说明今天已经打过卡 return FalseUNIQUE(person_id, check_date) 保证每人每天只有一条记录重复打卡直接触发 IntegrityError不用在应用层做判断。导出的时候一个 SQL 查询就能出月度报表。这套方案我从采集、训练、识别到落库完整跑过最深的教训是别在样本质量上偷懒。我最早做的时候每人只拍了 10 张全是正面正常表情结果有个同事戴了副新眼镜就认不出来了。后来老老实实每人拍 50 张覆盖各种角度和表情识别率从 70% 拉到 95% 以上。活体检测也千万别省我见过有人拿手机翻拍照片成功打卡的这在考勤场景是致命的。希望帮到你。本文还有配套的精品资源点击获取
返回列表