简介:面向毕业设计、课程设计及深度学习入门学习者,这是一套基于YOLOv8的工地焊接面罩佩戴检测完整项目,聚焦建筑工地安全施工场景下的目标检测问题。压缩包共8个文件,结构清晰:3个Python脚本分别覆盖模型训练、视频检测与可视化界面操作,3个预训练权重文件(yolov8n.pt、best.pt等)可直接加载,2个txt文档包含部署说明与项目说明,整体大小仅15.91MB,在常见GPU环境下简单配置即可运行。源码已经过测试,训练阶段可自动生成核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图,从模型评估到结果展示形成完整闭环;同时可视化界面支持对图像、视频等输入进行直观检测,特别适合毕设答辩现场演示。目前已有42人下载学习,对于需要快速获得可运行深度学习项目或完成类似检测任务的在校学生,这套资源具有拿来即用的实用价值。
1. 工地焊接面罩佩戴检测选YOLOv8,比你想的省事,也比你想的挑数据
在工地安全帽检测已经成为课题红海之后,焊接面罩佩戴检测是这两三年真正值得做的方向。面罩和普通安全帽是两种完全不同的视觉目标:它遮挡大半张脸,只在眼部留一道窄观察窗,而且焊接时飞溅的火花、防护玻璃的反光会让模型在明暗交替里丢目标。YOLOv8能在这个场景里成为主流,是因为它在小目标检测、训练收敛速度和部署自由度上都有现成的成熟配方:从闭源数据集做格式检查、Labelme标注、转YOLO格式、训练、导出ONNX,再到Windows上用PyQt5做一个可视化界面,半天就能跑出第一个像样的mAP。这篇笔记适合手头有源码但改不明白、或者打算从零自己撸一版的人。
2. 面罩检测选型与数据集准备:把“完整数据集”这块地基盘明白
2.1 为什么是YOLOv8而不是Faster R-CNN或SSD
焊接面罩检测的目标特征非常明确:它通常覆盖在工人面部,分为手持式和头戴式两种,反光镜片是画面里的强高光区域。选模型看三点:推理速度、小目标召回率、交付生态。快餐式结论是:Faster R-CNN精度高但交付慢,SSD快但小目标漏检多,YOLOv8在两者之间取了一个平衡点。
Faster R-CNN的两阶段设计确实准,RPN先提候选框再分类回归,但训练要维护RPN和ROI Head两个阶段,调anchor、调NMS参数很耗时。我自己在工地场景试过一次,一张1080P图片在GPU上推理几百毫秒,做实时视频监控很吃力,更别说部署到边缘盒子上。
SSD用VGG16当backbone的年代已经过去,它的anchor是预设的,对远景里只有十几像素的小目标召回很差。焊接面罩的尺寸变化很大,近景占半张画面,远景只有十几个像素点,固定anchor策略在这样一个尺度跨度很大的场景里非常吃亏。
YOLOv8走anchor-free路线,配合C2f结构和P3、P4、P5三层多尺度预测,对小目标的召回能力比前代强不少。而且Ultralytics官方库把训练、验证、导出全做成了命令行工具,数据集结构开箱即用,不用自己写DataLoader。这对毕设和课程设计来说太关键了,时间都花在业务逻辑上,而不是造轮子。
| 模型 | 推理速度 | 小目标召回 | 交付难度 | 部署生态 |
|---|---|---|---|---|
| Faster R-CNN | 慢 | 中 | 高 | 一般 |
| SSD | 快 | 低 | 中 | 一般 |
| YOLOv8 | 快 | 高 | 低 | 丰富 |
上面的对比表可以当答辩时的选型依据。单纯讲精度,Faster R-CNN在部分公开数据集上确实不输YOLOv8,但毕设不是只比一个指标,还要比“能不能按时交”。YOLOv8从数据到界面的链路最短,这是它成为这类项目默认选择的核心原因。
2.2 拿到数据集第一步:检查格式而不是直接训练
标题写着“完整数据集”,但完整不代表和你手里的训练脚本适配。常见的情况有三种:只有JPEGImages和VOC XML,没有YOLO需要的txt标签;有人用Labelme标了多边形,但类别名和代码里写的不一致;标签文件里混着空的txt,一张图里没有目标。
所以拿到数据集先做三件事:
第一,统计图片数量,用脚本把每张图的标注框数量打出来,看看有没有全是0标注的文件。
第二,确认类别名。多数项目只有两类:戴面罩的工人和没戴面罩的工人,英文名可能是welding_mask、person,也可能是mask_positive、mask_negative。这种细节不一致会让训练出来的模型什么都学到。
第三,对比图片尺寸和txt里的归一化坐标是否合理。如果出现大于1的坐标值,说明转换脚本写错了,或者txt不是YOLO格式而是别的格式。
如果原始数据是Labelme的json格式,需要转换成YOLO txt。我一般会写这样一个脚本:
import os import json classes = {"welding_mask": 0, "person": 1} def labelme_json_2_yolo(json_path, out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] lines = [] for shape in data["shapes"]: label = shape["label"] if label not in classes: continue pts = shape["points"] x_s = [p[0] for p in pts] y_s = [p[1] for p in pts] x_min, x_max = min(x_s), max(x_s) y_min, y_max = min(y_s), max(y_s) cx = ((x_min + x_max) / 2) / img_w cy = ((y_min + y_max) / 2) / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h lines.append(f"{classes[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_txt = os.path.join(out_dir, os.path.basename(json_path).replace(".json", ".txt")) with open(out_txt, "w", encoding="utf-8") as f: f.write("\n".join(lines))这段脚本的核心逻辑是把多边形的外接矩形算出来,再归一化到0到1之间。保留6位小数是标准做法,避免浮点坐标抖动。labels字典的键必须和json里的label完全一致,我之前吃过亏,标注员写的是“weld_mask”,脚本里写成“welding_mask”,等于所有目标都被过滤掉了,训练出来的模型检测什么都为空。
转换完之后还要做一次反向检查:随机挑几张图,把txt里的坐标画回去,肉眼看框是否贴合目标。这一步不是多余,坐标多一个小数点错位,目标框就会跑偏,而模型在训练时对这类噪声非常敏感。
2.3 数据集划分:train/val/test不能有同源帧
很多“完整数据集”的目录里只有images和labels两个平铺文件夹,没有划分。这时候不要直接训练,先自己划分。
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/对应的dataset/data.yaml这样写:
path: /home/qc/dataset train: images/train val: images/val test: images/test nc: 2 names: ['welding_mask', 'person']path建议写绝对路径。Ultralytics在训练时会拼接path和train,如果写成相对路径,换机器后大概率报目录不存在。
划分时有一个隐藏很深的坑:来自同一段监控视频的连续帧,如果简单地按时间顺序切成train和val,模型其实见过这些帧的“邻居”,验证集mAP会虚高。实际部署到工地新摄像头时,掉点会非常严重。划分前先对图片做感知哈希去重,或者至少把视频抽帧的时间间隔错开,确保同一场景的帧只出现在一个集合里。
如果数据集总量不足三百张,常规做法是先扩充。翻转、随机裁剪、HSV色彩抖动都有帮助,尤其要把面罩反光区域的亮度变化做足,这样模型才不会把强反光和正常镜片当成两种东西。
3. 从零跑通YOLOv8面罩训练:环境、命令和参数的一次性交代
3.1 用conda装出可复现的训练环境
网上很多环境教程写得很复杂,又是编译又是换源,实际普通项目用不上。我通常只做三步:
conda create -n yolo python=3.10 conda activate yolo pip install ultralytics如果机器没有独立显卡,或者显卡太老装不上CUDA,就装CPU版PyTorch:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu注意这里指定CPU版本的wheel包,避免pip自动拉下带CUDA的包。装了带CUDA的PyTorch但机器上没有对应驱动,运行时直接报TFH或者找不到libcudart。
装完验证一下:
python -c "import ultralytics; print(ultralytics.__version__)"这一步能过滤掉绝大多数环境问题。版本号不一致在YOLOv8的早期版本里很容易踩坑,老版本的detect命令参数和新版本不兼容,比如save_txt、save_conf这些参数在不同小版本里默认值改过。
3.2 训练命令:yolov8n还是yolov8m,只看数据集规模
面罩检测不是一个复杂视觉任务,没有细到需要识别面罩上的焊缝型号,所以模型规模不用太大。数据集在500张以下用yolov8n,500到2000张用yolov8s或yolov8m,再往上有条件就上yolov8l。
训练命令如下:
yolo detect train data=dataset/data.yaml model=yolov8n.pt epochs=30 imgsz=640 batch=8 device=0model传递的是一个预训练权重,这就是迁移学习的入口。Ultralytics会保留COCO上学到的通用特征,只把最后一层分类头替换成自己的类别数,训练起来收敛很快。
参数详细说明:
epochs设30到50就够。焊接面罩检测数据量小,训练超过50轮会开始过拟合,验证集mAP不见涨,训练集loss却一直掉。batch设置取决于显存。8G显存跑imgsz=640时batch最大6到8,6G显存的GTX1660Ti建议batch=4。device=0表示用第一张显卡,没有GPU就改成cpu。
如果预训练权重和类别数对不上,比如data.yaml里写nc=2,但yolov8n.pt是从COCO80类迁移的,会不会报错?正常情况不会,Ultralytics会自适应替换head层,但日志里会有一行warning。强迫症想从零训练,可以改用yolov8n.yaml作为model参数:
yolo detect train data=dataset/data.yaml model=yolov8n.yaml epochs=50 imgsz=640这样是从随机权重开始训练,收敛会慢很多,需要更多epoch才能到达相同精度,一般不建议。
3.3 训练完看什么:results.png和损失函数曲线
训练结束后,在runs/detect/train目录下会生成results.png,里面包含了train/val的box_loss、cls_loss、dfl_loss以及mAP曲线。很多人只看mAP最后停在多少,我建议先看loss曲线。如果val_loss在后期上升而train_loss还在下降,就是过拟合,需要停止训练或者加数据增强。
用训练好的best.pt对单张图片做推理:
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict("test_imgs/worker_01.jpg", conf=0.35, save=True)conf是置信度阈值,默认0.25,面罩检测建议调到0.35到0.5。工地上误报比漏报更让人厌烦,反光的防护玻璃和白色安全帽容易互相误判,调高阈值能少很多红色报警框。save=True会把标注框画在图上存到runs/detect/predict目录。
想直观理解网络结构,可以把best.pt导出成ONNX,再用Netron打开,看每一层的输出尺寸。命令行导出的方式:
yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=Truedynamic=True允许输入尺寸动态变化,后面部署到不同分辨率的视频源时不用重新导出。
4. 把检测模型做成可视化界面:PyQt5、推理线程与部署方式
4.1 界面功能拆解:什么样的界面适合毕设和真实工地
标题里的“可视化界面”是这套项目的加分项,也是答辩时最容易展示的部分。一个够用的面罩检测界面包含4个区域:左侧画面显示区,右侧是检测结果列表,底部有打开图片、打开摄像头、开始检测三个按钮,再加一个置信度阈值滑块。统计栏显示检测总人数、未佩戴人数和报警状态。
技术栈选PyQt5,不选Tkinter和OpenCV自带的高GUI,原因是PyQt5的布局控件美观,对视频流的刷新也友好。OpenCV的highgui窗口在连续刷新时不可控,做不了多控件交互。
4.2 推理线程和UI分离:避免界面卡死的核心代码
新手最容易犯的错误是把推理写在按钮的回调函数里。这样点一次检测,界面主线程被模型推理卡住,窗口变白屏,拖动窗口也没反应,在Windows上还可能出现“未响应”。
正确做法是单独开一个QThread负责检测,用信号把结果回传到UI线程刷新画面。
import cv2 import numpy as np from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectThread(QThread): frame_ready = pyqtSignal(np.ndarray, list) def __init__(self, model_path, source=0): super().__init__() self.model = YOLO(model_path) self.capture = cv2.VideoCapture(source) self.running = True self.conf = 0.4 def run(self): while self.running: ret, frame = self.capture.read() if not ret: break result = self.model.predict(frame, conf=self.conf, verbose=False)[0] boxes = result.boxes.xyxy.cpu().numpy() cls_ids = result.boxes.cls.cpu().numpy() plotted = result.plot() self.frame_ready.emit(plotted, cls_ids.tolist()) def stop(self): self.running = False self.capture.release()关键点有两个:摄像头读取和模型推理都放在run方法里,不在主线程执行;result.plot()返回的是带标注框的BGR图片,和cv2.VideoCapture读出来的通道顺序一致,可以直接交给QImage显示。不要在这里用RGB转换,会多一次无谓的开销。
界面上放一个QSlider控制置信度阈值,UI线程只更新self.conf的值,运行中的线程下一帧就会生效,不需要重启。busy脑回路想的是每次拖动阈值就重启线程,完全没必要。
4.3 佩戴判断逻辑:不是画一个框就完事的
模型只负责输出“这个框是welding_mask还是person”,但业务要判断“这位工人有没有戴面罩”。这里需要一点目标配对逻辑。
常见做法是:把person框和welding_mask框做匹配,计算两框的交并比IoU,如果同一帧里存在一个welding_mask框与某个person框的IoU大于0.3,就认为这个人戴了面罩;否则标记为未佩戴并报警。
这个阈值0.3是经验值。面罩紧贴人脸,框之间交叠面积很高,IoU一般在0.6以上;如果阈值设太低比如0.1,隔壁工友戴的面罩会被误判成当前工人佩戴,导致漏报。
4.4 部署到边缘设备:rk3588和ONNX的转换路径
在工厂实地部署,经常要把模型放到RK3588这类边缘盒子上。流程上先导出ONNX:
yolo export model=best.pt format=onnx opset=12 imgsz=640然后再用rknn-toolkit2把ONNX转换成rknn格式。转换中有两个注意点:RKNPU对某些算子支持不全,YOLOv8的DFL模块结构需要转换脚本里做特殊处理,直接转成rknn后输出维度是错的;固定imgsz=640导出,动态尺寸在RK3588上容易崩显存。
如果你只是做毕业设计,跑到这一步就够了,不必真的去购买RK3588开发板,但答辩的时候说得出“模型可以导出ONNX部署到边缘端,支持摄像头RTSP流接入”,比单纯演示界面效果好得多。
5. 焊接面罩检测避坑:5个翻车现场与对应的后悔药
5.1 训练时类别不匹配,模型从头学到尾
现象:训练日志里出现“WARNING, using inherited class names”之类的提示,训练过程不报错,但推理结果一直输出0号类别,或者识别到的面罩框全部落在背景上。
原因:data.yaml里nc写了和类别数量对不上的数字,或者names列表顺序和模型预训练权重的类别顺序不一致。YOLOv8在加载预训练权重时会尽量保留能匹配的层,匹配不上的随机初始化,这时候如果旧版本库没有正确处理nc的差异,分类头就会乱。
解决:先打开data.yaml确认nc和names一一对应,再训练。不要直接copy网上的data.yaml改个路径就完事。保险做法是先执行验证看数据加载:
yolo detect val data=dataset/data.yaml model=runs/detect/train/weights/best.ptval能正常跑通,说明数据标签基本没有格式问题了。
5.2 GTX1660Ti训练直接OOM
现象:训练刚开始,屏幕输出RuntimeError: CUDA out of memory,后面跟了一串Tried to allocate 128.00 MiB。
原因:6G显存跑yolov8s默认batch=8+imgsz=640,显存不够。
解决:把batch降到4,imgsz保持640。如果还想压,用yolov8n模型并开启gradient checkpointing。实际上6G显存跑yolov8n、batch=4、imgsz=640,大概占5G左右,能跑完。别去设batch=2,虽然能训练但BN层统计不稳定,模型精度可能受影响。
5.3 远距离面罩完全漏检
现象:近景识别正常,两米以外的工人戴没戴面罩完全判断不了,要么没框,要么框在人的胳膊上。
原因:训练数据里缺少小目标样本。大部分数据集从网上下载或者自己拍的近距离照片,小目标占比不到5%,模型没有见过足够多的“小面罩”,学不到对应特征。
解决:给训练集添加小目标。做法有两种,一是把大图缩小成小图再贴到背景上,模拟远景;二是直接从监控视频里抽全景帧,手工标注远处工人。imgsz从640提到1280也有帮助,但训练速度会慢很多。提升imgsz前先确认显存撑得住。
5.4 推理时中文路径直接闪退
现象:界面能正常打开,点选图片后程序崩溃,命令行反馈找不到文件,或者检测结果一直为空。
原因:OpenCV的imread不支持中文路径,Windows下最常见的坑。数据集放在“D:\课题资料\数据集\标注图片”这种路径下,图片读进来就是None,模型无图可推。
解决:不用imread,改用numpy读二进制再解码。
import numpy as np import cv2 def imread_chinese(path): data = np.fromfile(path, dtype=np.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)我一般在项目入口处直接写死这个读取函数,所有图片路径都走它,省得后面哪天路径里混进中文又翻车。
5.5 可视化界面点了开始检测就白屏
现象:界面按钮一点,窗口卡住,转动圆圈,几分钟后恢复,或者Windows提示“程序未响应”。监控视频源时尤其严重。
原因:把模型推理直接放在按钮的clicked信号槽函数里,推理是同步阻塞操作。视频流每帧推理十几毫秒到几十毫秒,但在UI线程里累积起来,界面响应就会被拖垮。
解决:用上一章写好的QThread线程方案。主线程只负责发信号、收信号、刷新画面,模型推理和摄像头读取全部挪到线程里。这算是最基本的多线程编程,但对毕设来说,这一条代码设计的细节往往直接决定演示环节顺不顺利。
6. 进阶技巧:用置信度阈值调优和难例挖掘,让面罩检测真正能用
模型训练完成只是起点,真正拿到工地场景里用,还有一个很重要的步骤:调报警策略。我自己的习惯是把置信度阈值调高到0.5,同时做一个报警确认机制:同一帧里连续3次检测到未佩戴,才触发报警。面罩在强光反射下偶尔会被模型漏掉一帧,如果每帧都报警,工地上几分钟就会产生大量误报,焊工不会再当回事。
难例挖掘也值得做。把验证集里预测错误的图专门收集出来,人工看一眼错在哪里。最容易出错的样本集中在两类:戴着墨镜的工人被识别为戴了面罩,以及手持面罩正在摘下的动作被识别为未佩戴。第一类可以通过加数据集去解决,第二类其实可以当成业务规则处理——检测对方的身体姿态再综合判断。这些工作不需要重新训练模型,但对最终演示效果影响很大。
还有一个值得投入的小技巧:把检测结果实时写入SQLite,记录检测时间、佩戴状态和截图路径。毕业设计的演示环节里,评委问“系统有没有数据记录功能”时,这个细节会很加分。用代码实现也不复杂,在检测线程里拿到结果后执行一条INSERT语句就行。
我自己的一个教训是:不要迷信网上所谓的高mAP模型。别人数据集里的“工地”和真实的工地不是一回事,光照、粉尘、遮挡情况都不一样。真正靠谱的办法是把公开数据集当成起点,自己补拍一段现场环境的视频,抽帧标上几十张图重新训练。这个补训练的过程,才是整个项目里让你能讲清楚“为什么这个模型在我这里有效”的关键。
这篇笔记把YOLOv8焊接面罩检测从选型、数据准备、训练参数、界面设计到部署方案都过了一遍。环境配置、代码路径、阈值设定这些细节都是我实际跑过之后的经验,按着这个顺序去做,半天时间应该能跑通第一个可用版本。如果中途卡在某个报错上,回头看第五部分的排查思路,大概率能找到对应的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取