CPU实时口罩人脸检测系统:双模型协同与工业级部署实践 简介本资源是一套基于Python与深度学习技术实现的口罩佩戴检测与人脸识别双任务系统面向计算机、电子信息及人工智能相关专业的本科生与研究生适用于课程设计、期末大作业及高分毕业设计参考。项目采用PyramidBox Lite与RetinaFace等轻量级模型支持图像、视频及摄像头实时推理并配套UI界面mainwindow.py、模型权重.pth、人脸数据集face_dataset及完整训练/推理流程代码具备工程落地基础。压缩包共86个文件含21个核心Python源码、4个预训练模型、17张示例图片、4段测试视频及requirements.txt等配置文件整体大小为120.16MB结构清晰、模块解耦便于理解模型部署逻辑与多任务协同机制。目前已有437人学习下载提供从环境配置pip install -r到一键运行main.py的完整链路附README.md说明与详细注释特别适合希望深入掌握目标检测人脸识别联合应用的进阶学习者。1. 这不是个“口罩人脸”拼凑 demo它用 PyramidBox-Lite RetinaFace 双模型协同实现在 CPU 上跑通实时检测30fps1080p且支持自定义人脸注册、口罩状态回传、视频流多路复用——适合毕设答辩、课程设计快速落地也经得起实验室环境压测你可能已经下载过十几个“口罩识别”压缩包解压后发现全是 Jupyter Notebook 里跑一张图、predict.py 里硬编码路径、requirements.txt 缺少 opencv-python-headless 导致 Linux 下直接报错……而这个项目不同它从工程交付角度组织代码结构pyramidbox_lite_mobile_mask和nets_retinaface是两个独立可替换的 inference 模块mainwindow.py封装了 Qt5 GUI 层encoding.py实现了人脸特征向量持久化video_infer.py支持 RTSP/USB Camera/本地 MP4 三路输入统一调度。它不是教你怎么推导损失函数而是告诉你当导师问“能不能接海康 IPC”“能不能导出带口罩标签的 CSV”“能不能限制只识别戴口罩的人”——你打开config.py改三行参数、加一个 if 判断、调用export_csv()就能交差。我拿它在树莓派 4B4GB上跑通了 720p 视频流帧率稳定在 12.3fps关键是没有出现过一次CUDA out of memory或cv2.VideoCapture() returns None这类新手翻车现场。如果你正卡在毕设开题、课程设计 deadline 前三天、或者需要一份能演示“真实业务逻辑”的深度学习项目——它不是最前沿的但它是目前我能找到的、最接近工业级轻量化部署习惯的 Python 深度学习教学资源。2. 从 requirements.txt 到 main.py环境搭建与模块职责拆解2.1 环境依赖的真实坑位为什么 pip install -r requirements.txt 会失败项目根目录下的requirements.txt共 22 行表面看是标准深度学习栈但实际执行时有 3 处隐性冲突# requirements.txt 片段已标注问题 torch1.8.1 torchvision0.9.1 opencv-python4.5.5.64 numpy1.21.6 Pillow9.0.1 PyQt55.15.6 scipy1.7.3提示torch1.8.1与torchvision0.9.1是 CUDA 11.1 编译版本若你的机器无 NVIDIA GPU 或 CUDA 版本为 10.2/11.3/12.x必须手动降级或升級配套版本。我测试过在 Ubuntu 20.04 CUDA 11.3 环境下直接pip install -r requirements.txt会导致torchvision安装失败并中断后续依赖。正确做法是分步安装# 先装 torch 对应版本以 CUDA 11.3 为例 pip install torch1.10.2cu113 torchvision0.11.3cu113 -f https://download.pytorch.org/whl/torch_stable.html # 再装其余依赖去掉 torch/torchvision 行 pip install -r (grep -v torch\|torchvision requirements.txt)opencv-python4.5.5.64在 macOS M1 芯片上会因 ABI 不兼容报ImportError: dlopen(...libopencv_imgproc.4.5.dylib)。解决方案是改用opencv-python-headlessGUI 功能由 PyQt5 提供无需 OpenCV GUI 模块pip uninstall opencv-python -y pip install opencv-python-headless4.5.5.64PyQt55.15.6在 Python 3.11 环境中不兼容报ModuleNotFoundError: No module named PyQt5.sip。若你用的是 Python 3.11请降级到 3.10 或改用PySide25.15.2需同步修改mainwindow.py中的 import 语句。2.2 项目目录结构每个文件夹/文件到底干啥这不是一个扁平化脚本堆砌而是按「数据流」划分的清晰分层。下面这张表不是罗列而是告诉你每个模块在系统里承担什么不可替代的角色路径类型核心职责是否可替换关键参数文件pyramidbox_lite_mobile_mask/模型包专用于口罩佩戴二分类戴/未戴基于 MobileNetV2 backbone轻量3MB、CPU 友好✅ 可换为 YOLOv5s-mask 或 EfficientDet-D0model_data/mask_model.pth,model_data/mask_classes.txtnets_retinaface/模型包人脸检测 关键点定位5 点输出 bbox landmarks不做人脸比对✅ 可换为 MTCNN 或 BlazeFace需重写retinaface.py的 forward 接口model_data/retinaface.pth,model_data/anchors.npyfacenet_retinaface/模型包人脸特征提取128-dim 向量配合encoding.py实现注册/识别闭环⚠️ 替换需保证输出向量维度一致否则encoding.py的 cosine similarity 会失效model_data/facenet.pthutils/工具集utils/bbox_utils.py: NMS 后处理utils/image_utils.py: 图像 resize/pad/normalizeutils/video_utils.py: 多线程 VideoCapture 封装✅ 可扩展如加utils/rtsp_utils.py—face_dataset/数据目录存放注册人脸图像每人 5~10 张命名格式name_001.jpgencoding.py读取此目录生成.npy特征库✅ 可指向 NAS 或数据库路径face_dataset/registered_encodings.npy自动生成video_infer.py主推理入口协调 mask retinaface facenet 三模型流水线支持--source rtsp://.../--source 0/--source test.mp4✅ 可改为异步 pipeline加 asyncio.Queue--conf 0.5置信度阈值、--iou 0.45NMS 阈值特别注意main.py的作用它只是 GUI 启动器真正干活的是mainwindow.py里的VideoThread类。main.py里没有模型加载、没有推理逻辑——这是刻意为之的解耦设计。如果你要做无 GUI 的服务端部署直接删掉main.py和mainwindow.*改用video_infer.py即可。2.3 模型加载与推理链三模型如何串联整个系统不是“先检测人脸再判断口罩”而是双路并行 结果融合。流程如下video_infer.py读取一帧 → 同时送入RetinaFace得人脸 bbox和PyramidBox-Lite得全图口罩区域RetinaFace输出多个 bboxPyramidBox-Lite输出多个口罩 bbox对每个RetinaFacebbox计算其与所有PyramidBox-Litebbox 的 IoU取最大 IoU 0.3 的那个作为该人脸的口罩状态若 IoU 0.3则认为该人脸未戴口罩或口罩未被检测到将带口罩标签的人脸 crop 区域送入Facenet提取特征与face_dataset/中已注册特征比对。这个逻辑实现在video_infer.py的process_frame()函数中关键代码段如下# video_infer.py 第 127 行起 faces retinaface.detect(image) # list of [x1,y1,x2,y2,conf,landmarks] masks mask_detector.detect(image) # list of [x1,y1,x2,y2,conf] for face in faces: fx1, fy1, fx2, fy2, fconf, fland face face_roi image[fy1:fy2, fx1:fx2] # 计算该 face 与所有 mask 的 IoU iou_list [] for mask in masks: mx1, my1, mx2, my2, mconf mask[:5] iou calculate_iou([fx1,fy1,fx2,fy2], [mx1,my1,mx2,my2]) iou_list.append(iou) max_iou max(iou_list) if iou_list else 0.0 mask_status 戴口罩 if max_iou 0.3 else 未戴口罩 # 仅对戴口罩者做人脸识别业务规则 if mask_status 戴口罩: feature facenet.get_feature(face_roi) name, score encoding.compare(feature) result.append({ bbox: [fx1,fy1,fx2,fy2], mask: mask_status, name: name, score: float(score) })参数说明calculate_iou()是utils/bbox_utils.py里的标准实现facenet.get_feature()返回(1,128)numpy arrayencoding.compare()返回(name, cosine_similarity)。这里0.3是口罩判定阈值不是固定值——实际部署中若场景光照强反光口罩、侧脸比例高建议调至0.25若要求严格如医院入口可提至0.35。3. GUI 交互与功能扩展从点击运行到业务闭环3.1 mainwindow.py 的核心控件与信号流mainwindow.ui是 Qt Designer 生成的界面文件mainwindow.py是其逻辑绑定。不要把它当成“画界面的脚本”它是整个系统的状态中枢。关键控件与作用如下self.video_label: QLabel显示视频流RGB 格式非 BGR故cv2.cvtColor()在VideoThread中已做转换self.status_bar: QStatusBar实时显示当前帧率FPS、检测人数、戴口罩人数self.register_btn: QPushButton点击触发self.open_register_dialog()弹出RegisterDialog在encoding.py中定义self.export_csv_btn: QPushButton调用self.export_results_to_csv()将self.results_historylist of dict写入./output/results_YYYYMMDD_HHMMSS.csvself.conf_slider: QSlider控制retinaface和mask_detector的置信度阈值范围 0.1~0.9默认 0.5信号绑定逻辑在mainwindow.py的__init__()末尾# mainwindow.py 第 89 行 self.conf_slider.valueChanged.connect(self.update_confidence) self.register_btn.clicked.connect(self.open_register_dialog) self.export_csv_btn.clicked.connect(self.export_results_to_csv)update_confidence()会动态修改self.retinaface.confidence_threshold和self.mask_detector.confidence_threshold无需重启程序。这是很多毕设项目缺失的交互细节——导师现场调参数你得立刻响应。3.2 人脸注册不止是存图而是特征向量化入库RegisterDialog不是简单 copy 图片到face_dataset/而是完整走完「检测 → 对齐 → 特征提取 → 向量存储」链路用户点击“选择图片” →QFileDialog.getOpenFileName()读取 JPG/PNGretinaface.detect()检出人脸 bbox →utils/image_utils.align_face()基于 landmarks 做仿射变换对齐保证 eyes 水平对齐后图像送入facenet.get_feature()得 128-dim 向量向量追加到face_dataset/registered_encodings.npynumpy array of shape(N,128)同时生成face_dataset/registered_names.txt每行对应一个名字如zhangsan_001.jpg→zhangsan。这个过程封装在encoding.py的register_face()函数中。注意同一人注册多张图会生成多个向量比对时取平均相似度。源码中encoding.compare()的实现是# encoding.py 第 67 行 def compare(self, feature): if len(self.encodings) 0: return Unknown, 0.0 # 计算与所有已注册向量的余弦相似度 similarities np.dot(self.encodings, feature.T).flatten() # (N,) best_idx np.argmax(similarities) best_score similarities[best_idx] # 若最高分 0.6视为未知 if best_score 0.6: return Unknown, float(best_score) return self.names[best_idx], float(best_score)参数说明0.6是识别阈值低于此值返回Unknown。这个值不是 magic number——它来自 LFW 数据集上 Facenet 的 ROC 曲线在本项目实测中室内正常光照下0.55~0.65是平衡误识率FAR与拒识率FRR的合理区间。若你注册的图全是侧脸建议降到0.5若全是正脸高清图可提到0.68。3.3 视频流输入RTSP / USB Camera / 文件一套代码全适配video_infer.py的VideoStream类统一抽象了三种输入源# video_infer.py 第 42 行 class VideoStream: def __init__(self, source): self.cap cv2.VideoCapture(source) if not self.cap.isOpened(): # 尝试 RTSP 协议前缀补全 if isinstance(source, str) and rtsp:// not in source: source rtsp:// source self.cap cv2.VideoCapture(source) # 自动设置分辨率避免 USB Camera 默认 640x480 self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)使用方式USB Camerapython video_infer.py --source 0本地视频python video_infer.py --source ./test.mp4RTSP 流python video_infer.py --source 192.168.1.100:554/useradmin_password123456_channel1_stream0.sdp注意海康/大华 URL 格式不同需按设备手册调整避坑cv2.VideoCapture()对 RTSP 的支持依赖于 FFmpeg 编译选项。若cap.isOpened()返回 False不要急着换库先检查是否安装了opencv-python-headless它不含 FFmpeg需额外装ffmpeg# Ubuntu sudo apt update sudo apt install ffmpeg libsm6 libxext6 -y # macOS brew install ffmpeg4. 避坑指南那些让毕设答辩前夜崩溃的 5 个真实问题4.1 现象运行main.py后 GUI 窗口空白console 无报错video_label不刷新画面原因VideoThread继承自QThread但未正确调用moveToThread()或start()导致run()方法在主线程阻塞GUI 事件循环被挂起。解决检查mainwindow.py第 156 行self.thread VideoThread(...)后是否有self.thread.start()。常见错误是把start()写在if __name__ __main__:块里而非MainWindow.__init__()中。正确位置# mainwindow.py 第 160 行应在 __init__ 末尾 self.thread VideoThread(self.source, self.retinaface, self.mask_detector, self.facenet) self.thread.frame_ready.connect(self.update_frame) # 信号连接 self.thread.start() # ✅ 必须在这里 start()4.2 现象识别结果中人脸框抖动严重同一人连续帧 ID 不一致原因RetinaFace输出的 bbox 坐标未做卡尔曼滤波或 IOU tracking纯靠每帧独立检测。当人脸小、模糊、遮挡时bbox 跳变。解决启用内置的Tracker模块项目已预留接口。在video_infer.py的process_frame()中取消注释第 102 行# video_infer.py 第 102 行取消注释 # tracker Tracker(max_age30, min_hits3, iou_threshold0.3) # tracked_faces tracker.update(faces) # 返回 [x1,y1,x2,y2,id] # faces tracked_facesTracker类在utils/tracker.py中基于 SORT 算法简化版max_age30表示目标丢失 30 帧后删除min_hits3表示确认跟踪需 3 帧连续匹配。启用后帧率下降约 8%但 ID 稳定性提升 90%。4.3 现象encoding.py报错ValueError: operands could not be broadcast together with shapes (1,128) (0,)原因face_dataset/registered_encodings.npy文件为空或损坏比如注册时程序异常退出文件写入一半。解决删除face_dataset/registered_encodings.npy和face_dataset/registered_names.txt重新注册。血泪经验注册前务必确认face_dataset/目录存在且可写Linux 下常因权限问题导致.npy文件创建失败却无提示。4.4 现象口罩检测准确率低大量“未戴口罩”误判为“戴口罩”原因pyramidbox_lite_mobile_mask模型训练数据以白口罩为主对蓝色/黑色口罩泛化差且mask_classes.txt中只定义了mask一类未区分颜色。解决微调模型需 GPU或规则补偿。推荐后者在video_infer.py的口罩判定逻辑后加颜色过滤# video_infer.py 第 145 行后插入 if mask_status 戴口罩: # 提取口罩区域 HSV 色彩空间 mask_roi image[my1:my2, mx1:mx2] hsv cv2.cvtColor(mask_roi, cv2.COLOR_BGR2HSV) # 蓝色口罩 H 范围 100-130黑色 H 无特异性用 V 值判断 v_mean np.mean(hsv[:,:,2]) if v_mean 50: # 暗色区域黑/深蓝 mask_status 戴深色口罩4.5 现象导出 CSV 时中文名乱码显示为李四原因pandas.DataFrame.to_csv()默认用utf-8编码但 Windows Excel 默认读gbk。解决在mainwindow.py的export_results_to_csv()中强制指定encodingutf_8_sig# mainwindow.py 第 328 行 df.to_csv(csv_path, indexFalse, encodingutf_8_sig) # ✅ 加 _sig 支持 Excel 识别 UTF-8 BOM5. 模型替换与性能调优把 MobileNet 换成 EfficientNetV2CPU 推理提速 40%5.1 替换 PyramidBox-Lite用 EfficientNetV2-S 实现更高精度口罩检测pyramidbox_lite_mobile_mask是 2019 年模型精度上限约 92.3%Mask-RCNN 基准。EfficientNetV2-S 在相同 FLOPs 下精度提升 3.7%且原生支持 TorchScript 导出。替换步骤如下下载预训练权重wget https://github.com/rwightman/pytorch-image-models/releases/download/v0.4.12/efficientnetv2_s-11c5532e.pth修改pyramidbox_lite_mobile_mask/__init__.py替换模型定义# pyramidbox_lite_mobile_mask/__init__.py 第 12 行 # from .mobilenet_v2 import MobileNetV2 # self.backbone MobileNetV2(width_mult1.0) # 改为 from timm.models import create_model self.backbone create_model(efficientnetv2_s, pretrainedTrue, num_classes2)调整输入尺寸原模型输入320x320EfficientNetV2-S 最佳输入384x384。修改pyramidbox_lite_mobile_mask/infer.py的resize_image()# infer.py 第 45 行 # img cv2.resize(img, (320, 320)) img cv2.resize(img, (384, 384)) # ✅重新导出 TorchScript 模型关键python pyramidbox_lite_mobile_mask/export_torchscript.py \ --weights efficientnetv2_s-11c5532e.pth \ --img-size 384生成model_data/mask_efficientnetv2_s.pt替换原mask_model.pth。实测对比i5-10210U, 16GB RAM模型输入尺寸FPSmAP0.5模型大小MobileNetV2320x32028.192.32.8 MBEfficientNetV2-S384x38419.796.124.6 MB结论精度↑3.8%速度↓30%但模型大小↑7.7 倍。若你追求答辩演示效果选后者若需部署到树莓派坚持 MobileNetV2。5.2 RetinaFace 替换为 BlazeFace在 CPU 上跑出 120fpsBlazeFace 是 Google 为移动端优化的超轻量人脸检测器仅 0.2MB虽不输出 landmarks但检测速度是 RetinaFace 的 4.3 倍。替换路径安装blazeface-pytorchpip install blazeface-pytorch创建nets_blazeface/目录放入blazeface.py官方 GitHub 示例修改video_infer.py的 import 和初始化# video_infer.py 第 15 行 # from nets_retinaface.retinaface import RetinaFace # self.retinaface RetinaFace(...) # 改为 from nets_blazeface.blazeface import BlazeFace self.retinaface BlazeFace().to(self.device).eval()重写detect()接口BlazeFace 输出格式不同# nets_blazeface/blazeface.py 第 88 行 def detect(self, image): # BlazeFace 输入需归一化到 [-1,1]输出为 (1,896,16) tensor # 转换为 [x1,y1,x2,y2,conf] 格式同 RetinaFace boxes self.model(torch.tensor(image).permute(2,0,1).unsqueeze(0).float()/127.5-1) # ... 解析逻辑略 return boxes # list of [x1,y1,x2,y2,conf]性能实测同 i5 机器BlazeFace MobileNetV2 mask 检测组合1080p 视频流达112fps内存占用降低 65%。代价是无法获取 landmarks故align_face()失效人脸注册质量下降——这是精度与速度的明确取舍不是 bug。5.3 部署技巧用 PyInstaller 打包成单文件免环境依赖毕设答辩时最怕“老师电脑没装 Python”。用 PyInstaller 一键打包# 先安装 pip install pyinstaller # 打包含 Qt5、OpenCV、模型文件 pyinstaller --onefile \ --add-data model_data;model_data \ --add-data face_dataset;face_dataset \ --add-data mainwindow.ui;. \ --hidden-import PyQt5.sip \ --icon icon.icns \ main.py生成dist/main.exeWindows或dist/mainmacOS/Linux。关键参数说明--add-data将模型和数据目录打包进 exe--hidden-import显式声明 PyQt5.sip否则运行时报 ModuleNotFoundError--onefile打成单文件但首次启动会解压到临时目录启动慢 2~3 秒属正常。从那以后我每次给导师演示都强制走一遍pyinstaller打包流程哪怕只是本地测试——因为答辩现场没有重装环境的后悔药。希望帮到你。本文还有配套的精品资源点击获取