简介:一套完整的基于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.xml,OpenCV 自带的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 检测器要先加载模型并生成输入 blob:
import 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), swapRB=False, crop=False ) 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), swapRB=False, crop=False ) 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, axis=0) # 分类输出:(戴口罩概率, 未戴口罩概率) (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的size | readNetFromCaffe之前 | 300x300 或 224x224 | 影响人脸检测的召回率和速度 |
| 均值减除向量 | blobFromImage | (104,177,123)或(0,0,0) | 匹配预训练模型的输入分布 |
| 分类模型的输入尺寸 | cv2.resize | 128~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( rescale=1.0/255.0, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, zoom_range=0.2, horizontal_flip=True ) val_datagen = ImageDataGenerator(rescale=1.0/255.0) train_generator = train_datagen.flow_from_directory( 'dataset/train', target_size=(224, 224), batch_size=32, class_mode='categorical' ) val_generator = val_datagen.flow_from_directory( 'dataset/val', target_size=(224, 224), batch_size=32, class_mode='categorical' )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( weights='imagenet', include_top=False, input_shape=(224, 224, 3) ) # 冻结基础模型,早期只训练分类层 base_model.trainable = False x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(64, activation='relu')(x) x = Dropout(0.5)(x) predictions = Dense(2, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions) model.compile( optimizer=RMSprop(learning_rate=0.0001), loss='categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( train_generator, steps_per_epoch=len(train_generator), epochs=10, validation_data=val_generator, validation_steps=len(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_threshold=0.5, nms_threshold=0.4)这里的nms_threshold=0.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, axis=0) 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(f"Accuracy: {correct / total:.4f}") print(f"Average inference time: {np.mean(times) * 1000:.1f} ms")验证脚本输出的两个数字要一起看。准确率高但单帧推理超过 100ms,说明模型在实时场景里会很吃力,可以考虑把target_size从 224 降到 160,或者将分类模型替换成 TFLite。准确率偏低时,优先检查测试集图片是否和训练集同分布,比如训练集全是正面脸,测试集却都是侧脸,那问题不在阈值而在数据。记住一个原则:任何调参都要以这个验证脚本的输出作为依据,而不是看着那一帧画面觉得“看起来变准了”。
本文还有配套的精品资源,点击获取