基于Python+Django的人脸识别门禁系统:从原理到工程落地
发布时间:2026/10/2 9:07:03来源:尧图网络
简介一套基于Python与Django框架开发的人脸识别门禁管理系统完整源码面向毕业设计、课程设计及希望掌握Web后端与生物识别技术结合的开发者。系统实现了从用户注册、人脸采集、特征提取到门禁鉴权的完整业务闭环后端基于Django提供RESTful接口、路由控制与数据库ORM管理前端页面采用HTML/CSS/JavaScript与Django模板引擎渲染核心模块集成OpenCV人脸检测算法涵盖图像灰度化、归一化等预处理环节并实现特征比对与通行记录管理同时具备用户认证、权限控制等安全机制。压缩包共9358个文件其中Python源码及编译文件py/pyc占比最高辅以HTML/JS/CSS前端资源、PO/MO国际化文件、PNG/JPG图像资源以及JSON/CSV等配置数据整体约410.73MB。目录结构清晰包含模型定义、视图函数、模板、静态资源等模块便于分阶段阅读与二次开发。目前已有342人学习使用适合需要快速搭建同类系统或深入理解Django项目架构的开发者参考。1. 拿到这个“基于PythonDjango人脸识别门禁管理系统源码.zip”你到底拿到了什么下载一个带“源码.zip”字样的项目很多人第一件事是解压、跑python manage.py runserver然后对着浏览器里的报错页发呆。这个标题描述的不是一段几十行的算法Demo而是一个把「人脸识别推理」和「业务管理系统」串起来的完整Web工程Django负责人员管理、通行记录、权限配置这些后台事务人脸识别引擎负责把摄像头里的人脸和库里的人脸做比对两者通过数据库和消息机制耦合。整个系统的核心价值不在“认脸”本身而在“认完脸之后门禁逻辑怎么处理、记录怎么留存、权限怎么联动”。这类项目的主力受众是两类人一是做毕设或课程设计的学生需要一套能展示完整业务闭环而不是只有算法精度的代码二是在园区、实验室或小办公楼里想低成本搭一套门禁demo的运维或嵌入式工程师——买一台人脸识别门禁机要两三千起用普通USB摄像头加一台旧电脑就能跑通流程预算差一个量级。接下来我按自己搭这种项目时的思路从技术选型、工程结构、识别链路到踩坑记录把整个方案拆给你看。2. 为什么是“PythonDjango本地人脸识别”而不是更流行的技术组合2.1 先定一个基调这套系统的技术选型是被“交付场景”逼出来的如果你只是做算法验证用face_recognition库写个脚本就够了根本不需要Django。但门禁管理系统的本质是一个多用户、有权限分级、有操作审计的业务系统管理员要录入人员、部门要批量导入、访客要预约登记、每一次开门都要留下“谁在什么时间刷脸通过”的记录。这些东西用Flask或FastAPI也能做但Django自带Admin后台、ORM、Session管理和认证体系能让你少写一大半业务代码。我自己做过一个对比用Flask从零搭用户认证、权限分组、操作日志这三个模块至少要额外写几百行装饰器和中间件Django的auth应用加django-admin配好INSTALLED_APPS之后直接有了能用的后台界面。对于“项目要交付、要给老师或甲方演示完整流程”的场景Django的开发速度和维护性是明显占优的。它的MTV架构也适合门禁系统的模块划分——Model管人员和通行记录Template管Web管理页面View管业务逻辑人脸识别的推理代码可以独立成一个服务模块不污染Django的请求处理链路。2.2 人脸识别选型离线本地推理仍是这类项目的主流方案项目标题里没有提到具体的人脸识别库但从热词里能看到“人脸识别算法”“人脸识别门禁机”“基于STM32的人脸识别门禁系统设计”这些搜索说明这个方向的主流做法是本地推理而不是调用云端API。原因很直接门禁系统对延迟和隐私敏感如果每次刷脸都请求云端接口网络抖动一次就可能导致几秒的延迟而且人脸特征数据出域在很多管理规范里本来就过不去。常见做法是用OpenCV做人脸检测用dlib或face_recognition做特征提取。这个组合的性能我在普通i5处理器上实测过检测一张640x480的帧大概20到30毫秒特征提取约80到150毫秒比对一次在特征库里十个量级内是亚毫秒级。注意这里的瓶颈几乎不在“比对”而在“检测特征提取”这组串行操作。如果门禁点有并发请求——比如上下班高峰期多人同时刷脸——就一定得处理“同时来多个人脸”的情况不能简单地在每个Django请求里同步跑一遍完整推理流程否则Django的worker会被全部卡住。这个问题我会在第4章的踩坑记录里展开。2.3 云服务对比用商用API做备选但别让核心逻辑依赖它有人会问为什么不直接用百度或虹软的商用人脸识别API精度确实更高SDK封装也好。但我建议把云API当作“二级比对通道”而不是主链路来设计本地推理置信度低于阈值时再降级调用云端接口二次确认。这样主流程自主可控损失的是偶尔一次几百毫秒的云请求延迟换取的是在没有外网的机房环境里系统依然能用。这个“本地为主、云端兜底”的思路在源码包的架构设计里应该体现在配置项而不是硬编码里。配置长这样——用环境变量声明是否启用云端兜底而不是在代码里写死调用逻辑import os # 是否启用人脸识别云端二次校验 ENABLE_CLOUD_FALLBACK os.getenv(FACE_CLOUD_FALLBACK, false).lower() true # 本地人脸识别置信度阈值低于此值时才走云端兜底 LOCAL_FACE_THRESHOLD float(os.getenv(FACE_LOCAL_THRESHOLD, 0.48)) # 云端接口的超时时间门禁场景不能等太久 CLOUD_API_TIMEOUT int(os.getenv(FACE_CLOUD_TIMEOUT, 3))参数说明LOCAL_FACE_THRESHOLD不是拍脑袋定的我后面会讲怎么用一组正负样本来标定它。CLOUD_API_TIMEOUT设3秒是底线——门禁场景下超过3秒的等待就会造成拥堵如果云端兜底超时就按“拒绝通行”处理并写入异常日志这个策略要提前和甲方说清楚。3. 把zip包解开Django工程的核心目录与配置3.1 工程结构源码包不是只有一个manage.py这类项目源码包的标准骨架一般是Django项目、业务应用和独立服务模块三层。我习惯在解压之后先不看代码而是先画一遍目录树确定每个目录的职责边界face_access/ ├── manage.py ├── config/ # Django项目配置settings/urls/wsgi ├── access/ # 门禁业务应用人员、设备、通行记录 ├── recognition/ # 人脸识别独立服务模块不依赖Django ORM │ ├── detector.py # 人脸检测封装 │ ├── embedder.py # 特征提取封装 │ ├── matcher.py # 特征比对与阈值策略 │ └── models/ # 存放训练好的特征提取模型文件 ├── static/ # 前端静态资源CSS/JS/UI图片 ├── media/ # 上传的人脸注册照片和抓拍截图 └── requirements.txt # 依赖清单为什么要把recognition独立成不依赖Django ORM的模块因为人脸识别是CPU密集型操作最好在独立进程中运行你可以用Redis队列或Celery把识别任务异步化。如果识别代码直接调用Django Model就很难脱离Web进程单独测试也没有办法在门禁一体机上单独复用这个模块——这正好呼应了标题里“门禁系统”而不只是“Web网站”的定位。3.2 配置settings.py跑不起来的第一道拦路虎先看几个必须改的配置项不然后面全在报错中度过# config/settings.py 关键配置片段 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, access, # 门禁业务应用 recognition, # 人脸识别服务模块纯逻辑 ] # 人脸注册照片和抓拍图片的存放位置 MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media # 静态文件收集目录部署到生产环境时必须执行 collectstatic STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles # 数据库默认使用 SQLite适合小规模门禁点数 DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } # 危险提示DEBUG 在生产环境必须为 False否则会泄露源码路径 DEBUG False ALLOWED_HOSTS [192.168.1.100, localhost]参数说明MEDIA_ROOT决定了你通过Django Admin上传的人脸注册照片落在磁盘哪里STATIC_ROOT是执行python manage.py collectstatic之后所有静态文件的汇集目录生产环境需要由Nginx直接指向它。ALLOWED_HOSTS在生产环境不能留空否则会报DisallowedHost错误。新手常见的翻车点解压后直接runserver页面样式全是裸HTML一看浏览器控制台全是静态文件404。原因通常是STATIC_ROOT或STATICFILES_DIRS配置缺失或者没有先在settings.py里把静态文件路由挂到URLConf上。调试阶段可以在config/urls.py里加一行from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT) urlpatterns static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)只有在DEBUGTrue时才建议这样挂生产环境必须交给Nginx托管否则Django会变成性能瓶颈而且有源码泄露风险。3.3 数据模型人员表、设备表、通行记录表怎么关联门禁系统的数据模型核心是三张表员工/访客信息表、门禁设备表、通行记录表。我一般会给人员表加一个face_encoding字段存128维特征向量的二进制序列化结果比对时一次性载入内存避免每来一次请求都查一次数据库、重新提取一次历史人员的特征。# access/models.py from django.db import models class Person(models.Model): 人员表员工和访客统一存储用 person_type 区分 name models.CharField(max_length64, verbose_name姓名) employee_no models.CharField(max_length32, uniqueTrue, verbose_name工号/证件号) person_type models.CharField( max_length16, choices[(staff, 员工), (visitor, 访客)], defaultstaff, ) face_image models.ImageField(upload_tofaces/%Y/%m/, verbose_name人脸照片) face_encoding models.BinaryField(nullTrue, blankTrue, verbose_name人脸特征向量) is_active models.BooleanField(defaultTrue, verbose_name是否启用) expired_at models.DateTimeField(nullTrue, blankTrue, verbose_name有效期截止时间) created_at models.DateTimeField(auto_now_addTrue) class AccessDevice(models.Model): 门禁设备表一台设备对应一路摄像头或一个门点 device_name models.CharField(max_length64) device_ip models.GenericIPAddressField() camera_url models.CharField(max_length256, help_text摄像头RTSP/HTTP地址或本地摄像头索引) is_online models.BooleanField(defaultFalse) last_heartbeat models.DateTimeField(nullTrue) class AccessLog(models.Model): 通行记录表每次比对结果都要落库用于审计和报表 person models.ForeignKey(Person, on_deletemodels.SET_NULL, nullTrue) device models.ForeignKey(AccessDevice, on_deletemodels.SET_NULL, nullTrue) timestamp models.DateTimeField(auto_now_addTrue) result models.CharField(max_length16, choices[(allow, 允许), (deny, 拒绝), (timeout, 超时)]) confidence models.FloatField(default0.0, help_text最高匹配相似度) snapshot models.ImageField(upload_tosnapshots/%Y/%m/, nullTrue) detail models.TextField(blankTrue, help_text识别细节或错误信息)参数说明face_encoding用BinaryField而不用TextField的原因是可以直接存序列化后的numpy数组字节流节省空间且读取效率高expired_at是访客管理的核心字段过期后自动拒绝通行。Snapshot字段保存抓拍图这在纠纷溯源时是硬需求一定要有。建完模型后执行两步操作python manage.py makemigrations access python manage.py migrate这两条命令分别生成模型迁移脚本、把变更写入数据库。如果你在用SQLite迁移失败通常是因为表结构冲突或字段类型不兼容删掉旧迁移文件重新生成是后悔药但生产环境不要这么干——正确做法是先python manage.py dbshell查看当前表结构判断是哪次迁移没对上。4. 人脸识别主链路注册、检测、特征提取、比对4.1 人脸注册不是存张照片就够了关键是特征入库注册流程的正确逻辑是员工上传照片或摄像头现场拍照 → 检测人脸区域 → 裁剪并对齐 → 提取128维特征向量 → 序列化存入Person.face_encoding字段。注意不要把原始照片直接当特征用每次比对时都重新提取特征那性能和准确性都不可控。# recognition/register.py import face_recognition import numpy as np def register_person(image_path): 注册流程加载图片 - 定位人脸 - 提取特征 - 返回特征向量 返回 (success, face_encoding_or_error) try: image face_recognition.load_image_file(image_path) face_locations face_recognition.face_locations(image, modelhog) if len(face_locations) 0: return False, 未检测到人脸请重新拍摄 if len(face_locations) 1: return False, f检测到 {len(face_locations)} 张人脸请确保照片里只有一个人 # 使用较大的人脸区域做特征提取小图特征区分度低 face_encodings face_recognition.face_encodings(image, known_face_locationsface_locations) if len(face_encodings) 0: return False, 人脸特征提取失败请检查照片清晰度 # encodings 是 128 维 float 数组 return True, face_encodings[0] except Exception as exc: return False, f注册异常: {exc}逻辑说明先定位整个人脸再把人脸区域交给face_encodings提取特征。如果你的注册图是从摄像头截取的建议先用OpenCV做人脸检测并画框给操作员确认——很多情况下员工上传的照片里戴了口罩或者光线极暗人脸检测能通过但特征质量很差后续误识别率会飙升。参数说明modelhog在CPU上比cnn快一个数量级但精度略低注册照片这种静态场景建议直接换modelcnn慢点无所谓特征准确度更重要代价是需要额外安装face_recognition的CNN检测依赖。4.2 实时比对Django请求里做推理是大坑改用消息队列假设门禁一体机每秒钟上报一帧画面那么高峰期一分钟就是几十次识别请求。如果每次请求都同步调用face_recognition.face_encodingsDjango进程会被卡死在CPU密集型计算里其他请求全部排队表现就是“查询后台都转圈”。我常用的方案是摄像头采集线程只负责抓帧并把图像推送到Redis队列消费者进程单独启动的Python进程负责检测和特征提取比对完成后把结果写回数据库并通过WebSocket推送前端。Django的请求处理线程只做“接收图像、丢队列、返回任务ID”不和推理直接打交道。# recognition/tasks.py # 模拟消费队列的循环实际生产使用 Redis 的 BRPOP 阻塞读取 import redis import json import base64 import face_recognition from .matcher import match_face r redis.Redis(host127.0.0.1, port6379, db0) def consume_recognize_queue(): 消费识别任务队列。 队列消息格式{image_b64: ..., device_id: 1, task_id: ...} while True: # BRPOP 阻塞读取避免空转消耗CPU _, payload r.brpop(face:recognize, timeout5) if payload is None: continue msg json.loads(payload) image_bytes base64.b64decode(msg[image_b64]) # 降到统一尺寸再做检测避免大图拖慢速度 image face_recognition.load_image_file(io.BytesIO(image_bytes)) locations face_recognition.face_locations(image, modelhog) if not locations: r.lpush(face:result, json.dumps({task_id: msg[task_id], status: no_face})) continue # 提取当前帧的人脸特征 encodings face_recognition.face_encodings(image, known_face_locationslocations) # match_face 内部会从数据库批量载入所有人员特征输出 best_match_id 和 confidence best_id, confidence match_face(encodings[0], threshold0.45) result { task_id: msg[task_id], person_id: best_id, confidence: round(confidence, 4), status: allow if best_id else deny, } r.lpush(face:result, json.dumps(result))参数说明BRPOP的超时设5秒是在“空队列时释放进程”和“阻塞等待的及时性”之间取折中如果设0就是永久阻塞进程退出时不好收场。threshold0.45是初始值我建议你先在项目现场采集50组正样本本人和50组负样本其他人画出置信度分布直方图再定值不要照抄别人的参数——不同摄像头、不同光线下的特征分布差距很大。4.3 比对逻辑与置信度分数阈值怎么定face_recognition库的face_distance返回的是欧氏距离数值越小越相似。很多人直接把阈值设为0.6结果就是误识别不断。我推荐的做法是把距离转成相似度分数再调阈值视觉效果更直观展示在管理后台也容易解释。# recognition/matcher.py import numpy as np def similarity_from_distance(distance, min_val0.0, max_val1.0): 把欧氏距离映射到 0~1 相似度。 distance 为 0 表示完全一样映射后接近 1。 return 1.0 - (distance - min_val) / (max_val - min_val) def match_face(query_encoding, threshold0.45, top_k3): 在全量人员特征库中寻找最相似的人脸。 返回 (best_person_id, best_similarity) # 从数据库批量读取所有人员的 face_encoding 并反序列化 from access.models import Person persons Person.objects.filter(is_activeTrue).exclude(face_encodingNone) if not persons: return None, 0.0 known_encodings [] person_ids [] for p in persons: known_encodings.append(np.frombuffer(p.face_encoding, dtypenp.float64)) person_ids.append(p.id) # face_recognition 的 compare_face 用欧氏距离0.6 是官方推荐阈值但要看环境 distances face_recognition.face_distance(known_encodings, query_encoding) best_idx int(np.argmin(distances)) best_similarity similarity_from_distance(float(distances[best_idx])) if distances[best_idx] threshold: return person_ids[best_idx], round(best_similarity, 4) return None, round(best_similarity, 4)参数说明这里threshold直接传的是距离阈值。官方0.6这个值在门禁场景偏松大部分项目控制在0.45~0.55之间。如果现场出现了“换个人也能开”的事故优先把阈值往下调如果出现了“本人经常被拒”往上调。这个来回调参的过程是门禁项目里最耗时的部分没有捷径。4.4 门禁控制逻辑识别结果怎么变成开门动作识别通过后并不等于直接给门禁机发开门信号——门禁控制不是软件里一个if result allow就完事还要检查设备在线状态、防重放、逃生门联动这些物理逻辑。常见做法是Django把通过结果写入AccessLog然后通过HTTP或串口向门禁控制器发送一个短脉冲信号。模拟环境里通常用GPIO控制继电器或者用ONVIF协议调用网络门禁设备。# access/views.py 节选 import requests from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from .models import AccessLog, AccessDevice csrf_exempt def access_callback(request): 门禁一体机上报识别结果的回调接口。 请求体示例{task_id: ..., status: allow, person_id: 1, confidence: 0.92} data request.json # 幂等处理同一 task_id 重复上报不重复开门 if AccessLog.objects.filter(detaildata.get(task_id)).exists(): return JsonResponse({code: 0, msg: duplicate}) device AccessDevice.objects.filter(pkdata.get(device_id)).first() if device is None or not device.is_online: return JsonResponse({code: 1, msg: device offline}) # 触发开门信号这里调用门禁控制器的 HTTP 接口 controller_url fhttp://{device.device_ip}/open_door try: resp requests.post(controller_url, json{channel: 1, duration: 3}, timeout2) resp.raise_for_status() except requests.Timeout: return JsonResponse({code: 1, msg: door controller timeout}) AccessLog.objects.create( person_iddata.get(person_id), device_iddevice.id, resultallow, confidencedata.get(confidence, 0.0), ) return JsonResponse({code: 0, msg: ok})逻辑说明先幂等防重放再检查设备在线状态最后通过控制器接口开门并落库。这个顺序不能变——如果先落库再调用控制器控制器超时时记录显示“已开门”实际没开事后审计对不上。参数说明duration3表示门锁继电器吸合3秒这个值要根据现场的门磁反馈时长来调整太短门还没推开就锁上了太长有尾随风险。5. 避坑记录人脸识别门禁项目里最常翻车的5个地方5.1 摄像头在服务器上打不开程序直接崩现象代码在Windows笔记本上跑得好好的一部署到Ubuntu服务器就报Camera index out of range或画面全是雪花噪点。原因本地的cv2.VideoCapture(0)在服务器上不成立——服务器没有显示器也没接USB摄像头索引为0的设备根不存在或者服务器用的是网络摄像头要走RTSP地址而不是索引。解决把所有摄像头连接方式统一抽象成配置项优先使用RTSP流。改一行配置就能从本地测试切换到远程摄像头# recognition/camera.py import cv2 def get_camera_source(config): 根据配置返回 (VideoCapture对象, 是否成功) source_type config.get(type, local) if source_type rtsp: cap cv2.VideoCapture(config[rtsp_url]) else: index config.get(index, 0) cap cv2.VideoCapture(index) if not cap.isOpened(): return None, False # 设置分辨率分辨率越高检测越慢门禁场景 640x480 足够 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) return cap, True提示远程调试RTSP摄像头时先拿ffprobe确认流地址有没有鉴权参数很多海康和大华摄像头需要?usernamexxxpasswordyyy这种追加参数地址错误时OpenCV不会给你明确报错只会一直返回空帧。5.2 Django的STATIC配置让人脸图片全部加载失败现象人员管理页面能看到文字数据但头像、抓拍图全部破图。浏览器F12里显示GET /media/faces/2025/03/xxx.jpg 404。如果是一个月前下的旧版源码包多数情况是因为下载包解压后media/目录根本不存在——Django的ImageField只负责生成相对路径不会自动创建目录。解决手动建立目录并确保Web进程有写权限另外本地开发时务必确认urls.py里挂了static()路由不然Django不会理会MEDIA_URL。假如你用Nginx部署还需要在nginx配置里加一段location /media/ { alias /path/to/your/media/; }注意alias后面的路径末尾斜杠要精确匹配少一个斜杠就会多出一层目录这是我见过最多的低级翻车现场。5.3 dlib安装慢到怀疑人生编译中断现象pip install face_recognition直接触发cmake编译dlib在低配机器上编译能持续十几分钟甚至报Killed内存不足错误。原因face_recognition库依赖dlib的C代码它的特征提取模型需要编译优化内存不够时GCC被系统OOM杀死。解决不要在裸机环境直接pip安装。先装cmake和g然后设置编译参数降低优化级别sudo apt install cmake g build-essential # 设置低内存编译选项避免OOM被杀 export CMAKE_BUILD_PARALLEL_LEVEL2 export CXXFLAGS-O2 -DNDEBUG pip install --no-cache-dir face_recognition如果我给业务机器装更推荐直接用docker pull带dlib预编译的环境镜像省掉一整个下午的编译时间。5.4 识别慢不是CPU不行是你的推理阻塞了Django进程现象门禁识别功能能用但识别的时候访问后台管理页面卡到怀疑人生。原因你在Django的请求处理线程里直接调用了face_recognition.face_encodings这个函数会把整个worker进程的CPU吃掉其他请求排队表现就是全站卡死。这在单worker的Django开发服务器上尤其明显。解决按第4.2节的方式把推理任务移到独立进程Django只负责把任务放队列并轮询/订阅结果。如果你的项目没有引入Redis也可以退一步用subprocess把识别逻辑跑成外部命令但开发和测试成本更高——我建议项目一开始就用Redis队列模式。# 部署时启动两个进程 python manage.py runserver 0.0.0.0:8000 python recognition/tasks.py5.5 阈值不当导致“换个人也能开门”的事故现象拿A的照片去刷门禁结果系统识别成了B并放行。原因阈值设得太宽比如直接用0.6或者注册照片质量太差导致A和B的特征距离本来就近。有些项目还会出现一个批次注册员工后特征互相混淆——工牌照片被美颜处理过、侧脸角度不同、注册时用了两张不同环境下的照片特征一致性本身就差。解决做一次阈值标定。收集现场真实场景的正负样本算一遍距离分布把阈值定在正负样本分布曲线的“最大间隔处”。具体做法每名员工现场拍3张不同角度照片两两计算特征距离记录同人最大距离和异人最小距离阈值取这两个值的中间值。这一步不能省是门禁系统防误识别的最后一道关。6. 进阶给门禁系统加一个活体检测命令挡住照片和视频攻击普通face_recognition只能证明“画面上有脸”不能证明“画面上是活人”。实际部署时你会发现用一张手机照片在摄像头前晃一下有些系统直接就放行了——这在验收演示时相当尴尬。加一个最简单的活体检测手段要求被识别者在检测期间眨一下眼。思路是连续读取若干帧用OpenCV检测人眼区域并计算眼睛开合度EAREye Aspect Ratio如果一段连续帧中有“从开到闭再到开”的过程就判定为活体。配合人脸识别比对一起用照片攻击基本就能挡掉了。# recognition/liveness.py import cv2 import numpy as np def eye_aspect_ratio(eye_landmarks): 计算眼睛开合度EAR 越小表示眼睛越接近闭合 p2_p6 np.linalg.norm(eye_landmarks[1] - eye_landmarks[5]) p3_p5 np.linalg.norm(eye_landmarks[2] - eye_landmarks[4]) p1_p4 np.linalg.norm(eye_landmarks[0] - eye_landmarks[3]) return (p2_p6 p3_p5) / (2.0 * p1_p4) def detect_blink(frame_gray, face_rect, predictor): 在指定人脸区域内检测眨眼返回是否检测到一次眨眼 landmarks predictor(frame_gray, face_rect) # 左右眼的68点关键点索引左眼36-41右眼42-47 left_eye landmarks.parts()[36:42] right_eye landmarks.parts()[42:48] left_ear eye_aspect_ratio(np.array([(p.x, p.y) for p in left_eye])) right_ear eye_aspect_ratio(np.array([(p.x, p.y) for p in right_eye])) ear (left_ear right_ear) / 2.0 # EAR 低于阈值表示闭眼这里阈值0.2是经验值戴眼镜场景需重新标定 if ear 0.2: return True return False逻辑说明这只是一个最简版本的眨眼检测实际门禁场景需要用状态机跟踪连续帧中“闭眼状态持续了多久、什么时候重新睁开”只在闭眼帧里返回True很容易被一帧噪点骗过去。参数说明predictor需要加载shape_predictor_68_face_landmarks.dat这是face_recognition模型库里的配套文件EAR阈值0.2是实验室环境经验值在强光和戴眼镜场景下可能漂移上线前要重新采集标定。最后聊两句我的习惯做这类项目我从来不会直接信一个“源码包跑通就行”而是先打印出所有日志落到文件记录每次识别请求的耗时、置信度和比对到的人员ID再攒三天数据回看阈值是否合适——门禁这种东西出一次事故的代价远大于调参花的那几天时间。希望这篇能帮你把这个方向真正落地少走几趟我走过的弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网