简介:这份资源是一套基于Python实现的人脸识别门禁系统源码与配套素材,面向计算机视觉初学者、安防门禁开发者和毕业设计人员,可帮助快速搭建具备人脸检测、特征提取和身份验证功能的小型门禁项目。压缩包共49个文件,整体大小约987KB,结构清晰:包含30个pgm格式的人脸样本图像、5个png以及若干jpg、bmp图片用于界面展示,4个xml级联分类器负责检测人脸与眼睛,4个py脚本分别完成人脸数据保存、实时预测、功能测试和UI交互,同时附有README.txt和master.txt说明文档。项目基于OpenCV和Haar特征实现,覆盖从图像采集、人脸对齐、特征匹配到门禁决策的完整流程,支持录入人脸模板并实时比对,为二次开发提供了可直接运行的参考模板。目前已有244人学习下载,对希望钻研人脸识别门禁工程细节、借鉴代码组织方式和数据处理流程的开发者具有不错的参考价值。
1. 人脸识别门禁系统,先想清楚“误拒”和“误认”哪个更致命
门禁和手机解锁有一处本质差异:手机解锁失败可以再输密码,门禁前如果员工刷脸被拒,所有人的通行都会堵在闸机口;反过来,如果陌生人被放进去,后续追责和补救的成本要高得多。很多人脸识别系统 Demo 在电脑上跑得风生水起,一放到门禁现场就翻车,原因往往不是模型的识别率不够,而是没有把“误拒率(FRR)”和“误识率(FAR)”这对矛盾放进同一个项目里调。face_pre_sys 这类人脸识别门禁项目的关键不是调通一个模型,而是把采集、预处理、特征比对和门禁控制串成一条能现场调整的流水线。本文从模型选型、最小可实现、现场排错到边缘设备落地,把这条流水线拆开讲透。
2. 搭建face_pre_sys前,先确定人脸识别系统里的模型与流水线
2.1 检测、对齐、特征提取三件套是人脸识别门禁的骨架
先纠正一个常见误区:门禁系统里说的“人脸识别”不是一个模型包办所有事,而是四段式流水线。第一段是人脸检测,负责在一帧画面里把人脸边界框找出来。门禁现场大概率有逆光、半遮挡和运动模糊,检测模型要能抗住低分辨率和小尺寸人脸。第二段是人脸对齐,根据眼睛、鼻尖、嘴角等关键点做仿射变换,把歪头、低头的人脸摆正。这一段做不好,后面的特征向量会直接漂移,哪怕同一个人也可能被判成不同的人。
第三段是特征提取,把对齐后的人脸图像压成一个固定长度的向量。InsightFace 里 ArcFace 系列模型在公开测试集上能把同一人的类内距离压得很小,不同人的类间距离拉得很大,这让门禁阈值变成一个可调参数,而不是拍脑袋定的经验值。第四段是比对,把当前向量和人脸库里的向量算余弦相似度或欧氏距离,超过阈值就放行。生产环境里,前两段影响“能不能找到脸”,后两段影响“找对了没有”,排查问题时要分开看。
提示:门禁现场常说“识别不上”,有经验的工程师会先问一句:到底是检测框没出来,还是对齐后人脸歪了,还是特征分低于阈值?三段对应三种修法,不要一上来就调阈值。
开源模型选型时,建议先跑一下这几组常见组合:
| 模型组合 | 特征维度 | CPU 单帧耗时参考 | 适合场景 |
|---|---|---|---|
| InsightFace buffalo_l | 512 | 200-400ms | 门禁/考勤,准确优先 |
| InsightFace buffalo_s | 512 | 80-150ms | 树莓派、Jetson 等边缘设备 |
| OpenCV YuNet + 自训练特征 | 128 | 10-30ms | 概念验证、低端 IPC 设备 |
选择依据很简单:门禁闸机通常有 500ms 左右的人脸识别门禁机响应阈值,超过这个时间,行人已经走到闸机口了。所以不是模型越准越好,而是要在设备算力允许的范围内选最强的模型。人脸识别门禁系统部署数量越多,越要考虑边缘设备的成本,轻量模型并不丢人,关键是知道它的 FAR 上限在哪。
2.2 face_pre_sys 的模块划分与数据流
face_pre_sys 这个名字里的 pre 通常指 preprocessing(预处理),但项目做到后面,线上线下的模块远不止预处理。一种常见的组织方式是把整个人脸识别系统拆成五个模块,每个模块只做一件事:
- capture:摄像头采集,负责抽帧、ROI 裁切、自动曝光补偿;
- preprocess:人脸检测、对齐、质量检查、光线校正;
- embed:特征提取,生成 512 维向量;
- match:与注册特征库比对,带阈值判断;
- relay:把比对结果转成门禁控制器的开锁信号。
这不是唯一标准,但按这种分工写,每个环节都能单独验证。现场误拒偏高时,可以先在 preprocess 里把对齐后的脸保存成图片看一眼,确认脸有没有被摆正,再去看 embed 输出的向量波动。face_pre_sys 的内部结构可以按你的硬件和团队习惯调整,但“可单独验证”这条一定要保住。对应到目录上,我一般会这样组织:
face_pre_sys/ ├── config.py # 阈值、摄像头ID、模型路径统一配置 ├── capture.py # 视频流抽帧与 ROI 裁切 ├── preprocess.py # 检测/对齐/质量检查 ├── embed.py # 特征提取 ├── match.py # 相似度比对与阈值判断 ├── relay.py # GPIO 或网络继电器输出 ├── register.py # 注册人脸入库 ├── recognize.py # 实时识别主流程 └── data/ ├── faces/ # 注册的原始人脸图片 └── features/ # 序列化后的特征向量 .npy这里最容易被忽视的是 data 目录的版本管理。注册的原始图片和特征向量必须成对保留,否则一旦发现某个人注册时拍糊了,特征库里已经混入脏数据,你只能全部重新注册。保留原始图,至少能根据文件名快速定位问题样本。
2.3 为什么门禁首选离线本地推理
人脸识别门禁不是不能做云端识别,而是现场约束把云端方案堵死了。闸机前不会有时间等 3 秒网络往返;更关键的是注册人脸属于个人敏感信息,每次识别都把照片传到云端,数据就出了设备边界,很多门禁管理方在合规审查上过不去。所以在边缘 人脸识别 大量数据场景下,主流方案是把检测、对齐、特征提取和比对全部放在门禁设备本地,云端只负责远程运维和日志回传。
离线推理不意味着系统一动不动。设备可以在夜间低峰期批量更新特征库,也可以通过管理平台下发新的黑名单。调试时如果发现现场效果不稳定,先确认设备是否因为离线导致时钟漂移,进而影响日志时间戳。这类问题在联网现场很难复现,离线部署后却经常冒出来。face_pre_sys 里建议在 relay 模块统一记录“识别时间+人名+分数+图片路径”,方便事后回溯。
3. 人脸识别门禁最小闭环:注册、比对、开门
3.1 注册流程:采集人脸并生成特征向量
门禁系统的人脸库不是随便拿张照片就能入库的。注册时拍糊了,后面所有识别都会带偏。我这里以 InsightFace 的 Python API 为例,注册流程的核心是“单张人脸+质量过滤+特征归档”:
# register.py import cv2 import numpy as np from insightface.app import FaceAnalysis app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) def register(name, img_path): img = cv2.imread(img_path) faces = app.get(img) if len(faces) != 1: print('需要单张人脸,当前检测到', len(faces)) return False # 只让质量过关的人脸进入特征库 face = faces[0] if face.det_score < 0.5: print('人脸质量过低,换一张更清晰的照片') return False # 压缩保存为 npy,后续 match 直接加载 np.save(f'data/features/{name}.npy', face.embedding) return Trueapp.get() 返回一个人脸列表,每个元素里带检测框、关键点和 embedding。det_score 是检测置信度,门禁注册时建议把这个门槛设高一点。注册阶段宁可多废几张照片,也不要把模糊特征写进库里。
3.2 识别流程:摄像头实时帧的人脸比对
识别主循环比注册多几步:读帧、检测、逐个比对、连续确认、触发开门。这里给出一个可直接改配置后跑起来的最小版本:
# recognize.py import cv2 import numpy as np import glob import config app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) def load_database(): db = {} for f in glob.glob('data/features/*.npy'): name = f.split('/')[-1][:-4] db[name] = np.load(f) return db def match_face(embedding, db, threshold=config.MATCH_THRESHOLD): best_name, best_score = None, -1.0 for name, ref in db.items(): # 余弦相似度,越接近 1 表示越像 score = np.dot(embedding, ref) / (np.linalg.norm(embedding) * np.linalg.norm(ref)) if score > best_score: best_name, best_score = name, score if best_score >= threshold: return best_name, best_score return None, best_score cap = cv2.VideoCapture(config.CAMERA_ID) db = load_database() # 连续命中的帧数,低于该值不开门 confirm_frames = 3 confirmed = {} while True: ok, frame = cap.read() if not ok: continue faces = app.get(frame) for face in faces: name, score = match_face(face.embedding, db) if name is None: confirmed.pop(name, None) # 未命中则清除计数 continue # 连续 N 帧命中同一人才触发继电器 confirmed[name] = confirmed.get(name, 0) + 1 if confirmed[name] >= confirm_frames: open_door_once(name, score) # relay.py 里的开锁函数 if cv2.waitKey(1) == 27: break连续确认是个容易被忽略的细节。单帧误识别可能因为反光、抽帧抖动而出现,连续三帧命中才开门能显著降低不必要的开锁。代价是平均识别延时增加约两三帧,对 500ms 量级的门禁系统来说完全可以接受。config.py 里建议把 MATCH_THRESHOLD、CAMERA_ID、det_size 都作为可配置项,方便现场不修改逻辑就调整行为。
3.3 阈值与输出:怎么驱动门禁控制器
比完分之后,门禁控制器需要的只是一个开关信号。常见做法有两种:GPIO 直接控制继电器,或者用网络继电器。后者只要发送一个 HTTP GET 请求就能完成开锁:
# relay.py import requests def open_door_once(name, score): # 门禁控制器收到请求后闭合继电器 2 秒 requests.get(config.RELAY_URL, timeout=1)这里的匹配阈值需要专门设计。一般门禁人脸识别系统先设一个初值,再用现场正负样本校准。经验值可以参考下面这张表:
| 余弦相似度阈值 | 行为特征 | 典型场景 |
|---|---|---|
| 0.4 | 非常宽松,FAR 高 | 快速验证,不建议直接上闸机 |
| 0.5 | 对角度和眼镜变化容忍度高 | 人流大的普通门禁 |
| 0.6 | 严格,误拒增加 | 机房、资产室等高安全区 |
| 0.7 | 极严格,只接受非常清晰的同人脸 | 需要人为参与的强验证场景 |
阈值不是越高越好。调高了陌生人进不来,但员工在逆光下也进不来;调低了通行顺畅,但相似脸或被误放的记录会多。工程上应该先记录一周的“识别分数分布”,再把阈值固定在正负样本重叠区以外。
4. 门禁现场三大坑:光线、角度、活体,以及对应排错手段
4.1 光线过曝与阴影:先处理图像再做识别
检测不到人脸或频繁误拒,排在第一位的原因是现场光线。InsightFace 的模型自身带一定的光照鲁棒性,但它的输入如果是一片死白或半张脸陷在阴影里,检测阶段就会直接丢框。常见做法是在送入模型前先做图像增强,但要对全图统一计算,避免局部过度增强:
# preprocess.py import cv2 import numpy as np def preprocess_frame(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) mean_brightness = gray.mean() # 只在整体亮度偏低或偏高时做 CLAHE if mean_brightness < 80: clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) frame[:, :, 0] = clahe.apply(gray) elif mean_brightness > 180: frame = cv2.normalize(frame, None, 0, 255, cv2.NORM_MINMAX) return frame门禁安装角度固定后,ROI 区域往往只有画面的一小块。可以先用 capture 模块把识别区域裁出来,再做光照统计,这样比整帧均值更符合实际场景。裁切还能减少背景里其他行人对检测的干扰,是硬件位置固定后最划算的优化手段。
4.2 阈值与误识率:调 threshold 还是换模型
很多人遇到误识别,第一反应是调高阈值,但阈值只能移动判断边界,不能扩大“本人分数”和“他人分数”之间的间隔。如果现场采集到足够多的负样本,画一张正负样本相似度直方图,重叠区很大时,调阈值只是把误报往后推,过几天还会出现。常见做法是先用注册库外的照片做一轮离线测试,把相似度分布统计出来:
python tools/score_hist.py --db data/features --test data/negative输出结果里如果正样本均值和负样本均值相差不到 0.1,说明当前模型在原始图像质量下区分度不够,这时候优先检查注册照片和对齐结果,而不是换一个更重的模型。注册时多拍几张,选择 det_score 最高的那张,通常比从 buffalo_l 换到更重的模型更见效。
如果确认图像质量没问题,再考虑换模型。门禁设备是 CPU 推理时,从 buffalo_s 升到 buffalo_l 带来的 FAR 改善一般要结合现场实测。换模型后,阈值要重新标定,之前在 buffalo_s 上用的 0.6 在 buffalo_l 上可能过于严格,因为不同模型的 embedding 空间分布不完全一致。
4.3 活体检测:防照片攻击的常见做法
门禁最怕的不是识别不准,而是拿着打印照片直接刷开。普通可见光摄像头可以做配合式活体,也就是让用户完成眨眼或转头动作。人脸识别门禁机上更常见的是红外双目方案,红外图像能直接分辨屏幕和真实皮肤,无需用户配合。
先进的一段式活体模型可以直接输出真假人脸概率,适合有 GPU 的嵌入式设备。CPU 受限时,简单眨眼检测也能起一定防御作用:
# liveness.py 配合式眨眼检测的几何特征 def eye_aspect_ratio(eye_points): # 眼睛纵轴两点距离之和与横轴距离的比值 v1 = np.linalg.norm(eye_points[1] - eye_points[5]) v2 = np.linalg.norm(eye_points[2] - eye_points[4]) h = np.linalg.norm(eye_points[0] - eye_points[3]) return (v1 + v2) / (2.0 * h)这里算出的 EAR 在睁眼时明显高于闭眼时。实际检测要在连续帧里记录 EAR 从高到低再到高的过程,才能确认是一次眨眼,而不是某帧闭眼导致误判。配合式活体不能只判断“眼睛小”这一帧,否则一张闭眼照片就可以绕过。
另外,活体失败和识别失败要分开记日志。现场经常出现“活体误报”被当成“识别不上”来排查,导致在检测和阈值上浪费时间。把两者日志分开,至少能看到是哪一段挂掉。
5. 落地技巧:边缘设备上让人脸识别系统跑得稳的最后几招
5.1 用视频抽帧率控制 CPU 占用
树莓派或低端 x86 盒子做人脸识别门禁时,最容易吃满 CPU。不必每一帧都做检测加特征提取,常见做法是把检测和识别解耦,识别按固定帧间隔执行:
frame_interval = 2 # 每 2 帧做一次完整识别 frame_count = 0 while cap.isOpened(): ok, frame = cap.read() if not ok: continue frame_count += 1 if frame_count % frame_interval != 0: continue run_recognition(frame)有人会直接把摄像头帧率调低来省资源,但帧率过低,快速通过闸机的人脸会拖影,检测反而更难。保持摄像头 25fps,只把推理频率降下来,检测框仍然可以从高帧率画面里拿到,只是特征比对少跑几次。
5.2 人脸库超过一万时,用特征索引别用线性扫描
我维护过的人脸识别门禁系统早期人脸库只有几百人,match 模块用 Python 列表线性扫描完全没压力。后面人员扩充到上万,单次匹配耗时从几毫秒涨到几十到上百毫秒。这时我会改用 Faiss,用一个精确内积索引替代线性循环:
# match.py 使用 Faiss 加速匹配 import faiss import numpy as np class FaceIndex: def __init__(self, features: np.ndarray, names: list[str]): dim = features.shape[1] self.index = faiss.IndexFlatIP(dim) self.index.add(features) self.names = names def query(self, embedding, k=5): scores, idx = self.index.search(np.expand_dims(embedding, axis=0), k) return [(self.names[int(i)], float(scores[0][j])) for j, i in enumerate(idx[0])]IndexFlatIP 是暴力精确搜索,但对上万人脸库已经比 Python 循环快很多。没必要一上来就上 IVF 或 HNSW 近似索引,门禁库可以离线重建,精确优先。
5.3 验证方法:用回放离线视频去评估误识率
实机做误识率测试很难凑人数,更可靠的方法是用一段现场拍摄的监控视频离线回放。把视频按秒抽帧,跑完整识别流程,再和标注结果比对,就能得到近似 FAR 和 FRR:
python evaluate.py --video door_cam.mp4 --database data/features --gt labels.jsonevaluate.py 的输出建议包含三列:误识别次数、漏识别次数、平均匹配耗时。前两项用来调阈值,第三项用来判断算法在设备上是否达到门禁响应时效。设备换摄像头或调整角度后,重新跑一遍同一段回放视频,比现场人工反复刷脸要省时间。把回放脚本纳入每次调整后的回归测试,能直接避免“昨天好的今天又坏了”的现场拉锯。
本文还有配套的精品资源,点击获取