口罩识别这两年在各种出入口场景里几乎是标配需求。无论是园区闸机、学校门口,还是公共场馆,都会碰到同一个问题:能不能用摄像头自动判断谁没戴口罩,并且实时提醒?这类系统背后的核心,就是用深度学习做目标检测,先定位人脸,再判断人脸区域有没有口罩、口罩佩戴得规不规范。这篇文章记录了我基于深度学习的口罩识别系统从零到一的全过程:数据准备、模型训练、优化压缩、边缘部署,以及现场部署时踩过的坑。内容偏工程落地,很适合正在做计算机视觉实战项目或者毕业设计的同学参考。
很多人以为口罩识别就是图片二分类:输入一张图,输出“戴了/没戴”。但真正在现实场景里跑过就知道,摄像头拍到的画面往往是一堆人、侧脸、低头、逆光,目标有大有小,还有人用手捂嘴、口罩挂在下巴上。所以完整的方案必须是“目标检测 + 状态判断”的结合。我最终选用的技术路线是 YOLOv5s 做人脸定位,再结合类别输出和业务后处理判断口罩状态,在边缘设备上做到实时告警。这篇文章会把每一步的关键细节、参数设置和为什么这么选讲清楚,尽量减少你自己摸索的时间。
1. 项目整体设计与方案选型
1.1 需求拆解:先想清楚“检测什么、在哪跑、怎么联动”
动手写代码之前,最该做的事情是拆需求。对于一个出入口场景,算法要在视频流里实时检测所有人的人脸,并且判断口罩状态。状态绝对不是只有“有”和“没有”两种,还必须覆盖“口罩挂在下巴上”“露出鼻子聊天”这类不规范佩戴,否则提醒形同虚设。我把类别定义为三类:mask(规范佩戴)、no_mask(未佩戴)、bad_mask(佩戴不规范)。不要小看这个定义,它直接决定了后面的数据标注、训练和后处理逻辑。
接下来要明确运行环境。如果模型跑在服务器上,随便上一个大的检测网络都行;如果是嵌入式盒子,算力有限,就必须做取舍。我这次的目标设备是一块常见的边缘计算板卡,算力大概在几TOPS级别,要求视频流处理速度不低于10 FPS,最好能到25 FPS,异常情况下还要联动现场语音或者闸机。因为算力受限,一开始就排除了特别重的模型,把方案锁定在轻量级目标检测模型上。
最后是业务联动需求:检测到未佩戴时要告警,检测到佩戴不规范时也要告警,规范佩戴不响应。告警不能只看单帧,需要连续几帧都异常才触发,否则人员快速经过会导致误报风暴。这些需求都定了,再去选模型,才不会走弯路。
1.2 技术选型:为什么最终选了YOLOv5s
目前做口罩检测可以走的路不少,我简单梳理一下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Haar/OpenCV人脸检测+图像分类 | 实现简单,适合跑通Demo | 复杂场景鲁棒性差,分类不准 |
| MediaPipe人脸检测 | 速度快,能出人脸框和关键点 | 口罩状态判断要额外接模型,流程复杂 |
| YOLOv5/YOLOv8 | 检测分类一步到位,部署工具链成熟 | 需要一定数据量和训练经验 |
| PP-YOLO等其它检测框架 | 精度上限可能更高 | 部署链路不一定通用,社区资料相对少 |
我的选型逻辑很直白:口罩区域在画面里通常是中小目标,模型需要足够强的特征提取能力;同时模型要能导出ONNX,再转换到TensorRT或者OpenVINO,方便在不同硬件上运行。YOLOv5s的模型体积小、平均精度够用、网上资料多,改起来方便,所以优先选了它。YOLOv8检测精度确实高一点,但我在折腾TensorRT部署时遇到更多算子和版本兼容问题,需要解决的问题更多。如果你的算力比我这还紧张,可以考虑YOLOv5n;如果精度优先,YOLOv8s也没问题,整体思路是一样的。
选定YOLOv5s之后,我设置输出三个类别:mask、no_mask、bad_mask。实际场景里有一种特殊情况很烦人:人手捂住嘴。模型在no_mask和bad_mask之间来回跳,单靠网络很难完美处理。这个我在方案选型时就做好了心理准备,留给业务后处理用视频时序和连续帧规则去兜底。
1.3 系统整体框架与模块划分
系统不是一个孤立的检测脚本,而是一套流水线。我把它拆成了六个模块:视频接入、目标检测、状态判断、事件聚合、告警联动、可视化。视频接入负责RTSP或者本地视频解码;目标检测模块负责输出人脸框和类别;状态判断模块根据置信度和业务规则输出最终状态;事件聚合模块负责多帧稳定判断;告警联动模块在发现异常时发送HTTP请求或者触发现场语音;可视化模块负责把检测框叠到画面里。
这样拆的好处是,训练阶段只需要关注模型文件,部署阶段可以独立调试每个模块。所有阈值参数,比如置信度、类别映射、感兴趣区域,我都放到同一个配置文件里。现场客户反馈“误报多”或者“漏检多”时,我直接改配置文件让现场远程验证,不用重新编译程序。这个习惯在后来的联调阶段省了很多时间。
2. 数据准备与标注细节
2.1 数据集怎么来:公开数据打底,自采数据补场景
开始训练前,我找了一些公开的口罩检测数据集作为基础。这类数据通常包含不同光照条件下的人脸和口罩标注,有蓝色口罩、布口罩、N95,也有不少未佩戴口罩的人。但直接拿来用有个问题:数据类别定义不完全统一,有的只有“戴口罩”和“不戴口罩”两类;很多图片是自拍或者社交媒体照片,摄像头视角和出入口实际画面差别很大。如果只用公开数据训练,部署到闸机场景时表现往往很差。
所以我额外补了大约2000张现场视角数据。把摄像头架在门口一侧,模拟白天、逆光、夜晚红外补光、阴天以及人员密集等情况。采集这件事别想着一口气做完,最好分几天采集,不同时间的光照差异会让模型学到更稳定的特征。采集时还要注意脱敏,我处理后的图只保留人脸区域,不在公网上存原图。
数据清洗也花了些功夫。对于人眼都看不清的模糊图、人脸占比过小的图、严重遮挡的图,我直接删掉。公开数据集也做了统一过滤,把类别映射成我需要的三类。这里有个要注意的:有些公开数据集会把“口罩挂在下巴上”标成“戴口罩”,这在业务里绝对不行,必须人工跑一遍筛出来改标签。
2.2 标注策略:三类目标怎么标,才有稳定的模型
我用的标注工具是labelImg,先标成Pascal VOC格式,再统一转换成YOLO需要的txt格式。标注规则很关键,我给自己定了几个硬性要求:
- 标注框必须紧贴人脸外轮廓,不要把额头或者头发包太多进去。
- 如果一个人出现在多张图里,模糊程度不同的样本都要保留,不能只留清晰的那张。
- 人脸被遮挡面积超过50%不标,标了只会给模型增加噪声;但如果遮挡部分是口罩,仍然按口罩标注。
- 遇到透明面罩、挡风面罩这类特殊情况,我会根据业务规则统一归为bad_mask,因为普通摄像头很难判断透明面罩下面到底有没有口罩。
类别不均衡在自采数据里非常明显:大部分人规范佩戴口罩,没戴和不规范样本很少。我没有硬调损失函数,而是通过复制少数类样本并配合随机增强,把三类比例控制在大概5:3:2。实测证明,比强改类别权重简单有效得多。难例图片,比如手捂嘴、低头只看到额头,我单独放在一个文件夹,后续做增量训练的时候再用。
2.3 数据增强和标签抖动
训练时我用了YOLOv5自带的数据增强策略,包括mosaic、mixup、HSV扰动、随机旋转、平移和缩放。mosaic增强对提升小目标检测能力很有用,口罩人脸在闸机视角下经常很小,这个增强基本是必开项。
但增强不是越多越好。我在实验里发现,旋转角度超过10度时,模型会把原本正常的侧脸旋成奇怪的角度,反而导致不稳定。最终我把角度限制在±5度。HSV饱和度扰动也不能调太大,否则会把口罩的蓝色、白色调偏,分类错误率上升。这种细节很多教程不会提,但实际调参时非常关键。
我通常分两版对比:开mosaic和关mosaic各跑一轮。开mosaic的mAP@0.5一般要高2-3个百分点,但在小目标多的场景训练时间更长。最后几十个epoch我会把mosaic关掉,用正常增强微调一下,精度还能回一点,模型也更稳。
3. YOLOv5模型训练与优化
3.1 训练环境与参数选择
我的实验环境是Python 3.8、PyTorch 1.12.1、CUDA 11.3,显卡显存12G。训练代码直接用的是YOLOv5 6.0版本,下载COCO预训练权重作为backbone初始化。之所以坚持用预训练权重,是因为口罩检测的人脸特征和COCO里的人物类别多多少少有点关系,迁移学习可以让模型收敛更快,尤其在我这种自采数据量只有2000张的情况下。
训练命令大致长这样:
python train.py --data mask.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 120 --device 0参数怎么定要解释一下:
- --img 640:输入分辨率640x640。口罩人脸在画面里尺寸小,用416容易漏检,用640综合效果最好。硬件更弱就用480。
- --batch 16:12G显存下已经非常接近上限,再大就OOM。想跑更大batch,可以开梯度累积,效果类似。
- --epochs 120:配合早停机制,验证集mAP长时间不涨会自动停。
- 超参数文件用了微调配置,只调了初始学习率和权重衰减,其余保持默认。
训练时我重点看mAP@0.5和各类别AP。实际跑到大概60个epoch,mAP@0.5就到了0.93;120个epoch停下来是0.95左右。bad_mask类别的AP最低,大概0.87。原因也清楚,“露出鼻子”和“规范佩戴”在视觉上差异实在太小,模型只能靠口罩边缘位置推断,难度确实大。
3.2 过拟合和类别不平衡处理
自采数据只有2000张,很容易过拟合。我的对策除了迁移学习和数据增强,还有就是把公开数据里光照差异大的样本混进来,验证集也特意放了现场不同时间段拍的图,避免模型只学到某种光照的“捷径”。训练时我会同时看训练损失和验证损失:如果训练损失持续下降而验证损失开始反弹,就是过拟合,要调整增强力度、减少epoch或者加些dropout。
类别不平衡导致的误判,靠推理时调阈值效果有限,问题根源还是在数据分布上。我的做法是通过数据层面的过采样和增强均衡样本比例。如果你不想动数据,也可以在YOLOv5的自定义数据集配置里增加no_mask类别的损失权重,但要改训练代码,调起来不如数据直接。
3.3 模型压缩与导出:ONNX到TensorRT
训练完的pt权重不能直接用于边缘部署,我先把它导出成ONNX格式:
python export.py --weights best.pt --img 640 --batch 1 --include onnx拿到ONNX之后,再转成TensorRT引擎。我用了fp16精度来跑GPU推理,速度和fp32相比几乎提升了一倍,精度损失在可接受范围内。转换时注意固定输入尺寸,尽量别用动态输入,动态尺寸会带来额外开销。如果是CPU设备,我会优先考虑OpenVINO,同样能导出一个IR模型。
我没有做严格的模型剪枝,因为YOLOv5s本身参数不多,只有大概7M,对这块边缘板卡已经比较友好。如果要求同时跑更多路视频,可以考虑结构化剪枝,把C3模块的通道数砍掉30%,精度损失通常在1-2个点以内,但需要重新微调。这个测试我做过,最终没有上线剪枝模型,原因是流水线的CPU抢占问题比模型大小影响更大。
4. 系统部署与工程集成
4.1 从视频流到推理结果的完整流程
部署阶段最容易被低估的是视频解码。一开始我用OpenCV的VideoCapture拉RTSP流,CPU占用高,网络一抖画面就卡,整个检测链路都被拖慢。后来换成GStreamer取流,并把视频帧直接转成推理需要的张量格式,情况好了很多。如果你的设备CPU吃紧,用FFmpeg加多线程取流也是不错的选择。
主流程是:取流线程不断拉帧,放进环形缓冲;推理线程取帧做letterbox缩放和归一化,送入TensorRT引擎得到检测结果;后处理线程解析输出,做NMS和类别过滤,再把结果交给事件模块。这种流水线设计比单线程循环有本质提升,推理速度从不到10 FPS提升到25 FPS以上。当然,要控制队列长度,处理不过来时宁可丢帧也不要追帧,否则延迟越来越大。
推理部分我一开始用Python写作。后来发现GIL对多线程性能有影响,就把预处理里的resize和归一化、后处理里的NMS用Cython/C++重写了接口,整个链路的CPU占用下降了一截。如果你不想碰C++,可以只用PyTorch的TensorRT接口,也能把大部分计算放到GPU上,效果尚可。
4.2 业务规则:置信度、NMS和事件联动
模型输出三个类别,我根据置信度取最高那个,但阈值设定很有讲究。最终我把conf_thres设为0.45,NMS IoU设为0.5。这个值不是随便定的。conf太低,闸机边框、宣传海报上的人脸都会被当成真人;conf太高,戴深色口罩或者人脸比较小的时候漏检率上升。我做过一次阈值扫描,发现0.45到0.5之间是当前场景的均衡点。
事件联动部分要特别注意“稳定判定”。单帧检测到未佩戴口罩时先不告警,而是等连续3帧都检测到no_mask或者bad_mask,才触发一次告警。这相当于一个时间滤波,把短时的误检和瞬时遮挡过滤掉。告警内容会附带截图和时间戳,通过群机器人Webhook推送给值班人员。如果要联动闸机,还要等目标框越过一条“警戒线”后再做判断,避免远处的人还没走到闸机就被误拦。
4.3 性能数据的评估与调优
部署完成后,我记录了几组典型数据:边缘GPU上fp16推理一帧约30ms,CPU上OpenVINO大概50-70ms一帧。按这个数据,单路视频流畅跑没问题,但多路视频就要小心。逆光时漏报率最高,我加了一个“亮度自适应”预处理:整帧平均亮度低于阈值时,先做自适应直方图均衡化再送模型。虽然多花几毫秒,但漏报率下降明显。
多摄像头共用一台机器时,我建议用抽帧检测加并发控制。比如5路视频每1帧检测1次,或者4路视频每2帧检测1次。我在项目里做了摄像头优先级调度,重要卡口检测频率更高,普通走廊降低频率,整体吞吐稳定了很多。
5. 实战中踩过的坑与排查技巧
5.1 误检集中在哪几种情况
我把现场跑出来的误检整理成三类。
第一类是人手捂住嘴。模型在no_mask和bad_mask之间反复横跳,因为人脸框里没有口罩纹理,而嘴部区域又被手遮住。想彻底解决很难,最有效的办法是采集大量手捂嘴的样本做难例挖掘。另外还可以依靠视频时序,手不可能一直捂嘴,连续几帧判断能缓解不少。
第二类是后脑勺或者背影被检测成人脸。训练数据里侧脸和背影混在一起时容易这样。想做得稳,可以加一个人脸关键点模型校验五个关键点;不想增加模块,也可以用检测框宽高比和位置关系做简单过滤,虽然不完美,但能过滤一部分。
第三类是反光玻璃、广告牌上的人像被识别成真人。这种静态误检设置ROI就能解决,让模型只关注门口实际通行区域,同时结合背景建模或者运动检测,只有运动区域里的检测结果才有效。实测误报率下降了一半以上。
5.2 漏检与分类错误排查思路
漏检大多数是小目标问题。我排查时先看原图分辨率,再看letterbox之后检测框面积。如果检测框面积小于1600像素,基本不可靠。针对这种情况,要么把输入尺寸提高到768,要么把ROI区域裁剪出来单独放大检测。闸机近距场景我直接只检测门口ROI区域,相当于在输入端变相放大目标,效果立竿见影。
分类错误最典型的是bad_mask。比如口罩挂在下巴上,检测框包含了下巴和嘴部,类别容易偏向no_mask。我后来重新调整标注策略:把下巴区域尽量框完整,增加口罩下移的样本,再补充一些“露出鼻子”的近景图,AP提升非常明显。如果bad_mask的AP偏低,先怀疑标注一致性,而不是急着换模型结构。
5.3 实战中最好用的三个调试技巧
第一,把模型输出的类别框和原图叠加保存。出现误报时,让现场人员直接把截图发回来,不用远程看视频,沟通效率高很多。
第二,做一个阈值扫描脚本。把每天的测试视频反复跑一遍,从0.2到0.7循环测试置信度阈值,统计误报和漏报曲线,选曲线最低点作为最终阈值。不要凭感觉改参数。
第三,先用小输入尺寸部署跑通全链路,再逐步提高分辨率。很多人一开始就在640分辨率下调来调去,最后发现速度不达标,又得回到416重来。先保证代码链路正确,再追求精度,是最省时间的做法。
我自己的体会是,基于深度学习的口罩识别系统,技术难点往往不在“深度学习”本身,而在数据采集、标注一致性和工程流水线。把这三个方面做扎实,模型精度自然就稳了,后面再谈性能优化和业务联动也才有基础。