简介:本资源面向计算机视觉入门与安防场景研究者,提供高空抛物检测的完整数据集与配套训练成果。数据来源于6段简短抛物视频,逐帧截取259张图像并用labelImg完成标注,同时给出VOC与YOLO两种格式,方便直接接入不同检测框架开展训练与验证。压缩包共807个文件,约377.94MB,包含259个xml标注、261个txt标签、269张jpg图像,以及6段mp4原始视频、2个pt权重、1个yaml配置、1个csv训练记录和若干png与日志文件,覆盖数据、配置、模型与训练过程。作者已提供yolov8s训练好的模型及训练日志,不想自行训练可直接选用best.pt或last.pt进行推理测试。目前已有1194人学习下载,适合希望快速复现高空抛物检测流程、对比标注格式或开展二次开发的研究者与工程人员。
1. 高空抛物数据集怎么选:259 张 VOC+YOLO 双格式到底够不够用
高空抛物检测这个方向,真正卡住大多数人的从来不是模型结构,而是数据。你搜「高空抛物数据集 VOC+YOLO 格式 259 张」的时候,心里大概率在盘算两件事:这点量能不能训出可用的 yolov8 模型,以及那 6 段视频和现成日志到底能省多少事。先说结论——259 张标注图做通用目标检测确实偏少,但高空抛物是一个背景固定、目标类别单一、运动轨迹可预测的场景,配合视频抽帧和合理的增强策略,它足以跑通一条从数据到 yolov8 推理的完整链路,用来验证方案可行性、搭 demo、做算法讲解 PPT 都够用。它不适合直接上生产,但非常适合让你在半天内看到第一个能框住坠落物的模型。这篇就按「数据集长什么样 → 怎么转格式 → 怎么训 yolov8 → 怎么避坑 → 怎么榨干那 6 段视频」的顺序讲透,新手能照着敲,熟手能直接看到边界在哪。
2. 拆开这个压缩包:VOC 与 YOLO 双格式的目录结构和字段含义
拿到一个数据集压缩包,第一件事不是急着训练,而是把目录结构和标注字段摸清楚。高空抛物这类数据最常见的坑就是「图能打开、标签对不上」,等你训到一半发现 loss 不降,回头查才发现坐标越界或者类别名写错。所以这一章先把包里的东西拆开看。
2.1 VOC 格式的 Annotations 与 JPEGImages 怎么对应
VOC 格式的核心是Annotations目录下的 XML 文件和JPEGImages目录下的图片一一对应,文件名(不含扩展名)必须完全一致。高空抛物数据集的 XML 里,你重点看四个字段:filename、size(width/height/depth)、object下的name和bndbox(xmin/ymin/xmax/ymax)。name就是类别名,这类数据集通常只有一个类,常见命名是falling、object或者中文拼音,转换前一定要先统计一遍到底有几个不同的 name。
import os import xml.etree.ElementTree as ET from collections import Counter anno_dir = "Annotations" cls_counter = Counter() bad_boxes = [] for xml_file in os.listdir(anno_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(anno_dir, xml_file)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) for obj in root.findall("object"): name = obj.find("name").text cls_counter[name] += 1 box = obj.find("bndbox") xmin = int(float(box.find("xmin").text)) ymin = int(float(box.find("ymin").text)) xmax = int(float(box.find("xmax").text)) ymax = int(float(box.find("ymax").text)) # 检查坐标是否越界,这是高空抛物小目标最常见的脏数据 if xmin < 0 or ymin < 0 or xmax > w or ymax > h or xmax <= xmin or ymax <= ymin: bad_boxes.append((xml_file, xmin, ymin, xmax, ymax, w, h)) print("类别分布:", cls_counter) print("异常框数量:", len(bad_boxes)) for b in bad_boxes[:10]: print(b)这段脚本干三件事:统计类别分布、检查坐标越界、打印前十条异常。参数上没什么可调的,直接改anno_dir路径即可。逻辑说明:高空抛物目标往往只占画面几十个像素,标注时手一抖就容易把 xmax 标到图像宽度之外,YOLO 训练时这类框会被静默丢弃或产生 NaN loss,所以必须在转换前清洗。跑完如果bad_boxes不为空,要么修正坐标,要么直接删掉这几张图,别心存侥幸。
2.2 YOLO 格式的 txt 标签:归一化坐标与类别索引
YOLO 格式每张图对应一个同名.txt,每行是class_id x_center y_center width height,全部是归一化到 0~1 的相对值。这里有两个高频翻车点:一是有人直接把 VOC 的绝对像素坐标写进去,训练时框全跑到左上角;二是class_id从 0 开始还是从 1 开始搞混,导致类别错位。高空抛物单类别的话,class_id就是 0。
| 字段 | VOC (XML) | YOLO (txt) | 换算关系 |
|---|---|---|---|
| 类别 | name 文本 | class_id 整数 | 需建类别映射表 |
| 中心 x | 无 | x_center | (xmin+xmax)/2/w |
| 中心 y | 无 | y_center | (ymin+ymax)/2/h |
| 宽 | xmax-xmin | width | (xmax-xmin)/w |
| 高 | ymax-ymin | height | (ymax-ymin)/h |
表格里 w、h 是图像原始宽高。记住这个换算,下一章的转换脚本就是照它写的。如果你的数据集已经同时给了 VOC 和 YOLO 两套标签,那恭喜,省一步,但仍然要抽查——我见过不少「双格式」数据集里 YOLO 那套是坏的,只有 VOC 是准的。
3. 从 VOC 转 YOLO:一份能直接跑的转换脚本和四个必查项
双格式数据集最省心的地方在于,你可以拿 VOC 当「真源」,自己转一遍 YOLO,转完和包里自带的 YOLO 标签对比,不一致的地方就是脏数据。这一章给你一份完整可复现的转换脚本,再讲清楚训练前必须查的四项。
3.1 转换脚本:划分 train/val 并生成 data.yaml
import os import random import xml.etree.ElementTree as ET import shutil # 类别列表,顺序即 class_id,务必和后续训练一致 CLASSES = ["falling"] random.seed(42) voc_img_dir = "JPEGImages" voc_anno_dir = "Annotations" out_root = "highfall_yolo" val_ratio = 0.2 for split in ["images/train", "images/val", "labels/train", "labels/val"]: os.makedirs(os.path.join(out_root, split), exist_ok=True) def convert_box(size, box): w, h = size xmin, ymin, xmax, ymax = box x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 裁剪到 [0,1],防止浮点误差导致越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) bw = min(max(bw, 0.0), 1.0) bh = min(max(bh, 0.0), 1.0) return x_center, y_center, bw, bh xml_files = [f for f in os.listdir(voc_anno_dir) if f.endswith(".xml")] random.shuffle(xml_files) val_count = int(len(xml_files) * val_ratio) val_set = set(xml_files[:val_count]) for xml_file in xml_files: stem = os.path.splitext(xml_file)[0] tree = ET.parse(os.path.join(voc_anno_dir, xml_file)) root = tree.getroot() size_node = root.find("size") w = int(size_node.find("width").text) h = int(size_node.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) xc, yc, bw, bh = convert_box((w, h), (xmin, ymin, xmax, ymax)) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") split = "val" if xml_file in val_set else "train" # 图片扩展名按实际情况改,常见 jpg src_img = os.path.join(voc_img_dir, stem + ".jpg") if not os.path.exists(src_img): continue shutil.copy(src_img, os.path.join(out_root, "images", split, stem + ".jpg")) with open(os.path.join(out_root, "labels", split, stem + ".txt"), "w") as f: f.write("\n".join(lines)) # 生成 data.yaml yaml_content = f"""path: {os.path.abspath(out_root)} train: images/train val: images/val nc: {len(CLASSES)} names: {CLASSES} """ with open(os.path.join(out_root, "data.yaml"), "w") as f: f.write(yaml_content) print("转换完成,train/val 已划分")逻辑说明:脚本先固定随机种子保证划分可复现,再逐 XML 解析、换算、裁剪、写 txt,最后生成data.yaml。参数上val_ratio建议 0.2,259 张的话验证集约 52 张,够看趋势但不够做严谨评估,心里要有数。CLASSES必须和 XML 里的 name 完全一致,大小写都不能差。跑完检查highfall_yolo下四个子目录的文件数是否对得上。
3.2 训练前必查的四项:空标签、越界框、类别错位、图文不匹配
转换完别急着开训,花五分钟查这四项,能省你几小时 debug。第一,空标签:有些图里没有目标,txt 是空的,YOLO 会当负样本,少量可以,多了会拉低召回。第二,越界框:脚本里已经裁剪,但如果你用的是包里自带的 YOLO 标签,就得自己查。第三,类别错位:data.yaml的names顺序和 txt 里的class_id必须严格对应。第四,图文不匹配:图片和标签文件名对不上,训练时直接报找不到标签。
# 快速统计空标签和标签行数分布 find highfall_yolo/labels -name "*.txt" -empty | wc -l find highfall_yolo/labels/train -name "*.txt" -exec wc -l {} + | sort -n | tail -5第一条命令数空标签数量,第二条看标签行数最多的几个文件。高空抛物单类别,正常每张图 1~3 行,如果某个文件几十行,多半是标注时把整片区域都框了,得人工复核。
提示:259 张图训 yolov8n 时,batch 设 16、epochs 设 100 起步,别一上来就 300,先看 loss 曲线有没有正常下降。
4. 用 yolov8 训练高空抛物模型:参数怎么设、日志怎么看
数据准备好了,接下来是训练。这一章按「环境 → 命令 → 参数 → 日志解读」走,重点讲清楚每个参数为什么这么设,以及日志里哪些信号说明模型在正常学、哪些说明要停下来改。
4.1 环境配置与最小训练命令
yolov8 用 ultralytics 包,CPU 和 GPU 都能跑。高空抛物 259 张图,CPU 训 100 epoch 大概几小时,有张 GTX1660Ti 级别的卡就快很多。环境配置不复杂,关键是版本别乱。
# 建议 python 3.8~3.10,新建虚拟环境 python -m venv yolo_env source yolo_env/bin/activate # windows 用 yolo_env\Scripts\activate pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple # 验证安装 yolo checksyolo checks会打印环境信息,重点看 torch 是否装好、CUDA 是否可用。如果只有 CPU,训练时把device设成cpu,别让它去找 GPU 报错。
yolo detect train \ data=highfall_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/highfall \ name=exp1逻辑说明:model=yolov8n.pt用官方预训练权重做迁移学习,259 张图从零训基本没戏,必须靠预训练。imgsz=640是默认输入尺寸,高空抛物目标小,可以试 960 但显存翻倍。batch=16在 8G 显存上比较稳,爆显存就降到 8。device=0指第一块 GPU,CPU 改成cpu。project和name决定日志和权重存哪。
4.2 关键参数逐个说:imgsz、batch、lr0、patience
参数不是越多越好,高空抛物这个场景真正影响结果的就那么几个。下面这张表是我反复试出来的经验值,直接抄。
| 参数 | 建议值 | 作用 | 调整信号 |
|---|---|---|---|
| imgsz | 640(小目标试 960) | 输入分辨率 | 目标框普遍小于 32px 就调大 |
| batch | 8~16 | 批大小 | 爆显存就减半 |
| lr0 | 0.01 | 初始学习率 | loss 震荡就降到 0.001 |
| patience | 30 | 早停轮数 | 数据少时别设太小 |
| epochs | 100~200 | 训练轮数 | 看 mAP 是否还在涨 |
| mosaic | 1.0 | 马赛克增强 | 小目标多时保留,别关 |
lr0是最容易翻车的参数。高空抛物数据量小,学习率大了 loss 直接飞,表现为前几个 epoch loss 变成 nan 或者几百。遇到这种情况别慌,把lr0降到 0.001 重跑。patience=30意思是 30 轮验证指标不涨就停,259 张图建议设 30~50,太小会提前停。
4.3 从 results.csv 和日志里读出模型到底学没学会
训练时终端会刷一堆指标,别只看最后一行。真正要盯的是runs/highfall/exp1/results.csv,里面有每轮的 box_loss、cls_loss、mAP50、mAP50-95。正常曲线是 loss 稳步下降、mAP 稳步上升,最后趋于平缓。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/highfall/exp1/results.csv") df.columns = [c.strip() for c in df.columns] fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df["epoch"], df["train/box_loss"], label="box_loss") axes[0].plot(df["epoch"], df["train/cls_loss"], label="cls_loss") axes[0].legend(); axes[0].set_title("loss") axes[1].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") axes[1].plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") axes[1].legend(); axes[1].set_title("mAP") plt.savefig("train_curve.png", dpi=150)这段脚本把 loss 和 mAP 画出来,比盯终端直观得多。判断标准:box_loss 降到 0.5 以下、mAP50 能到 0.7 以上,说明模型基本学到了;如果 mAP50 一直在 0.2 以下晃,要么数据标注有问题,要么学习率不对。高空抛物因为目标小、样本少,mAP50 能到 0.6~0.8 就算这个数据集的合理上限,别拿 COCO 的标准要求它。
注意:
results.csv的列名在不同 ultralytics 版本里可能带空格,读进来先 strip 一遍,否则 KeyError 找半天。
5. 高空抛物训练避坑:5 个我真实踩过的坑
这一章全是血泪经验,每条按「现象 → 原因 → 解决」写,你照着排查能少走很多弯路。
坑一:loss 一直是 nan。现象是训练第一个 epoch 就打印 nan。原因八成是标签里有越界坐标或宽高为 0 的框,归一化后出现非法值。解决:回到第 2 章的检查脚本,把所有异常框清掉,或者用第 3 章脚本重新转一遍,转换时已经做了裁剪。
坑二:mAP 死活上不去,一直 0.1 左右。现象是 loss 在降但 mAP 不动。原因通常是类别索引错位——data.yaml里names的顺序和 txt 里的class_id对不上,模型学的是错的映射。解决:打印几张图的 txt 和对应 XML,人工核对 class_id 和 name 的对应关系。
坑三:验证集指标比训练集还高。现象是 val 的 mAP 高于 train。原因一般是验证集太小(259 张里才 50 来张)且和训练集分布太像,或者划分时没打乱。解决:确认划分用了随机种子,必要时做交叉验证,别被虚高的数字骗了。
坑四:模型只框大目标,小目标全漏。现象是画面里明显的坠落物框不住。原因是imgsz=640下小目标特征在下采样中丢了。解决:把imgsz提到 960 或 1280,同时确认 mosaic 增强没关,mosaic 能增加小目标的出现频率。
坑五:推理时框的位置整体偏移。现象是框比实际目标偏一圈。原因多半是训练时的 letterbox 填充和推理时不一致,或者你拿 VOC 绝对坐标直接当 YOLO 用了。解决:统一用 ultralytics 的推理接口,别自己写预处理;确认标签是归一化后的相对坐标。
6. 榨干那 6 段视频:抽帧补数据与推理验证的完整闭环
259 张图训出来的模型,直接拿去跑视频大概率会失望——因为视频里的光照、角度、背景和训练图不完全一样。但这个数据集附带的 6 段视频恰恰是最有价值的资源,用好了能把有效数据翻倍。这一章讲怎么用视频抽帧补数据,以及怎么用模型跑推理做闭环验证。
6.1 用 OpenCV 抽帧并去重,把视频变成训练数据
视频抽帧的关键不是抽得多,而是抽得不重复。高空抛物视频里大量帧是静止背景,全抽出来只会让数据集充满冗余负样本。我的做法是按间隔抽,再用简单的帧差去重。
import cv2 import os import numpy as np video_dir = "videos" out_dir = "frames" os.makedirs(out_dir, exist_ok=True) frame_interval = 10 # 每 10 帧抽一张 diff_threshold = 2.0 # 帧差均值阈值,低于它认为画面没变化 for vid in os.listdir(video_dir): if not vid.endswith((".mp4", ".avi")): continue cap = cv2.VideoCapture(os.path.join(video_dir, vid)) idx = 0 saved = 0 prev_gray = None while True: ret, frame = cap.read() if not ret: break if idx % frame_interval == 0: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if prev_gray is not None: diff = np.mean(cv2.absdiff(gray, prev_gray)) if diff < diff_threshold: idx += 1 continue prev_gray = gray name = f"{os.path.splitext(vid)[0]}_{idx:05d}.jpg" cv2.imwrite(os.path.join(out_dir, name), frame) saved += 1 idx += 1 cap.release() print(f"{vid}: 抽帧 {saved} 张")逻辑说明:frame_interval=10控制抽帧密度,视频 30fps 的话相当于每秒 3 张。diff_threshold是帧差均值,低于它说明画面几乎没变,跳过。这两个参数要按视频实际长度调,视频短就减小 interval。抽完的帧需要人工标注才能进训练集,别指望自动生成标签——高空抛物目标小,自动标注误检率很高。
6.2 用训练好的模型跑视频推理,验证真实场景表现
抽帧补完数据、重新训一轮后,用模型直接跑视频,看它在连续帧上的表现。这一步能暴露静态图片评估发现不了的问题,比如目标运动模糊、帧间抖动。
yolo detect predict \ model=runs/highfall/exp1/weights/best.pt \ source=videos/test.mp4 \ conf=0.25 \ iou=0.45 \ save=True \ project=runs/predictconf=0.25是置信度阈值,高空抛物宁可漏检也别误检太多,可以先设 0.3 看效果。iou=0.45控制 NMS 重叠阈值。跑完在runs/predict下看输出视频,重点观察三件事:目标出现的前几帧能不能框住、快速下落时框会不会丢、背景里有没有误检。如果误检多,提高conf;如果漏检多,降低conf同时回头补小目标训练数据。
6.3 一个判断数据集值不值得投入的土办法
最后给你一个我常用的判断习惯:拿到任何高空抛物数据集,先花十分钟做「最小闭环」——转格式、训 20 epoch、跑一段视频。如果 20 epoch 后 mAP50 能到 0.4 以上,说明数据质量过关,值得继续投入标注和调参;如果 20 epoch 还是 0.1 上下,别急着加数据,先回去查标注和类别映射,八成是数据本身有问题。259 张图加 6 段视频这个体量,做算法验证和 demo 完全够,但真要上生产,你得按同样的流程把抽帧标注的量级做到几千张,并且覆盖不同时段、不同天气、不同楼层的画面。我自己的习惯是每补一批数据就重跑一次这个最小闭环,用同一套参数对比 mAP,涨了就留,不涨就查原因,别凭感觉堆数据。希望帮到你。
本文还有配套的精品资源,点击获取