简介:《基于YOLOv3的口罩识别》是一份面向计算机科学与技术专业本科生的毕业论文设计文档,旨在通过YOLOv3目标检测算法解决口罩佩戴自动识别问题,适合用于毕业设计参考、人工智能课程实践或目标检测技术入门。文档从研究背景和国内外现状出发,系统回顾卷积神经网络从LeNet、AlexNet到ResNet、DenseNet的发展,并重点解析YOLOv3的多尺度预测、Darknet-53网络、特征金字塔FPN及锚框改进等核心机制。口罩识别系统的实现流程也被完整涵盖,包括数据集准备、数据增强、模型训练以及NMS后处理等环节。资源为单个docx文件,压缩包大小仅2.99MB,内容结构包含诚信声明、目录、各章节正文及参考文献框架,便于读者直接查看或修改格式。目前已有3335人学习浏览,可帮助读者快速了解基于深度卷积网络的目标检测系统设计思路,获得从理论分析到实验落地的完整参考框架。
1. 用 YOLOv3 做口罩识别,没必要追新模型
2025 年再看口罩识别,很多人第一反应是“换 YOLOv8 不香吗”。实际落到门禁、通道闸机、工地安全帽检测这类设备后台,相当一部分 IPC 和人脸识别盒子用的仍是 YOLOv3 结构下的 darknet 权重或转出的 ONNX 模型。原因不复杂:YOLOv3 结构直白、先验锚框可手工调整、部署链路成熟,而它面对只有“戴口罩、未戴口罩”两类目标时,精度上限完全够用。口罩识别和通用检测不同,真正难点在小脸、侧脸、低照度、口罩半摘这些边界状态。下面从检测原理、数据准备、模型训练、推理部署,一直聊到常见翻车点的调优,整条链路可以直接照做。
2. YOLOv3 的三尺度检测与锚框,为什么适配固定形态的口罩目标
进入实现之前,先把 YOLOv3 的检测原理拆开讲清楚。很多人第一次用 darknet,最大的困惑是:为什么一张图会产生上万个候选框,配置里的 anchors 和每个尺度之间到底是什么关系。把这两个问题打通,后面调 filters、调锚框才有判断依据,而不是只照抄命令。
2.1 三尺度网格、先验锚框与 10647 个候选框的来源
以最常见的 416×416 输入为例。图像经主干网络下采样,分别在三个倍数处引出检测头:13×13、26×26、52×52。每个网格单元配置 3 个先验锚框,那么整个流程生成的候选框总数就是 13×13×3 + 26×26×3 + 52×52×3 = 10647 个。这上万候选框多数会在后处理中被丢掉,真正被利用的只有目标附近的几十个。先验锚框不是最终输出,它只提供宽高的初始值,网络回归的是相对锚框的偏移量和缩放,难度比直接回归绝对坐标小得多。
放到口罩识别的场景里,这一机制直接影响检测效果。口罩在画面中形态比较固定:正脸时宽高比接近 1:1,侧脸时变成细长条。默认的 COCO 锚框里有大量针对人体或车辆的广义形状,直接沿用可以跑通,但收敛速度和精度都不是最优。常见做法是统计训练集标注的宽高比,用 k-means 重新聚出 9 个锚框。聚类时先把宽高值转成 log,再算 IoU 距离,避免大目标主导聚类中心,然后把新锚框按面积从小到大写回 cfg 中。这个过程在口罩项目里通常能带来 1~3 个点的 mAP 提升,不改也不至于跑不出来。
2.2 为什么别一上来就换 YOLOv3-tiny
训练资源吃紧的人往往第一反应是换 YOLOv3-tiny,它只有 13×13 和 26×26 两个检测头,速度确实快。但注意,被删掉的 52×52 分支正是小目标的主要出口。摄像头装在闸机上方 3 米远时,人脸宽度可能只有 40~60 像素,口罩区域更小,只靠 26×26 尺度去回归,特征图上的细节信息已经堆叠多次,边缘轮廓丢失严重。结果是近景识别很正常,稍远一点就出现漏检。这是结构缺陷,调任何后处理都补不回来。
下表是我在选型时习惯做的一张定性判断表,不看具体跑分,只看结构差异下各场景的适配性:
| 模型 | 检测头 | 小目标能力 | 适合机位 | 算力开销 |
|---|---|---|---|---|
| YOLOv3 | 3 个 | 强 | 1~5 米门禁、通道 | 基准 |
| YOLOv3-tiny | 2 个 | 弱 | 1~2 米固定近景 | 明显更低 |
| YOLOv3-SPP | 3 个 | 更强 | 光照变化大的户外 | 更高 |
完整 YOLOv3 对小目标的支撑不只靠多一个尺度,还靠特征金字塔把深层语义和浅层纹理逐级融合。浅层特征保留口罩轮廓、褶皱和肤色差异,深层特征判断这是人脸区域,两者结合,模型才能区分“口罩边缘”和“下巴阴影”。如果只看重帧率,tiny 确实可以做;但相同数据下 tiny 通常要多付出 10% 以上的漏检代价,园区门口这类复核成本高的场景不建议用。
2.3 读懂 filters=21 这个数字,才不会改崩配置
配置修改有个高频翻车点:filters 到底填多少。以 classes=2 为例,每个锚框需要预测 5 个坐标和目标置信度,再加 2 个类别概率,一共 7 个值。cfg 中每个网格布置 3 个锚框,因此最终输出通道是 3×7=21,这就是 filters=21 的来源。COCO 预训练模型的 filters=255 对应 3×(5+80),同一个公式。
值得注意,mask参数(如 mask=0,1,2)在这里是锚框索引,指当前检测头使用 anchors 列表中的哪几组,并不是给目标做掩码。首次接触 darknet 的人容易把 mask=0,1,2 理解成“哪一类目标”,改类的时候顺手改掉 mask,导致锚框分配错乱,模型训练不收敛。真正要同步改的只有 classes 和它前一层 conv 的 filters。
3. 训练可用的口罩识别模型:数据、cfg 与最小命令
3.1 标注格式与目录组织:一张图对应一个 txt
darknet 训练读的是 YOLO 文本标注,不是 VOC XML,也不是 COCO JSON。每张图片对应一个同名 txt,行格式是:类别 id、中心点 x、中心点 y、宽度 w、高度 h,全部归一化到 [0,1] 区间。拿到公开口罩数据集如果带的是 XML 标注,先做一次转换,顺便把损坏样本过滤掉。下面是常用转换脚本,逻辑简单但值得逐行确认:
import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path: str, out_path: str, classes: list) -> None: tree = ET.parse(xml_path) root = tree.getroot() w = int(root.findtext('size/width')) h = int(root.findtext('size/height')) lines = [] for obj in root.iter('object'): name = obj.findtext('name') if name not in classes: continue cls_id = classes.index(name) box = obj.find('bndbox') xmin = float(box.findtext('xmin')) ymin = float(box.findtext('ymin')) xmax = float(box.findtext('xmax')) ymax = float(box.findtext('ymax')) # YOLO 格式:中心点 + 宽高,全部归一化到 0~1 x_center = ((xmin + xmax) / 2) / w y_center = ((ymin + ymax) / 2) / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}') Path(out_path).write_text('\n'.join(lines)) # 类别顺序必须和后续 mask.names 文件完全一致 classes = ['mask', 'no_mask'] voc_to_yolo('annotations/0001.xml', 'labels/0001.txt', classes)代码做三件事:读取 XML 的 bndbox、归一化计算、写入 txt。findtext会自动去除空白字符,XML 缺字段时会直接抛异常,所以批量转换前先用单张跑通。特别要注意越界标注,xmax 略大于图宽的情况在人工标注里很常见,转换后宽高会超过 1,darknet 训练时偶尔会出现 nan loss。建议在写入前加一步min(max(v, 0), 1)的裁剪。另外classes的顺序不能随意排,训练用的mask.names也按这个顺序写两行,逐行对应。
3.2 修改 classes、filters 与锚框的三步操作
拿标准 yolov3.cfg 来改,分三步:全文搜classes=80,共有三处,改成classes=2;每个[yolo]层前面那个卷积层,把filters=255改成filters=21,这个 21 就是上一节里 3×(5+2) 的结果;最后一处是确认三处[yolo]的mask参数分别是 0,1,2、3,4,5、6,7,8,不涉及这两类场景就不要动。修改后的片段长这样:
[convolutional] size=1 stride=1 pad=1 filters=21 activation=linear [yolo] mask=0,1,2 classes=2锚框要不要动,取决于镜头到人脸的距离是否固定。如果摄像头安装高度固定、人员走的是固定通道,先不动锚框,用默认 9 组也能跑到不错的效果。如果画面上口罩大多只有 20~30 像素,默认锚框最小一组(10,13)也偏大,建议对标注做一次聚类,得到 9 组新锚框后按面积从小到大写入 cfg 对应的三组mask中。换锚框之后,建议把max_batches增加 20%,给网络重新适配的时间。
3.3 obj.data 与最小训练命令、参数解读
现在项目里应该有的文件:images/放原图,labels/放对应 txt,mask.names、obj.data、yolov3-mask.cfg都就位。obj.data 内容如下:
classes=2 train=/absolute/path/to/train.txt valid=/absolute/path/to/val.txt names=/absolute/path/to/mask.names backup=/absolute/path/to/backuptrain.txt是图片的绝对路径列表,每行一张。路径写错时 darknet 会立刻报Can't open,此时先检查文件而不是重训。然后执行训练:
# -map 每 4 个 epoch 在验证集上算一次 mAP,监控精度趋势 ./darknet detector train data/obj.data cfg/yolov3-mask.cfg \ darknet53.conv.74 -dont_show -map -gpus 0参数说明:-dont_show不开预览窗口,适合 SSH 远程连接训练;-map很关键,初期训练损失下降慢,可以不加,到 2000 轮后加上,避免验证拖慢整体速度;-gpus 0指定第一块显卡,多卡可以写0,1,2。darknet53.conv.74是预训练权重,没有它从头训练收敛时间通常要多 30% 以上。
cfg 里的batch=64保持不变,subdivisions=8。显存不够时不要降 batch,调大 subdivisions 到 16 或 32,单次真正送入显存的图就变成 64/16=4 张。learning_rate建议 0.001,steps设为总迭代的 80% 和 90%,比如max_batches=8000时写steps=6400,7200。训练日志看avg_loss:开始时 20 以上,几百轮后降到 2 以下,到 0.4~0.8 基本收敛。avg_loss 很低但 map 不再涨,说明过拟合,停止即可。
3.4 数据增强参数与类别不平衡的常见处理
cfg 文件头附近有增强开关,默认情况下flip=1水平翻转在口罩场景保留,angle保持 0,人脸不会倒着出现,saturation、exposure、hue用默认值就行。YOLOv3 原版不包含 mosaic,cfg 里即使有对应开关也保持关闭,想用 mosaic 要换到 YOLOv4 一类结构,不建议在 YOLOv3 上硬改。
提示:公开口罩数据集里 mask 样本通常比 no_mask 多,训练出的模型对 mask 更自信,no_mask 召回偏低。这种情况不建议只调推理阈值,因为阈值是整体平移的,压不低误报。常见做法是对 no_mask 样本做重采样,数量不足时复制若干份,保持两类接近 1:1,效果比后期调参治本。
4. 口罩识别推理侧的关键参数:置信度、NMS 与 mAP 评估
4.1 darknet 推理代码中的阈值发生在哪一步
当直接用 darknet 的 Python 接口做推理,代码很短:
import cv2 import darknet # 最后一个 0 表示不加载 GPU 权重,改 1 会优先使用 CUDA net = darknet.load_net(b"cfg/yolov3-mask.cfg", b"backup/yolov3-mask_final.weights", 0) meta = darknet.load_meta(b"data/mask.names") img = cv2.imread("gate.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # letterbox 保持宽高比,返回缩放比和填充尺寸 resized, ratio, pad = darknet.letterbox_image(img_rgb, (416, 416)) detections = darknet.detect_image(net, meta, resized, thresh=0.3) # 返回格式:(b'no_mask', 0.87, (218.3, 150.2, 26.7, 31.0)) # 坐标是 416x416 网格下的中心点和宽高,需要映射回原图推理链路里有三个过滤点:第一个是thresh,darknet 内核对每个候选框先判断目标置信度,小于阈值的直接丢弃;第二个是类别维度,置信度达标后取概率最高的类别;第三个是 NMS,同类别相邻框按 IoU 合并。很多人以为返回结果不需要再做过滤,其实内部已经处理过,但它不做类间过滤——如果一个 mask 框和一个 no_mask 框高度重叠,你会同时看到两类预测。这种临界帧多出现在“口罩刚摘下”的瞬间。后处理建议加一条硬规则:同一区域同时出现 mask 和 no_mask,只保留置信度高的那个。
4.2 0.25 / 0.45 / 0.7,三档阈值分别适合什么场景
| 阈值 | 召回 | 误检 | 适合场景 |
|---|---|---|---|
| 0.25 | 高 | 较高 | 大范围画面、人工复核 |
| 0.45 | 中 | 中 | 闸机联动、风险告警 |
| 0.7 | 较低 | 很低 | 固定近景、出报告场景 |
先说结论:直接取 0.45 大概率能工作,但不会最优。摄像头在 3 米外时人脸很小,模型输出概率普遍在 0.4~0.6,阈值提到 0.7 会漏掉一大批;近景人脸很大,no_mask 预测常超 0.9,阈值降到 0.25 只会增加误报。正确做法是在验证集上先画 PR 曲线,在误检率可接受范围内选召回最高的点,再用真实场景录 5 分钟视频做一次回归测试。固定机位还有一种更优先的技巧:帧间多数投票,连续 5 帧对同一个目标取众数,能明显压掉单帧闪烁,这比反复调阈值更能解决线上告警抖动。
4.3 用 mAP 和分列 AP 判断模型能否上线
训练结束不能只看 loss,loss 低只说明训练集拟合,判断指标要用验证集 mAP。darknet 内置命令:
./darknet detector map data/obj.data cfg/yolov3-mask.cfg backup/yolov3-mask_last.weights输出会按类别给 AP,再算平均 mAP。口罩识别项目里,我会把标准定为:mask 类 AP 在 0.95 以上,no_mask 类 AP 在 0.9 以上,mAP@0.5 才建议上线。这里特别提醒看分列:如果 mAP 整体 0.96 但 no_mask AP 只有 0.82,风险很大,因为漏过一个没戴口罩的人比误报一个戴上的人严重得多。这种情况优先回数据层补 no_mask 样本,再用 3.4 的方法重采样,而不是往下压推理阈值——阈值压低能找回召回,但误报会同时飙升。
5. 口罩识别常见的翻车点与三个实战技巧
5.1 侧脸、半摘口罩与遮挡样本的处理
侧脸场景中,口罩在被遮挡的一侧往往只剩一条细长条,模型容易把它当成背景。不要只在正面样本上训练,我一般对原始数据集做两类补充:一是旋转增强,把训练图中的人脸区域旋转 ±15°,模拟转头;二是人工挑选“半摘口罩”照片,口罩在下巴位置、嘴鼻暴露,这类统一标注为 no_mask。如果项目里经常出现“口罩挂在脖子上”的误报,可以在数据里额外加一类mask_in_hand作为干扰项,推理时直接过滤这个类别。加类别后,3.2 里的 classes 和 filters 要同步更新。
5.2 用翻转测试时增强提升小目标召回
如果验证集上 no_mask 的漏检集中在远距离,不用重训,先试测试时增强。推理时把输入水平翻转后再跑一次,将得到的框做坐标镜像,与正常推理结果合并。关键一步是坐标转换:
# 翻转 TTA:镜像后的中心点 x 映射回原图坐标系 x = 416 - x这个公式只适用正方形输入。如果用了 letterbox,还要先把坐标减掉 pad、除以 ratio 还原到原图。TTA 能提升小目标召回,但推理时间翻倍。线上无法接受时,可以在夜间模式或远距离模式下单独启用 TTA,平时走单次推理。
5.3 转 ONNX 和 TensorRT 时的实际取舍
边缘设备部署,常见链路是 darknet 权重转 ONNX,再转 TensorRT。导出 ONNX 时尽量固定输入尺寸,避免动态 shape 带来额外复杂度。转 TensorRT 优先用 FP16:
trtexec --onnx=yolov3-mask.onnx --saveEngine=yolov3-mask.engine \ --fp16 --workspace=1024--workspace根据目标显存调整,嵌入式设备给 512~1024 即可。FP16 在口罩识别上几乎不掉点,INT8 需要额外校准,校准集必须同时包含 mask 和 no_mask 两类,并且覆盖白天、夜间和逆光三种情况。确认校准集覆盖不全时,INT8 的精度波动可能比 FP16 更明显,不要为了帧率盲目上 INT8。
本文还有配套的精品资源,点击获取