☰
MobileNetV2口罩检测实战:数据准备、模型训练与量化部署
2026/9/26 17:50:43 网站建设 项目流程

简介:一套基于MobileNet v2的口罩实时检测系统完整项目,面向需要部署轻量级目标检测应用的开发者和学习者,适用于公共场所出入口、教室等快速核验场景。系统以Flask作为Web框架,同时提供实时视频流检测与图片上传检测两种使用方式,涵盖从模型训练到Web集成的完整工程链路。压缩包内含48个文件,大小约10.82MB,主要包含Python源码、预训练模型权重(.h5)、网页模板、环境配置依赖、训练图表及演示动图,训练脚本、视频检测器和图片测试脚本均可直接运行或二次改造。已有106人学习下载。通过该项目可以掌握MobileNet v2模型的微调训练方法、深度可分离卷积在实时任务中的应用,以及Flask与深度学习模型协同工作的工程实践,适合具备Python和深度学习基础的中级开发者作为实战参考,也可作为后续迁移到其他检测任务的代码基底。

1. 口罩检测为什么非MobileNet v2不可:实时性背后的选型逻辑

在出入口闸机、教室和园区大门的实际场景里,口罩实时检测的难点从来不是“能不能识别”,而是“在普通摄像头配套的低算力盒子上能不能每帧都跑完”。我接过好几个类似项目,第一版用ResNet50当骨干,离线测试准确率很好看,一上RK3399这类边缘盒子就掉到5帧以内,画面明显卡顿。换到MobileNet v2做骨干后,同样的视觉任务在保持可接受精度的前提下,推理延迟降了一个数量级,系统终于能跟着人的走动实时刷新检测结果。

这里的核心原因是MobileNet v2把标准卷积拆成了深度卷积和逐点卷积,参数量和计算量被大幅压缩,还引入了线性瓶颈和反向残差结构,让小模型在某些层里反而学得更充分。对口罩检测这种类别少、目标尺度集中的任务,MobileNet v2不需要ResNet那样的深度也能拟合得不错。下面我按自己从数据准备到部署上线的完整路径来拆解系统实现,每一步都会给出可复现的命令和参数,也会把训练和部署阶段最容易翻车的地方单独列出来。

2. 从数据集到标签:口罩检测的数据准备与预处理细节

2.1 选择公开数据集还是自采:主流口罩数据集与标注格式

口罩检测在公开领域已经有几份常用数据集,带包围框标注的主要是两类:一类是“戴口罩的人脸”与“不戴口罩的人脸”图片集,标签是整张图的类别;另一类是从人脸检测数据集改造来的,标注框框住人脸区域,并额外标注是否佩戴口罩。如果你只需要判断画面里有没有人没戴口罩,用整图分类数据集就可以,但真实门禁系统里往往要定位到具体人,所以框级检测更实用。

常见做法是先用公开的人脸检测模型(比如OpenCV的DNN人脸检测器)跑一遍原始图片,把所有人脸区域裁出来,再人工修正框的位置,最后打上“mask”或“no_mask”标签。这个流程虽然啰嗦,但能保证你的训练分布接近目标场景——比如摄像头装在1.8米高度、俯视人脸,而不是数据集中常见的正脸平视。我一般会混合公开数据和现场自采数据,自采比例不低于三成,否则一到现场就会因为视角、光线差异掉点。

标注格式方面,如果你打算直接用Keras的水平翻转、旋转来做增强,最简单的是把框信息存成VOC格式的XML,或者干脆整理成一个CSV,每一行是文件名、四个坐标值(左上角x、y、宽、高)和类别名。后面训练时用一个数据生成器读取这些标签并转换成模型需要的张量。

2.2 数据增强与归一化:让模型见过多样的口罩

口罩检测最常遇到的环境差异不是口罩样式,而是光照和遮挡。数据增强不是锦上添花,它直接决定模型在闸机逆光、夜间红外补光下会不会频繁把“没戴口罩”误判成“戴了”。我实际增强策略是这样的:

# 数据增强配置,用于训练时的在线增强 from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen = ImageDataGenerator( rescale=1.0 / 255, # 像素归一化到 [0,1] 区间,匹配MobileNet v2的输入要求 rotation_range=15, # 随机旋转正负15度,模拟头部倾斜 width_shift_range=0.1, # 水平平移10%,应对人脸不在画面中央的情况 height_shift_range=0.1, brightness_range=[0.6, 1.4], # 亮度抖动,模拟逆光和阴影 shear_range=0.05, # 小角度错切,增强几何鲁棒性 zoom_range=0.1, # 随机缩放,适应不同人脸距离 horizontal_flip=True, # 水平翻转,口罩检测无方向性 fill_mode='nearest' # 变换后空白区域用最近邻填充 )

这里有一个容易被忽略的细节:rescale=1.0/255会把输入归一化到0到1之间,而预训练的ImageNet权重最初接收的是标准化后的数值(均值和方差)。如果你只做线性缩放而不做标准化,前几层卷积的激活分布会偏移,训练收敛变慢。Keras的applications.MobileNetV2内置了preprocess_input函数,更稳妥的做法是在数据增强流水线里只做几何和亮度增强,然后在数据输入模型前调用preprocess_input完成标准化。

参数设置的经验值是:brightness_range不建议超过[0.5, 1.5],太强会让模型把暗光下正常肤色的人脸学成噪声样本;rotation_range=15在额头被刘海遮挡时效果好,但超过20度会让口罩边框严重变形,反而引入错误监督。翻转只做水平翻转,垂直翻转没有物理意义,不要开。

2.3 训练集/验证集划分与标签编码

如果数据集来自多个来源,不能简单随机划分,一定要按“来源”分组。我遇到过把同一人的连续视频帧拆到训练集和验证集里,导致验证精度虚高到99%,真实现场掉到80%的情况。正确做法是用sklearn.model_selection.GroupShuffleSplit按视频片段或图像文件夹分组。

标签编码上,框级检测任务通常把类别映射成整数:0表示no_mask,1表示mask。如果你的项目还要求检测“口罩戴歪”状态,就扩成3类:0未戴、1正确佩戴、2佩戴不规范。类别顺序一旦固定,训练脚本和推理脚本必须共用同一个映射字典,我会把映射写成一个JSON文件随模型一起保存,避免部署时忘了顺序。

# 将CSV标注中的字符串类别转换为整数编码 class_map = { "no_mask": 0, "mask": 1, "mask_worn_incorrectly": 2 } def encode_label(label_str): return class_map[label_str]

这段代码本身没有难度,但它是整个数据流水线中最容易出错的地方。我见过不止一次因为label_map顺序写反,模型训练时把“戴了口罩”和“没戴口罩”完全学反,准确率约等于瞎猜。建议把映射固化在版本控制里,训练前打印几条样本的(文件名, 编码)对来人工核对。

3. 用TensorFlow/Keras搭建MobileNet v2口罩检测模型:迁移学习与模型结构

3.1 迁移学习:为什么冻结前层,微调后层

移动端部署环境下,从零训练一个MobileNet v2需要大量数据,而且收敛极慢,没必要。常见做法是加载在ImageNet上预训练好的MobileNetV2权重,把最后的分类层替换掉。冻结前层的原因是前几层学到的是通用边缘、纹理特征,这些特征对口罩识别同样有用;而后几层学到的是狗、猫、树之类的特定物体组合,对口罩没有直接用。

我一般先冻结全部卷积层,只训练新加的检测头,跑几个epoch看验证精度能否快速上来。如果不到90%就有问题,可能是数据标注或类别映射错了;如果很容易到95%以上,再解冻最后几十层卷积,用小学习率微调,让深层特征向口罩形态偏移。

# 加载预训练MobileNet v2,不包含顶层分类器 from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, Model base_model = MobileNetV2( weights='imagenet', include_top=False, # 去掉1000类分类头 input_shape=(224, 224, 3) ) base_model.trainable = False # 第一阶段冻结全部卷积层 # 自定义检测头 x = layers.GlobalAveragePooling2D()(base_model.output) x = layers.Dense(64, activation='relu')(x) x = layers.Dropout(0.3)(x) # 防止小数据过拟合 output = layers.Dense(2, activation='softmax')(x) # 2类:戴口罩/未戴口罩 model = Model(inputs=base_model.input, outputs=output)

这里input_shape固定为(224,224,3),是因为预训练权重要求这个输入尺寸。如果为了更高帧率改用192或128,MobileNet v2也能接受非正方形输入,但预训练权重会在某些层产生尺寸不匹配问题,需要额外调整。建议第一版先跑224,后面再做输入尺寸缩减。

3.2 在MobileNet v2之上加检测头:全连接层与输出设计

很多人把口罩检测做成整图分类,也就是把每一帧画面直接丢进模型,判断整张图里有没有人没戴口罩。这种方式在单人或人群稀疏时勉强可用,但人群一密集就傻了,因为画面里同时有戴和不戴的人,分类结果无法定位问题个体。因此一个负责任的系统实现要在MobileNet v2前面加一个人脸检测前置模块,先用轻量级人脸检测器把人脸框出来,再把每个人脸区域裁剪到224x224,送进MobileNet v2做戴口罩分类。这就形成“人脸检测 + 口罩分类”的级联结构。

上面的代码定义了一个二分类头(戴/未戴)。如果使用三分类(戴上/未戴/戴歪),只需把最后一层改成Dense(3, activation='softmax')。不要用sigmoid代替softmax做多分类,虽然二分类时它们理论上等价,但softmax在输出层配合categorical_crossentropy训练更稳定,且不容易出现输出总和大于1的问题。

3.3 模型编译:损失函数、优化器与学习率的选择

口罩识别是典型的类别不均衡任务——采集到的“不戴口罩”样本往往远少于“戴口罩”样本,因为常规场景里大家都会掩好口鼻。直接使用categorical_crossentropy会把多数类学得很好,少数类几乎学不到。我一般会在损失函数里加类别权重,让少数类的错误贡献更大权重。

# 编译模型,带类别权重 class_weight = { 0: 3.0, # no_mask 样本少,加大权重 1: 1.0 # mask 样本充足,正常权重 } model.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] )

class_weight的具体值需要根据训练集统计计算。一个简单的经验法则是:少数类权重 = 多数类样本数 / 少数类样本数,但不要超过10倍权重,否则模型会过度偏向少数类,把很多“戴口罩”误判成“没戴”。我见过一个同学把权重设成15,结果测试时所有人脸都被标注为未佩戴,因为模型宁可错杀也不漏杀。

优化器首选Adam,学习率初始设1e-4或3e-4。预训练模型被冻结时,学习率可以稍高一点;一旦解冻微调,必须降到1e-5到5e-5,否则预训练权重会被粗暴地冲走,精度反而下跌。训练时配合ReduceLROnPlateau回调,当验证损失连续几个epoch不下降时自动降低学习率,这是减少手动调参的稳妥做法。

4. 实时检测系统实现:从摄像头读取到画面绘制的最小可用流程

4.1 打开摄像头并做帧预处理:分辨率与推理速度的平衡

实时系统的首要工作是把摄像头帧流转成模型输入。OpenCV的VideoCapture是最直接的接口,但默认输出的1080p画面直接缩放成224x224会丢失人脸小目标,直接不缩放又会让预处理时间飙升。常见做法是先把帧缩放到一个中间分辨率,比如640x480,再做人脸检测,再做224x224的口罩分类。

import cv2 import numpy as np from tensorflow.keras.models import load_model cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) mask_model = load_model('mask_classifier.h5') class_names = ['no_mask', 'mask']

设置摄像头分辨率时,很多USB摄像头不支持任意分辨率,如果640x480设置失败,读取的帧仍然是默认值。建议设置后立刻读一次实际分辨率,如果和预期不符就自动切换到下一个支持档位,比如1280x720。

4.2 模型推理与置信度阈值:如何判定“戴”与“未戴”

模型输出的是每个类别的概率,不能直接argmax,因为在复杂场景下模型对“戴”和“未戴”的置信度可能都只有60%。如果盲目取最大值,系统会在边界情况下来回抖动。我通常设置一个CONFIDENCE_THRESHOLD,只当最大概率超过0.7时才给出判定,否则标记为“不确定”。在工厂入口这类严格场景,阈值可以提高到0.8;在室内通行场景,0.6就够了。

# 对面部区域进行口罩分类推理 def classify_face(face_crop): face_resized = cv2.resize(face_crop, (224, 224)) face_rgb = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB) face_normalized = face_rgb.astype('float32') / 255.0 face_input = np.expand_dims(face_normalized, axis=0) pred = mask_model.predict(face_input, verbose=0)[0] idx = np.argmax(pred) confidence = pred[idx] if confidence < CONFIDENCE_THRESHOLD: return 'unknown', confidence return class_names[idx], confidence

这里predict(face_input, verbose=0)每次调用会重新初始化部分计算图,如果每帧有多个面部区域,频繁调用会严重影响性能。更好的做法是用model.predict传入批量数据,或者使用model.signatures在TensorFlow 2里构建推理函数。最简单的高效化是batch_predict:把一帧里的所有人脸区域都resize后堆成数组,一次性预测。

4.3 在画面中绘制检测框与状态提示:显示层的细节

拿到分类结果后,需要把框和标签画在原帧上。这里有两个小坑:一是OpenCV的坐标系统和模型输入裁剪后的坐标系统要一致,不要忘了把裁剪框的原点偏移加回去;二是中文文字“未戴口罩”直接画在画面上会乱码,因为OpenCV的putText只支持ASCII字符,常见解决办法是先用英文标注,或者用PIL先绘制中文再转回OpenCV图像。

# 在人脸框上方绘制英文状态标签 label = f"{class_name}: {confidence:.2f}" color = (0, 0, 255) if class_name == 'no_mask' else (0, 255, 0) cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2) cv2.putText(frame, label, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)

如果你要用中文提示,我会先把帧从BGR转到RGB,用PIL的ImageDraw.text渲染中文字体,再转回BGR。这个转换本身开销不小,建议只在检测到“未戴口罩”时触发,不要每帧都做。

5. 模型训练的5个避坑记录:过拟合、误检与部署差异

5.1 类别不均衡导致“未戴口罩”被吞掉

现象:验证集准确率97%,但现场把所有未戴口罩的人都放行了。原因:训练集中“戴口罩”样本是“未戴口罩”的10倍以上,模型学到的最优策略是永远预测“戴口罩”。解决:除了加class_weight,还要统计训练时的混淆矩阵,不要只看accuracy指标。我习惯在每次验证时单独打印no_mask类别的召回率,这个值低于0.9就说明类别不均衡处理不到位。

5.2 光照与遮挡让模型频繁抖动

现象:同一张脸,只是在窗前站了一下,检测结果就从“戴”变“未戴”又变回“戴”。原因:训练数据的亮度范围太窄,模型没有见过强逆光和侧光下的口罩人脸。解决:在增强里加入brightness_range和gamma调整,同时采集少量现场逆光样本单独做微调。注意增强幅度不能太猛,我用过brightness_range=[0.4, 1.6],结果模型对正常光照的深色口罩完全失灵。

5.3 训练精度高但实时帧率低

现象:同一块Jetson Nano上,训练时模型推理要80ms一帧,达不到实时要求。原因:输入分辨率224太高,且模型没有量化。解决:先把输入缩小到160x160,推理延迟能降低40%;再把模型转成TensorFlow Lite并做动态范围量化,延迟能再降30%。这个过程中精度会掉2到3个百分点,但可用性大幅提升。

5.4 误把下巴当口罩:区域裁剪与锚点问题

现象:检测到有人没戴口罩,但框住的其实是下巴到脖子的区域。原因:人脸检测器在侧面或低头时把下脸部区域当成了完整人脸,裁剪框里本来就不包含口鼻部位,模型自然给“未戴”。解决:不要直接用原始人脸框,而是只取人脸框的下半部分作为口罩分类输入。我在实践中取人脸框垂直方向的底部55%区域作为分类输入,这样能把帽子、刘海等无关特征排除掉。具体比例需要根据摄像头特性调,我这里的0.55是经验值。

5.5 保存与加载模型时输入张量不一致

现象:训练好的模型model.save('mask_classifier.h5'),部署时加载发现输入形状变成(None, None, None, 3),与预期不符。原因:如果模型中某个层用了None尺寸占位,或者保存的是SavedModel格式,加载时输入形状不会固定。解决:在导出前调用model.build((1, 224, 224, 3)),并显式声明model.input.set_shape((1, 224, 224, 3)),再保存为.h5或.tflite。加载后用model.input_shape打印确认。

6. 从原型到可交付:模型量化与性能验证的实用技巧

当模型在PC上能跑到合理精度后,下一步通常是部署到ARM盒子或摄像头内置芯片上。移动端部署的第一步是用TensorFlow Lite转换脚本把模型转成轻量格式。注意如果你在训练时用了tf.keras.applications.MobileNetV2,转换时要把预处理方式也一并固化为输入张量的一个算子,否则部署端很容易漏掉标准化步骤。

# 将h5模型转换为TensorFlow Lite格式,启用动态范围量化 import tensorflow as tf converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open('mask_model.tflite', 'wb') as f: f.write(tflite_model)

动态范围量化是见效最快的方案:模型权重从float32压到float16或int8,体积缩小为四分之一,推理延迟大幅下降,精度通常只掉1~2个点。如果你的部署端是纯int8芯片,比如RK3399Pro的NPU,还需要提供代表性数据集做全整数量化,并且把输入张量也量化为int8。我在RK平台踩过坑:忘了量化输入,结果NPU直接拒绝运行。

验证性能时不要只测端到端延迟,要分开测各模块耗时:摄像头读帧占多少、人脸检测占多少、口罩分类占多少、绘制占多少。我习惯把每段耗时打印出来,用毫秒为单位统计。一次实测中,摄像头读帧28ms,人脸检测60ms,口罩分类12ms,绘制3ms,瓶颈非常明显,于是我换用了更轻量的人脸检测模型,整体帧率立刻翻倍。这个排查思路比盲目优化模型有效得多。

最后说一个我自己的习惯:每次训练完,我都会拿一段从现场录的30秒视频做离线回放,用和线上完全相同的预处理流程跑一遍,记录每一帧的预测结果,再逐帧对照检查。这个习惯帮我抓出过不少问题,比如系统对特定角度的人脸会连续误报,或者某种蓝色口罩在暗光下被判定为“未戴”。视频回放的成本很低,但比任何测试指标都能真实反映部署后的表现。希望这个流程能帮到你避开我当年翻过的那些车。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询