翻越栏杆检测实战:VOC/YOLO数据集清洗与YOLOv8训练全流程 简介面向翻越栏杆检测任务的目标检测数据集包含1680张JPEG图片及对应的Pascal VOC与YOLO双格式标注文件适合需要训练翻越围栏/攀爬行为识别模型的算法工程师、科研人员及计算机视觉学习者。数据集采用labelImg工具画矩形框标注共定义climbing和person两个类别总计2454个标注框其中climbing框2060个、person框394个覆盖多人、多角度及部分增强场景可为模型训练提供较充足的样本并减少格式转换带来的额外工作量。压缩包内含约2000个文件以jpg图片、xml标注和txt标注为主整体大小约100.25MB解压后目录结构清晰便于按VOC或YOLO流程直接读取使用同时内置说明文件便于快速了解标注规则。已有598人学习/下载适合用于翻越栏杆、周界安防、行为分析等视觉项目的前期数据准备与算法验证。1. 翻越栏杆围栏检测先看数据集里到底有什么翻越栏杆围栏检测不是让你把照片里的行人框出来而是要判断“一个人有没有正在跨越栏杆、围栏、闸机口这类边界物”。这个动作在安防场景里是典型的高风险行为生产现场、工地周界、轨道交通都在找现成的目标检测数据集做基线。标题里的这份数据是 1680 张 VOCYOLO 格式的翻越栏杆围栏数据集附带增强副本意思是除了原始标注还提供经过翻转、旋转、亮度扰动等手段扩增的样张适合直接丢进 YOLO 系列做训练。它解决的核心问题很明确让你不必花两周时间自己标注“翻越/正常”这种边界模糊的动作直接拿到一套能跑起来的数据先复现结果再往自己业务场景迁移。适合谁正在做周界安防算法、想用 YOLOv8 训练自定义行为检测以及还没想清楚增强策略该怎么设的工程师。2. VOC 和 YOLO 两套标注解压后怎么快速核对拿到压缩包别急着解压先说一个背景VOC 和 YOLO 是目标检测数据集最常见的两种标注载体VOC 是 PASCAL VOC 项目定义的 XML 格式YOLO 是 Darknet/YOLO 系列使用的 txt 格式二者能互转但很多新手直接把 zip 解开就训练结果类别名对不上、图片找不到卡了一整天。常见做法是先从 zip 里把两个标注目录结构统一列出来再抽样核对三五个文件确保 XML 的 object.name 和 txt 的第一列数字是一一对应的。我每次拿到新数据集都会先用一条 bash 把目录和文件数看清楚再写个小脚本刷一遍标注避免后面训练时被黑匣子一样的报错拖死。2.1 解压后先做三件事文件数、目录、标注一致第一件事是确认真实图像数量和匹配的标注数量。1680 张是原始图还是总图数这类数据集通常会在压缩包里区分 origin 和 augmented所以不要看到 5050 个jpg 就激动。先用命令把 zip 内部结构和文件数量拉出来# 统计压缩包内不同扩展名的文件数量确认 xml/txt/jpg 是否对齐 unzip -l ${dataset}.zip | awk {print $4} | grep -E \.(xml|txt|jpg|jpeg|png)$ | awk -F. {print $NF} | sort | uniq -c这里的unzip -l先看包内文件路径前缀比如JPEGImages/、Annotations/、labels/然后第二条命令统计每种扩展名的文件数重点看xml和txt数量是否一致jpg 数量是否等于 xml 数。很多分享出来的数据集VOC 和 YOLO 标注会各放在VOC/Annotations与YOLO/labels两个目录因为当初标注工具导出过一次。看到数字对不上不要急着删文件先继续往下查。第二件事是确认图片名与标注名是否同主文件名。常见做法是再写一个两行的 Python 脚本去对比set()防止出现IMG_0001.xml对应0001.jpg这种命名错位。第三件事是抽查一张 XML看类别名是person还是cross还是trespassYOLO 的标签文件里第一列是不是同一个类别。这个只靠肉眼无法全部完成所以我在下面给了完整核对脚本。import xml.etree.ElementTree as ET from pathlib import Path xml_root Path(VOC/Annotations) txt_root Path(YOLO/labels) img_root Path(JPEGImages) classes set() for xml_file in sorted(xml_root.glob(*.xml)): tree ET.parse(xml_file) for obj in tree.findall(object): classes.add(obj.findtext(name).strip()) base xml_file.stem # 同主文件名的图片和 txt 标签必须同时存在 if not (img_root / f{base}.jpg).exists(): print(f[missing image] {base}.jpg) txt_file txt_root / f{base}.txt if not txt_file.exists(): print(f[missing label] {base}.txt) print(classes:, sorted(classes))这段脚本的目的是在训练之前把最隐蔽的三类问题暴露出来XML 里的类别名集合、图片缺失、标签缺失。为什么单独把类别打印出来因为 VOC 转 YOLO 时最常见的坑就是类别顺序VOC 的name是字符串YOLO 的class_id是整数你必须在自己的data.yaml里让names: [person, climb]的顺序和 txt 第一列的 0/1 对齐。如果数据集本身就有person和person_climb两个类你的 yaml 就要写两个名字顺序不能乱。2.2 VOC 的 XML 里藏着哪些字段接着看一张 XML 的关键字段。一个典型的 VOC 文件长这样annotation filenameIMG_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object nameclimbing/name bndbox xmin340/xmin ymin220/ymin xmax610/xmax ymax740/ymax /bndbox /object /annotation需要看的字段有三个filename决定图片文件名size里的宽高决定后续归一化计算的分母object/name和bndbox决定类别与框坐标。需要注意bndbox是整数像素坐标左上角和右下角闭区间很多老标注工具会把xmax写成xmin width的格式语义上差一个像素对模型影响几乎为零但转 YOLO 时建议统一按xmin, ymin, xmax, ymax处理。一个文件可能包含多个object代表一张图里有多个人在翻越也可能一个框同时框住两个人这种数据后期识别时会被视为单目标mAP 算起来会有偏差。用 XML 读取的话len(tree.findall(object))是框数量不是人数。翻越栏杆数据集里还有一类特殊样本就是一张图里只有半个人跨过栏杆另一个人的下半身被栏杆完全挡住这种在标注时通常会只标注可见区域。2.3 YOLO txt 是怎么从 XML 算出来的YOLO 标签文件的每一行是class_id x_center y_center width height所有值都是相对于图片宽高归一化的小数范围 0~1。转换公式就是xc (xmin xmax) / 2 / width yc (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height我给一个严格按上面公式写的 Python 转换函数顺便加一个越界检查import xml.etree.ElementTree as ET def convert_xml_to_yolo(xml_path, class_map, img_w, img_h): tree ET.parse(xml_path) lines [] for obj in tree.findall(object): name obj.findtext(name).strip() cls_id class_map[name] box obj.find(bndbox) xmin float(box.findtext(xmin)); xmax float(box.findtext(xmax)) ymin float(box.findtext(ymin)); ymax float(box.findtext(ymax)) xc (xmin xmax) / 2 / img_w yc (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 防止边缘样本越界避免训练报 labels out of bounds xc min(1.0, max(0.0, xc)); yc min(1.0, max(0.0, yc)) w min(1.0, w); h min(1.0, h) lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) return lines class_map {climbing: 0, normal: 1} lines convert_xml_to_yolo(VOC/Annotations/IMG_0001.xml, class_map, 1920, 1080) print(\n.join(lines))参数说明class_map决定了类别编号这个映射必须和训练时的data.yaml完全一致img_w、img_h从 XML 的size节点读取不建议从图片文件重新读宽高因为增强后的图片有可能在保存时被压缩尺寸对不上。最后的越界处理是把宽高钳制到 1.0实际当中如果漏写了这一行一个 xmax 等于图片宽度的小数误差可能触发 YOLO 训练时 “labels out of bounds” 的断言报错。3. 数据增强的取舍1680 张原始图怎么用出 5000 张的效果很多人的第一个目标检测数据集都是 1680 张这个规模直接丢进 YOLOv8 也能训练但翻越栏杆这个动作有一个天然问题正样本占比低栏杆、围栏本身又长得像背景。我一般把增强看成两个层面经典几何扰动翻转、旋转、缩放、光度扰动亮度、对比度、颜色抖动和高级组合马赛克、随机拼贴。这个数据集里提到的“含增强”指的就是已经做过的变换副本通常会增加 2~3 倍样本量但增强不是堆数量方向错了会让模型学到“左右翻转后被识别成翻越”这种假规律。3.1 增强方式的适用场景与瓶颈栏杆翻越数据集里最有效的增强按经验排前三水平翻转、亮度扰动、随机裁剪马赛克。水平翻转对翻越动作其实有点敏感栏杆的扶手位置在左右翻转后仍然真实但人翻越的方向变了模型应该学到“动作模式”而不是“方向”所以翻转是安全的亮度扰动主要防黑夜安防摄像头很多是红外补光夜间图偏灰马赛克增强会让模型被迫在遮挡中寻找人对栏杆这类长条遮挡物很有帮助。不太建议上来就做大幅旋转或拉伸因为人会变形和真实摄像头视角差距太大。增强方式适合场景翻车点水平翻转横杆、围栏、闸机场景本身有左右不对称比如栏杆背后就是墙时会把墙翻错边亮度/对比度扰动夜间、逆光、红外补光拉太高会把暗部细节全部吃掉模型学到“全黑”这个特征随机裁剪/马赛克多目标遮挡、小目标裁得太狠把“翻越”动作裁成“站立”旋转 ±10°枪机俯视、球机旋转视角超过 15° 后透视失真栏杆形变严重色彩抖动不同墙体和栏杆颜色只对颜色敏感的场景无效还会引入色偏我在自己的数据集上遇到的现实就是效果最好的组合是“水平翻转 亮度抖动 少量马赛克”把原始 1680 张扩到 4500 张左右性价比最高。增强的力度要看图片内容如果原图本身就有很强的方向信息比如“禁止翻越”的指示牌文字那就别做翻转否则模型会把倒着的文字当成正样本特征。3.2 训练集/验证集划分和增强范围增强样本只能进训练集验证集和测试集必须用原始图。这是很多人会踩的坑把增强后的图片也随机分到 val 里然后 val 的 mAP 看起来特别高骗自己模型很强实拍视频里却漏检。常见做法是先把原始 1680 张按场景分层划分比如按“白天/黑夜/室内/室外”各抽 15% 做验证剩下的做训练然后再对训练部分做增强。这样验证集里不会出现和训练图几乎一样的翻转副本。下面的脚本用固定随机种子把 VOC 两个标注目录同时拆分保证图像和标注一一对应import random from pathlib import Path import shutil random.seed(42) img_files sorted(Path(JPEGImages).glob(*.jpg)) random.shuffle(img_files) val_ratio 0.15 val_num int(len(img_files) * val_ratio) val_set set(img_files[:val_num]) train_set set(img_files[val_num:]) for split, files in [(train, train_set), (val, val_set)]: out_img Path(fsplit/{split}/images) out_lbl Path(fsplit/{split}/labels) out_img.mkdir(parentsTrue, exist_okTrue) out_lbl.mkdir(parentsTrue, exist_okTrue) for f in files: shutil.copy(f, out_img / f.name) lbl Path(YOLO/labels) / f.with_suffix(.txt).name if lbl.exists(): shutil.copy(lbl, out_lbl / lbl.name)这段脚本的逻辑是先打乱所有原始图再按比例切分出 val 集合。把 val 集合固定成set之后交给后续增强流程去做判断如果图片在train_set才做增强如果在val_set原样保留。为什么要用set而不是列表因为增强脚本会反复查询文件名是否属于训练集set 的查询复杂度是 O(1)几千张图差距不大但代码读起来更清晰。随机种子42固定能让每次拆分的边界完全一致后面跑多次实验对比 mAP 时即使没有保存拆分文件也能重新生成同样的划分。如果你用 ultralytics 训练它会自动把 train 目录下的图作为训练集不需要你手动复制图片到split/train/images但手动拆分的好处是后续用 OpenCV 写增强逻辑时可以直接在源目录上操作。3.3 增强后的分布统计防止类别失衡和小目标消失数据增强完成后先别急着训练花五分钟统计每个类别的框数量、框面积和长宽比。翻越行为是细长目标人体跨越时框的长宽比通常在 0.4 到 1.2 之间如果增强脚本把目标裁成了小方块模型的 anchor 就匹配不上了。下面统计脚本可以直接输出分布import glob import numpy as np boxes [] for txt in glob.glob(split/train/labels/*.txt): for line in open(txt): parts line.strip().split() if len(parts) 5: continue cls int(parts[0]); w float(parts[3]); h float(parts[4]) # 面积 归一化宽乘高用于判断小目标占比 boxes.append((cls, w, h, w * h)) boxes np.array([b[1:] for b in boxes], dtypefloat) print(box count:, len(boxes)) print(area percentiles:, np.percentile(boxes[:, 2], [10, 50, 90])) print(width percentiles:, np.percentile(boxes[:, 0], [10, 50, 90])) print(height percentiles:, np.percentile(boxes[:, 1], [10, 50, 90]))输出里w*h是归一化面积10%分位数如果在 0.01 以下说明有一批极小目标YOLOv8n 在小目标上效果会打折建议换更大分辨率训练或者把这种极小目标样本单独拿出来多增强几轮。长宽比在这里没有直接体现但看宽度和高度的分位数就能判断是不是细长框宽度中位数远小于高度中位数说明动作框是纵向的这和跨栏姿势相符。如果发现有一类占比特别高比如 “normal” 占八成、只有两成是 “climbing”那后面训练时一定要给少数类提高 loss 权重或者用类别权重参数去加权。4. 用 YOLO 训练翻栏杆数据集配置文件、关键参数和一条训练命令VOC 和 YOLO 格式都核对完了增强批次也划分好了接下来就到了把数据集变成模型的过程。我自己最常用的是 YOLOv8 或 YOLO11 的 ultralytics 库原因很简单不需要手写数据加载器YAML 写清楚路径就能跑数据集结构天然兼容 VOC/YOLO。但这里的“兼容”有个前提——你要把数据组织成images/train/、images/val/、labels/train/、labels/val/这种布局YOLO 格式的 txt 放在对应 labels 目录下训练时改一下数据配置文件和两三个超参数就行。4.1 数据配置文件怎么写先给一份可以直接改的 data.yamlpath: ./datasets/climb train: images/train val: images/val names: 0: climbing 1: normal参数说明path是数据集根目录建议填相对路径或绝对路径但不要填../这种容易在 Docker 容器里跑挂的相对路径train和val要写相对于path的目录名不要写带*.jpg的匹配模式ultralytics 会自己在目录里找图片names的顺序必须和 txt 第一列的类别编号一致尤其是这个数据集同时提供 VOC 和 YOLO 两套标注时很多人直接把names写成[cross]去跑结果类别对不上loss 全部变成 NaN。如果数据集里存在“含增强”标记但增强文件名字相同还有个更隐蔽的坑目录下既有原始图也有增强图文件名重复会被 ultralytics 自动跳过实际训练的远远不是 1680 张。我一般会把增强图统一命名为原始名_aug1.jpg这种带后缀的形式然后用下面的脚本扫一遍目录里的重名from collections import Counter from pathlib import Path # 检查同主文件名的图片数量重复会导致训练集被覆盖 names [p.stem for p in Path(images/train).glob(*.jpg)] dups [k for k, v in Counter(names).items() if v 1] print(duplicate stems:, dups[:10], count:, len(dups))重名检查之所以值得单独跑一次是因为 zip 包在 Windows 上解压时大小写不敏感IMG_0001.jpg和img_0001.jpg会互相覆盖Linux 上不会但训练脚本在 Linux 容器里跑数据是 Windows 传过来的这种错位很难一眼看出来。跑完发现大量重复主文件名就直接在解压阶段统一做一次小写化重命名。4.2 三个必调参数imgsz、batch 和类别权重先说imgsz。翻越栏杆这些目标在监控画面里通常只占整图 5%~10% 的面积用默认的 640 能跑但小目标容易丢。我的做法是先跑一次默认 640 作为基线把 val 的 mAP50 记录下来如果低于 70%再把imgsz提到 960 或 1280 重跑代价是显存占用翻倍。这个数据集有 1680 张原始图单卡 8G 显存上 960 分辨率配合 batch 8 基本是上限。第二个是batch。YOLOv8 默认 batch 16对于 1680 张的小数据集batch 太大容易过拟合batch 太小又导致训练时间长、BN 统计不稳定。我常用的组合是batch8配合epochs150和patience20。patience是早停参数验证集 mAP 连续 20 轮不涨就停止这时候基本已经收敛再跑下去只会记住训练集里的噪声。第三个是类别权重。这个数据集如果只有“翻越”一个动作类别其实不用调但如果像前面统计那样类别不平衡就要用类别权重去拉一把。Ultralytics 训练接口支持传入class_weights常见做法是建一个类别到权重的字典少数类给高权重。我给一个最可行的方案from ultralytics import YOLO model YOLO(yolov8n.pt) weights {0: 1.0, 1: 0.6} # climbing 少给相对更高权重 model.train( dataclimb.yaml, imgsz960, batch8, epochs150, patience20, lr00.001, class_weightsweights, )class_weights的取值是靠统计训练集每个类别的框数决定的少数类给大于 1 的权重多数类小于 1但差值不要拉太大1.0 对 0.6 就够拉到 3 以上会让模型产生重复检测同一个人的倾向。上面的代码里有一个细节lr0设成 0.001比默认的 0.01 小了一个数量级。理由是小数据集用大学习率容易震荡跨栏动作的边界又模糊第一次跑我用 0.01 在 20 轮后 loss 直接跳到 NaN换成 0.001 才稳定。4.3 训练命令和终端里该看什么如果你不想写 Python直接在终端里跑# 训练翻栏杆模型runs 目录保留每次实验的完整结果 yolo train modelyolov8n.pt dataclimb.yaml imgsz960 batch8 epochs150 patience20 lr00.001 projectruns nameclimb_v1命令参数和上面 Python 完全一致。project和name最好每次实验换一个名字这样runs目录下会保留每一轮的结果后面对比 mAP 时不用重新训练。训练过程中要盯三个东西train/box_loss曲线是否震荡、val/box_loss是否早停、metrics/mAP50(B)前 10 个 epoch 有没有上涨趋势。如果前 10 轮 mAP50 一直是 0检查方向优先看数据是不是加载对了不要先怀疑模型因为 YOLOv8n 在这种单类数据集上通常 5 轮就能出 0.1 以上的 mAP50。验证命令也别忘掉训练完跑一次验证会生成混淆矩阵和各类别 AP# 用 best.pt 在原始 val 集上评估不要用增强后的图片 yolo val modelruns/climb_v1/weights/best.pt dataclimb.yaml imgsz960验证输出里的all行会看到mAP50(B)和mAP50-95(B)。如果 mAP50 高但有大量漏检去看类别那一行normal的 AP说明模型把翻越行为误判成了站立如果空白 AP 竖直向上说明模型学会了用背景骗分数。5. 翻栏杆数据集训练的 5 个常见坑现象、原因与排查后面这部分其实才是这次下载数据集真正值得花时间的地方。1680 张图的数据集训练时间短你看不到传统超大数据集上没那么明显的坑。我按自己的踩坑频率排了前五个。5.1 现象loss 不降训练早期 mAP50 就是 0原因最常见的是 XML 里的类别名和 YOLO txt 的第一列没对齐。比如 VOC 里是person_climbtxt 里第一列是 1但data.yaml里把names写成了 0: climbing这样相当于模型在预测一个空类loss 计算里根本没有真实标签。还有一种是 txt 存在空行或全 0 坐标ultralytics 会直接跳过那条样本。解决用第 2 章的脚本重新刷一遍classes打印把 yaml 的names改成和 XML 的name一一对应顺便检查 txt 的行数和 XML 的object数量是否一致。我遇到过 xml 有 5 个框、txt 只有 3 行的数据这种情况往往是数据集作者做 VOC→YOLO 转换时丢了一部分框直接换用 VOC 标注再转一次最保险。5.2 现象验证集 mAP 很高实拍视频里漏检严重原因这是增强方式和验证集划分共同导致的。如果 1680 张图里原始样本多数是白天摄像头拍的你做了亮度扰动增强但验证集还是白天图片增强后的模型实际没学会黑夜的抗干扰能力。另一种情况是把增强副本放进了 val 集val 里有一半是训练图的翻转版本mAP 自然虚高。解决把 val 集全部换成原始图重新计算 mAP。如果实拍视频仍然漏检从视频里抽 50 帧用yolo predict看一下把置信度阈值从默认 0.25 降到 0.1试着找是阈值问题还是特征问题。阈值降下去能检出来说明模型学到的特征是对的只是置信度分布偏低后面在推理端调阈值就行。5.3 现象训练日志报 “assertion failed labels shape”原因这个报错在 ultralytics 常见于标注文件出现小数越界或 nan。翻越栏杆数据集在增强时如果对 XML 做旋转但图片是用 OpenCV 的warpAffine转的XML 里的框坐标没有跟着变换最后保存的 txt 会出现 xmax width 的情况。解决写一个解析检查脚本把每一个 txt 里的 5 个值都读出来校验是否在 [0,1] 区间遇到越界就打印文件名和行号import glob bad [] for path in glob.glob(labels/**/*.txt, recursiveTrue): for line_no, line in enumerate(open(path), 1): parts line.strip().split() if len(parts) ! 5: bad.append((path, line_no, len)) continue cls int(parts[0]) vals [float(x) for x in parts[1:]] # 归一化坐标超出 [0,1] 的样本需要重建 if any(v 0 or v 1 for v in vals): bad.append((path, line_no, range)) print(bad[:20])输出里如果出现range不要只改这个值更稳妥的做法是从 VOC 的 XML 重新转一遍 txt。修复一次的成本低带着坏数据训练到一半再排查浪费的时间更多。5.4 现象模型把“翻越”和“站立靠栏”混淆原因静止图片里的动作歧义是这类数据集的固有属性。一个人站在栏杆前和一个人正在跨栏在若干连续帧里可能看起来几乎一样标注人员在标注时也可能只凭肉眼判断。数据增强里的随机裁剪如果裁掉了栏杆上部模型就更容易把部分人体当成“翻越”。解决如果业务上允许把模型输出与时间上下文结合例如使用连续 3 帧的检测框做位置平滑框序列从站立变成跨栏再变成另一侧站立才触发报警。或者重新标注时增加第三类stand_near让模型先学会区分“靠近栏杆”和“翻越动作”。这个数据集如果只有两个类别那就把训练图片中“靠近栏杆但没跨越”的样本挑出来单独作为 hard negative 加入训练数量大概 10% 就够。5.5 现象增强后图片文件名重复训练集实际不足 1680 张原因从 zip 解压时原始图和增强图可能同时叫IMG_0001.jpg只是在不同目录。你写成images/train时ultralytics 只保留了其中一个副本另一个在导入时被覆盖实际训练数量缩水。解决解压后立刻用脚本重命名给增强图追加_flip、_mosaic等后缀并且保持同名 xml/txt 同步更新。下面的脚本可以处理这一类的后缀追加# 遍历增强目录给 jpg 和对应的 txt 追加 _aug 后缀避免与原始图重名 for f in augmented/*.jpg; do base$(basename $f .jpg) if [ -f augmented/${base}.txt ]; then mv augmented/${base}.jpg augmented/${base}_aug.jpg mv augmented/${base}.txt augmented/${base}_aug.txt fi done这个 bash 片段简单但有效注意mv之后同时移动了 xml 或 txt不要只移动 jpg 不管标签。更彻底的做法是在解压时就用带 hash 的文件名但 zip 内的路径结构往往已经定死事后改后缀是成本最低的方案。6. 从数据集到上线帧级验证与置信度阈值调整6.1 先做帧级验证再谈部署训练收尾不等于模型能上线。翻越栏杆这种动作错报和漏报都有实际代价所以我会在真正部署前加上两个验证环节帧级测试和阈值-召回走查。从原始 1680 张里再随机抽出 100 张完全没进过训练和验证集的原始图这份数据集如果有 test 子目录直接用 test跑一遍推理把置信度阈值从 0.05 到 0.5 按 0.05 步进扫一遍画出召回率-精确率曲线。选择工作点的原则是让误报数量满足业务要求。比如周界报警系统允许每 100 帧出现 1 次误报那就把阈值定在对应的精度最高点同时接受漏掉一部分低置信度的翻越行为。实际操作里这个阈值往往在黑匣子之外需要反复用真实视频验证。6.2 时序滤波单帧检测的后悔药单帧模型的输出抖动在栏杆场景特别明显人在跨栏过程中身体前后晃动框中心点会在栏杆两侧来回跳。一个简单有效的做法是保存最近 5 帧的检测结果只有当连续 3 帧都是climbing且框中心点 y 坐标单调变化时才触发报警。这个滤波逻辑不复杂但能消掉一大半因单帧误判造成的告警风暴。我自己的教训是第一次部署时没做时序滤波结果一个工人在栏杆边摸了摸口袋被重复报警 37 次后来加了连续帧确认漏报率没变误报率直接下降了 80%。6.3 导出 ONNX 时保持 imgsz 一致最后建议你把best.pt导出成 ONNX 再走测试导出时的imgsz要和训练时保持一致# 导出 ONNXopset 12 对 onnxruntime 兼容性最好 yolo export modelruns/climb_v1/weights/best.pt formatonnx imgsz960 opset12导出的 ONNX 后用 onnxruntime 做推理能看到每一帧的耗时。如果每帧推理超过摄像头帧间隔优先降低imgsz到 640 而不是换更小的模型因为模型变小对细长目标的影响比分辨率降低更明显。希望这套从拆包到调阈值、再到上线的流程能帮到你至少可以让你拿到数据集后少走我走过的这两步弯路。本文还有配套的精品资源点击获取