首页 / 资讯中心 / 文章详情

基于Python的人脸识别签到系统开发实战

基于Python的人脸识别签到系统开发实战 ★ FEATURED ARTICLE
简介人脸识别技术是计算机视觉领域的重要应用其核心原理是通过深度学习模型提取人脸特征向量并利用欧氏距离进行身份比对。这一技术无需额外硬件仅需普通摄像头即可实现高精度身份验证在考勤签到、门禁系统等场景中具有极高的实用价值。基于Python生态开发者可以借助face_recognition、OpenCV等开源库快速构建一套完整的人脸识别签到系统。从人脸检测、特征编码、数据库存储到实时识别比对再到签到记录生成与可视化报表导出整个流程均可通过简洁的代码实现。本文结合实际工程经验详细讲解了如何搭建开发环境、避免常见踩坑问题并介绍了GUI界面嵌入、活体检测及多摄像头支持等进阶方案帮助读者从零搭建一套可靠易用的考勤工具。1. 人脸识别签到为什么学校里、公司里都在换这套方案考勤签到这件事传统做法无非是纸质签名、刷卡、手机定位打卡。纸质表可以代签刷卡可以代刷手机定位可以用虚拟位置作弊。你真正想要的是“物理意义上这个人来了”而不是“某个设备来了”。Python人脸识别签到解决的正是这个问题摄像头前站一下系统判断你是不是登记过的人是就自动记一条签到记录。它不依赖任何额外硬件一台带摄像头的电脑就能跑起来成本几乎为零。这个方向我前后做过两套一套用OpenCV传统人脸检测一套用face_recognition库做特征比对。结论先说如果你要的是“够用、能演示、能交差、能自己改”face_recognition库几乎是首选它封装了dlib的深度学习模型调用简单识别效果在室内光线可控的场景下相当稳定。适合的学生、小型工作室、社团活动、实验课考勤甚至一些小公司的内部打卡。真正要上企业级门禁的人脸识别那是海康、大华那些硬件方案的活不在本文讨论范围。这套方案的完整链路是人脸检测 → 特征编码 → 存储登记 → 实时识别比对 → 写入签到记录 → 导出考勤表。听起来复杂实际拆开每一步都有现成的库能接核心代码不到两百行。但中间有几个细节不处理好会出现“识别不准、偶尔翻车、摄像头卡死”的问题比如编码的存储格式、比对阈值、摄像头索引选择这些我会在后面的章节里一个一个给你说清楚。2. 技术选型与环境搭建为什么用 face_recognition 而不是纯 OpenCV2.1 技术选型精度优先还是开发效率优先早期OpenCV自带的人脸检测器是Haar Cascade它的检测速度尚可但问题很明显对角度、光照敏感侧脸基本凉凉而且它只能告诉你“这里有一张脸”不能告诉你“这是谁”。要完成签到必须再做特征提取和比对。常见的做法用LBPH或者EigenFace但这些都是传统机器学习算法对表情、眼镜、发型变化非常脆弱在室内光线稍微不均匀的情况下就有概率误识或漏识。face_recognition库内部用的是dlib的resnet模型它会把人脸编码成一个128维的特征向量。不管人脸在画面里是大是小、有没有侧转只要不是完全背对镜头这个向量在同一个人的不同照片上距离都很近不同人之间距离很远。比对时只需要算欧氏距离给一个阈值就能判断“是不是同一个人”。在Intel的CPU上一次人脸编码大约0.1到0.2秒实时视频流完全扛得住。还有一个关键点face_recognition对开发者友好得不讲理。它只有两个核心函数用到极致——face_locations负责找脸的位置face_encodings负责生成特征向量compare_faces负责比对。你不需要理解resnet的残差结构、不需要知道128维特征怎么来的把它当成一个“人脸特征提取黑匣子”直接用就行了。说实话我自己平时做项目也经常调侃它像个黑匣子但黑匣子不丢人丢人的是调不通还找不到问题在哪。2.2 环境搭建Windows 和 macOS 的安装差异环境安装看着简单实际上坑非常密集。尤其是macOS用户经常在dlib安装这一步翻车编译直接报错。下面把Windows和macOS的安装步骤分开写。Windows用户Python 3.8到3.11之间都能用我推荐3.9兼容性最稳。pip install cmake pip install dlib pip install face_recognition pip install opencv-pythonmacOS用户注意需要先装CMake和Xcode Command Line Toolsxcode-select --install brew install cmake pip install dlib pip install face_recognition pip install opencv-python注意如果你的mac是Apple Silicon芯片建议直接装Python 3.9的arm64版本Intel版Python在Rosetta下跑dlib有概率出现内存溢出的玄学问题。参数说明face_recognition会依赖numpy、Pillow、Click等一堆库pip会自动解决依赖不用手动逐个装。cmake是编译dlib必须的dlib的C代码需要它生成构建文件。装完之后验证一下环境是否正常写一行代码跑通就说明没问题import face_recognition import cv2 print(face_recognition.__version__) print(cv2.__version__)能成功打印出版本号环境就没问题了。如果你的机器CPU比较老别紧张face_recognition在纯CPU上跑完全没问题不需要GPU。我自己在一台十年前的老笔记本上跑过视频流识别虽然有点吃紧但单张照片编码的速度还是能接受的。3. 核心实现从人脸登记到实时识别再到自动签到3.1 第零步人脸登记把“谁”变成特征向量签到的前提是系统得知道有哪些人。这一步要做的是拿每个人的照片提取他的128维特征向量然后把“人名 特征向量”存到本地文件里作为后续签到的对照库。用照片登记而不是摄像头现场采集有两个好处一是照片可以反复使用二是可以拿证件照、生活照做登记不要求本人到场。登记的核心代码如下import face_recognition import pickle import os def register_face(name, image_path, db_pathface_db.pkl): # 加载图片并提取人脸特征 image face_recognition.load_image_file(image_path) face_locations face_recognition.face_locations(image) if len(face_locations) ! 1: print(f图片中检测到 {len(face_locations)} 张脸请传单人照) return False face_encoding face_recognition.face_encodings(image, face_locations)[0] # 加载已有数据库没有就新建 try: with open(db_path, rb) as f: face_db pickle.load(f) except FileNotFoundError: face_db {} face_db[name] face_encoding with open(db_path, wb) as f: pickle.dump(face_db, f) print(f{name} 登记成功当前共 {len(face_db)} 人) return True if __name__ __main__: register_face(张三, zhangsan.jpg)逻辑说明load_image_file把图片读成numpy数组face_locations返回所有人脸位置的坐标列表face_encodings对每个位置的人脸生成128维特征向量。这里强制要求照片中只能有一张脸多人照会导致编码错乱你无法确定取到的是哪张脸的特征。数据库用pickle存成字典键是人名值是numpy数组简单直接。注意face_encodings必须传入face_locations的结果否则它会重新检测一遍人脸。虽然结果一样但白白多跑一次检测纯属浪费算力。3.2 实时识别与签到视频流里每一帧做什么事实时签到的流程是打开摄像头 → 循环读帧 → 检测人脸 → 编码人脸 → 和数据库比对 → 找到匹配就记录签到。完整代码如下import face_recognition import cv2 import pickle import datetime def load_face_db(db_pathface_db.pkl): with open(db_path, rb) as f: return pickle.load(f) def recognition_loop(db_pathface_db.pkl, output_fileattendance.csv): face_db load_face_db(db_path) known_names list(face_db.keys()) known_encodings list(face_db.values()) # 已签到的人防止同一人反复打卡 checked_in set() video_capture cv2.VideoCapture(0) if not video_capture.isOpened(): print(摄像头打开失败检查索引或权限) return while True: ret, frame video_capture.read() if not ret: print(获取视频帧失败) break # 缩小帧尺寸加速处理 small_frame cv2.resize(frame, (0, 0), fx0.5, fy0.5) rgb_small_frame cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) # 检测人脸并编码 face_locations face_recognition.face_locations(rgb_small_frame) face_encodings face_recognition.face_encodings(rgb_small_frame, face_locations) for face_encoding, face_location in zip(face_encodings, face_locations): matches face_recognition.compare_faces(known_encodings, face_encoding, tolerance0.45) name 未知 if True in matches: # 取距离最近的人名避免多个匹配的误判 face_distances face_recognition.face_distance(known_encodings, face_encoding) best_match_index min(range(len(face_distances)), keyface_distances.__getitem__) name known_names[best_match_index] if name not in checked_in: now datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) checked_in.add(name) with open(output_file, a, encodingutf-8) as f: f.write(f{name},{now}\n) print(f[签到成功] {name} 时间: {now}) # 绘制人脸框和名字 top, right, bottom, left face_location scale 2 # 因为缩小了帧坐标要放大回来 top, right, bottom, left top*scale, right*scale, bottom*scale, left*scale cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.putText(frame, name, (left, top-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(Attendance System, frame) if cv2.waitKey(1) 0xFF ord(q): break video_capture.release() cv2.destroyAllWindows() if __name__ __main__: recognition_loop()逻辑说明compare_faces的参数tolerance0.45是关键阈值数值越小越严格。默认值是0.6但实测0.6在室内普通摄像头上会出现跨脸误判的风险偏高0.4到0.45是比较稳的范围。face_distance用来从多个匹配结果中取最接近的那个避免出现“张三的脸既匹配张三又匹配李四”的极端情况。checked_in这个集合很重要没有它一个人在摄像头前晃10秒就会产生10条签到记录。参数说明cv2.VideoCapture(0)的0是摄像头索引笔记本内置摄像头一般是0外接USB摄像头可能变成1或2。判断isOpened()是必须的很多时候程序崩溃不是因为逻辑错了而是摄像头被其他软件占用。fx0.5把帧缩小一半人脸检测的耗时能下降将近一半这是用分辨率换速度的典型取舍。在小团队内部考勤场景中签到记录优先保证“有名有姓、时间准确”速度不能太拉胯。3.3 签到记录的性能和扩容设计上面的代码能跑通但如果你有几十个人、签到时间集中有几件小事值得提前做。第一直接把记录以追加方式写CSV不要攒着最后一起写因为中途断电、程序崩溃会丢掉全部记录。追加写的方式即使崩溃已经写入的记录还在。第二可以用SQLite替代CSV满足更复杂的查询需求比如查某个人最近30天的出勤率、导出月度汇总表。CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, check_time TIMESTAMP NOT NULL ); CREATE INDEX idx_name_time ON attendance(name, check_time);逻辑说明SQLite方案的好处是数据更结构化可以按人、按时间范围快速查询。check_time用ISO格式存字符串就好不要搞时区转换本地小系统没那个必要。索引idx_name_time是给后续频繁执行“SELECT WHERE name? AND check_time BETWEEN ? AND ?”准备的数据量上千条之后索引能让查询时间从秒级降到毫秒级。再往后如果跨部门、跨校区把数据迁移到MySQL也是顺手的事表结构基本不用改。4. 给签到系统加一层好用的外衣GUI 界面与可视化报表4.1 为什么命令行不够用从脚本到工具的距离命令行版的签到这个东西自己调试用没毛病但真要给别人用就会碰壁。很多使用场景是前台或行政人员操作他们没有命令行基础也不会在黑色窗口里输命令启动程序。你需要的是一个能双击打开的窗口界面显示摄像头画面、显示签到状态、能看到今天谁来了谁没来。桌面GUI有Tkinter和PyQt两条路。Tkinter是Python标准库里的不用额外安装但做出来的界面确实土。PyQt5界面现代控件丰富但安装包大、学习曲线也陡一些。签到系统用Tkinter完全足够原因很直接这个软件的核心功能就两个——显示摄像头画面、显示签到信息不需要复杂布局。4.2 Tkinter 实时视频画面嵌入的要点Tkinter嵌入OpenCV视频流有一个经典坑点你不能直接在Tk主循环里跑while True的摄像头读取那样会把界面卡死。正确的做法是用after方法周期性地更新画面每次只处理一帧。代码如下import tkinter as tk from tkinter import ttk import cv2 import face_recognition import pickle import datetime from PIL import Image, ImageTk class AttendanceApp: def __init__(self, window, window_title人脸识别签到系统): self.window window self.window.title(window_title) self.window.geometry(900x650) # 摄像头初始化 self.video_capture cv2.VideoCapture(0) if not self.video_capture.isOpened(): print(摄像头打开失败) # 加载数据库 with open(face_db.pkl, rb) as f: self.face_db pickle.load(f) self.known_names list(self.face_db.keys()) self.known_encodings list(self.face_db.values()) self.checked_in set() # 视频显示区域 self.video_label tk.Label(window) self.video_label.pack(sidetk.LEFT, padx10, pady10) # 信息面板 info_frame tk.Frame(window) info_frame.pack(sidetk.RIGHT, filltk.Y, padx10, pady10) self.info_text tk.Text(info_frame, width30, height25, statetk.DISABLED) self.info_text.pack() self.quit_button tk.Button(info_frame, text退出, commandwindow.quit) self.quit_button.pack(pady10) # 启动视频刷新循环 self.update_frame() self.window.mainloop() def update_frame(self): ret, frame self.video_capture.read() if ret: # 缩放画面到标签大小 frame cv2.resize(frame, (640, 480)) rgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) orange (0, 102, 255) # BGR颜色注意OpenCV里是BGR顺序 face_locations face_recognition.face_locations(rgb_frame) face_encodings face_recognition.face_encodings(rgb_frame, face_locations) for face_encoding, face_location in zip(face_encodings, face_locations): matches face_recognition.compare_faces(self.known_encodings, face_encoding, tolerance0.45) name 未知 if True in matches: face_distances face_recognition.face_distance(self.known_encodings, face_encoding) best_match_index min(range(len(face_distances)), keyface_distances.__getitem__) name self.known_names[best_match_index] if name not in self.checked_in: self.checked_in.add(name) now datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) with open(attendance.csv, a, encodingutf-8) as f: f.write(f{name},{now}\n) self.update_info(f[{now}] {name} 签到成功) top, right, bottom, left face_location cv2.rectangle(frame, (left, top), (right, bottom), orange, 2) cv2.putText(frame, name, (left, top-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, orange, 2) # Tkinter用PIL的ImageTk转换才能显示 img ImageTk.PhotoImage(imageImage.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))) self.video_label.configure(imageimg) self.video_label.image img self.window.after(30, self.update_frame) # 每30毫秒刷新一帧约33FPS def update_info(self, message): self.info_text.configure(statetk.NORMAL) self.info_text.insert(tk.END, message \n) self.info_text.see(tk.END) self.info_text.configure(statetk.DISABLED) if __name__ __main__: root tk.Tk() app AttendanceApp(root)逻辑说明window.after(30, self.update_frame)是Tkinter的定时回调机制它会在不阻塞主循环的前提下每30毫秒调用一次自己实现视频帧的周期性刷新。界面右侧的Text组件用来显示签到日志只追加、可滚动。颜色这里我故意用了一个橙色系的BGR元组提醒你注意OpenCV的颜色通道顺序是BGR不是RGB直接写(255, 102, 0)那就是RGB的值画出来颜色会不对。GUI版本相比命令行的核心差异在于后台逻辑完全复用第三章的代码只是把打印输出换成了界面刷新把OpenCV的imshow换成了Tkinter的Label显示。这就体现出模块化的好处了识别逻辑和展示层解耦换界面不用重写识别代码。4.3 签到导出Excel打卡表的最后一公里签到数据的最终归宿往往是给人事或辅导员看Excel。CSV直接用Excel打开会有中文乱码问题原因是编码不匹配。正确姿势是用pandas直接生成真正的xlsx文件import pandas as pd df pd.read_csv(attendance.csv, names[姓名, 签到时间]) summary df.groupby(姓名)[签到时间].agg([count, min, max]) summary.columns [签到次数, 首次签到, 末次签到] with pd.ExcelWriter(attendance_summary.xlsx, engineopenpyxl) as writer: df.to_excel(writer, sheet_name明细, indexFalse) summary.to_excel(writer, sheet_name汇总) print(已导出 attendance_summary.xlsx)逻辑说明groupby(姓名)按人分组agg([count, min, max])分别统计签到次数、最早和最晚的签到时间。一天内多次签到的场景如果你只想保留第一次按姓名和日期去重后再统计。这个导出脚本是纯离线执行的不依赖摄像头可以定时跑或者手动跑。5. 避坑与排查人脸签到实战中最常踩的五个坑5.1 摄像头打不开或索引不对现象报错VideoCapture(0) failed或者打开了但画面黑屏。原因笔记本内置摄像头被其他软件微信、腾讯会议、浏览器占用或者是USB摄像头导致索引变了。解决先关掉所有可能占用摄像头的软件再运行代码。如果还是不行写一个摄像头索引枚举脚本逐个试import cv2 for i in range(5): cap cv2.VideoCapture(i) if cap.isOpened(): ret, frame cap.read() if ret: cv2.imshow(fCamera {i}, frame) cv2.waitKey(0) cap.release() print(f索引 {i} 可用)注意Windows的摄像头权限设置也可能导致黑屏。检查系统设置里允许桌面应用访问摄像头否则OpenCV拿到的帧全是黑色的。这个问题在Windows更新后尤其常见属于系统权限被重置了不是代码的问题。5.2 dlib 编译失败Windows 上最劝退的一步现象pip install dlib报错一堆C编译错误。原因没有预先安装CMake或者VS Build Tools版本不匹配。解决先pip install cmake再安装Visual Studio Build Tools时需要勾选“使用C的桌面开发”工作负载。如果你实在不想折腾编译可以用conda装预编译包conda install -c conda-forge dlibconda-forge的dlib是预编译的不用本地编译。省心程度拉满。这就是为什么很多教程推荐直接装Anaconda不是因为它做科学计算好用而是因为它避开了Windows编译地狱这个最大的翻车点。5.3 识别框乱跳一张脸多个框现象人脸框不停闪烁同一张脸被画出来好几个框。原因视频帧连续帧之间存在抖动或者face_locations返回了多个重叠的检测结果。解决引入简单的帧间平滑——只当人脸位置在一定范围内连续出现多帧才确认或者把重叠的框合并。最简单的办法是只取面积最大的那个框这样才能保证识别的输入稳定if len(face_locations) 1: areas [(b-a)*(c-d) for top, right, bottom, left in face_locations for a, b, c, d in [(top, right, bottom, left)]] face_locations [face_locations[areas.index(max(areas))]]逻辑说明这个逻辑是对每个候选框计算宽高乘积取最大的那个当作目标人脸。对于签到场景人脸在画面中通常占据最大面积这个近似是合理的。多人同时入镜时选最大的脸是符合签到习惯的——靠近摄像头的人大概率就是来签到的那个。5.4 识别准确率低同一个人被识别成另一个人现象张三的脸被识别成李四或者每次都识别成“未知”。原因阈值设置偏松或偏严或者登记照片和实时画面的光线差异太大。解决先调tolerance参数。默认0.6我建议从0.5开始往下调。每调一次就用现场灯光下的测试数据跑一轮找那个“能认出自家人、不认错别人”的平衡点。如果0.4都认不出来说明登记照片和实际画面的差异太大重新拍登记照片保证光线和角度接近实际使用场景。5.5 签到重复记录每次眨眼都打一次卡现象一个人在摄像头前签了三次到。原因代码里没有去重逻辑每帧识别到同一个人都会触发写入。解决除了用checked_in集合去重外还可以加一个冷却时间机制。有些场景允许一人多次签到比如上课的中途进出但默认的考勤需要“每天只签一次”。两种做法的区别在于去重粒度按天去重还是按次去重。6. 落地进阶把签到系统改造成支持多摄像头和人脸活体检测前面的系统能用了但离真正放出去长期跑还有一段距离。这里我给出三个最值得做的进阶方向和一些具体实现思路。第一识别同类照片的人脸代替真人打卡就是拿照片或手机屏幕放在摄像头前也能被识别为签到成功因为face_recognition不具备活体检测能力。第二多摄像头支持。每个教室、每个办公室装一个摄像头签到数据需要汇总到一张表。第三性能优化和异常处理。长时间运行时内存泄漏、摄像头掉线、磁盘写满等问题都需要处理。活体检测最简单的一个技巧是让用户做指定动作眨眼、点头、张嘴。OpenCV里可以用face_landmarks提取眼部关键点计算眼睛的开合度如果连续多帧没有检测到眨眼动作就判定为照片攻击。完整代码稍微多这里给出一个核心思路def detect_blink(face_landmarks): left_eye face_landmarks[left_eye] right_eye face_landmarks[right_eye] # 计算眼睛的纵横比 EAR (Eye Aspect Ratio) def eye_aspect_ratio(eye): A ((eye[1][0]-eye[5][0])**2 (eye[1][1]-eye[5][1])**2) ** 0.5 B ((eye[2][0]-eye[4][0])**2 (eye[2][1]-eye[4][1])**2) ** 0.5 C ((eye[0][0]-eye[3][0])**2 (eye[0][1]-eye[3][1])**2) ** 0.5 return (A B) / (2.0 * C) left_ear eye_aspect_ratio(left_eye) right_ear eye_aspect_ratio(right_eye) return (left_ear right_ear) / 2.0 0.25 # 低于阈值视为闭眼逻辑说明眼睛纵横比的原理是当人眨眼时上下眼睑距离变短EAR值会显著下降。连续多帧EAR都处于高位睁眼状态说明画面中的人始终睁着眼极大概率是照片——真人不可能10秒不眨眼。这个检测的阈值0.25和连续帧数需要实测校准。另一个j简单方案是在登记时让用户转头、抬手但那需要交互流程比较麻烦。多摄像头支持方面简单的做法是用时间戳和摄像头标识把数据分开存在汇总层面再合并。比如每个摄像头启动时打上自己的标志camera_id sys.argv[1] if len(sys.argv) 1 else CAM01 output_file fattendance_{camera_id}.csv最后汇总时把所有CSV根据姓名和日期去重保留最早的那条记录。这就是一个可以跑的分布式方案不需要改核心识别代码。长期稳定运行也值得一提。建议做计划任务每天重启一次程序避免长时间运行导致的内存碎片。签到数据每天自动备份。这些细节不在代码里但在运维里实战项目能不能让人放心用往往就是靠这些笨功夫。愿你照着这套方案搭出来的签到系统能从演示一路跑到真刀真枪的考勤使用中希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站