ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python口罩佩戴检测:OpenCV+MobileNetV2从训练到调优

Python口罩佩戴检测:OpenCV+MobileNetV2从训练到调优 简介一套完整的基于Python的口罩佩戴检测项目资料包面向高校学生与Python开发者适合作为课程设计、毕业设计或目标检测方向的入门实践素材。项目采用人脸检测与口罩识别两阶段思路优先复用现成人脸检测模型再训练口罩分类模型兼顾训练成本与准确率同时涉及VGG、MobileNet、ResNet等Backbone以及Mask-Rcnn、SSD、YOLO等检测网络知识便于学习者对照设计报告梳理目标检测体系。资源共22个文件压缩包仅4.86MB包含Python训练与测试脚本、设计报告Word文档、测试图片、示例截图、说明文档与LICENSE文件覆盖从模型训练到测试验证的主要环节。测试目录内置多张JPG样例可直接运行脚本观察检测效果设计报告则对算法原理和实现细节做了整理适合在此基础上二次开发或答辩讲解。已有2865人学习下载是快速上手口罩佩戴检测任务的一套轻量参考。1. 基于Python的口罩佩戴检测先想清楚要识别什么把“口罩佩戴检测”作为一个 Python 项目来做意味着你要处理的不只是“有没有照片里有人戴口罩”而是从一帧视频或一张图片中定位人脸并判断人脸区域是否被口罩遮挡。这个任务在门禁闸机、课堂考勤、工地安全监督等场景里很常见。之所以选择 Python是因为 OpenCV、TensorFlow 等库已经把底层图像处理和深度学习的复杂度封装好了你可以用几百行代码拼出一套可用的流程。但真正落地时卡点往往集中在模型选型、阈值调节和多人遮挡这几个地方。这篇文章从环境准备讲到调优验证目标不是给你一个一次性脚本而是让你理解每一步为什么要那么写参数从哪里来以及换了场景后应该改哪里。2. 口罩佩戴检测的环境准备与模型选型2.1 Python 安装与 OpenCV/TensorFlow 依赖清单在写代码之前先把 Python 运行环境理清。现在的项目大多跑在 Windows 或 Ubuntu 上Python 版本建议用 3.8 到 3.10。太老的版本对 TensorFlow 2.x 支持不友好太新的版本又会因为编译问题踩坑。安装时注意勾选“Add Python to PATH”不然后面命令都用不了。依赖安装用 pip 一把梭就可以python -m pip install --upgrade pip pip install opencv-python pip install numpy pip install tensorflow pip install scikit-learn这里opencv-python提供的是cv2模块包含图像处理、人脸检测、视频捕获等基础能力tensorflow用来加载和训练口罩分类模型如果你只做推理也可以用tensorflow-cpu减少体积。scikit-learn只在验证阶段计算准确率等指标时用到不训练可以跳过。安装完成后可以用一句命令验证依赖是否正常python -c import cv2; print(cv2.__version__)如果输出类似4.8.0的版本号说明 OpenCV 可用。TensorFlow 的验证稍微慢一点因为第一次导入会执行初始化但只要能打印出版本号就没问题。2.2 人脸检测后分类与端到端目标检测哪个项目更适合你口罩佩戴检测不是单一模型任务常见的攻坚路径有两条。一条是人脸检测 口罩分类的组合另一条是直接用目标检测模型同时输出人脸框和“戴口罩/未戴口罩”两个类别。下表把两条路线摆在一起比较。评估维度人脸检测 分类端到端目标检测模型复杂度需要两个模型但各自简单单模型训练开销大精度人脸框准的话分类精度高密集小目标容易漏检测推理速度较快取决于人脸数量较慢尤其 YOLO 大模型训练成本分类模型只需几百张图微调需要人工标注人脸框和口罩类别调参难度阈值参数多但直观NMS、anchor 等参数难调部署灵活性可独立替换人脸检测器模型整体不好替换常见做法是先做人脸检测再对每个人脸区域做二分类戴口罩/未戴口罩。这么做的好处是准备数据集时你只需要把大量戴口罩和不戴口罩的图片裁剪成固定大小就可以训练分类器不需要精标人脸框坐标。而端到端模型如 YOLO、SSD需要同时回归边界框和类别数据标注成本高在硬件资源有限的场景里不容易跑得快。如果你的项目是给实验室或小规模闸机做原型选组合方案最稳。如果要做海量人群密集场景的实时检测才需要认真考虑端到端模型。本文后面给出的代码都基于组合方案。2.3 预训练模型文件与数据集常见获取方式与格式约定组合方案里人脸检测器通常直接用 OpenCV 的预训练模型。一种是传统 Haar 级联分类器文件是haarcascade_frontalface_default.xmlOpenCV 自带的data目录里就有也很容易搜到另一种是基于深度学习的人脸检测模型比如res10_300x300_ssd_iter_140000_fp16.caffemodel配合对应的deploy.prototxt文件。这两类模型的格式差异很大Haar 模型直接用cv2.CascadeClassifier加载深度学习模型要用cv2.dnn.readNetFromCaffe加载。口罩分类模型的预训练权重则没有统一标准。常见做法是在 ImageNet 预训练的 MobileNetV2 基础上用公开的口罩人脸数据集微调。有一批开源脑图数据比如 RMFD 和 MAFA包含真实场景下的戴口罩人脸但下载地址经常变动建议直接搜索“masked face dataset”找镜像或学术资源。如果你的场景很特殊比如是工地安全帽与口罩同时检测最好自己采集 500 到 2000 张现场照片裁剪后作为数据集。格式上训练脚本通常需要你把数据集整理成两个目录with_mask和without_mask所有图片统一缩放到 224×224 或 128×128。这里要注意图片需要是纯人脸区域不要带大块背景否则分类器会把“背景”学到模型里去导致真实场景识别率骤降。3. 用 OpenCV 和 MobileNetV2 实现口罩佩戴检测核心代码3.1 人脸检测器选择Haar Cascade 与 DNN 的性能差异在实际编写口罩佩戴检测代码时我一般会优先用 OpenCV DNN 的人脸检测器而不是 Haar Cascade。原因很直接Haar 级联在正面、光照均匀的情况下表现不错但人脸一旦侧对摄像头、戴眼镜或处在逆光环境就很容易漏检测误检率也比较高。DNN 人脸检测器对角度、遮挡和光线的鲁棒性要好很多代价是需要下载约 10MB 的模型文件。用 DNN 检测器要先加载模型并生成输入 blobimport cv2 face_net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000_fp16.caffemodel ) image cv2.imread(test.jpg) h, w image.shape[:2] blob cv2.dnn.blobFromImage( image, 1.0, (300, 300), (104.0, 177.0, 123.0), swapRBFalse, cropFalse ) face_net.setInput(blob) detections face_net.forward()这段代码里的blobFromImage是关键。第一个参数是原始图像第二个参数1.0是缩放因子因为 MobileNet 的输入归一化方式不同这里不做额外缩放第三个(300, 300)是输入尺寸第四个元组(104.0, 177.0, 123.0)是均值减除数每个通道减去这个值。很多初学者会把这组参数直接抄到别的模型上导致检测结果异常这里要特别注意。detections的维度是(1, 1, N, 7)其中 N 表示检测到的人脸候选框数量。每个候选框的第 2 个元素是置信度第 3 到第 6 个元素是归一化的边框坐标。你需要先用置信度过滤再把坐标缩放回原图尺寸。3.2 编写 detect_mask.py图片检测与摄像头实时检测有了人脸检测器再加上一个口罩分类模型就可以拼出完整的检测脚本。这里假定你已经有一个训练好的口罩分类模型保存成mask_detector.h5。对实时视频流流程是每一帧先做人脸检测再对每个人脸区域做分类。import cv2 import numpy as np from tensorflow.keras.models import load_model # 加载人脸检测模型 face_net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000_fp16.caffemodel ) # 加载口罩分类模型h5 文件由训练脚本导出 mask_net load_model(mask_detector.h5) # 打开摄像头参数 0 表示默认摄像头 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break h, w frame.shape[:2] blob cv2.dnn.blobFromImage( frame, 1.0, (300, 300), (104.0, 177.0, 123.0), swapRBFalse, cropFalse ) face_net.setInput(blob) detections face_net.forward() for i in range(0, detections.shape[2]): confidence detections[0, 0, i, 2] # 低于置信度阈值的候选框直接忽略 if confidence 0.6: continue # 将归一化坐标映射到原图 box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (x1, y1, x2, y2) box.astype(int) # 防止坐标超出图像边界 x1 max(0, x1) y1 max(0, y1) x2 min(w - 1, x2) y2 min(h - 1, y2) face frame[y1:y2, x1:x2] if face.size 0: continue # 转换为 RGB 并缩放到分类模型要求的 224x224 face_rgb cv2.cvtColor(face, cv2.COLOR_BGR2RGB) resized cv2.resize(face_rgb, (224, 224)) normalized resized / 255.0 input_tensor np.expand_dims(normalized, axis0) # 分类输出(戴口罩概率, 未戴口罩概率) (with_mask, without_mask) mask_net.predict(input_tensor)[0] label Mask if with_mask without_mask else No Mask # 戴口罩用绿色框未戴口罩用红色框 color (0, 255, 0) if label Mask else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText( frame, f{label}: {max(with_mask, without_mask):.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2 ) cv2.imshow(Mask Detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里最容易出错的地方是mask_net.predict(input_tensor)。很多人会直接拿 BGR 图喂给模型但训练时用的是 RGB 图颜色通道顺序不一致会让模型输出混乱。所以在cvtColor那里必须转换。另外predict每次调用都会创建一次 TensorFlow 计算过程如果一帧里有 5 张脸就要调用 5 次这会对实时性有影响。一个优化是将predict换成model.call或者使用批处理后续章节再展开。3.3 识别脚本的关键参数表与调参顺序不管是图片检测还是摄像头检测影响最终效果的核心参数就那么几个。我把它们列成表格方便你按顺序调试。参数所在代码位置推荐取值范围作用confidence阈值人脸检测循环里0.5 ~ 0.8过滤低质量人脸框越高越严格blobFromImage的sizereadNetFromCaffe之前300x300 或 224x224影响人脸检测的召回率和速度均值减除向量blobFromImage(104,177,123)或(0,0,0)匹配预训练模型的输入分布分类模型的输入尺寸cv2.resize128~224尺寸越大精度越高速度越慢waitKey参数视频循环末尾1~30控制帧率值越大越卡但更省 CPU调参的顺序我一般是先固定人脸检测部分把人脸框画出来确认每个人脸都能被框住再去调分类阈值。如果发现有人脸没被检测到优先降低confidence到 0.5或者把输入尺寸从 300 改成 224 并重新测试。如果检测到了但分类结果不稳定则是分类模型的问题需要回到训练侧看数据和模型。4. 从零训练口罩分类模型数据组织、增强与模型导出4.1 数据集的目录结构与 ImageDataGenerator 增强如果手头没有现成的口罩分类模型就需要自己训练。训练一个好的二分类器不需要几十万张图几百张到几千张真实场景裁剪图就能达到不错的效果。关键在于数据要“像真实场景”——图片里人脸要足够大角度要多样光线要杂。推荐目录结构如下dataset/ ├── train/ │ ├── with_mask/ │ │ ├── mask_001.jpg │ │ └── ... │ └── without_mask/ │ ├── face_001.jpg │ └── ... └── val/ ├── with_mask/ └── without_mask/使用 Keras 的ImageDataGenerator可以自动做数据增强避免模型过拟合。常见的增强配置如下from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1.0/255.0, rotation_range20, width_shift_range0.2, height_shift_range0.2, zoom_range0.2, horizontal_flipTrue ) val_datagen ImageDataGenerator(rescale1.0/255.0) train_generator train_datagen.flow_from_directory( dataset/train, target_size(224, 224), batch_size32, class_modecategorical ) val_generator val_datagen.flow_from_directory( dataset/val, target_size(224, 224), batch_size32, class_modecategorical )rotation_range和horizontal_flip听起来美好但要注意口罩是贴在脸上的如果旋转角度太大反而会造出“口罩歪出脸”的假训练样本。所以旋转不要超过 20 度水平翻转更适合人脸这种近似对称的物体。zoom_range可以模拟远近变化但过大会让人脸边缘被截断削弱模型的感知能力。4.2 迁移学习训练脚本基于 MobileNetV2 的二分类使用迁移学习可以大幅减少训练时间和数据量。这里以 MobileNetV2 作为基础模型去掉顶层后接一个全连接分类层。MobileNetV2 的体积小适合部署到 CPU 或嵌入式设备和 OpenCV 配合也比较顺。from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D from tensorflow.keras.models import Model from tensorflow.keras.optimizers import RMSprop base_model MobileNetV2( weightsimagenet, include_topFalse, input_shape(224, 224, 3) ) # 冻结基础模型早期只训练分类层 base_model.trainable False x base_model.output x GlobalAveragePooling2D()(x) x Dense(64, activationrelu)(x) x Dropout(0.5)(x) predictions Dense(2, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) model.compile( optimizerRMSprop(learning_rate0.0001), losscategorical_crossentropy, metrics[accuracy] ) history model.fit( train_generator, steps_per_epochlen(train_generator), epochs10, validation_dataval_generator, validation_stepslen(val_generator) )MobileNetV2 在 ImageNet 上学到的低级特征边缘、纹理可以直接用到口罩分类上所以冻结前 10 个 epoch 不要动基础参数。先用小学习率训练顶层等验证集准确率不再提升再把base_model.trainable True并降低学习率进行微调。这里Dropout(0.5)很关键数据量少时它能显著抑制过拟合。训练完成后需要保存整个模型。注意至少要保存模型结构只保存权重文件会丢失自定义构建信息model.save(mask_detector.h5)4.3 将模型导出为 OpenCV 可加载的格式如果你希望检测脚本完全不依赖 TensorFlow而是直接用 OpenCV 的cv2.dnn推理分类模型就要把 Keras 模型转换成 OpenCV 能识别的格式。OpenCV 支持加载 TensorFlow 的 SavedModel 或冻结的.pb文件但不支持.h5。转换路径常见的有两种。第一种是使用 TensorFlow 自带的 TFLite 转换器把模型转成.tflite然后用 Python 版 TFLite 解释器加载。第二种是导出为 SavedModel再用tf.compat.v1.graph_util.convert_variables_to_constants冻结变量。下面给出第一个方法的最小代码import tensorflow as tf model tf.keras.models.load_model(mask_detector.h5) converter tf.lite.TFLiteConverter.from_keras_model(model) tflite_model converter.convert() with open(mask_detector.tflite, wb) as f: f.write(tflite_model)TFLite 模型比.h5小不少推理速度也更快。如果你在检测脚本里改用tflite.Interpreter需要自己指定输入输出的张量名称和索引代码会比model.predict啰嗦但寒暄式地提升了一截性能。对于想走通全流程的读者我建议先保留.h5版本做验证再在调优稳定后转 TFLite 部署。5. 口罩佩戴检测的调优技巧阈值、NMS 与验证脚本5.1 用置信度阈值和 IoU 过滤抑制误报在实际测试中置信度阈值设 0.6 只是一个起点。如果现场总有非人脸物体被检测成人脸你可以把阈值提高到 0.75。但要注意阈值越高真正的人脸可能也会被过滤。另一种更精细的办法是使用 IoU 过滤器当两个人脸框重叠面积超过一定比率时只保留置信度更高的那个。OpenCV 提供了cv2.dnn.NMSBoxes方法可以直接对检测框做非极大值抑制boxes [] confidences [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: continue box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 box.astype(int) boxes.append([x1, y1, x2 - x1, y2 - y1]) confidences.append(float(confidence)) # 参数框列表、置信度列表、置信度阈值、NMS 阈值 idx cv2.dnn.NMSBoxes(boxes, confidences, score_threshold0.5, nms_threshold0.4)这里的nms_threshold0.4表示重叠面积达到 40% 时就会合并两个框。如果你的摄像头角度很斜人脸互相遮挡严重可以把nms_threshold调到 0.3减少误合并如果人脸框经常重叠但实际是不同的人就调到 0.5 以上。5.2 多人场景下的 NMS 参数调整策略当画面里同时出现三四个人每个人脸框都输出一个置信度NMS 的作用尤其明显。一个典型的问题是同一个人的下巴和额头被检测成两个框这时 NMS 会因为 IoU 过高而保留置信度高的那个从而避免重复画框。另一个问题是摄像头分辨率较低时远处的小人脸框置信度不高很容易被 NMS 直接干掉。我的经验是在这类场景下把score_threshold降低到 0.4同时把输入尺寸从 300×300 降到 250×250因为高分辨率并不总能提高小目标检测率反而会让两个相邻框更窄NMS 更易误删。5.3 一段 30 行的验证脚本统计准确率和 FPS调参不能光靠感觉最好写一个离线验证脚本用一批标注好的人脸图片统计分类准确率同时测出一帧的平均推理耗时。这里给出一个简化版本import cv2 import numpy as np import os import time from tensorflow.keras.models import load_model model load_model(mask_detector.h5) test_dir test_set total 0 correct 0 times [] for label_name in [with_mask, without_mask]: label 1 if label_name with_mask else 0 folder os.path.join(test_dir, label_name) for fname in os.listdir(folder): path os.path.join(folder, fname) img cv2.imread(path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (224, 224)) / 255.0 tensor np.expand_dims(img_resized, axis0) t0 time.time() pred model.predict(tensor)[0] times.append(time.time() - t0) pred_label 1 if pred[0] pred[1] else 0 total 1 correct (pred_label label) print(fAccuracy: {correct / total:.4f}) print(fAverage inference time: {np.mean(times) * 1000:.1f} ms)验证脚本输出的两个数字要一起看。准确率高但单帧推理超过 100ms说明模型在实时场景里会很吃力可以考虑把target_size从 224 降到 160或者将分类模型替换成 TFLite。准确率偏低时优先检查测试集图片是否和训练集同分布比如训练集全是正面脸测试集却都是侧脸那问题不在阈值而在数据。记住一个原则任何调参都要以这个验证脚本的输出作为依据而不是看着那一帧画面觉得“看起来变准了”。本文还有配套的精品资源点击获取
返回列表