
简介一套面向计算机视觉风筝检测任务的高质量数据集提供Pascal VOC与YOLO两种通用标注格式适合需要训练风筝目标检测模型的算法工程师、科研人员与学生省去自行采集图片和手工标注的繁琐步骤。资源包共2000个文件以xml标注文件为核心压缩包大小约268MB标注框总数达8790个类别只包含kite目标单一明确便于快速搭建单类检测实验。目前已有76人学习浏览适合作为目标检测入门练习或实际项目的基础数据。数据集内每张图片均配有对应的VOC格式xml和YOLO格式txt标注使用labelImg绘制的矩形框覆盖不同尺寸、角度与背景下的风筝样本txt文件采用YOLO标准格式可直接划分训练集与验证集导入YOLOv5、YOLOv8、Faster R-CNN等主流框架训练。此外仓库内还附有版权声明与使用前必读说明帮助使用者快速了解目录结构和标注规则显著降低数据准备成本。1. 风筝检测数据集2260 张单类别图片到底能帮你验证什么风筝检测数据集VOCYOLO格式2260张1类别从名字看是一份已经整理好的检测训练数据2260 张 JPEG 图只标注“风筝”一个类别同时提供 VOC XML 和 YOLO txt 两种标注。它不是训练好的模型也不自带推理脚本真正解决的是“拿什么数据训练、怎么验证数据链路、怎么踩掉格式转换的坑”这件事。风筝这个目标看着简单实际非常典型——目标小、背景是云和天空、风筝线会把框拉偏还有飞鸟和塑料袋这种天然负样本干扰拿它做单类别检测实验成本低但信息量大。适合读这篇笔记的人有三类第一次用检测框架跑自定义数据集的新手想找一份现成数据把整个流程走通做无人机避障、低空安防、户外监控的从业者需要验证模型在强背景干扰下的小目标能力以及想彻底搞懂 VOC 和 YOLO 两种标注格式怎么互转、怎么排查标注问题的工程同学。单类别不等于简单2260 张的规模又刚好能在一两个小时内完成一轮训练调参试错周期短非常适合当基线实验。2. 解包后先看懂两种标注格式VOC XML 与 YOLO txt 的坐标换算拿到压缩包的第一件事不是找训练脚本而是确认图片和标注是不是一一对应。这种双格式数据集常见的打包方式是把 JPG、XML、TXT 分目录放或者把 XML 与 TXT 都塞进 annotation 目录里文件名前缀保持一致例如 kite_0001.jpg 对应 kite_0001.xml 和 kite_0001.txt。先解压、再统计数量、再抽看单个样本比直接开训靠谱得多。2.1 解压与数量核对先确认 2260 这个数字是真的解压 .7z 用 7z 命令行工具。系统里没有的话先装 p7zip。apt-get install -y p7zip-full 7z x 风筝检测数据集VOCYOLO格式2260张1类别.7z -o./kite find ./kite -type f \( -name *.jpg -o -name *.jpeg \) | wc -l find ./kite -type f -name *.xml | wc -l find ./kite -type f -name *.txt | wc -l逻辑说明7z x 表示解压并保留原有目录层级-o 指定输出目录注意 -o 后面不要漏空格。三条 find 命令分别统计图片、XML、TXT 的数量用来核对数据是否完整。参数说明图片数量应接近 2260XML 和 TXT 数量应各与图片数相同。如果 txt 比图片少说明有图片没有标签后面训练必然报 No labels如果 xml 和 txt 数量不一致说明双格式转换时可能有文件被丢弃需要先查原因。解压后再看一眼目录层级确认图片和标注的实际位置便于第三步写重排脚本。tree -L 2 ./kitetree -L 2 只展示两层目录够用。系统没有 tree 就换成 find ./kite -maxdepth 2 -type d。我拿到任何数据集都会做这一步因为训练脚本里所有路径都依赖这一层目录结构后面改来改去最容易出错。2.2 VOC XML 长什么样像素坐标与图像尺寸是关键VOC 格式的 XML 是对一张图中所有目标框的完整描述核心字段是 filename、size 中的 width 与 height、object 中的 name 和 bndbox。一个典型样本长这样annotation folderkite/folder filenamekite_0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namekite/name difficult0/difficult bndbox xmin320/xmin ymin560/ymin xmax1010/xmax ymax1280/ymax /bndbox /object /annotation字段含义注意点filename图片文件名必须与真实文件名完全一致size/width、size/height图片原始像素宽高YOLO 转换时除以的就是这两个值object/name目标类别名本数据集中只会出现 kiteobject/difficult是否困难样本转格式时要决定保留还是过滤bndbox目标框左上与右下像素坐标xmax 不应超过 widthymax 不应超过 height实际数据集里可能出现没有 depth 字段、缺 folder 字段的情况写转换脚本时要做容错。特别要注意 difficult如果原标注把被遮挡的小风筝标成了 difficult1而转换脚本直接忽略验证时召回率会虚低反而误导你去做增强。从 VOC 转成 YOLO txt 的换算关系是固定的中心点 x 等于 (xminxmax)/2再除以图片宽度中心点 y 等于 (yminymax)/2再除以图片高度框宽等于 xmax-xmin除以宽度框高等于 ymax-ymin除以高度。看下面这段转换脚本就清楚了。import xml.etree.ElementTree as ET CLASS_MAP {kite: 0} def voc_to_yolo_txt(xml_path, out_path): root ET.parse(xml_path).getroot() w float(root.find(size/width).text) h float(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAP: continue 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) x_center ((xmin xmax) / 2) / w y_center ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) if not lines: return with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines) \n)逻辑说明代码先读 XML 里的原始图片宽高再遍历所有 object把每个 bndbox 坐标转成归一化数值。CLASS_MAP 是字符串类别到数字编号的映射本数据集只有 kite 一个类所以只映射到 0。参数说明输出小数保留 6 位这是 YOLO 训练时的常见精度足够用。root.find(size/width) 如果找不到节点会返回 None实际工程里要加判空否则解压包里的某些早期 VOC 文件会直接让脚本中断。这段脚本唯一的“坑”是 bndbox 的 xmax 或 ymax 如果等于宽度或高度本身除以 w 或 h 会得到 1.0这不会报错但属于越界框的前兆。2.3 YOLO txt 长什么样一行一个框五个数字一个空格YOLO txt 是纯文本每一行是一个目标框类别编号、中心点 x、中心点 y、框宽、框高全部归一化空格分隔。对应上面这段 XML转换结果大概是0 0.346354 0.851852 0.359375 0.333336第一个数字 0 是类别编号。单类别数据集固定就是 0不是 1。后面四个数字按顺序是 x_center、y_center、w、h取值范围应该在 0 到 1 之间但实际数据里因为标注误差偶尔会出现 0.000001 或 0.999999 这种边角值不一定是错误。标题里的“1类别”对应训练配置里 nc: 1names: [kite]。txt 第一列只允许出现 0 这个编号。如果出现 1 或 2可能是转换脚本带入了背景类也可能是原始标注里混入了其它类别。快速检查命令如下find ./kite -name *.txt -exec cut -d -f1 {} \; | sort -u解释find 找出所有 txt-exec 对每个文件执行 cut-d 表示按空格分隔-f1 取第一列sort -u 去重后输出。看到 0 就是正常看到 0 和 1 混在一起必须停下来修正转换脚本或数据本身。看完这两节你应该能自己写一段双向转换代码。VOC 转 YOLO 难在除以宽高YOLO 转 VOC 难在乘以宽高后要取整并防止越界。如果数据集里的图片宽度高度各不相同这两步都要逐图读取不能用一个固定值代替。3. 重排目录到 YOLO 训练结构划分脚本与 data.yaml 一次配好很多新手拿到数据集后直接把解压目录扔给训练命令然后被各种路径报错折磨。原因是主流 YOLO 系检测框架默认读取的数据结构是固定的数据集根目录下有 images 和 labels 两个平行目录各自的 train 和 val 子目录里放对应文件。解压包里的原始组织方式可能是 JPEGImages 加 Annotations也可能是图片和 txt 混在一起直接当训练目录用大概率跑不通。3.1 为什么必须重建 images/labels 目录结构目标结构是下面这样训练代码不需要任何路径参数就能找到所有数据kite_yolo/ ├── data.yaml ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/和这个结构相关的几个硬性要求图片和 txt 不能混在同一个目录train 和 val 必须分开标签文件的扩展名必须是 .txt标签文件名与图片文件名保持相同的 stem图片扩展名统一为 .jpg 或 .png大小写最好也统一。XML 放哪我一般单独保留一个 annotations 目录不放进训练路径。虽然 YOLO 训练只认 txt但 XML 是原始标注以后要转旋转框、做实例分割、或者排查标注问题时还能用。直接把 XML 删掉会失去后悔药。这里有一个常见误区有人把 images 和 labels 搭好了却忘记 val 目录里也要有 labels导致训练能跑但验证时全部门槛为 0。划分脚本必须同时对图片和 txt 操作不能只搬图片。3.2 划分训练集与验证集的脚本划分逻辑按常见做法来读取全部图片随机打乱按 8:2 拆成 train 和 val图片和同名 txt 一起复制到目标目录。写成脚本如下import os import random import shutil SRC_IMG kite/JPEGImages # 解压包里的图片目录按实际路径改 SRC_TXT kite/labels # 解压包里的标签目录与图片同目录时写相同路径 DST kite_yolo VAL_RATIO 0.2 random.seed(2024) os.makedirs(f{DST}/images/train, exist_okTrue) os.makedirs(f{DST}/images/val, exist_okTrue) os.makedirs(f{DST}/labels/train, exist_okTrue) os.makedirs(f{DST}/labels/val, exist_okTrue) images [f for f in os.listdir(SRC_IMG) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) n_val int(len(images) * VAL_RATIO) used 0 for i, img in enumerate(images): stem os.path.splitext(img)[0] txt os.path.join(SRC_TXT, stem .txt) if not os.path.exists(txt): print(fskip {img}: no label) continue split val if i n_val else train shutil.copy(os.path.join(SRC_IMG, img), f{DST}/images/{split}/{img}) shutil.copy(txt, f{DST}/labels/{split}/{stem}.txt) used 1 print(ftotal images with label: {used}) print(ftrain: {used - n_val}, val: {n_val})逻辑说明random.seed(2024) 固定随机种子保证重跑结果一致。val 数量用总图片数乘 VAL_RATIO再取整前 n_val 张分到 val其余分到 train。找不到 txt 的图片直接跳过保证训练时数据配对完整。参数说明VAL_RATIO 可以按需调数据量少时建议 0.15 或 0.1验证集 300 张左右也够看趋势写死 0.2 时 2260 张大约得到 452 张 val 和 1808 张 train。脚本用的是拷贝不是移动原始解压目录保留着划分不满意可以随时重跑。如果 SRC_IMG 和 SRC_TXT 是同一个目录两个变量写一样即可。跑完脚本后顺便统计一下标签数量分布单类不存在类别不平衡但至少确认所有 txt 都写的是 0find kite_yolo/labels/val -name *.txt -exec cat {} \; | cut -d -f1 | sort | uniq -c这个命令把 val 所有标签的第一列汇总计数输出只有一行 “452 0” 之类的结果就正常出现其他数字就要回头查转换脚本。3.3 data.yaml 别写成绝对路径训练前最后一步是在 kite_yolo 下建 data.yaml内容如下path: ./kite_yolo train: images/train val: images/val nc: 1 names: [kite]字段说明path 是数据集根目录相对路径在训练命令所在工作目录下解析train 和 val 是相对 path 的图片目录nc 是类别数单类别必填 1names 列表顺序必须与 txt 第一列编号一一对应txt 里是 0names[0] 就必须是 kite不要写成 background 在前否则背景被当成目标整个训练含义全变了。我习惯把 data.yaml 写成相对路径因为换机器时整个 kite_yolo 目录拷走就能跑。如果始终报找不到图片再改成绝对路径排错。一个常见的翻车现场是data.yaml 里 path 写了 ./kite_yolo但训练命令在别的目录执行相对路径解析不到。所以在哪训练、data.yaml 放哪、path 怎么写三者必须一致。配置完成后直接进入下一章的训练步骤。第一次训练先别调一堆参数用最简命令验证数据链路。4. 把训练跑通模型选择、imgsz 与天空小目标的调参顺序这一章讲的是从零跑通一次风筝检测训练。目标不是刷最高分而是先确认数据能喂进框架、loss 能正常下降、验证集能输出指标。风筝场景的主矛盾是小目标在画面中占比低所以调参顺序和普通物体检测略有不同。4.1 第一次跑用最小命令先验证数据链路先用最朴素的命令跑一轮任何增强都不动。yolo detect train \ datakite_yolo/data.yaml \ modelyolov8n.pt \ epochs60 \ imgsz640 \ batch16 \ device0 \ nameexp_kite参数说明modelyolov8n.pt 是最小规模的预训练权重合适的数据集格式它会自动适配第一次运行需要联网下载epochs60 对 2260 张图来说够用先看有没有收敛趋势不必一步到位跑 300 轮imgsz640 是训练输入分辨率这也是后面最需要调整的参数batch16 取决于显存显存不够就降到 8device0 指第一张 GPU用 CPU 训练可以改成 devicecpu但速度会慢很多name 决定输出目录名后续结果在 runs/detect/exp_kite 下。跑起来后先看两个东西。第一是终端日志里 train 和 val 的图片数量train 约 1800、val 约 450 就对了第二是训练结束时输出的结果曲线框回归 loss 应该在下降验证集的 mAP50 至少应该有 0.5 以上。如果 loss 全程不动大概率是学习率问题如果 loss 下降但验证指标乱跳先怀疑数据集划分再怀疑标注质量不要急着调模型。第一次跑的意义不是得到好模型而是确认数据链路没问题。我见过有人跳过这步直接上最好的模型跑了一晚上发现 80% 的图片没有被加载全部时间都浪费在排查路径上。4.2 天空小目标的增强与超参怎么调数据链路通了之后再针对“风筝在天空背景下经常很小”这个特点调整增强。常见做法是把 mosaic 打开同时适度增加翻转类增强因为天空场景上下翻转不会破坏目标语义。下面这段是训练时推荐的增强参数写在独立的 yaml 里或用命令行覆盖都可以。mosaic: 1.0 mixup: 0.2 flipud: 0.6 hsv_h: 0.015 hsv_s: 0.6 hsv_v: 0.4 scale: 0.4参数说明mosaic 把四张图拼成一张训练样本对小目标检测有明显帮助但训练后期可能会让曲线震荡很多项目会最后 10 轮关掉flipud 是上下翻转概率风筝上下颠倒依然可识别所以可以开大hsv 三项是对颜色和亮度的扰动应对阴天、逆光、晚霞这类天色变化scale 控制随机缩放幅度配合 mosaic 可以让模型看到不同尺寸的风筝。注意增强不是开得越多越好。mixup 对单类别检测的收益有限开 0.2 是为了增加背景多样性开太高反而让框变模糊。如果你只是想验证数据链路这一整段都可以先不调整等基线跑出来再说。我的经验是风筝检测这类任务max 分辨率的影响比模型大小更直接。imgsz 从 640 提到 896小目标召回率往往能涨几个点代价是显存占用和训练时间上升。如果 GPU 显存在 12GB 以上建议直接测试 imgsz896 配合 batch8。4.3 别只盯 mAP50mAP50-95 才是框质量的照妖镜单类别检测任务里 mAP50 很容易虚高因为一个框大致落在目标附近就能算命中但风筝本身形状不规则风筝线的存在会让框的面积虚大。只看 mAP50 会高估模型真实水平应该同时看 mAP50-95。训练结束后输出目录里有个 results.csv可以直接读取指标import pandas as pd df pd.read_csv(runs/detect/exp_kite/results.csv) cols [c for c in df.columns if map in c or recall in c or precision in c] print(df[cols].tail(1).to_string(indexFalse))逻辑说明results.csv 每一行是一个 epoch 的评估结果tail(1) 取最后一轮。把包含 map、recall、precision 的列筛出来一眼就能看到 mAP50 和 mAP50-95 的差距。参数说明无需任何参数。如果 mAP50 高但 mAP50-95 很低说明框和真实目标贴合度差优先排查标注框是不是太大或太小其次再考虑提升输入分辨率。除了指标文件我还会把 val 里每个类别的准确率画出来。单类任务这一步可以省但如果是以后扩展到多类别这个表格能快速定位是哪一类拉低了整体分数。5. 避坑4 个标注问题会让风筝检测训练白跑一轮标注数据集的格式问题是目标检测工程里最隐蔽的坑。模型本身没问题loss 也能正常下降但训练完换一批图就翻车。下面这 4 个问题是我在类似数据里实际见过的按现象、原因、解决的顺序写每个都可以照着排查。5.1 训练日志里出现 No labels验证集指标全是 0现象训练刚启动日志里出现 train: No labels in ... 的警告或者 val 阶段的 precision、recall 一直为 0loss 却还在下降。原因最常见是 txt 文件名与图片文件名不匹配。比如 jpg 是 kite_0001.jpgtxt 却叫 kite_001.txt差一位数字另一种是划分脚本只复制了图片没复制标签训练目录里全是孤儿图片还有一种情况是 txt 文件是空的脚本没有报错但实际没有框。解决拿一段脚本遍历配对关系把缺失标签的图片直接打印出来。import os for split in [train, val]: img_dir fkite_yolo/images/{split} txt_dir fkite_yolo/labels/{split} for img in os.listdir(img_dir): stem os.path.splitext(img)[0] if not os.path.exists(os.path.join(txt_dir, stem .txt)): print(split, img)这段代码会列出所有缺标签的图片文件名。如果输出为空再用 grep 确认 txt 内容是否为空文件find kite_yolo/labels -name *.txt -size 0 | head空文件必须删除否则训练时它对应的图片会被当成无目标样本干扰收敛。处理完这一步重新划分数据再训。5.2 训练警告 out of bounds画出来的框跑到图片外面现象训练时终端反复出现 WARNING 提示 labels out of bounds用预测结果画图时有的框边界超出图片边缘或者框宽是负值。原因VOC 转 YOLO 时除以的图片尺寸与真实图片尺寸不一致。例如 XML 里写 1920×1080但图片实际是 1280×720也可能是标注框左上角比右下角大数据本身就是脏的。解决先扫出所有异常行。for root, _, files in os.walk(kite_yolo/labels): for name in files: path os.path.join(root, name) for line in open(path, r): parts line.strip().split() if len(parts) ! 5: print(path, line) continue _, x, y, w, h parts if float(x) 0 or float(y) 0 or \ float(w) 0 or float(h) 0: print(path, line)参数说明这段脚本按空格切分每一行字段数不是 5 或坐标出现负值就打印。发现问题后不要手动改写转换脚本时统一对坐标做裁剪把 x、y 限制在 0 到 1 之间w、h 不小于 0.005。小于 0.005 的框基本是小到不可能有效学习的噪声直接删掉这一行。越界框必须处理干净因为训练框架会把归一化坐标换算回像素尺寸越界框可能导致损失计算异常表现就是 mAP 忽高忽低复现性极差。5.3 类别编号对不上单类别任务 AP 却忽高忽低现象训练曲线正常但 val 的 mAP 在不同轮次间剧烈波动可视化结果里 label 文字显示混乱。原因txt 第一列里混入了 0 和 1 之外的数字而 data.yaml 的 names 只有 1 个元素。索引越界后框架可能报错或忽略对应目标导致一部分样本在训练和验证中的语义完全不同。还有人会在单类别任务里把 names 写成 [background, kite]等于硬造出两个类背景被当成正样本训练。解决先统计当前数据里实际出现的类别编号。find kite_yolo/labels -name *.txt -exec cut -d -f1 {} \; | sort | uniq -c如果输出只有 0问题在 data.yaml如果输出混有 1 或 2问题在转换脚本。单类别数据集的约束很简单txt 第一列只允许是 0names 只允许是 [kite]nc 只允许是 1。任何多余的数字都说明标注源头有其它类别名需要回到 XML 里 grep 一遍 name 字段。5.4 验证集太像训练集泛化能力虚高现象训练时验证集 mAP50 到了 0.8但一换真实场景图召回率低得离谱框也偏。原因划分脚本按文件名随机分配 train 和 val没有考虑图片来源。如果数据集中包含同一场景的连拍或多角度同帧这些高度相似的图片会被同时分到训练集和验证集造成信息泄漏验证指标虚高。解决划分前先看文件名结构如果文件名前缀包含场景编号按前缀分组后再划分。常见做法是先把同前缀图片组成一个组整个组进 train 或 val而不是单张随机分。改划分脚本时只需要把 random.shuffle(images) 换成先对前缀去重再按前缀划分。判断这个问题用一句话就够打印 train 和 val 的文件名前 6 位看看重叠比例。重叠高就重做划分。这个坑最容易在“公开数据集 连拍素材”组合里出现单看指标完全发现不了。6. 把 val 可视化当成固定动作提交前只花十分钟的检查习惯训练跑完先别急着调参也别急着写报告。最值得养成的习惯是把初版验证结果可视化出来人眼扫一遍这比任何指标都更能暴露问题。用训练输出里的 best.pt 对 val 数据集做一次推理yolo detect predict \ modelruns/detect/exp_kite/weights/best.pt \ sourcekite_yolo/images/val \ saveTrue save_txtTrue save_confTrue参数说明saveTrue 保存画好框的预测图save_txtTrue 同时保存检测结果的 txt 文件里面包含类别编号、置信度和坐标save_confTrue 把置信度写进结果。跑完到 runs/detect/predict 目录下翻图重点看三类问题真实风筝有没有漏检框是不是明显偏大或贴边有没有把云、鸟、塑料袋误检成风筝。我自己还有一个固定动作单独准备一个“外场验证文件夹”放 20 到 30 张不参与训练的、不同天色和建筑背景的图片每次训练完都用同样的命令跑一遍。这一步不追求指标只看可视化结果。某个模拟项目里我拿公开数据训练风筝检测模型val 的 mAP50-95 到了 0.7 左右内眼看起来完全能用。但一跑外场验证阴天背景、风筝小到 30×30 像素的情况几乎全漏。最终追查发现问题不在标注而在验证集图片里混入了大量同一场景的连拍帧随机划分时又被分到同一侧导致指标虚高。从那以后我再也不只信指标任何模型提交前都过一遍外验文件夹。建议你把这个“可视化 外验”的动作固定到每次实验收尾。尤其是在只依赖公开数据集做前期验证时这一步能拦下大部分翻车的模型成本只有十分钟。希望帮到你。本文还有配套的精品资源点击获取