拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

改进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注意力:代码与插入位置

EMA(Efficient 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, factor=32): 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_size=1) self.conv3x3 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=3, padding=1) 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层,它的作用是稳定分组后的特征分布,避免注意力权重在某些通道上过大或过小。实际使用时分母的factor=32,如果输入通道是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.2M,6G显存的卡需要稍微注意batch size。

# yolov8-p2-ema.yaml 完整网络配置示意 backbone: - [ -1, 1, Conv, [ 64, 3, 2 ] ] # 0-P1/2 - [ -1, 1, Conv, [ 128, 3, 2 ] ] # 1-P2/4,P2从这里引出 - [ -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最后一次上采样后的特征concat,concat索引要对应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", encoding="utf-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", encoding="utf-8") as f: f.write("\n".join(lines)) # 批量转换labelme_json目录下所有json文件 os.makedirs("labels/train", exist_ok=True) os.makedirs("labels/val", exist_ok=True) 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到1,0.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 python=3.8 -y conda activate yolov8 # GPU版本安装(CUDA 11.7) pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 pip install ultralytics==8.0.47 # CPU版本安装(无NVIDIA显卡) pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics==8.0.47

CPU版本装完后用torch.cuda.is_available()验证,返回False是正常的。CPU训练非常慢,一个epoch可能跑十几分钟,但有两个场景值得用:一是刚拿到数据集时先用CPU跑一个epoch验证标注格式对不对,提前把错位问题暴露出来,比GPU跑两个小时后再发现强得多;二是只有单张1050Ti以下的老卡,显存只有2G,跑YOLOv8会直接OOM,CPU模式至少能出结果。ultralytics版本建议锁死8.0.47,新版改动频繁,自定义yaml的解析规则经常变,同一个yaml在不同版本下跑出来的结果对不上,这是环境配置里最常见的玄学问题。

4.2 data.yaml 与一键训练命令:参数逐项说清

数据准备好后,训练本身就是一个命令的事。关键是把data.yaml、模型yaml和数据集路径对好,路径建议写绝对路径,很多人在训练时把相对路径写错,报FileNotFoundError。data.yaml的内容在上一章已经给了,这里直接看训练命令:

# 进入项目根目录,激活环境后执行 yolo train \ data=config/safety_data.yaml \ model=config/yolov8-p2-ema.yaml \ epochs=200 \ batch=16 \ imgsz=640 \ patience=30 \ device=0 \ workers=8 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01

参数含义逐项说明:epochs是最大训练轮数,不是必须跑满,patience=30表示验证集指标连续30轮不提升就早停,一般跑不到200轮就停了;batch=16是单卡batch size,1660Ti 6G显存下配合AMP刚好能跑,显存不够时先降imgsz再降batch;imgsz=640是训练时的输入尺寸,P2头在这个尺寸下特征图160x160,显存压力比原生YOLOv8大,降到512能明显缓解;workers=8是数据加载线程数,如果你的机器CPU核数少可以降到4,worker太多会把内存吃满,反而拖慢训练;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.pt,best.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为0,loss值一直徘徊在20以上不往下走。

原因:绝大多数情况是标注质量问题,最常见的是漏标——图片里明明有五个工人,只标了两个安全帽,模型被反复告知这五个区域没有目标,梯度方向被错误标签带偏了。另一个可能原因是学习率太低,warmup结束后还在0.001附近爬。

解决:先去验证标注,不要一上来就调学习率。写一段可视化脚本,随机抽50张训练图片,把GT标注框画出来肉眼检查。如果确实漏标,补标后重新训练,而不是简单加大学习率。排错顺序一定是先查数据再动超参数。

5.2 标注错位:转换脚本里不要做Letterbox

现象:训练时loss能正常下降,但验证集的预测框整体偏移到左上角或右下角,目标位置跟框对不上,越小的目标偏得越厉害。

原因:Labelme标注的坐标是原始图片分辨率,训练时ultralytics会用letterbox把图片等比缩放到640x640,并在多余区域填充灰色。如果你的转换脚本先做了letterbox padding,训练端又做了一次,GT坐标和图片内容就错位了。这是Labelme转换类项目翻车率最高的地方。

解决:转换脚本只输出归一化的中心点坐标和宽高,不做任何padding或resize,letterbox完全交给训练端。如果还是错位,检查数据集的图片是不是和Labelme标注时用的原图分辨率一致,有些人在标注前把图片压缩过,JSON里存的imageHeight还是旧值。

5.3 OOM显存溢出:调整顺序比单降Batch更有用

现象:batch=16、imgsz=640直接报CUDA out of memory,而同样的配置原生YOLOv8能跑起来。

原因:加了P2头之后,neck部分多了一个160x160的大特征图流,显存峰值比原生YOLOv8高30%左右,6G卡直接顶不住。很多人第一反应是把batch降到2,但batch太小会导致BN统计量不稳定,loss曲线抖得厉害。

解决:调整顺序一定是先降imgsz再降batch。把imgsz从640降到512,P2特征图从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. 实时预警落地的一个关键技巧:置信度过滤与帧间NMS

6.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_thresh=0.55, iou_thresh=0.45, min_hits=3): # 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次以下,代价是个别快速转头瞬间的漏检。从那以后,我每次做工地检测项目都会强制在推理流程里加上帧间过滤这一层,先调阈值再看效果,不再相信单帧检测的置信度。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表