改进YOLOv8+EMA注意力机制的工地安全装备检测实战

发布时间:2026/9/29 18:08:48
改进YOLOv8+EMA注意力机制的工地安全装备检测实战
简介面向建筑工地高空作业场景基于改进YOLOv8的安全防护装备智能视觉检测与实时预警系统源码包适合安全管理人员、AI算法初学者及智慧工地项目开发者。系统核心采用YOLOv8可对安全帽、安全带等装备佩戴情况进行实时识别视频监控或图像输入下发现未佩戴或穿戴不规范时立即触发报警。资源共26个文件大小4.35MB以Python脚本训练、验证、预测、界面、PNG效果图、Markdown和Word说明文档为主并包含从YOLOv8标注数据集到一键训练的全流程教学指引。已有88人学习下载。除可运行代码外资料还提供详细架构说明与操作文档使用者可从零起步完成数据准备、模型训练和参数调整系统设计兼顾低照度环境与工人行为分析可辅助工地预防因安全装备缺失导致的高空坠落事故。整体轻量、步骤清晰适合用来快速搭建安全检测原型或作为相关课题研究的基础。1. 建筑工地安全装备检测为什么我最终选了改进YOLOv8这条路工地现场装再多的摄像头末端还是需要人盯着屏幕而人的注意力撑不过二十分钟安全帽、安全绳这类小目标一漏就是安全事故。这套基于改进YOLOv8的检测预警系统本质上就是把你从盯屏里解脱出来它把原生YOLOv8在小目标检测上的短板P5层32倍下采样后几像素的目标在特征图上几乎消失通过加P2小目标检测头和EMA注意力机制补上来再配合数据标注、一键训练和实时帧过滤让高空作业场景下未戴安全帽、未系安全绳、未穿反光衣的行为能被自动抓出来。适合正在做毕设、课设准备用YOLOv8训练自己的数据集但不想从零攒数据和调参的人也适合工地智能监控项目里需要快速跑通一个可演示原型的从业者。2. 改进YOLOv8的落地思路从网络结构图开始看该改哪里2.1 原生YOLOv8对小目标的瓶颈在哪里先看网络结构图里最核心的几条线。YOLOv8的backbone是CSPDarknet结构由C2f模块堆叠而成后面接SPPF做多尺度特征提取neck用PAN-FPN做自顶向下的特征融合最终输出3个检测头分别对应下采样8倍、16倍、32倍的特征图也就是P3、P4、P5层。以输入640x640为例三个头的特征图尺寸是80x80、40x40、20x20。问题就出在P5层。32倍下采样意味着一个20x20像素的安全帽在最后一层特征图上只剩不到1个像素的响应基本等于丢失。工地高空作业场景里摄像头通常装在塔吊臂或者基坑边缘距离远安全帽、安全绳在画面里可能只有10到20个像素。原生YOLOv8的P3层虽然保留了8倍下采样特征但PAN-FPN的融合路径太长小目标的语义信息在多次卷积里被稀释。这就是为什么很多人拿原生YOLOv8跑安全帽数据集结果mAP50看着有80多放到现场视频里漏检率依然很高的原因。改进方向有两条路一是加注意力机制让网络在特征提取时对小目标区域更敏感二是加P2检测头把4倍下采样特征引进来。这两个方向互补注意力机制提升特征的判别力P2头补足空间分辨率。后面两节分别落地。2.2 在C2f模块里嵌入EMA注意力代码与插入位置EMAEfficient Multi-scale Attention是CBAM之后比较实用的改进方案它把通道分组后分别做横向和纵向的空间注意力再做跨组信息融合。相比SE注意力只有通道注意力EMA多了一个空间维度对密集小目标更友好相比CBAM它的参数量和计算量都更小。我一般把它插在neck的C2f模块后面而不是backbone里因为backbone后半段的特征语义已经很抽象注意力对空间位置的纠正能力有限neck阶段做多尺度融合时加注意力收益更明显。import torch import torch.nn as nn class EMA(nn.Module): def __init__(self, channels, factor32): super(EMA, self).__init__() self.groups factor assert channels // self.groups 0 self.softmax nn.Softmax(dim-1) self.agp nn.AdaptiveAvgPool2d((1, 1)) self.pool_h nn.AdaptiveAvgPool2d((None, 1)) self.pool_w nn.AdaptiveAvgPool2d((1, None)) self.gn nn.GroupNorm(channels // self.groups, channels // self.groups) self.conv1x1 nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size1) self.conv3x3 nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size3, padding1) def forward(self, x): b, c, h, w x.size() group_x x.reshape(b, self.groups, c // self.groups, h, w) x_h self.pool_h(group_x) x_w self.pool_w(group_x) hw self.conv1x1(torch.cat([x_h, x_w], dim-1)) att self.softmax(hw) out group_x * att out self.gn(out.reshape(b, c // self.groups, h, w)) return out这段代码的逻辑是先把输入按通道分成factor组每组单独做水平方向和垂直方向的自适应池化再通过1x1卷积融合成注意力权重最后用softmax归一化后乘回原特征图。注意这里有个关键的group norm层它的作用是稳定分组后的特征分布避免注意力权重在某些通道上过大或过小。实际使用时分母的factor32如果输入通道是512每组就是16个通道计算量很小。插入位置在yaml文件里以neck部分为例C2f之后紧跟一个EMA层# yolov8-p2-ema.yaml neck片段结构示意 head: - [ -1, 1, Conv, [ 768, 1, 1 ] ] - [ -1, 1, nn.Upsample, [ None, 2, nearest ] ] - [ [ -1, 6 ], 1, Concat, [ 1 ] ] - [ -1, 3, C2f, [ 512 ] ] - [ -1, 1, EMA, [ 512 ] ] # 第一处EMA - [ -1, 1, Conv, [ 256, 1, 1 ] ] - [ -1, 1, nn.Upsample, [ None, 2, nearest ] ] - [ [ -1, 4 ], 1, Concat, [ 1 ] ] - [ -1, 3, C2f, [ 256 ] ] - [ -1, 1, EMA, [ 256 ] ] # 第二处EMA注意EMA层不改变特征图的shape所以不需要额外调整后续concat的索引。但yaml解析要求自定义模块在ultralytics的注册表里提前声明否则会报module not found。最常见的做法是把EMA类放到ultralytics/nn/modules/conv.py里然后在__init__.py导出。2.3 增加P2小目标检测头一段yaml讲清楚改法P2头就是在原有P3/P4/P5三个检测头的基础上再加一个4倍下采样的检测分支。对640x640输入来说P2特征图是160x160一个10像素的安全帽在P2上还有2到3个像素的响应这就是它能提升小目标recall的根本原因。代价是neck部分多了一次上采样和concat整体计算量会增加三分之一左右参数量多大约1.2M6G显存的卡需要稍微注意batch size。# yolov8-p2-ema.yaml 完整网络配置示意 backbone: - [ -1, 1, Conv, [ 64, 3, 2 ] ] # 0-P1/2 - [ -1, 1, Conv, [ 128, 3, 2 ] ] # 1-P2/4P2从这里引出 - [ -1, 3, C2f, [ 128, True ] ] # 2 - [ -1, 1, Conv, [ 256, 3, 2 ] ] # 3-P3/8 - [ -1, 6, C2f, [ 256, True ] ] # 4 - [ -1, 1, Conv, [ 512, 3, 2 ] ] # 5-P4/16 - [ -1, 6, C2f, [ 512, True ] ] # 6 - [ -1, 1, Conv, [ 1024, 3, 2 ] ] # 7-P5/32 - [ -1, 3, C2f, [ 1024, True ] ] # 8 - [ -1, 1, SPPF, [ 1024, 5 ] ] # 9 head: - [ -1, 1, Conv, [ 768, 1, 1 ] ] # 压缩P5通道 - [ -1, 1, nn.Upsample, [ None, 2, nearest ] ] - [ [ -1, 6 ], 1, Concat, [ 1 ] ] # 与P4融合 - [ -1, 3, C2f, [ 512 ] ] - [ -1, 1, EMA, [ 512 ] ] - [ -1, 1, Conv, [ 256, 1, 1 ] ] - [ -1, 1, nn.Upsample, [ None, 2, nearest ] ] - [ [ -1, 4 ], 1, Concat, [ 1 ] ] # 与P3融合 - [ -1, 3, C2f, [ 256 ] ] - [ -1, 1, EMA, [ 256 ] ] - [ -1, 1, Conv, [ 128, 1, 1 ] ] - [ -1, 1, nn.Upsample, [ None, 2, nearest ] ] - [ [ -1, 2 ], 1, Concat, [ 1 ] ] # 与P2融合新增分支 - [ -1, 3, C2f, [ 128 ] ] - [ -1, 1, EMA, [ 128 ] ]关键点在于backbone第2层输出的P2特征和head最后一次上采样后的特征concatconcat索引要对应backbone里的第2层。改完这个yaml后副本的检测头数量从3个变成4个ultralytics会自动为P2分配一个输出尺度不需要手动改detect层代码。显存不够时优先把imgsz从640降到512实测P2分支在512输入下特征图是128x128显存占用能降三分之一速度损失也小。3. 把原始图片变成能训练的格式Labelme标注、转换脚本与数据划分3.1 类别设计安全帽检测里的头与帽逻辑很多第一次做安全帽检测的人只标一个helmet类训练出来的模型确实能检测到帽子但后端报警逻辑不好写——你要判断工人没戴帽子而不是判断帽子的有无。我用的方案是标四个类helmet已戴的安全帽、head未戴帽子的裸露头部、vest反光衣、rope安全绳。预警逻辑写起来非常直观画面里出现head且该head附近没有helmet就判定为未戴安全帽出现人形区域但rope置信度低就判定未系安全绳。标注规范上安全帽和头用多边形或者矩形都行但安全绳一定要用分段矩形。安全绳是弯曲的细长目标如果你沿着绳子的弧度画一个多边形转换脚本取外接矩形时会把一大片背景框进来模型学到的是绳子加背景的混合特征检测率会很差。我的做法是画3到5个矩形分段覆盖绳子主体每个分段独立标一个rope。3.2 Labelme JSON转YOLO格式的转换脚本Labelme导出的JSON是shapes数组points是多边形顶点坐标需要转换成YOLO格式的归一化中心点坐标和宽高。转换脚本的逻辑是读取原始图片宽高取多边形顶点集合的最小外接矩形然后归一化到0到1区间。注意不要在里面做任何letterbox操作letterbox是训练端自动处理的转换脚本只负责归一化。import json import os from glob import glob CLASS_MAP {helmet: 0, head: 1, vest: 2, rope: 3} def convert_labelme_to_yolo(json_path, out_dir): # 读取labelme标注文件提取图片宽高 with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w, img_h data[imageWidth], data[imageHeight] txt_name os.path.splitext(os.path.basename(json_path))[0] .txt lines [] # 遍历每个标注对象取多边形外接矩形并归一化 for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x1, y1 min(xs), min(ys) x2, y2 max(xs), max(ys) bw, bh x2 - x1, y2 - y1 if bw 2 or bh 2: # 过滤掉退化标注宽度或高度小于2像素 continue cx x1 bw / 2.0 cy y1 bh / 2.0 lines.append(f{CLASS_MAP[label]} {cx / img_w:.6f} {cy / img_h:.6f} {bw / img_w:.6f} {bh / img_h:.6f}) if lines: with open(os.path.join(out_dir, txt_name), w, encodingutf-8) as f: f.write(\n.join(lines)) # 批量转换labelme_json目录下所有json文件 os.makedirs(labels/train, exist_okTrue) os.makedirs(labels/val, exist_okTrue) for jp in glob(labelme_json/*.json): convert_labelme_to_yolo(jp, labels/train)这段代码里有三个值得注意的地方。一是bw 2 or bh 2的过滤条件labelme里很容易误点出一个只有1像素宽的碎点这种标注会让loss突然跳高直接过滤比后期清洗省事。二是cx / img_w的归一化除以的是原始图片分辨率不是缩放后的分辨率。三是输出目录按train和val分开建好转换时手动分拣json文件避免训练集和验证集出现同一张图的不同副本。转换完成后一定要做一步可视化检查用ultralytics的yolo predict把txt标注画回图片上如果发现框整体偏移或者缩放异常基本可以断定是letterbox处理重复了。我在这一步翻车过两次浪费了整整一个周末的清洗时间。3.3 数据增强与划分细长目标不要盲目开Mosaic数据划分上我一般按9比1切分训练集和验证集切分时保证四个类别的目标数量在各集合里的比例大致一致而不是简单按文件数切。写一个小脚本统计每个类别的box数量如果rope只有几十个helmet有几千个先别急着训练优先补rope的数据或者给rope做在线数据增强。Ultralytics默认开启Mosaic和MixUp这两个增强对常规目标是有效的但安全绳这种细长目标在Mosaic拼接时频繁被裁断。Mosaic把四张图缩放后拼成一张roi区域被切掉一半是常态模型反复看到半截绳子学到的特征是被截断的推理时自然对完整绳子不敏感。所以我训练这个数据集时会显式关闭Mosaic和MixUp只保留翻转、缩放和HSV扰动# config/safety_data.yaml path: /home/user/safety_dataset train: images/train val: images/val nc: 4 names: [helmet, head, vest, rope] # ultralytics会读取下面的augment配置 mosaic: 0.0 mixup: 0.0 fliplr: 0.5 scale: 0.3 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4关闭Mosaic后训练速度会快一些但模型的泛化能力会稍微下降所以scale和hsv的扰动幅度可以适当加大。需要注意hsv_h是色相偏移取值0到10.015表示在色相环上做很小的偏移不要调到0.1以上否则安全帽的黄色、反光衣的荧光色都会变色反而学不到颜色特征。4. Ubuntu 20.04 上把训练跑通环境配置、一键训练命令与loss曲线4.1 环境配置GPU版与CPU版的分叉路Ubuntu 20.04搭建YOLOv8环境GPU版和CPU版的差别只在torch的安装方式上。GPU版要求先确认NVIDIA驱动和CUDA版本nvidia-smi里显示的CUDA Version是驱动支持的最高版本不一定要装那个版本只要PyTorch的CUDA运行时版本不高于它就行。我用的是CUDA 11.7配torch 1.13.1在GTX 1660Ti上跑YOLOv8训练6G显存能跑batch 16加imgsz 640前提是开启AMP自动混合精度。# 创建虚拟环境Python 3.8是ultralytics兼容性最稳的版本 conda create -n yolov8 python3.8 -y conda activate yolov8 # GPU版本安装CUDA 11.7 pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install ultralytics8.0.47 # CPU版本安装无NVIDIA显卡 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics8.0.47CPU版本装完后用torch.cuda.is_available()验证返回False是正常的。CPU训练非常慢一个epoch可能跑十几分钟但有两个场景值得用一是刚拿到数据集时先用CPU跑一个epoch验证标注格式对不对提前把错位问题暴露出来比GPU跑两个小时后再发现强得多二是只有单张1050Ti以下的老卡显存只有2G跑YOLOv8会直接OOMCPU模式至少能出结果。ultralytics版本建议锁死8.0.47新版改动频繁自定义yaml的解析规则经常变同一个yaml在不同版本下跑出来的结果对不上这是环境配置里最常见的玄学问题。4.2 data.yaml 与一键训练命令参数逐项说清数据准备好后训练本身就是一个命令的事。关键是把data.yaml、模型yaml和数据集路径对好路径建议写绝对路径很多人在训练时把相对路径写错报FileNotFoundError。data.yaml的内容在上一章已经给了这里直接看训练命令# 进入项目根目录激活环境后执行 yolo train \ dataconfig/safety_data.yaml \ modelconfig/yolov8-p2-ema.yaml \ epochs200 \ batch16 \ imgsz640 \ patience30 \ device0 \ workers8 \ optimizerAdamW \ lr00.001 \ lrf0.01参数含义逐项说明epochs是最大训练轮数不是必须跑满patience30表示验证集指标连续30轮不提升就早停一般跑不到200轮就停了batch16是单卡batch size1660Ti 6G显存下配合AMP刚好能跑显存不够时先降imgsz再降batchimgsz640是训练时的输入尺寸P2头在这个尺寸下特征图160x160显存压力比原生YOLOv8大降到512能明显缓解workers8是数据加载线程数如果你的机器CPU核数少可以降到4worker太多会把内存吃满反而拖慢训练optimizer换成AdamW比默认的SGD收敛快一些尤其在数据量只有几千张的中小数据集上但注意lr0要相应调小AdamW的0.001相当于SGD的0.01。train_loss在第一轮开始时下降明显后面趋于平缓。如果看到train_loss曲线先升后降那是warmup阶段学习率在爬坡正常的。真正要盯的是val/loss和metrics/mAP50这两条曲线val/loss连续多轮不降反而上升说明过拟合了早停会自动触发。4.3 训练结果与loss曲线怎么解读训练结束后weights目录下会有best.pt和last.ptbest.pt是验证集mAP最高的权重日常部署就选它。results.png是训练过程曲线总览包含train/loss、val/loss、metrics/mAP50、metrics/mAP50-95四组曲线重点看val/loss和mAP50曲线是否同步收敛。confusion_matrix.png看类别之间的混淆情况如果helmet和head互相混淆严重很大概率是标注时没有区分戴帽和未戴的边界也就是帽子戴得很往后仰、露出大面积额头时标注员的标准不一致。loss曲线图在训练日志里默认生成不需要额外脚本。如果你用的是自定义改进模型想在训练结束后自己画loss曲线可以通过ultralytics.utils.metrics里的回调函数读取每个epoch的loss值但日常用results.png就够了。真正值得花时间的是打开val_batch*.jpg看验证集的预测可视化随机抽几十张放大看小目标的框准不准这比看mAP数字更能发现问题。比如安全绳的目标框是否只有一半进入了GT的IoU范围这种问题在mAP曲线上几乎看不出来。5. 训练与检测过程的避坑笔记五条真实踩坑记录5.1 前20轮loss降不下去不一定是模型问题现象训练到10到20轮train_loss曲线基本走平val mAP为0loss值一直徘徊在20以上不往下走。原因绝大多数情况是标注质量问题最常见的是漏标——图片里明明有五个工人只标了两个安全帽模型被反复告知这五个区域没有目标梯度方向被错误标签带偏了。另一个可能原因是学习率太低warmup结束后还在0.001附近爬。解决先去验证标注不要一上来就调学习率。写一段可视化脚本随机抽50张训练图片把GT标注框画出来肉眼检查。如果确实漏标补标后重新训练而不是简单加大学习率。排错顺序一定是先查数据再动超参数。5.2 标注错位转换脚本里不要做Letterbox现象训练时loss能正常下降但验证集的预测框整体偏移到左上角或右下角目标位置跟框对不上越小的目标偏得越厉害。原因Labelme标注的坐标是原始图片分辨率训练时ultralytics会用letterbox把图片等比缩放到640x640并在多余区域填充灰色。如果你的转换脚本先做了letterbox padding训练端又做了一次GT坐标和图片内容就错位了。这是Labelme转换类项目翻车率最高的地方。解决转换脚本只输出归一化的中心点坐标和宽高不做任何padding或resizeletterbox完全交给训练端。如果还是错位检查数据集的图片是不是和Labelme标注时用的原图分辨率一致有些人在标注前把图片压缩过JSON里存的imageHeight还是旧值。5.3 OOM显存溢出调整顺序比单降Batch更有用现象batch16、imgsz640直接报CUDA out of memory而同样的配置原生YOLOv8能跑起来。原因加了P2头之后neck部分多了一个160x160的大特征图流显存峰值比原生YOLOv8高30%左右6G卡直接顶不住。很多人第一反应是把batch降到2但batch太小会导致BN统计量不稳定loss曲线抖得厉害。解决调整顺序一定是先降imgsz再降batch。把imgsz从640降到512P2特征图从160x160变成128x128显存占用能降近三分之一batch保持16不动。如果还OOM再把batch降到8。AMP保持开启它是白送的显存节省不用白不用。5.4 验证集mAP很高现场视频却乱报现象验证集mAP50有92%但放到现场实时视频里安全帽漏检、雨伞和安全网误报为反光衣一秒钟跳好几个框。原因两个层面。第一是推理时没有做letterbox现场视频是1920x1080直接resize到640x640把画面压扁了目标形状失真第二是训练集都是白天正午拍的现场视频有傍晚、阴天、雨雾分布差异大。还有一个隐藏原因置信度阈值太低了ultralytics默认0.25适合学术指标但对现场误报容忍度极低。解决推理代码里用letterbox预处理或者直接把imgsz调成1280前提是训练时imgsz至少到960否则模型没见过这么大分辨率效果反而差。把现场不同时段的视频抽帧补充进训练集跑一轮增量训练。置信度阈值从0.25提到0.55配合帧间过滤来降低误报这一步在第6章展开。5.5 安全绳检测不到细长目标别开强增强现象安全帽的mAP50到88%反光衣到82%安全绳只有33%大部分漏检偶尔检测到的框也只有绳子的一半。原因安全绳是细长目标长宽比经常到10比1以上YOLO系列的矩形框对这种极端长宽比本来就不友好。更关键的是Mosaic增强把绳子切成碎片模型学到的是绳头而不是绳子本体。加上样本量少一两百条标注撑不起一个类别的学习。解决第一关掉Mosaic和MixUp只保留翻转和HSV扰动第二把安全绳标注改成多段矩形每段独立一个框相当于给模型降低了学习难度第三显式给rope类别的loss增加权重ultralytics没有直接的接口我是在损失计算时对类别做加权把rope的权重从1提到2.5第四如果现场是固定摄像头把绳子的ROI区域单独裁出来做一次切片推理比全图硬扛可靠。6. 实时预警落地的一个关键技巧置信度过滤与帧间NMS6.1 单帧检测为什么会误报视频检测和单张图片检测是两个世界。单张图片里模型输出0.7置信度八成是准的但在视频流里同一目标在30帧里可能有一两帧因运动模糊、遮挡或者编码噪声置信度突然掉到0.3以下甚至完全漏检相反的情况是某个背景块在某一帧被误判成安全帽置信度还很高。如果每帧都做决策报警信息会像机关枪一样跳。帧间NMS的思路是一个检测框要连续在至少3帧里出现并且位置基本重合才认为它是真的目标。6.2 帧间NMS的实现与参数配置import numpy as np def compute_iou(box1, box2): # box格式为[x1, y1, x2, y2] ix1, iy1 max(box1[0], box2[0]), max(box1[1], box2[1]) ix2, iy2 min(box1[2], box2[2]), min(box1[3], box2[3]) iw, ih max(0, ix2 - ix1), max(0, iy2 - iy1) inter iw * ih area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter / (area1 area2 - inter) def frame_nms(history, conf_thresh0.55, iou_thresh0.45, min_hits3): # history是最近几帧的检测框列表每帧元素为[cls, x1, y1, x2, y2, score] if len(history) min_hits: return [] mid len(history) // 2 base history[mid] # 取中间帧作为基准 keep [] for det in base: if det[5] conf_thresh: continue hit 1 for i, frame in enumerate(history): if i mid: continue for other in frame: if det[0] other[0] and compute_iou(det[1:5], other[1:5]) iou_thresh: hit 1 break if hit min_hits: keep.append(det) return keep代码逻辑是维护一个长度为5的滑动窗口取窗口中间帧的检测结果作为基准对每个框检查它在其余4帧里有没有同类别且IoU大于0.45的目标至少出现3次才保留。IoU阈值0.45是经验值太大会漏掉快速移动的目标太小又过滤不掉乱跳的误报。置信度阈值需要按类别单独调安全帽和头部的报警宁可多报也要减少漏报阈值可以放到0.5安全绳误报率低但漏报率高阈值放到0.4更合适。我在实际项目里用这个方法把误报率从每小时30次压到了每天3次以下代价是个别快速转头瞬间的漏检。从那以后我每次做工地检测项目都会强制在推理流程里加上帧间过滤这一层先调阈值再看效果不再相信单帧检测的置信度。希望帮到你。本文还有配套的精品资源点击获取