ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv10的口罩检测系统实战:从数据集到UI界面

基于YOLOv10的口罩检测系统实战:从数据集到UI界面

口罩检测系统这个项目,我一开始想得很简单:在某个场所门口放一台带摄像头的电脑,自动识别进出的人有没有规范佩戴口罩,没戴的马上弹窗提醒。真正动手做起来才发现,从模型训练到UI界面、再到能给别人用,中间隔着好几个“普通人看不见的坑”。整个过程走下来,我把基于YOLOv10的检测模型、常见的YOLO数据集处理流程、Python推理代码、以及带界面的交互程序完整串联了一遍,这套东西不仅能跑,还能根据实际场景快速改造成别的目标检测应用。

这篇文章我会把整个项目从设计思路上拆开,再把数据集准备、模型训练、推理优化、界面集成、常见问题一条一条讲清楚。特别适合正在做人脸目标检测方向毕设、比赛项目,或者想把“训练一个模型”升级成“交付一个能用系统”的开发者。我尽量按实际动手顺序写,很多细节是踩过坑之后才补上的,希望能帮你少走弯路。

1. 项目背景与整体设计思路

1.1 口罩检测到底在检测什么

很多人以为口罩检测就是把图片丢给神经网络,输出“有口罩”或“没口罩”。真实场景远没有这么简单。摄像头拍到的是一整个画面,里面可能有一个人,也可能有一群人,每个人脸大小不一样,光线、角度、遮挡千变万化。所以这个任务本质上是目标检测问题:先找到画面里所有人的脸,再判断每张脸上口罩的状态。

需求方往往还要区分三种状态:规范佩戴口罩、未佩戴口罩、口罩佩戴不规范。不规范包括口罩挂在下巴上、只遮住嘴没遮住鼻子、口罩明显歪斜等。如果只做二分类,这些情况大概率会被直接归为“未佩戴”,或者更糟——“已佩戴”,因为模型看到嘴部区域颜色接近口罩,就容易产生误报。我最初图省事用二分类跑了一版,实测下来不规范佩戴基本全被当成正常,这个方向从数据设计阶段就走错了。

从实际部署角度讲,这个系统的价值在于把人工巡检变成机器辅助:办公室前台、医院入口、工地闸机、校园宿舍楼,这些人流固定、光线相对稳定的场景比较适合落地。真到了室外强光、人员密集到互相遮挡的极端情况,再强的模型也会掉点,需要配合硬件选型和场景约束才能保持可用。

1.2 技术选型:为什么是YOLOv10

YOLO系列在目标检测里属于单阶段算法,核心特点是“一次前向推理同时完成框的定位和分类”,速度天然有优势。口罩检测虽然看起来简单,但场景往往需要实时处理视频流,每秒至少要处理十几帧,只有检测器足够快,后续才有空间做画面显示、报警弹窗、数据统计这些附加功能。

YOLOv10相比早期的YOLOv5/YOLOv8,主要改进在于检测头设计:它抛弃了传统依赖非极大值抑制(NMS)的流程,通过更干净的端到端结构直接输出最终检测结果。NMS的作用是把多个重叠框合并成一个框,如果在模型架构层面解决这个问题,推理少一步后处理,代码更简洁,部署也更友好。

我实际对比过同样数据下YOLOv8和YOLOv10的表现,在显卡资源有限的条件下,YOLOv10在精度和速度的平衡上更好。最容易感知的差异是推理时不再需要小心翼翼地调NMS参数,模型输出置信度和框坐标后,直接按置信度阈值过滤即可。对于封装成UI程序、给别人安装使用的场景,推理逻辑越简单,出问题的概率越低。

1.3 系统整体架构与模块拆解

我把整个系统拆成四个相对独立的模块:数据集模块、训练模块、推理模块、界面展示模块。数据集模块负责从公开渠道和自拍数据中收集图片,做标注格式统一和增强处理;训练模块负责加载YOLOv10预训练权重,在口罩数据上微调;推理模块负责加载导出后的模型,对图片、视频流、摄像头画面执行检测;界面模块负责把检测结果可视化,同时提供图片上传、视频播放、摄像头开关等交互入口。

这样的拆分不是拍脑袋。项目过程中我反复改过需求:先换了数据集,又试了两种导出格式,最后界面也从桌面程序改成了支持局域网访问的版本。如果四个部分耦合在一起,改一处就要动全部代码;拆开之后,数据只管供给,模型只负责输出坐标和类别,界面只做展示和交互,每一层都可以独立替换。这个思路放到任何视觉项目里都成立。

2. 数据集准备与预处理

2.1 数据来源与类别定义

口罩检测的数据集大体上有两个来源:公开的口罩检测数据集,以及自己补充拍摄的现场数据。公开数据集的优势是量大、标注完整,适合作为训练主体;缺点是场景相对固定,比如很多公开图是半身证件照风格,背景简单、人脸正对镜头。如果只靠它训练,部署到真实摄像头场景后,侧面人脸、低头、暗光这些情况几乎一定会漏检。

我的做法是“公开数据打底,自拍数据补短板”。自拍不需要太多,几百张就够了,重点覆盖公开数据集里缺的场景:侧脸、多人同框、口罩挂下巴、进出门口逆光、隔着玻璃门等。标注时严格按三个类别:with_mask、without_mask、mask_weared_incorrect。类别名称本身不重要,但“佩戴不规范”必须有独立标签,否则模型学不到这个概念的边界。

有人可能会问,为什么不直接用网络上现成的“口罩人脸识别数据集”?用是可以的,但要注意那些数据集的标注体系跟YOLO格式不一定匹配,有些还带着水印或错误标注。拿到任何数据集,我先做一遍可视化检查,把标注框画在图上随机抽几百张看,这一步不能省,否则模型训练完你会发现验证集精度虚高,因为训练集里本身就混着错误标签。

2.2 标注格式转换与校验

YOLO系列训练的标签格式是纯文本:每行一个目标,内容是“类别编号 归一化中心点x 归一化中心点y 归一化宽度 归一化高度”。坐标值全部缩放到0到1之间,除以图片实际宽高得到。很多公开数据集给的是COCO格式的JSON或Pascal VOC格式的XML,需要写脚本转换。

转换脚本逻辑很简单:从原始标注文件里读出每个目标的类别和坐标,把框的x_min、y_min、x_max、y_max换算成中心点和宽高,再除以图片尺寸。代码如下:

import json import os def coco_to_yolo(coco_json, output_dir, img_dir): with open(coco_json, 'r') as f: data = json.load(f) # 构建类别映射,按自己定义的顺序排列 id2name = {cat['id']: cat['name'] for cat in data['categories']} name2id = {name: idx for idx, name in enumerate(['with_mask', 'without_mask', 'mask_weared_incorrect'])} for img_info in data['images']: img_id = img_info['id'] img_w, img_h = img_info['width'], img_info['height'] txt_path = os.path.join(output_dir, img_info['file_name'].replace('.jpg', '.txt')) lines = [] for ann in data['annotations']: if ann['image_id'] != img_id: continue cat_id = ann['category_id'] cat_name = id2name[cat_id] if cat_name not in name2id: continue # 忽略无关类别 x, y, w, h = ann['bbox'] cx = (x + w / 2) / img_w cy = (y + h / 2) / img_h nw, nh = w / img_w, h / img_h lines.append(f"{name2id[cat_name]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") if lines: with open(txt_path, 'w') as f: f.write('\n'.join(lines))

转换后必须做一次反向校验:把txt标签重新画到原图上,肉眼确认框的位置没有偏移。最容易出的问题是某些数据集标注框的坐标是相对坐标而非像素坐标,不仔细看会一下子把框全部画歪。还有一个隐蔽问题:小尺寸人脸图经过缩放后,标注框仍保持原始比例,但在实际训练时图片会统一缩放到640x640,如果原图是细长条,缩放后框的坐标要重新理解,这也是我建议在转换时就记录原始宽高的原因。

2.3 数据增强与划分策略

数据集规模方面,我个人的体感是:每类至少一千张图,总数超过三千张效果才会比较稳。公开的口罩数据集通常能凑到几千张,再加上补充数据就够用了。太少的话,模型容易把背景里的圆形物体误判成口罩,或者把深色衣服误判成人脸。

增强策略我用的是YOLOv8/v10生态里默认附带的那一套组合拳:随机翻转、HSV色彩抖动、马赛克增强、随机缩放和位移。马赛克把四张图拼成一张,变相扩大每批次里的小目标数量,对提升小脸检测效果很有效。但马赛克增强不能全程开着,训练后期要关掉,否则模型一直在看不自然的拼贴图,验证集精度会波动。

数据划分我按8:1:1切分训练、验证、测试集。切片前先按视频来源或场景分组,不能把一个摄像头拍出来的连续帧一部分放训练集、一部分放验证集,那样验证集几乎等于泄漏,评估结果会虚高。按场景分组的实现并不复杂:给每张图片的路径前缀做一个分组ID,再在组级别上做随机划分,就能避免“同一个人的不同帧同时出现在训练和验证集里”的问题。

3. YOLOv10训练:参数、过程与结果分析

3.1 环境准备与依赖安装

用Python项目跑YOLOv10,基础环境是Python 3.9以上,加PyTorch深度学习框架。显卡方面,训练阶段有NVIDIA独立显卡会舒服很多,显存4G以上就能跑小模型;没有显卡纯用CPU也能训练,只是同样150轮可能要从两小时变成二十小时,不推荐。推理阶段则宽松得多,CPU跑YOLOv10小模型也能有每秒几帧的速度,界面演示够用。

安装依赖我用的是官方文档推荐的命令,把核心框架和配套工具链一并装好。配置好CUDA和GPU版本的PyTorch之后,可以打印一下框架内置的可用设备信息,确认显卡能被识别。这一步看似基础,实际很多人卡在“装了包但训练时提示找不到GPU”,多半是PyTorch版本和CUDA驱动不匹配。

3.2 训练参数配置与超参数选择

YOLOv10的模型权重有不同尺寸,命名规律是模型越小速度越快、精度越低,模型越大相反。我第一版用YOLOv10n(最轻量),主要考虑是接口机性能弱,加载速度要快;后来发现小模型在“口罩挂下巴”这种难样本上容易误判,换成了YOLOv10s,精度明显提升,速度损失仍然在可接受范围内。

训练参数我给出一份实际跑通的配置:

参数推荐值说明
输入图片尺寸640x640常规选择,兼顾精度和速度
训练轮数150先用50轮看趋势,再加到150
每批数量16显存不足时降到8或使用梯度累积
优化器SGD自带动量,收敛稳定
初始学习率0.01配合前置预热,防止初期震荡
权重衰减0.0005抑制过拟合
预训练权重yolov10s.pt用别人在大型数据集上训好的权重做起点

数据配置用YAML文件描述,指定训练集、验证集路径和类别名称。一个典型的口罩数据集配置文件长这样:

path: mask_data train: images/train val: images/val nc: 3 names: ['with_mask', 'without_mask', 'mask_weared_incorrect']

训练命令写起来很收敛,几乎所有支持YOLOv10的开源脚手架都沿用类似风格:

yolo train model=yolov10s.pt data=mask.yaml epochs=150 imgsz=640 batch=16 device=0

这里有个新手很容易忽略的点:预训练权重和自定义数据集里的类别数量不一致时,模型会自动调整输出层。正常不需要手工改网络结构,但训练日志里要留意标注数量是否变成了3而不是默认的成千上万类,否则说明配置没生效。

3.3 训练过程监控与结果分析

训练过程中我会盯三个东西:损失曲线的下降趋势、验证集精度指标、以及训练集和验证集之间的差距。损失下降说明模型在拟合数据;验证集精度稳步走高说明泛化能力正常;如果训练集损失一直降而验证集指标停滞甚至回落,那就是过拟合的信号,要提前停止或者加强数据增强。

评估指标主要看mAP50和mAP50-95。mAP50是检测框与真实框重叠超过50%就视为正确的平均精度,直观但偏宽松;mAP50-95则把阈值从50%逐步提到95%,更能反映框定位的精细程度。口罩检测这个任务,框稍微大一点小一点问题不大,我用mAP50作为主要参考,目标做到0.93以上;mAP50-95能到0.8以上就算可用。

我第二版模型训练完,验证集的mAP50大约0.94,mAP50-95在0.82左右。实际拿到摄像头画面里测,正脸、侧脸、戴口罩不规范的场景都能稳定检出,偶尔会把远处极小的人脸漏掉,这是正常的,毕竟像素太少,人眼也看不清。这里要强调:指标够用就行,不要陷入“刷精度”的怪圈,工程项目的验收标准是真实场景好用,不是排行榜名次。

4. 模型导出与推理优化

4.1 模型导出格式怎么选

训练结束后拿到的best.pt是PyTorch格式模型。直接用它做推理没有问题,但如果要把系统分发给别人,源码和训练框架的依赖对接收方是个负担。我尝试了三种导出格式,最终根据使用场景做了取舍:

导出格式优点缺点适用场景
PyTorch原始改起来方便,能继续训练依赖重,推理稍慢项目调试阶段
ONNX跨平台、可CPU可GPU,安装依赖少动态尺寸支持要配置主推方案
TensorRT英伟达GPU上极快绑定特定显卡型号固定GPU服务器部署

我最终选择ONNX作为正式交付格式。ONNX是行业通用的模型交换格式,运行时环境只需要安装对应runtime库,不用把整个深度学习训练框架带上。导出命令一行即可:

yolo export model=best.pt format=onnx opset=11 simplify=True

导出后务必用ONNX格式重新跑一遍推理,跟PyTorch格式的检测结果做对比,确保坐标和置信度一致。这一步我踩过坑:有一次导出后框的位置整体偏移,原因是导出时输入尺寸没有固定,推理时又用了不同尺寸的图,导致坐标缩放不匹配。解决办法是导出时固定opset和输入尺寸,推理代码跟训练时保持一致。

4.2 推理Pipeline:从图片到检测结果

推理流程里面藏着很多“隐蔽的细节”。直接拿摄像头原始帧喂给模型,效果往往很差,因为摄像头分辨率很高,而模型训练时统一用640x640正方形输入。直接把图片resize成正方形,会让原本横向或纵向的画面被压扁,检测框位置也会跟着变形。正确的做法是letterbox处理:保持图片原始宽高比,在四周补灰边,让画面内容不变形地放进正方形框里。

推理代码的核心流程如下:

import cv2 import numpy as np import onnxruntime def letterbox(img, new_size=(640, 640), color=(114, 114, 114)): h, w = img.shape[:2] ratio = min(new_size[0] / h, new_size[1] / w) nh, nw = int(round(h * ratio)), int(round(w * ratio)) resized = cv2.resize(img, (nw, nh)) canvas = np.full((new_size[0], new_size[1], 3), color, dtype=np.uint8) dh, dw = (new_size[0] - nh) // 2, (new_size[1] - nw) // 2 canvas[dh:dh+nh, dw:dw+nw] = resized ratio_original = ratio return canvas, dh, dw, ratio_original img = cv2.imread('test.jpg') img_input, pad_h, pad_w, scale = letterbox(img) img_rgb = cv2.cvtColor(img_input, cv2.COLOR_BGR2RGB) img_norm = img_rgb.astype(np.float32) / 255.0 blob = np.transpose(img_norm, (2, 0, 1))[None, ...] # 转成(CHW)并加batch维度 sess = onnxruntime.InferenceSession('best.onnx', providers=['CPUExecutionProvider']) outputs = sess.run(None, {sess.get_inputs()[0].name: blob}) # 对outputs做阈值过滤后,坐标要减掉pad再除以scale,还原回原图坐标系

坐标还原是新手出错率最高的地方。模型输出的框坐标是letterbox之后坐标系里的值,必须先把框的中心点坐标减去填充的灰边宽度,再除以缩放比例,才能对应到原始图像上。画框前再额外做一次越界裁剪,防止框超出图像边界导致显示异常。置信度阈值我一般设在0.35到0.45之间,阈值太低会看到一堆误检框,太高又容易漏掉佩戴不规范的目标。

4.3 实时视频流的性能优化

处理单张图片,这套流程跑得再慢也无所谓;换成视频流就要精打细算。最简单有效的优化是“解耦采集和推理”:用开一个读取摄像头帧的子线程,把帧放进队列,主线程不断从队列里取帧做检测。这样即使某一帧检测耗时稍长,采集线程也不会丢帧,UI界面不会卡死,视频展示仍然连续。

推理后端我也想多说一句。ONNX runtime同时支持CPU和GPU,选择哪个运行时直接影响FPS。普通办公电脑CPU跑YOLOv10s,640输入大约每秒5到10帧,基本能看;换成GPU版本后提升非常明显,每秒能到30帧以上。实际部署时先确认目标机器的显卡型号,再决定是否启用GPU推理,不用盲目追求高性能。

显存较小的机器可以进一步使用FP16半精度,推理速度进一步提升,精度损失通常在一个点以内。口罩检测这个任务精度冗余比较大,半精度完全够用。转换方式和NVIDIA相关环境的配置在导出阶段就能集成,不需要改推理代码。

5. UI界面设计与系统集成

5.1 界面框架怎么选:桌面还是Web

项目标题里要求有UI界面,具体选型我做过两版对比。第一版用Python自带的桌面UI库(类似Tkinter或PySide6),优点是程序独立、双击就能跑,适合单机使用;缺点是界面样式比较朴素,打包分发时要小心各种动态库缺失。第二版改用轻量级Web前端框架,直接把检测服务跑在本地端口上,用浏览器访问,界面美观度、跨平台性都更好,还省去了界面打包的麻烦。

我最终选择了Web方案来交付演示版本。原因很具体:现场验收时不一定有条件给每台电脑装Python环境和摄像头驱动,但几乎所有人都能用浏览器打开页面。直接把视频流推送到页面上,操作人员零学习成本,截图、回传、放大都不受限制。

5.2 界面逻辑与核心功能实现

UI的主流程是这样的:页面加载时优先初始化检测模型,读取摄像头画面并送入模型推理,把识别结果显示在视频上方。我额外加了一个“历史记录”区域,每检测到一次“未佩戴口罩”,就保存一张截图和时间戳,方便事后查验。有人觉得这是多余功能,但实际用起来很刚需,安保人员不可能一直盯着屏幕,有记录才能追溯。

实现上需要注意一个细节:模型初始化必须放在线程启动之前。如果边播视频边加载模型,视频会卡顿几秒,让人误以为程序崩溃。我习惯用延迟加载模式设计页面初始状态:先显示“模型加载中”的提示,加载完成后自动切换到视频画面,整个过程有明确的状态反馈。

5.3 打包与分发时的常见问题

把Python视觉项目打包成可执行程序这事,比训练模型还折腾。最大的坑是资源文件路径:模型权重文件、配置文件如果放在项目根目录,打包后路径可能失效,程序双击就报“找不到文件”。我的做法是把模型文件打包进可执行程序内部的资源目录,代码里用相对资源路径读取,避免依赖外部目录。

另一个常见问题是界面运行时报错“找不到某个链接库”。一般涉及的是某些动态链接库在打包脚本里没有被自动收集。解决办法是在打包配置里显式声明隐藏导入项或额外二进制文件路径。这个不能完全靠纯粹源码逻辑解决,需要多试几次打包产物,所以我建议对打包细节提前做一轮单独测试,别在交付前才临时打包。

6. 常见问题与排查技巧实录

6.1 训练阶段的典型问题

一说到训练,第一个高频问题是显存不够。批量大小设为16跑不动,报CUDA out of memory。最简单的处理是把批量降到8或4,或者启用梯度累积,效果类似,显存占用大幅下降。如果8都跑不动,建议换更轻量的模型,比如从s降到n,或者缩小输入尺寸到480,多数口罩检测场景精度损失不明显。

第二个高频问题是损失不下降。出现这种情况先检查数据集标签是不是全乱了,再检查类别数量和配置文件是否一致。我犯过的错误是标注里的类别编号从1开始,而YOLO训练从0开始,导致模型把背景当第0类,Loss数值稳定在很高位置,验证集精度始终为零。排查方式是在训练脚本里加载一次数据,把每一批的标签分布打印出来人工检查。

第三个问题是过拟合。样本总量小、类别不平衡是主要原因。除了常规的增强策略,我建议给数量多的类别做欠采样,给数量少的类别做重复采样,让每个批次的类别比例接近。口罩佩戴不规范这个类别往往最少,而恰恰是需求方最关心的类别,更要注意平衡。

6.2 推理部署阶段的典型问题

推理阶段最让我头疼的问题是ONNX模型在部分机器上跑出来的结果和PyTorch完全不一致。排查后发现是opset版本太新,目标机器上的runtime版本不支持某个算子,导致部分层计算错误。解决办法是导出时把opset固定在较低版本,导出前先跑一个简单的验证图片,对比两种格式输出的框坐标。版本兼容这种事,宁可保守一点,也不要追求最新。

摄像头调用是另一个高频问题。OpenCV自带的摄像头调用接口在桌面机器上通常能直接打开设备索引0,但笔记本内置摄像头、外接USB摄像头、虚拟摄像头会占用不同的设备号。界面里我把摄像头参数做成可配置项,同时增加一个“刷新设备列表”按钮,避免用户拿着程序却找不到自己的摄像头的尴尬。

还会遇到“程序正常运行但界面没有画面”的情况。这多半是视频帧读取线程和界面刷新线程没有同步好。我的处理方案是给帧队列设置最大长度,读取线程只存最新一帧,检测显示线程按固定频率取用,既不影响推理速度,又不会让内存持续增长。

6.3 工程实施与交付建议

项目交付阶段,我强烈建议做好数据和模型的版本管理。模型A在这个场景好用,换个场景效果变差,如果自己没有留下训练参数、数据集版本、导出配置的完整记录,回头排查非常痛苦。我用最朴素的方式:每次实验新建一个文件夹,里面同时保存训练配置、模型权重和推理测试截图,命名格式统一为“日期-模型尺寸-数据集版本”。这个方法虽然土,但真的可靠。

模型文件命名也要避坑。不要用中文名或带空格的路径,这在代码跨平台运行时会带来一系列编码问题。所有配置路径统一用英文加下划线命名,说是小细节,实际能省很多沟通成本。

合规方面也要多说一句:口罩检测系统会接触到人脸区域,无论是训练数据还是部署现场,都要遵循正当用途、最小化采集的原则。做技术的人容易只盯着精度和速度,忽略了数据合规,这个意识越早建立越好。

7. 写在最后:个人经验与扩展方向

整个项目做下来,我最深的体会是:训练模型只是最顺的一环,真正决定项目能不能落地的是数据质量和工程细节。数据标注做不好,后面所有环节都是白费;推理坐标还原没处理好,界面再花哨也是错的;UI交互不贴合使用习惯,再准的模型也得不到用户信任。能跑通一个Demo不代表能交付一个系统,这条界线不仅在技术深度上,更在工程成熟度上。

如果让我重新做一遍这个项目,我一开始就会把类别设计成三分类而不是二分类,先把数据可视化检查的脚本写好,界面直接用浏览器方案而不是桌面方案。这三件事能让整个项目的返工量减少一半。

后续如果要扩展,最直接的方向是加一个目标跟踪模块,检测到未佩戴口罩的人后,在视频流里持续跟踪他的轨迹;更进一步可以接入消息通知,把识别结果推送给值班人员的手机;硬件上也可以把模型转换到边缘设备运行,彻底脱离电脑主机。口罩检测的系统架构完全能迁移到安全帽检测、吸烟检测、人员聚集检测等场景,核心代码几乎不用改,换数据集重新训练即可。

我个人实际操作下来,最爽的时刻并不是看到模型精度刷到多高,而是傍晚验收时,系统在门口连续识别出三个没戴口罩的人,值班员在浏览器页面上看到截图弹出,跟我说了一句“这东西还真有用”。技术文章写得再多,都不如那一刻来得实在。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进