简介:本资源为风筝检测数据集,面向计算机视觉方向的学习者与算法开发者,适用于目标检测模型的训练、验证与课程实验。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及一一对应的xml、txt标注文件,标注类别仅一类kite,共8790个矩形框,使用labelImg工具完成标注,可直接接入主流检测框架进行训练与评估。压缩包为7z格式,共约2000个文件,以1998个xml标注文件和2个说明类txt为主,整体大小约268MB,目录结构清晰,便于按图片与标注对应关系快速检索使用。目前已有75人学习下载,适合需要单一类别检测数据、希望快速验证模型效果或开展对比实验的读者参考使用。
1. 风筝检测数据集 VOC+YOLO 格式 2260 张 1 类别:从拿到压缩包到跑通训练
拿到一个标注好的数据集压缩包,最怕的不是数据量小,而是格式对不上、路径写错、类别编号错位,训练跑起来 loss 不降反升,回头查半天发现是 xml 里的 filename 和实际图片名差了一个下划线。风筝检测这个方向本身不算热门,公开可用的高质量数据不多,2260 张、单类别、同时给 VOC 和 YOLO 两套格式,对想做小目标检测、想快速验证 YOLO 训练链路的人来说,省掉了最耗时的标注环节。这篇笔记按我实际处理这类数据集的顺序写:先搞清楚 VOC 和 YOLO 两套格式到底差在哪、为什么值得同时保留,再讲怎么把压缩包拆开、校验、转成 YOLOv8 能直接吃的目录结构,然后给出一份能跑通的训练配置和参数解释,最后把我在单类别小数据集上踩过的坑逐条列出来。适合已经装好 CUDA 和 ultralytics、手里有这张压缩包、想尽快看到第一轮 mAP 的人。
2. VOC 与 YOLO 双格式:为什么这个数据集要同时给两套
2.1 两套格式的本质差异不在文件后缀
VOC 格式的核心是一张图对应一个 XML,标注信息写在<object>节点里,坐标是绝对像素值xmin/ymin/xmax/ymax,类别用<name>文本表示。YOLO 格式的核心是一张图对应一个 txt,每行是class_id x_center y_center width height,全部归一化到 0~1,类别用从 0 开始的整数表示。两者最容易被忽略的差异是坐标系原点:VOC 的 y 轴向下、坐标是边界框左上角和右下角;YOLO 的中心点坐标是框中心,宽高是相对整图尺寸的比例。很多人转格式时只除了图像宽高,忘了中心点要先算(xmin+xmax)/2,结果框整体偏移半格,训练时模型学到的位置先验就是错的。
单类别数据集在这件事上反而更危险。多类别时类别错位会直接表现为某类 AP 为 0,容易发现;单类别只有0一个 id,xml 里写的是kite还是风筝还是Kite,转成 YOLO 后都是 0,表面看没问题,但如果你后续要合并其他数据集、或者用 COCO 预训练权重做微调,类别名对不上就会在验证阶段报 key error。所以拿到双格式数据集,第一件事不是急着训练,而是确认两套格式的类别名和 id 映射是否自洽。
2.2 2260 张单类别数据集的定位
2260 张在检测数据集里属于小规模。作为参照,VOC2007 训练验证集约 5000 张、COCO2017 超过 12 万张。这个量级适合做三件事:验证自建训练流水线是否跑通、做迁移学习后的快速迭代、作为某个更大数据集的补充子集。不适合从零训练一个大 backbone,也不适合做需要大量长尾样本的开放词汇检测。单类别意味着模型只需要学一个前景概念,收敛会比多类别快,但也更容易过拟合到背景纹理上——如果 2260 张里风筝出现的天空、草地、树枝背景高度重复,模型可能记住的是背景而不是风筝本身。这一点在划分验证集时要特别注意,尽量让验证集的背景分布和训练集有差异。
2.3 双格式并存的实用价值
同时保留 VOC 和 YOLO,实际用起来是这样分工的:VOC 的 XML 可读性好,排查标注问题时直接打开看坐标和类别,比 txt 直观;YOLO 的 txt 是 ultralytics 系列、Darknet 系列直接吃的格式,训练时不用再转。另外 VOC 格式方便和 LabelImg、Labelme 这类标注工具对接,如果你后续要在这个数据集上追加标注,从 XML 继续标比从 txt 反推回 XML 省事。所以我的习惯是:原始 VOC 目录只读不动,所有转换产物放到单独的datasets/kite_yolo下,转换脚本保留,出问题能一键重跑。
3. 解压、校验与目录重组:把压缩包变成可训练结构
3.1 解压后先做三件事
7z 压缩包解压后,目录结构往往和训练框架期望的不一样。常见的是VOC2007/Annotations、VOC2007/JPEGImages、VOC2007/ImageSets/Main这一套,也可能作者直接平铺成images/和labels/。先别改,按下面三步确认。
第一步,数文件。图片数、xml 数、txt 数(如果有)应该一致,2260 张图对应 2260 个 xml。数量对不上说明有图没标或标了没图,这种样本在训练时会报missing label或直接被跳过。
第二步,抽查文件名。VOC 的 xml 里<filename>字段必须和实际图片文件名完全一致,包括扩展名大小写。我遇到过.JPG和.jpg混用的情况,Linux 下区分大小写,训练时找不到图。
第三步,看类别名。把所有 xml 的<name>抽出来去重,确认只有一种写法。
# 统计图片、xml 数量,并列出所有出现过的类别名 find VOC2007/JPEGImages -type f \( -iname "*.jpg" -o -iname "*.png" \) | wc -l find VOC2007/Annotations -name "*.xml" | wc -l grep -h "<name>" VOC2007/Annotations/*.xml | sort | uniq -c这段命令的逻辑很直接:find按扩展名统计图片,grep -h去掉文件名前缀只留匹配行,sort | uniq -c统计每个类别名出现次数。如果输出里出现两种以上写法,比如kite和Kite各占一半,必须在转换前统一,否则 YOLO 侧会生成两个 class id,而你的data.yaml只写了一个类别,训练直接报维度不匹配。
3.2 用脚本把 VOC 转成 YOLO 并生成目录
转换脚本我一般写成可重复执行的,输入 VOC 根目录,输出 YOLO 结构。核心是解析 XML、算归一化坐标、按比例划分 train/val。
import os import glob import random import xml.etree.ElementTree as ET from PIL import Image VOC_ROOT = "VOC2007" OUT_ROOT = "kite_yolo" CLASSES = ["kite"] # 单类别,顺序即 class_id VAL_RATIO = 0.2 random.seed(42) # 固定种子,保证每次划分一致 def convert_one(xml_path, out_img_dir, out_lbl_dir): tree = ET.parse(xml_path) root = tree.getroot() img_name = root.find("filename").text img_path = os.path.join(VOC_ROOT, "JPEGImages", img_name) w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASSES: continue cls_id = CLASSES.index(name) bnd = obj.find("bndbox") xmin = float(bnd.find("xmin").text) ymin = float(bnd.find("ymin").text) xmax = float(bnd.find("xmax").text) ymax = float(bnd.find("ymax").text) # 归一化:中心点 + 宽高,全部除以图像尺寸 xc = (xmin + xmax) / 2.0 / w yc = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 裁剪到 [0,1],防止标注越界导致训练报错 xc, yc = min(max(xc, 0), 1), min(max(yc, 0), 1) bw, bh = min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") if not lines: return None # 图片软链接或复制到输出目录,这里用复制保证可移植 import shutil shutil.copy(img_path, os.path.join(out_img_dir, img_name)) with open(os.path.join(out_lbl_dir, os.path.splitext(img_name)[0] + ".txt"), "w") as f: f.write("\n".join(lines)) return img_name xmls = sorted(glob.glob(os.path.join(VOC_ROOT, "Annotations", "*.xml"))) random.shuffle(xmls) n_val = int(len(xmls) * VAL_RATIO) splits = {"val": xmls[:n_val], "train": xmls[n_val:]} for split, files in splits.items(): img_dir = os.path.join(OUT_ROOT, "images", split) lbl_dir = os.path.join(OUT_ROOT, "labels", split) os.makedirs(img_dir, exist_ok=True) os.makedirs(lbl_dir, exist_ok=True) ok = 0 for x in files: if convert_one(x, img_dir, lbl_dir): ok += 1 print(split, "converted:", ok, "/", len(files))逻辑说明:convert_one负责单文件转换,先读图像真实宽高(不要用 xml 里可能存在的<size>字段,有些标注工具写的尺寸和实际图不符),再遍历 object 算归一化坐标。random.seed(42)是关键,数据集划分必须可复现,否则你调参时换了验证集,指标波动根本分不清是模型变了还是数据变了。VAL_RATIO=0.2对 2260 张来说验证集约 452 张,够看趋势;如果做最终评估,建议再单独切一份 test。
参数说明:CLASSES列表顺序决定 class_id,单类别就一个元素,但保留列表形式方便以后加类。坐标裁剪到 [0,1] 是防御性写法,标注越界在人工标注里很常见,不裁的话 YOLO 训练会警告甚至报错。输出用复制而非软链接,是为了整个kite_yolo目录能直接打包搬走。
3.3 生成 data.yaml 并做一次加载自检
YOLOv8 训练靠data.yaml找数据,路径写错是最常见的翻车点。
path: /abs/path/to/kite_yolo train: images/train val: images/val nc: 1 names: 0: kitepath用绝对路径,train/val相对path。写完先跑一次自检,不训练,只让 ultralytics 加载数据集:
yolo detect train data=data.yaml model=yolov8n.pt epochs=1 imgsz=640 batch=8 \ workers=0 device=0 val=Falseval=False跳过验证,epochs=1只跑一轮,目的是确认数据能被读进来、标签能被解析。如果这一步报No labels found,八成是labels/train目录名或层级不对;报class index out of range,就是 txt 里的 id 超过了nc。
4. 用 YOLOv8 跑通第一轮训练:配置、参数与观察点
4.1 模型和输入尺寸怎么选
2260 张单类别,backbone 不要贪大。yolov8n参数量约 3.2M,yolov8s约 11.2M。小数据集上 n 往往比 s 更稳,因为 s 更容易在几百轮内过拟合。输入尺寸imgsz默认 640,如果风筝在图中占比很小(比如航拍远景),可以提到 960 或 1280,但显存和训练时间会明显上升。判断标准:统计一下标注框的宽高分布,如果中位数宽高小于 32 像素,就值得提分辨率。
# 快速看标注框尺寸分布,决定 imgsz import glob, statistics ws, hs = [], [] for f in glob.glob("kite_yolo/labels/train/*.txt"): for line in open(f): _, _, _, w, h = map(float, line.split()) ws.append(w); hs.append(h) print("width median:", statistics.median(ws)) print("height median:", statistics.median(hs))输出的是归一化值,乘以imgsz就是像素。中位数乘 640 后如果小于 32,说明小目标偏多,考虑提分辨率或开mosaic增强。
4.2 一份能直接用的训练命令
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ mosaic=1.0 \ close_mosaic=10 \ patience=30 \ device=0 \ project=runs/kite \ name=exp1参数逐个说:lr0=0.01是初始学习率,用预训练权重微调时这个值偏大,如果 loss 前几轮就震荡,降到 0.001。lrf=0.01是最终学习率相对初始值的比例,余弦退火到 0.0001。warmup_epochs=3让学习率从很小线性升上来,避免一开始就大步长破坏预训练权重。mosaic=1.0是四图拼接增强,对小数据集提升明显,但close_mosaic=10表示最后 10 轮关掉,让模型在真实分布上收尾。patience=30是早停,30 轮验证指标不升就停,省时间也防过拟合。
4.3 训练过程中盯哪几个数
启动后先看第一轮有没有报错,然后重点盯三个输出:box_loss、cls_loss、mAP50。单类别时cls_loss会降得很快,如果它一直不降,检查标签文件是不是空的或者类别 id 写错。box_loss反映定位精度,正常应该在前 20 轮快速下降然后趋缓。mAP50在 2260 张上,用预训练权重微调,通常 50 轮内能到 0.7 以上;如果 100 轮还低于 0.5,先别调模型,回去查数据——大概率是验证集里有训练集没覆盖的背景,或者标注框有系统性偏移。
验证阶段还会输出混淆矩阵。单类别时矩阵是 2x2,看的是kite被预测成背景的比例。如果这个比例高,说明漏检严重,可能是小目标太多或置信度阈值设太高,可以在推理时把conf从默认 0.25 降到 0.1 试试。
5. 单类别小数据集避坑:5 条血泪记录
5.1 现象:训练 loss 正常下降,但 mAP 始终为 0
原因:验证集的标签路径对,但图片路径不对,模型在验证时读到的是空白或全黑图,预测全为背景,和真实标签完全错开。ultralytics 在图片缺失时不一定报错,可能静默跳过或用占位图。
解决:训练前用yolo detect train ... val=False跑一轮确认数据加载,再单独跑一次yolo detect val model=best.pt data=data.yaml,看验证集图片数是否等于你划分的数量。数量对不上就查images/val和labels/val的文件名是否一一对应。
5.2 现象:训练到一半突然报 CUDA out of memory
原因:mosaic增强会把四张图拼成一张,实际输入尺寸不变但内容密度变大,显存占用比单图高;另外batch=16在 640 分辨率下对 8G 显存偏大。
解决:先把batch降到 8,再不行降到 4,同时把workers设为 0 排除多进程读图的干扰。如果还爆,把imgsz降到 512。不要一上来就换大显存卡,先确认是不是增强和 batch 的组合问题。
5.3 现象:验证集 mAP 比训练集高很多
原因:2260 张里如果按随机划分,验证集可能恰好抽到了背景简单、目标清晰的样本,而训练集里混入了大量遮挡、模糊的难例。单类别小数据集上这种分布不均很常见。
解决:不要用纯随机划分。按标注框数量或目标像素面积分层抽样,让训练集和验证集的难度分布接近。简单做法是把每张图的框面积中位数算出来,排序后每隔 5 张抽 1 张进验证集。
5.4 现象:推理时框位置整体偏移
原因:转换脚本里用了 xml 的<size>字段而不是实际图像尺寸,而标注时的尺寸和实际图不一致(常见于图片被缩放后没更新 xml)。
解决:转换时一律用PIL.Image.open(img_path).size读真实尺寸。已经转完的,写个校验脚本抽查 20 张,把 txt 里的框反算回像素,画到图上肉眼确认。
5.5 现象:训练日志里cls_loss为 nan
原因:标签文件里有空行或坐标值超出 [0,1],归一化后出现负数或大于 1 的值,计算损失时 log(0) 导致 nan。
解决:转换脚本里加坐标裁剪(见 3.2 的min(max(...))),并在转换后跑一遍校验,删掉空 txt 和越界行。已经生成的标签可以用一行命令扫:
awk 'NF!=5 || $2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1 {print FILENAME": "$0}' kite_yolo/labels/train/*.txt输出为空说明全部合法,有输出就按文件名定位修正。
6. 把 2260 张用到极致:增强策略与验证技巧
数据量小的时候,增强不是可选项而是必需项。除了 YOLOv8 默认开的 mosaic、HSV 抖动、随机翻转,我还会针对风筝这个场景加两样:一是随机缩放,因为风筝在画面里的尺度变化大,从近景特写到远景小点都有;二是随机旋转小角度,风筝在空中姿态多变,水平框对旋转不敏感,但小角度旋转能让模型见到更多姿态组合。ultralytics 的配置里可以通过scale、degrees参数控制,scale=0.5表示随机缩放 0.5~1.5 倍,degrees=10表示正负 10 度旋转。
验证阶段有个容易被忽略的点:单类别数据集的 mAP 对置信度阈值很敏感。默认conf=0.001算 mAP 时会把大量低置信预测纳入,如果模型输出很发散,mAP 会被拉低。我一般会在验证时固定conf=0.25看实际可用指标,同时看conf=0.001的理论上限,两个数差太多说明模型置信度校准不好,需要更多数据或更强增强。
最后说一个我自己的习惯:每次在这个数据集上改完配置,不管结果好坏,都把data.yaml、训练命令、results.csv和best.pt的路径记到同一个笔记里。2260 张跑一轮 150 epoch 在单卡上大概几十分钟到一小时,试错成本不高,但不记录的话,两周后你根本想不起来当时lr0设的是 0.01 还是 0.001。这个数据集的价值不在于它有多大,而在于它足够小、足够干净,能让你把从 VOC 到 YOLO 再到训练评估的整条链路快速走通,走通之后换任何数据集都是同一套流程。希望帮到你。
本文还有配套的精品资源,点击获取