ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CNN口罩检测实战:从数据准备到实时部署的完整指南

CNN口罩检测实战:从数据准备到实时部署的完整指南 1. 这个项目到底要做什么先想清楚再动手1.1 需求来源与目标设定做这个口罩分检器的起因挺实际2020年之后很多公共场所进门都要检查是否佩戴口罩人工盯久了真的会疲劳保安不可能一直盯着每个进门的脸。我当时想如果能在门口放一块屏幕加一个摄像头有人没戴口罩就提示一下多少能减轻点管理压力。于是就有了这篇博文里的项目——用CNN做一个行人是否佩戴口罩的二分类器接上摄像头之后能实时判断人脸区域有没有口罩。先说清楚目标避免做到一半跑偏。我给自己定的验收标准是在一张包含人脸的图片上能够框出人脸位置并判断这个人脸是否佩戴口罩准确率目标定在95%以上。这个准确率指的是被正确识别的测试图片数 / 总测试图片数不是指整段视频流里的表现。为什么只定95%而不是99%因为口罩遮挡情况复杂纯二分类模型做到95%可复现做到99%需要大量脏数据和精细调参对小白第一次上手来说没必要把标准拉满。这个项目是CNN入门系列的第七篇前面六篇已经铺垫了卷积、池化、全连接、激活函数这些基础概念。如果你还没读过前面几篇也不要紧这篇文章会把所有实操细节重新讲一遍代码层面从零到一能跑通。1.2 为什么选CNN而不是传统图像方法有人会问判断有没有戴口罩有必要上CNN吗我用传统图像处理试过当时想用颜色检测——口罩通常是蓝色或白色统计人脸区域里蓝色像素占比超过某个阈值就认为戴了口罩。听起来可行实际一测就翻车白口罩和皮肤颜色太接近蓝口罩在蓝色背景墙前直接失效深色口罩更是完全没辙。传统方法本质上是人手动设计特征而口罩这个目标的外观变化太大——颜色、形状、佩戴角度、遮挡程度都不一样。CNN的思路是把提取特征这件事也交给网络自己学。卷积层自动学习边缘、纹理、色块组合浅层学到的可能是口罩边缘的形状深层学到的可能是口罩覆盖鼻子嘴巴这种抽象概念。这就是为什么这类项目用CNN更合适它不需要我们手动定义什么叫做口罩只需要给足够的样本网络自己会找到判别依据。此外CNN对局部遮挡有天然鲁棒性。口罩识别的核心判别区域是鼻子和嘴巴如果这张脸真的没戴口罩CNN一般能从纹理上区分这里是皮肤还是这里是布料。就算测试者用手捂着嘴模型也容易产生误判这个问题后面单独说。1.3 整体流程与里程碑我建议第一次做这类项目时把流程切成下面几段每一段都有明确的产出方便定位问题出在哪数据准备准备带口罩/不带口罩两类图片统一尺寸划分训练集和验证集。模型搭建用Keras搭一个三层卷积的小CNN输入尺寸128x128。模型训练记录loss和准确率曲线检查是否过拟合或欠拟合。模型评估在没参与训练的真实图片上计算准确率、精确率、召回率并输出混淆矩阵。部署演示用OpenCV调用摄像头对人脸区域做实时推理。为什么把部署放到最后因为很多新手会迫不及待先接摄像头然后发现模型效果奇差根本分不清是模型问题还是人脸框问题。先把离线评估做好确认模型本身靠谱再接摄像头排错就会容易很多。2. 数据集决定模型上限的第一关2.1 数据集选择与数据配比口罩识别属于深度学习中比较成熟的应用开源数据集很容易找到最常见的是Kaggle上的Face Mask Detection数据集里面有几千张带标签的人脸图片分WithMask和WithoutMask两个类别。还有MAFA遮挡人脸数据集虽然主要做遮挡研究但里面的口罩样本也可以用来补充。我当时没有只依赖一个数据集而是采用了公开数据集自己补充拍摄的组合。原因很简单公开数据集里的口罩基本都是标准外科口罩颜色和佩戴方式比较单一真实场景里还有黑色口罩、格子口罩、挂在下巴上的口罩。自己补充拍摄的成本并不高找几个同事当模特戴不同口罩、在室内外不同光线条件下各拍几十张就能显著提升模型在真实场景的鲁棒性。数据配比上我踩过一个坑。初期我只想尽快跑通随便把公开数据集的正负样本按1:1丢进去训练结果在测试集上准确率看起来不错一拿到真实场景就露馅。后来分析发现公开数据集的负样本没有口罩的人脸大多来自人脸识别标准测试集都是正脸、光线均匀的高质量图片而真实场景里大量出现的是侧脸、低头、逆光的人脸。所以后面我做了两件事把补充拍摄的图片混入训练集同时有意增加一些人体但戴了口罩的样本让模型学会识别口罩区域而不是靠整张图的统计特征蒙答案。建议训练集总量控制在5000到8000张两类比例接近1:1。类别不均衡会导致模型偏向多数类比如负样本多了模型会把所有图片都判成无口罩因为这样准确率就已经很高了。如果你的数据本身就是不均衡的可以在loss函数里给少数类加权重或者对少数类做更多数据增强。但最好的办法还是先把数据配比调整到接近均衡。2.2 为什么必须做数据增强数据增强不是锦上添花是必须做。CNN模型动辄几十万个参数训练集的图片数量覆盖不了真实世界的变化。不做增强的话模型很容易把背景特征学进去。我之前就出现过模型把戴眼镜和没戴口罩关联起来的情况因为训练集里没戴口罩的样本大多戴着眼镜——这种笑话式的事故就是数据多样性不足导致的。我的增强策略很保守但有效随机水平翻转、随机旋转不超过15度、随机缩放10%、随机平移、随机亮度调整。为什么不做垂直翻转因为人脸倒过来在实际场景里几乎不存在做了反而会把模型带偏。旋转角度为什么不加大因为口罩位置对旋转非常敏感旋转超过20度后人脸区域会裁到无关内容模型会学到错误特征。使用Keras的ImageDataGenerator可以很方便地做在线增强边训练边生成新图片不占硬盘空间。注意一点不要手动保存增强后的图片来扩充数据集。一是占空间二是在线增强每次迭代都会生成不同变体相当于模型每次看到的都是不一样的样本泛化能力更好。2.3 标签划分与预处理数据准备好了下一步是生成训练集和验证集。很多人会把所有数据混在一起随机打乱然后按比例切分这样简单但有个隐患如果你的补充拍摄图片里同一个人的多张照片被同时分到了训练集和验证集验证集的指标就会被作弊抬高。模型见过这个人只是见过不同的照片验证时当然认得出来。正确做法是按人物ID切分保证验证集中的人和训练集中的人没有重叠。对小白项目来说可能不好操作但至少要保证不同来源的数据集不交叉混入验证集。预处理流程我固定为三步读取图片转成RGB格式裁出人脸区域然后缩放为128x128。这里有一个非常重要的点分类器输入的是人脸区域而不是整张图片。如果直接把整张图片缩放后丢给模型模型会去学习背景、衣服、头发这些特征模型对人脸位置的偏移极度敏感。所以部署时一定要先做人脸检测把人脸框出来再输入模型。至于数据集里的图片如果本身已经是人脸特写可以直接缩放如果不是就要先用OpenCV的人脸检测器把人脸区域抠出来。预处理里还有一个小细节图像归一化。CNN对输入尺度比较敏感统一除以255把像素值归一化到0到1之间。如果有的图片是0到255有的是0到1训练时loss会震得很厉害。我自己踩过这个坑后来都写死在预处理函数里保证每个进来的图片都是同一套处理流程。3. 模型结构从最朴素的CNN搭起3.1 用Keras搭一个三层卷积模型第一次做这个项目我强烈建议不要直接上ResNet50、MobileNetV3这些预训练大模型先用最朴素的卷积网络把流程跑通。网络结构越简单每一步出问题越好排查。我的模型结构如下输入层是128x128x3也就是一张128x128的彩色图片。第一层卷积用32个3x3卷积核激活函数用ReLU后面接一个2x2最大池化。第二层卷积用64个3x3卷积核同样接2x2最大池化。第三层卷积用128个3x3卷积核再接池化。三组卷积之后特征图尺寸会从128x128降到16x16通道数从3变成128。然后展平成一维向量接一个128个神经元的全连接层最后输出2个神经元的softmax层对应佩戴口罩和未佩戴口罩两个类别。从参数量上看这个模型大约在130万参数左右用CPU训练几百张图片一个epoch也只需要十几秒。为什么用3x3卷积核而不是5x5或7x73x3卷积核是实践中最常用的配置两个3x3堆叠能获得和5x5一样的感受野但参数更少、非线性更强。口罩这个任务的特征粒度比较小3x3的局部感受野更合适。展平之前的特征图是16x16x128展平之后是一维向量。这个维度直接决定了全连接层的参数规模16x16x128等于32768个输入特征全连接层128个神经元这一层就有420万参数——比三层卷积加起来的参数还多。所以如果觉得模型太笨重可以先把全连接层的神经元降到64个效果不会差太多。3.2 关于结构设计的三个关键选择第一点每层卷积后要不要加Batch Normalization我建议加。BN能加速收敛还能缓解梯度消失。第一次跑的时候我懒得加训练到第10个epoch时loss还在0.6附近徘徊加上BN之后第4个epoch就降到0.2了。代价是模型多了不少参数但对训练稳定性的提升非常值得。第二点池化用最大池化还是平均池化在我的实验里口罩检测用最大池化效果更好。直觉上的解释是最大池化提取的是区域里最显著的特征对应口罩边缘、纹理这些强响应平均池化则会把特征平均掉导致判别信息被稀释。分类任务通常优先用最大池化。第三点Dropout层放在哪里Dropout一般放在全连接层之前而不是卷积层后面。因为卷积层的参数是共享的过拟合风险相对低全连接层参数量大最容易过拟合。我在全连接层之前加了0.5的Dropout验证集准确率提升了大约1到2个百分点。3.3 为什么要热身训练而不是直接上大模型有读者可能会问直接用预训练模型做迁移学习不是更快吗对但我故意不在第一版就这么干。原因是做技术项目要学会控制变量。第一次跑通时我希望所有环节都是自己可控的——数据加载自己写的模型结构自己能看懂训练脚本自己调的。如果直接用MobileNetV2做特征提取器一旦效果不好你连是数据的问题还是预训练模型的问题都分不清。先跑通小模型可以得到一个工程基线。这个基线告诉你数据质量、增强策略、训练参数大致在什么水平。后面想提升效果再用同样的数据去迁移学习对比才有意义。我自己的实际体验是小模型做到95%准确率迁移学习大概能到98%多出来的这3个百分点用在小场景里并不明显但模型体积和推理功耗却大了不少。这个取舍要做到心里有数。4. 训练过程第一次跑通和第一次过拟合4.1 训练参数与代码训练脚本的核心框架我用Keras写简洁清晰适合展示给刚入门的读者。选择Adam优化器初始学习率0.001损失函数用交叉熵batch size取32。为什么batch size取32太小了梯度更新噪声大loss曲线不稳太大了单次更新太保守训练慢。32对128x128输入和这个规模的模型是一个经过验证的平衡点。训练轮数我设置成30个epoch同时加了EarlyStopping回调如果验证集loss连续5个epoch没有下降就停止训练并恢复最优权重。为什么要加这个回调因为新手经常会看一眼loss发现还没降到底就手动加训练轮数结果无限过拟合。EarlyStopping把训练多少轮这个决策从人手里交给验证集指标更客观。训练途中的日志输出我看三样东西训练集loss、训练集准确率、验证集准确率。不要只看训练集准确率那个数字很容易虚高。4.2 训练曲线解读loss、acc、val_acc我这次训练的实际曲线大致是这样前面3个epoch训练集loss从0.68快速降到0.25左右验证集准确率同步涨到90%附近第4到第12个epoch训练集loss继续缓慢下降验证集准确率在93%到95%之间波动第13个epoch之后训练集loss降到0.05以下但验证集准确率不再明显上涨开始出现小幅回落。这是典型的过拟合信号。训练集loss一直降说明模型有足够强的表达能力去记住训练集验证集指标不再涨说明模型的泛化能力到顶了。此时回想前面章节提到的EarlyStopping为什么设为验证集loss连续5轮不降才停——就是为了在过拟合加深之前截住。关于验证集和测试集的关系这里多说一句。验证集用于调整超参和早停但如果多次在同一个验证集上观察效果并反过来调整参数验证集也会被污染。我习惯额外留出一部分完全不参与训练的测试集只在整个流程跑完后评估一次。用测试集分数作为最终效果的宣称值才不会出现验证集很好、上线就拉胯的尴尬。4.3 踩坑记录验证集loss一直不降怎么办训练中最大的一次翻车发生在第二版数据组合之后。我加入了补充拍摄的几百张图结果验证集loss从第1个epoch就卡在0.6左右准确率始终不超过60%。排查过程如下第一步查看训练集loss发现同样迟迟降不下去说明不是数据划分问题而是模型本身学不动。第二步检查新加入的数据图片尺寸和标注格式发现错误我在手工分割人脸区域时不小心把一批图片的RGB通道顺序弄反了。OpenCV读取图片默认是BGR格式而模型训练时用的Keras预处理接口期望RGB。前几层卷积学到的颜色特征完全错乱梯度更新基本在朝错误方向走。第三步修正之后重跑训练曲线立刻恢复正常。这件事是很好的教训CNN对输入数据格式极其敏感颜色通道错位这种肉眼很难发现的低级错误对模型的影响却是致命的。后来我写了一个独立的数据校验脚本随机抽几十张处理后的图片人工确认通道、亮度、标注都正确后再开训。还有一个更隐蔽的坑类别标签错误。公开数据集的不同来源标注习惯不一样有的标签0代表戴口罩有的标签1代表戴口罩。合并数据集时一定要统一标签映射否则模型会学到混乱的映射关系。我自己合并时写了标签映射表并在训练前打印每个标签对应的样本数量确保两类都符合预期。5. 模型评估与实测脱离数据的第一次考验5.1 混淆矩阵与阈值调整训练结束后我在完全独立的测试集上评估模型。测试集包含800张图其中戴口罩400张没戴口罩400张。直接调用模型预测并统计混淆矩阵得到如下结果预测戴口罩预测未戴口罩总计实际戴口罩38218实际未戴口罩12388准确率是 (382388)/800 96.25%看起来不错。但仔细分析会发现两个问题一是18张戴口罩的图被误判为没戴口罩对应召回率真正例率为95.5%二是12张没戴口罩的图被误判为戴了口罩对应精确率预测为戴口罩中真正戴口罩的比例为96.9%。对于分检场景漏判把没戴口罩的放进去比误报把戴口罩的拦下来要严重得多所以我想让模型更保守一些。调整方法是修改决策阈值。模型的输出是softmax概率默认取两个类别中概率更大的那个。如果我把判定为戴口罩的阈值从0.5提高到0.65那么只有模型非常有把握时才判定为戴口罩其余情况视为未戴口罩相当于把确定性不足的样本一律拉响警报。调整后漏判数量从18张降到9张而误报数量从12张涨到21张。这个取舍在真实分检场景中是可接受的——宁可多拦几个也不能放水。5.2 摄像头实时识别的真实表现离线评估通过后我架起摄像头做实时测试。用的是OpenCV的摄像头读取配合OpenCV内置的前置级联分类器做人脸检测检测出人脸框之后裁剪区域缩放为128x128预处理后输入模型输出结果显示在人脸框上方。第一次实时测试的效果让我印象很深模型在标准正脸、光线均匀的情况下表现很好基本稳定识别但一旦有人侧身走过来或者光线逆光准确率就明显下降。摄像头测试暴露了离线评估掩盖的三个问题第一个问题是人脸检测框不稳定。OpenCV的Haar级联分类器有时会把人脸框框得偏大或偏小框大了会把头发、额头边缘带入分类器框小了会裁掉口罩顶部。我最初的解决方案是保留检测框并外扩15%再裁剪这样一个稳定的框就能覆盖整个中脸区域模型输入的内容更规整。第二个问题是运动模糊。有人快步走过摄像头时画面会有轻微拖影模型的置信度大幅下降。我的处理是在帧上叠加一个简单判断如果模型输出的两个类别置信度差值小于0.2就显示请稍等不强行给出结论。与其在一帧模糊画面上给出错误结果不如让系统在连续多帧中搜集更稳定的判定。第三个问题是遮挡变化。实验中发现用手捂住嘴巴会被模型判断为佩戴口罩因为手和口罩在视觉特征上有相似之处——都覆盖了嘴巴区域纹理都不规则。这个问题属于模型本身的缺陷离线数据里没有这类负样本。解决思路是收集用手捂嘴的图片加入训练集让模型学习区分布料覆盖和手覆盖。但更稳妥的方案是结合多帧时序判断如果覆盖状态在短时间内反复变化说明不是口罩。5.3 应对脏数据的几个小技巧真实场景里大量出现的不是标准人脸而是歪头、低头、侧脸人脸检测框可能只框住半边脸。我实测中发现模型对遮挡超过40%的人脸基本崩盘输出置信度很低。这里有一个通用的技巧不只看最终类别还要看置信度。置信度低于0.8的识别结果一律视为不确定而不是强行归到某一类。分检器的逻辑可以设计成三态输出佩戴口罩、未佩戴口罩、无法判断。无法判断时系统只提醒请面向摄像头不产生放行或少拦的判定避免误伤。另外一个小技巧是给验证集加入难样本。从测试集里找出所有预测错误和置信度接近阈值的样本单独存成一个难样本集合在每次调整模型后都优先看这批样本的表现。这样能快速判断模型改动是真正变好了还是只是在容易的样本上没退步。6. 部署与改进让它成为一个能用的分检器6.1 实时检测逻辑与报警提示最后一步是把训练好的模型部署成本地程序。我用的是OpenCV读取摄像头画面主线逻辑大致如下从摄像头读取一帧画面帧率控制在10到15帧每秒。用Haar级联检测人脸如果画面中没有检测到人脸直接跳过。对每个检测到的人脸框外扩15%后裁剪缩放为128x128做归一化预处理。输入CNN模型得到佩戴口罩和未佩戴口罩的概率。根据设定阈值输出判定未佩戴口罩则在画面上用红框标注并触发报警提示。报警提示的落地方式可以根据场景选择。在PC端演示时我用的是窗口标题栏闪烁加一个简易蜂鸣声如果做成独立设备可以用GPIO控制一个LED灯或者在屏幕上叠加大号文字提示。关键点是报警逻辑必须基于连续帧的稳定结果而不是单帧。我设置的是连续5帧中有4帧判定未佩戴口罩才触发报警这样能过滤掉瞬时误报。部署时还有一个容易踩的坑图像缩放和归一化要和训练时完全一致。我训练时用的是Keras的ImageDataGenerator做预处理部署时如果偷懒直接用OpenCV的resize缩放插值算法不一致会带来细微的像素差异虽然通常不影响结果但遇到边界样本时可能差出几个百分点的置信度足以让判定翻转。6.2 性能优化与小模型推理对于实时分检器推理速度直接决定可用性。我最初的实现在普通笔记本上跑每帧人脸检测加推理耗时约40毫秒25帧一秒看起来够用。但后来我发现一个明显的瓶颈Haar人脸检测在多人同框时耗时暴增单帧耗时可能冲到200毫秒以上。解决办法是缩小检测图像尺寸。把原始帧先缩小到宽度640再做检测和分类推理精度几乎没有肉眼可见的损失帧率却从15帧提升到25帧以上。如果想进一步优化可以考虑把CNN模型转换为TensorFlow Lite格式然后用CPU加速推理。我的小模型转换后体积约5MB在树莓派上单次推理耗时大约80毫秒在嵌入式设备上也算可用的水平。对小白来说这一步可以作为进阶练习但不建议在第一次项目里折腾格式转换和推理框架更换先把流程跑通更重要。6.3 下一步扩展迁移学习、移动端部署当这个简单的分检器能稳定工作之后自然地会想到下一步方向。最大的收益来源是迁移学习我后来用MobileNetV2作为骨干网络只保留自己训练的全连接层在同样的数据上准确率从96%提升到98%左右。迁移学习对数据的依赖更小特别适合样本不足的细分场景同时MobileNetV2本身是轻量级结构部署压力不大。另一个扩展方向是加入门禁联动逻辑。分检器的输出不只是显示结果还可以变成一个控制信号比如未佩戴口罩时不允许闸机开门。此时系统设计要考虑的是响应延迟和容错机制最好在门口摄像头稳定检测到人脸并保持2秒后才给出放行动作避免行人匆匆走过造成的误判。最后如果你想让这个项目更有挑战性可以在同一个模型上加一个MTCNN人脸检测模型替换掉OpenCV的Haar级联。MTCNN对侧脸、模糊人脸的检测效果好很多但会引入额外的模型和推理开销这个就留给后续实践去折腾了。我在整个项目里体会最深的一点是跑通一个CNN模型并不难难的是让它在真实场景里稳定工作。数据、预处理、阈值、部署细节每一步都藏着决定成败的小坑。建议你也像我一样把这个拆成离线评估和真实测试两个阶段一步步来别急着一步到位。
返回列表