YOLO26口罩佩戴识别实战:数据标注到边缘部署复盘 上个月我们接到一个园区门禁改造需求技术清单里最不起眼的一项竟是口罩佩戴识别。团队里有人拍胸脯说这是成熟功能拉个二分类检测模型就够用结果真实摄像头画面一进来就翻车口罩拉到下巴、手拿口罩挡脸、背光环境下口罩和皮肤糊成一片明明是一段再普通不过的入园人流模型却给出了大量互相矛盾的框。最后让我们把整个流程跑通的是基于 YOLO26 的口罩佩戴智能识别方案。这篇内容是我们从数据采集、标注、模型训练一路到边缘端部署的完整复盘适合正在做目标检测落地应用、想复现口罩识别项目或者准备把视觉模型装到门禁、安防场景里的同学参考。1. 为什么最终选了 YOLO26先想清楚三个问题定义1.1 把佩戴口罩拆成三种状态而不是二分类大多数人对口罩识别的第一反应是有没有口罩二分类。但真实需求不是这样。园区门禁关心的往往不是人脸区域里是否出现口罩纹理而是这个人能不能进规范佩戴可以进没有佩戴需要提醒口罩挂在下巴或只罩住嘴露出鼻子则需要按管理规则单独处理。如果我们只训一个二分类模型那么口罩拉在下巴这种最常见的违规状态模型大概率会输出positive检测到口罩标签本身就把问题搞错了。所以我在项目一开始就把检测目标拆成三类mask_ok规范佩戴、mask_bad佩戴不规范比如露出鼻子、口罩挂下巴、no_mask未佩戴。这一改动表面上只是标签数量从 2 变 3实际上改变了模型的决策边界。后面所有训练和评估都围绕这三类来做门禁逻辑才能落到具体规则上no_mask直接拦截mask_bad提醒纠正mask_ok放行。还会有额外的定制需求比如运营方要求透明面罩不算口罩、N95 和医用外科口罩都算规范佩戴。这些都必须提前写进标注规范里模型才会按场景规则去学习而不是按图像直觉去学习。问题定义清楚之后算法选型才有讨论基础。1.2 YOLO26 这代解决了我们哪些历史痛点选 YOLO26 之前我们对比过两类方案一类是两阶段检测器精度确实高但推理速度和部署复杂度在边缘盒子上撑不住另一类是近两年讨论较多的 transformer 类检测器在大目标上表现惊艳一旦落到远距离密集小目标场景耗时和显存占用立刻成为突出问题。口罩识别恰恰是小目标为主、需要在普通硬件上实时运行的场景所以我最终回到 YOLO 系列。YOLO26 是 YOLO 系列迭代到这一阶段的较新版本社区里讨论比较集中的改进点是骨干网络在 C2f 结构基础上加入了轻量注意力分支检测头沿用解耦设计同时标签分配策略改成了动态阈值。用我们项目里实际跑下来的感受说它对小尺寸人脸口罩遮挡这类目标的召回率比前几代明显更稳。把人脸区域按 30 到 60 像素估算640 输入下约占 6 个左右的网格放到早期版本里这种目标在下采样过程中很容易被特征层直接吞掉。另外YOLO26 的训练和导出生态比较完整。它保留了预训练权重、迁移学习、ONNX 导出这些成熟链路这对一个要快速落地的团队非常关键。我们不需要从头设计损失函数和检测头而是可以把时间压在数据清洗、现场调试这些真正决定上线效果的事情上——阿里团队常说的80% 的收益来自数据在我这个项目里完全成立。2. 数据集的短板会直接变成模型的短板2.1 数据来源现场采集为主公开集补充为辅很多人做目标检测习惯直接下公开数据集拿来就用。但口罩识别场景高度依赖摄像头安装角度、光线、画质。公开集里的正面大头照再规范放到园区入口的俯拍画面里往往全废。所以我们的数据以现场采集为主在出入口摄像头连续录了一周视频按 5 到 10 帧随机抽样覆盖早中晚、晴天雨天、顺光逆光筛掉大量重复帧之后保留了两万多张可用帧。公开集只做补充主要目的是扩充mask_bad这一类样本。因为现场里不规范佩戴的比例其实不高大多数人都戴得规规矩矩模型很容易把no_mask和mask_bad学成一团。这里我的做法是从公开的口罩检测数据里手动筛出口罩在脖子口罩遮住嘴没遮鼻子用手把口罩拉下来这些画面统一做一次亮度归一化后混入训练集把三类的数量差距控制在 1.5 倍以内。还有一个小技巧把训练初期误检率最高的样本单独存一个hard set。每次迭代模型都重新跑一遍这批图发现哪些框从模糊变得清晰哪些依然乱报逐个去补标注或调阈值。这比盲目加数据高效得多。2.2 标注规范一套统一到人脸框的口径口罩识别项目最容易犯的标注错误是拿口罩的物理位置去画框。比如一个人规范戴口罩有人画出半张脸有人框住整个口罩mask_bad状态下口罩在脖子如果按口罩位置画框模型就会去学脖子上的口罩导致人脸区域漏检。我们团队的统一口径是一律框人脸所在区域口罩区域包含在人脸框内。哪怕口罩遮住了半张脸边界框也覆盖整个头部皮肤可见区域如果人脸几乎完全被帽子、衣领或手挡住无法判断佩戴状态该帧不参与训练。同时设定最小目标尺寸人脸区域小于 24×24 像素的样本直接丢弃因为训练下去只会贡献噪声。多人协作标注时不同人的宽松和严格差异非常大。我们抽查了一轮初标结果部分标签的框中心偏移超过 15 个像素。解决办法是花半天时间对标注员做标准样张培训另写一份包含二十几个边界案例的标注规则文档比如口罩上方露出眉毛算不算规范透明面屏怎么标墨镜遮挡算不算无效样本把这些都钉死在文档里才避免反复返工。2.3 数据增强不是越多越好YOLO 系列本身集成了 Mosaic、MixUp、HSV 扰动、随机平移缩放等增强策略默认参数对通用目标检测表现不错但口罩场景需要重新思考。我最终训练时把 Mosaic 概率开到 0.8MixUp 只留 0.2关闭了直角的 90 度旋转。原因很直接人脸戴口罩是强先验结构旋转 90 度后头朝下、口罩跑到额头的样本在真实场景里根本不存在只会让模型学出奇怪的纹理关联。HSV 扰动也做了收敛。口罩的颜色主要是蓝、白、黑适度变动亮度能模拟不同光环境但饱和度和色相剧烈抖动会让口罩区域的纹理失真反而把模型带偏。数据增强的要义是保持语义不变、尽量扩展变化而不是无脑堆强度。我在训练后期还验证过一个现象始终开启 Mosaic 会让验证集上的小目标精度下降因为 Mosaic 生成的图片里目标尺度分布和真实场景差距较大最后 10 个 epoch 关闭 Mosaic 后指标明显回升。3. 用 YOLO26 训练前我把这些配置一项项钉死3.1 环境与依赖先别急着装最新版训练环境看起来人人会搭但往往是翻车重灾区。YOLO26 的训练框架依赖 PyTorch、CUDA、OpenCV 等一整套组件版本之间偶有兼容问题。我们的经验是不要追求所有库的最新版本而是用文档里对应验证过的组合。第一步用 conda 建独立环境Python 用 3.9 附近稳定版本PyTorch 装与 CUDA 匹配的预编译版本其他依赖按 requirements 安装。如果后面导出 ONNX 遇到算子兼容报错大概率是 PyTorch 和 ONNX 的版本组合问题调整版本而不是改代码通常更有效。硬件方面8GB 显存可以跑通小模型但训练效率和 batch 上限会受到明显限制。我们最终用了一张 24GB 显存的卡做训练。如果你的机器只有 8GB建议直接用n或s规格的 YOLO26 权重关闭大尺寸训练并把 batch 调到 8 以下。3.2 一套能直接复现的配置模板口罩识别项目本质是三分类目标检测。数据配置文件data.yaml长这样path: /data/mask_dataset train: images/train val: images/val nc: 3 names: 0: mask_ok 1: mask_bad 2: no_mask关键就在nc: 3和 names 的类别顺序。训练时保证全流程类别顺序一致导出 ONNX 后后处理才不用额外翻转索引。训练参数我们最终稳定在这组配置上参数取值说明img640输入分辨率默认主流尺寸batch1624GB 显存下比较稳epochs180实际第 120 epoch 左右接近收敛optimizerSGDlr00.01weight_decay0.0005warmup_epochs3防止前期学习率过大导致损失爆炸mosaic0.8训练中期开启close_mosaic10最后 10 个 epoch 关闭 Mosaicmixup0.2只做轻量扰动seed42固定种子方便复现这些参数在 YOLO26 的模型配置文件里都能直接对应。SGD 比 AdamW 在中等数据量下收敛更稳定不会因为自适应学习率把样本量少的mask_bad类推得太激进。3.3 迁移学习冻结骨干怎么冻才划算口罩识别项目没必要从随机初始化开始训练。YOLO26 官方发布的预训练权重已经学过大规模通用物体底层特征边缘、纹理、颜色梯度可以直接复用。我们的做法是加载预训练权重后先冻结骨干网络前 10 层只训练检测头和后面的特征融合层跑 10 个 epoch之后再解冻全部层用较低学习率微调 30 到 50 个 epoch。为什么不能一开始就全网络微调口罩场景下训练集规模通常只有几万张如果从第一个 epoch 就全量更新参数底层通用特征很容易被少量数据带偏最后收敛到只认识这条走廊里的人脸的过拟合模型。先冻结训练相当于把前面的知识固定住只让模型先学会把通用特征映射到三个口罩状态解冻后再慢慢适配真实画面的光线、角度和画质。训练命令大致如下python train.py --data mask_dataset.yaml \ --weights ckpt/yolo26_m.pt \ --img 640 \ --batch 16 \ --epochs 180 \ --optimizer SGD \ --seed 42训练过程中我习惯盯着runs/exp下的曲线和日志而不是只看终端打印的 loss 数值。曲线趋势比单点值重要得多后文会专门讲怎么看。4. 训练期最容易翻车的地方我都替你踩了一遍4.1 Loss 不降先怀疑这几处训练前 20 个 epoch 里 loss 如果完全不动第一反应不应该是调学习率而是回去检查数据和配置。最容易犯的错是数据配置里路径写错导致图片加载全部失败但训练流程没有及时报错日志里显示 loss 一直是同一个值。所以训练脚本跑起来之后第一件事是点开训练日志确认图片数量正确加载、标注框确实出现而不是直接干等。如果数据没问题loss 还是震荡不降我会依次检查warmup 是不是没生效学习率会不会太大导致梯度来回振荡batch 太小也会让 loss 曲线像锯齿一样。最有效的调整方式是按顺序来先把lr0降到 0.001把batch提到 16 或 32再把 Mosaic 关掉试试。如果这样 loss 依然不稳定问题多半在标签上有脏数据比如类别索引越界、框坐标出现负数、某些图被标注成空框这些在 YOLO 训练里都会造成隐性伤害。我还有一个习惯单独写一小段脚本把训练集里的图像和对应标签可视化输出几张图。这一步能同时发现问题标签框有没有跑偏、有没有把整张图都框进去、类别和内容是不是对应。花十分钟可视化能省下后面三个小时的无效调参。4.2 NaN 与显存爆炸训练过程中 loss 直接变成 NaN 是最让人头疼的问题。绝大多数情况是梯度爆炸罪魁祸首是学习率太大或者某个 batch 里出现了极端样本。我碰到过一次奇怪案例损失在某个 epoch 后突然 NaN后来发现是数据集里有十几张全黑的监控帧模型在黑色图像上输出了奇异梯度。解决了这批帧之后训练恢复正常。显存溢出OOM本身不是 bug而是资源配置问题。常见的应对思路是减 batch但我的第一选择是关闭 Mosaic因为 Mosaic 会一次性拼接四张图显存占用和计算量明显大于普通样本。如果关掉 Mosaic 还溢再减半 batch或者把img从 640 降到 480 做快速验证。这里有一个容易被忽略的点混合精度AMP在显存紧张时能省不少空间但也可能带来轻微精度损失。如果项目对精度更敏感建议只在验证阶段开启 AMP正式训练全程使用 FP32。训练过程中我建议每隔几个 epoch 就保存一次权重同时开启自动断电续训。YOLO26 系列都支持从最新 checkpoint 恢复。我吃过大亏训练到 130 个 epoch 机器被强制重启没开续训直接从头再来。从此之后每 10 个 epoch 保存一次权重成了团队标配。4.3 过拟合提前到来小模型的应对策略过拟合在口罩识别项目里非常典型尤其是训练集只有一两万张、模型规格又选得偏大的时候。现象是训练 loss 持续走低验证集 mAP 在前 50 个 epoch 上涨之后开始掉点。很多人第一反应是加 dropout但目标检测模型更有效的工具是增强、早停和数据扩充。我的做法是把早停 patience 设成 20 个 epoch验证集指标连续 20 轮不涨就停。另外一个重要指标是看训练集和验证集之间的 mAP 差距差距超过 8 个点基本就是在过拟合这时候优先补mask_bad类别的样本而不是疯狂调参数。python train.py ... --patience 20训练结束后runs/exp目录里会生成混淆矩阵图和精度曲线这是判断过拟合最直观的方式如果mask_bad类别被大规模误判成no_mask说明这个类别的特征还没有真正被模型分开需要继续补样本而不是加大增强强度。现象可能原因排查顺序loss 恒定不动数据路径错误 / 标签空可视化加载结果loss 震荡剧烈lr 过大 / batch 过小降 lr调 batchloss 变 NaN梯度爆炸 / 极端样本降 lr清洗黑色帧OOM 溢出Mosaic 显存压力大先关 Mosaic再减 batch验证集掉点过拟合补数据开早停5. 模型不只听 mAP 的话评估要看这三组数字5.1 精度、召回、混淆矩阵训练完第一版模型mAP0.5 跑到 0.9 以上团队有人觉得够了。但打开confusion_matrix.png才发现问题no_mask的漏检主要指向mask_ok说明部分人脸在光线下被误判成规范佩戴而mask_bad的漏检大量指向no_mask这是最致命的方向性错误——模型把不规范佩戴当成没戴门禁逻辑就只能放着人进去。mAP 的漂亮数字掩盖了类别间的不平衡单独看 mAP 很容易被骗。评估时必须拆类别看 Precision 和 Recall。我给自己定的标准是no_mask的 Recall 必须最高宁可多拦也不能放过mask_bad对 Precision 的要求更高否则系统天天把规范佩戴的人拦下来提醒用不了三天就会被人骂。这会直接影响置信度阈值的选择比如把no_mask的阈值从 0.25 下调到 0.2同时把mask_bad的阈值上调到 0.35虽然整体 mAP 不变但实际规则效果好了很多。验证集的构成也要讲究。不能简单随机切分因为随机切分会让同一摄像头同一时段的帧同时出现在训练集和验证集造成指标虚高。我按时段机位分层切分保证验证集里有逆光的、夜间的、不同角度的独立画面这样评估结果才接近真实部署。5.2 分辨率与速度的取舍口罩识别的核心痛点之一是小目标。训练和推理分辨率直接影响两个指标小目标检出率和推理速度。我们做过一组对比输入分辨率mAP0.5推理耗时相对值小目标漏检印象3200.841.0严重远距离人脸大量漏检4800.902.2可接受偶发漏检6400.923.5稳定夜间场景更好道理很简单一个人脸框在 640 输入里占 6 个左右网格降到 320 后只占 3 个网格再经过下采样对应特征层可能只剩 1 到 2 个像素几乎不可能可靠检出。所以我宁可牺牲一点速度部署时也保留 480 到 640 的分辨率而不是为了 FPS 盲目降到 320。如果你最后速度还是不够优先优化推理框架和半精度推理不要在输入分辨率上过度让步。6. 部署阶段真正影响上线效果的几个细节6.1 导出与加速分辨率固定是第一原则模型训练完成后要导出成 ONNX 格式部署到边缘设备。导出时最容易犯的错是使用动态尺寸导出把batch和输入宽高都设成 -1。动态尺寸虽然灵活但推理性能通常明显下降。我的建议是部署的输入尺寸固定 640×640导出时直接固定 batch 为 1静态尺寸。如果确实要处理不同分辨率画面在前处理里做 letterbox 缩放把输入统一到 640 的尺寸模型本身不需要动态分辨率。导出成功之后可以在不同推理后端上做精度对照验证。这里有一个细节直接跑导出的 ONNX 和原来 PyTorch 的推理结果会有微小差异因为算子实现不同。上线前要在实际部署设备上重新跑一遍评测集确认端到端指标没有明显下降而不是默认导出后效果不变。部署设备上如果跑不到实时优先开半精度推理。口罩识别这类任务的精度冗余相对充足FP16 基本不损失性能但速度可以提升近一倍。开半精度之前值得先看目标设备是否支持不支持的设备强行开启反而会变慢。6.2 时序抖动单帧判断会发出错误警报单帧检测推到现场必定遇到抖动问题。行人低头看手机、转头、手从脸前挥过都可能造成个别帧误判成no_mask。如果门禁系统按单帧结果弹警报一天能误报几十次。我们的解决办法是做一个轻量级的时序平滑逻辑连续 5 帧里如果 3 帧判为no_mask才触发拦截如果当前帧是no_mask但前后几帧都是mask_ok只当作瞬时扰动忽略。这个策略简单有效代价是检测结果会滞后半秒在门禁场景完全可接受。工程上我们给检测结果打一个标签 置信度 目标 ID的结构体目标 ID 通过相邻帧 IOU 匹配来维护避免同一目标在连续帧里被反复计数。时序平滑的阈值要按现场行为调整。入口通道比较窄、行人移动快时5 帧窗口太大会漏掉快速通过的人通道宽、人流慢的场景窗口反而可以更宽。没有一劳永逸的参数部署后第一个星期就是不断调这个窗口值。6.3 摄像头安装角度比想象中更影响成绩这个结论是我在整个项目里最有价值的一条经验摄像头安装角度几乎决定了模型的天花板。同一套模型装在 30 度俯拍角度时 mAP 明显高于平视安装。平视安装容易被逆光、反光和遮挡干扰人脸区域经常只露出一半俯拍角度能同时拍到额头、眼睛和口罩区域特征更完整。现场布置时建议把摄像头安装在距地面 2.5 米左右、向下倾斜 15 到 30 度让人脸处于画面中上部、高度在整幅画面的三分之一到一半之间。如果门禁上方的摄像头装在 1.5 米平的台子上模型再准也救不回来。这个环节直接影响训练和部署效果排期时一定要提前和施工方确认。另外夜间环境必须有补光否则模型在暗光下会把大量人脸当成no_mask。我们调试后发现红外补光下的模型表现和可见光补光有明显差异最终训练集里加入了一部分红外样本才让夜间指标回到白天水平。现场调试时带着屏幕去看实时检测结果比事后翻日志高效得多。6.4 踩到最后我发现最大的瓶颈不是模型我们在这个项目上最耗时间的环节不是训练参数调优而是反复确认什么算规范佩戴和边界 case 怎么定义。模型本身在两周内就收敛了但现场逻辑和运营规则之间的拉扯持续了整个项目。如果现在有人让我再做一次类似的口罩识别项目我会把更多时间花在数据采集方案和标注规范上模型配置反而可以更快定下来。对你来说如果项目刚起步我最大的建议也是先花一个星期去现场盯素材把摄像头角度、光线、人群行为都记录清楚再回来跑模型。模型解决的是看清的问题而看清的前提是数据里真的包含你要看清的那些场景。