简介:一套面向风筝目标检测的高质量数据集,包含2260张图片对应的Pascal VOC与YOLO双重格式标注文件,类别仅kite一种。所有标注均由labelImg工具按矩形框规则绘制,累计标注框8790个,边界框定位准确、类别纯净,能够直接用于训练YOLO系列、Faster R-CNN等常见目标检测模型,也适合作为算法对比或深度学习入门练习数据。压缩包共2000个文件,主体为XML标注文件(1999个),并附有说明文档,整体大小约268MB。其中VOC格式便于使用LabelImg等可视化工具快速校对,YOLO格式则能无缝接入Ultralytics等主流训练框架,两种格式同时提供可免去手动转换流程。资源包内未提供分割路径信息,仅包含图片及对应的xml与txt标注文件,结构轻量简单。目前已有366人浏览学习,数据量适中、标注规范,对需要快速获取风筝检测训练数据的开发者而言,能显著减少数据准备时间,是一份可直接落地的标注数据基础。
1. 拿到2260张风筝数据集后,我做的第一件事不是训练
做目标检测的人都有过这种经历:数据集找了一晚上,压缩包解出来发现要么是标注格式不统一,要么是标注到一半的烂尾工程。这份风筝数据集的内容很直接——2260张jpg图片、2260个VOC格式xml文件、2260个YOLO格式txt文件,对应关系是一张图一个xml一个txt,类别只有kite(风筝)一类,总计8790个矩形框。对于单类别检测任务来说,这个数据规模足够支撑从零训练或者做迁移学习微调,不需要再花大量时间人工补标注。适合的人群也很明确:正在做无人机识别、空中物体检测、遥感目标定位相关项目的开发者,或者需要一份“干净、格式双备”的数据集来跑通训练流程的入门者。
2. VOC与YOLO两种标注格式:文件对应关系与坐标换算细节
2.1 一张图片如何对应三个文件
解压数据集后,图片、xml、txt三种文件混在同一目录是常见现象。数据集的命名方式为kite_xyxr_序号.jpg/xml/txt,例如kite_xyxr_30.xml对应kite_xyxr_30.jpg和kite_xyxr_30.txt。这一点很重要,很多数据集会把xml与txt分别放在子目录里,而这份数据集全部平铺,所以先确认文件名前缀一致,再谈后续处理。
| 文件类型 | 命名示例 | 内容说明 |
|---|---|---|
| 图片 | kite_xyxr_30.jpg | 原始图像,尺寸可能不一致 |
| VOC标注 | kite_xyxr_30.xml | 包含对象类别、边界框坐标、图像尺寸 |
| YOLO标注 | kite_xyxr_30.txt | 每行一个目标:类别ID + 归一化坐标 |
实际使用中,我习惯先把xml文件全部解析一遍,统计每张图的宽度和高度是否有异常值。因为VOC格式里bndbox保存的是像素绝对坐标,而YOLO格式需要的是归一化相对坐标,如果xml里记录的image width/height和实际图片尺寸不一致,后期转换会直接出错。所以第一步不是看图片,而是先跑一个脚本核对基础信息。
2.2 VOC的xml标注结构里藏着哪些关键字段
一个标准的VOC xml文件核心节点如下:
<annotation> <filename>kite_xyxr_30.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>kite</name> <bndbox> <xmin>312</xmin> <ymin>184</ymin> <xmax>857</xmax> <ymax>623</ymax> </bndbox> </object> </annotation>filename字段看起来是记录图片名的,但很多标注工具存在一个翻车点:如果标注时图片改名了,xml里记录的filename并不会自动同步更新。所以用代码遍历时,我一般不用xml里的filename去找图片,而是直接解析文件名前缀,以实际存在的图片文件为基准来做关联匹配。size节点记录了图像宽度、高度和通道数,这三个值必须和图片本身一致,否则后续归一化时一切白算。object节点里的name是该目标的类别名,这份数据集中只有kite一种;bndbox是目标框的像素坐标,注意是左上角和右下角两个点。
2.3 YOLO格式的txt标注换算逻辑
YOLO格式的txt文件每一行代表一个目标,固定五列:class_id x_center y_center width height。和VOC的绝对像素坐标不同,YOLO的四个数值全部做了归一化处理,公式为:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height box_width = (xmax - xmin) / image_width box_height = (ymax - ymin) / image_height其中class_id从0开始计数。这份数据集只有kite一个类别,所以所有txt文件里的第一列都是0。如果你后续要在这个数据集上增加新类别,比如加一个bird类,那就要把kite映射为0、bird映射为1,并且重新生成所有txt文件,不能手动改某一个文件了事。
# 从VOC xml转换到YOLO txt的参考实现 import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, output_txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): class_name = obj.find('name').text # 单类别数据集直接把id写死为0,多类别需要维护映射表 class_id = 0 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) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f'{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') with open(output_txt_path, 'w') as f: f.write('\n'.join(lines))转换脚本的逻辑并不复杂:先读取xml里的图像尺寸,再遍历每个object节点读取类别和框坐标,最后按归一化公式计算并写入txt。关键参数是img_w和img_h必须取实际图片的宽高,而不是某个固定值。我曾经遇到过一次数据集里xml标注的图片尺寸和实际图片尺寸不一致,原因是标注时图片被resize过但xml没更新,导致生成的yolo坐标全部偏移——这是这类数据集最常见的翻车现场。
2.4 这份数据集的txt是否真的可以直接用
摘要里明确提到“不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件”。换句话说,这份数据的yolo txt可以直接用于训练,不需要再转一次。但有一件事必须做:抽几个xml和对应的txt做交叉验证。常见做法是写一个脚本读取某个xml,按2.3的公式计算出yolo坐标,再和已有的txt内容做比对,数值误差应该在1e-4以内。
# 校验已有txt与xml是否匹配 import xml.etree.ElementTree as ET def verify_txt_matches_xml(xml_path, txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) with open(txt_path) as f: txt_lines = [line.strip().split() for line in f if line.strip()] xml_boxes = [] for obj in root.findall('object'): 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) xml_boxes.append([ (xmin + xmax) / 2.0 / img_w, (ymin + ymax) / 2.0 / img_h, (xmax - xmin) / img_w, (ymax - ymin) / img_h, ]) for i, (txt_box, xml_box) in enumerate(zip(txt_lines, xml_boxes)): diff = [abs(float(a) - b) for a, b in zip(txt_box[1:], xml_box)] if max(diff) > 1e-3: print(f'检测到第{i+1}个目标的坐标偏差: {diff}') return False print(f'xml和txt共{len(xml_boxes)}个目标完全匹配') return True我一般会用这个脚本抽查大概20个样本,只要这20个全过,基本可以判定整份数据的yolo标注是可靠的。当然,抽查位置要分散,不能只从序号1到20连续抽,而应该用随机种子取不同区间的文件,这样才能覆盖不同批次的标注质量。
3. 训练前的数据准备工作:划分训练集、验证集与检查标注
3.1 数据目录结构如何组织
拿到原始素材后,第一步不是直接开始训练,而是先创建规范的数据目录结构。很多初学者踩过的坑是把所有图片和标注文件都放在一个大目录里,然后直接丢给训练脚本,结果验证集和训练集重叠,最终模型的mAP虚高。我使用的目录组织方式是:
kite_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml这份数据集的原始结构是图片、xml、txt在同一目录下,所以首先需要写一个脚本完成划分和复制。注意是复制而不是移动,原始文件保留一份备份永远是好习惯。
# 创建目录结构 mkdir -p kite_dataset/{images/{train,val,test},labels/{train,val,test}} # 按8:1:1比例划分 python split_dataset.py --source ./kite_xyxr_2260 --dest ./kite_dataset3.2 划分脚本的核心逻辑
import os import random import shutil from glob import glob random.seed(42) source_dir = './kite_xyxr_2260' dest_dir = './kite_dataset' # 获取所有jpg文件并按文件名排序,保证可复现 jpg_files = sorted(glob(os.path.join(source_dir, '*.jpg'))) random.shuffle(jpg_files) total = len(jpg_files) train_end = int(total * 0.8) val_end = int(total * 0.9) # 按比例切分 file_splits = { 'train': jpg_files[:train_end], 'val': jpg_files[train_end:val_end], 'test': jpg_files[val_end:], } for split_name, files in file_splits.items(): for jpg_path in files: base_name = os.path.basename(jpg_path) stem = os.path.splitext(base_name)[0] # 复制图片 shutil.copy(jpg_path, os.path.join(dest_dir, 'images', split_name, base_name)) # 复制对应txt标注(YOLO格式) txt_path = os.path.join(source_dir, stem + '.txt') if os.path.exists(txt_path): shutil.copy(txt_path, os.path.join(dest_dir, 'labels', split_name, stem + '.txt'))这对脚本的关键在于固定随机种子。random.seed(42)保证了每次运行划分结果完全一致,复现实验时不会因为数据集划分不同而影响对比结论。8:1:1的划分比例对单类别数据集而言合理,如果后续想增加数据量或做交叉验证,可以改成7:2:1甚至k-fold,但对于2260张的风筝检测场景,8:1:1已经足够覆盖训练和验证需求。
划分完成后要检查每个目录的文件数是否对得上,一个简单的方法是直接看终端输出:
find kite_dataset/images/train -name "*.jpg" | wc -l find kite_dataset/labels/train -name "*.txt" | wc -l两个数字必须相等且等于1808(如果按8%比例划分)。如果图片数大于txt数,说明有部分图片缺少标注文件,训练时会被yolo脚本跳过或直接报错。
3.3 标注类别配置文件怎么写
数据集的data.yaml文件是训练脚本读取的核心配置文件,YOLOv5、YOLOv8、YOLOv11都使用类似的格式:
# kite_dataset/data.yaml train: kite_dataset/images/train val: kite_dataset/images/val test: kite_dataset/images/test nc: 1 names: ['kite']这里有个容易翻车的细节:train和val字段填的是图片目录路径,而不是label目录路径。训练脚本会自动根据图片目录推导label目录(把images替换为labels),所以只要目录结构按照3.1的方式组织,yaml里的路径填images那一层就够了。nc: 1代表类别数,names列表里的元素顺序必须和标注txt文件里的class_id顺序一致。这份数据集的txt里class_id都是0,对应的就是names列表第一个元素kite。
3.4 用脚本检查漏标、错标和越界
标注质量检查是训练前最值得投入时间的一步,而这也是最容易被新手忽略的一步。我总结了三个高频问题:
- 越界框:目标的一部分在图片外面,或者框坐标负值
- 空标注:某些图片对应txt文件为空,训练时脚本跳过该图
- 类别不一致:xml里类别名是kite,某个txt第一列却写成了1
import os from glob import glob label_dir = './kite_dataset/labels/train' error_files = [] for txt_path in sorted(glob(os.path.join(label_dir, '*.txt'))): with open(txt_path) as f: lines = [line.strip() for line in f if line.strip()] if not lines: error_files.append((txt_path, '空标注文件')) continue for line in lines: parts = line.split() if len(parts) != 5: error_files.append((txt_path, '列数不为5')) break class_id, x_c, y_c, w, h = parts if not (0 <= float(x_c) <= 1 and 0 <= float(y_c) <= 1): error_files.append((txt_path, '坐标越界')) break if not (0 < float(w) <= 1 and 0 < float(h) <= 1): error_files.append((txt_path, '宽高越界')) break for path, reason in error_files: print(f'{path}: {reason}')这段脚本的核心思路是逐行检查每个标注文件的五列数值是否在合法范围内。x_c和y_c代表归一化中心点坐标,理论范围是0到1之间;宽高值也应该在0到1之间且大于0。如果出现坐标越界,说明标注时点和点没对好,训练过程会在loss计算时产生梯度异常,模型最终的框定位精度会显著下降。
4. 使用过程中的最新踩坑:标签文件后缀混淆与模型收敛异常排查
4.1 避坑:txt文件名后缀被二次修改
现象:训练时发现某几张图片的目标完全没有被检测出来,打开对应的txt文件发现内容是正确的,但训练日志中显示该图片的标注数量为0。
原因:部分压缩包解压工具在解压时会对重名文件添加后缀,例如kite_xyxr_30.txt和kite_xyxr_30.TXT在大小写不敏感的文件系统上会冲突,解压得到kite_xyxr_30 (1).txt。训练脚本按.txt后缀匹配时会漏掉这些文件,导致图片被当作无标注样本处理。
解决:在解压完成后的第一时间跑一遍清单对比,列出所有图片文件名,检查是否存在与之一一对应的txt和xml文件,找到孤儿文件后手动改名,恢复关联关系。
4.2 避坑:图片分辨率差异导致batch训练报错
现象:训练跑到一半,突然报错Image size must be multiple of stride,然后进程崩溃。
原因:这份数据集里的图片不是统一尺寸,有些是1920x1080,有些是1280x720,也存在一些长宽比特殊的竖构图。YOLO系列模型在训练时会将图片缩放到固定尺寸再输入网络,但缩放时如果图像的宽或高不是stride(通常为32)的倍数,某些框架会直接抛异常。
解决:在训练配置里加上宽高自动补边。YOLOv8默认会做letterbox处理,但如果你用的是某些精简修改版训练脚本,需要手动确保输入尺寸为64的倍数,或者在LoadImagesAndLabels类的__init__里开启auto=True让系统自动计算合适的填充尺寸。
4.3 避坑:用小学习率训练单类别数据集导致loss一直震荡
现象:loss曲线前100轮都在0.5附近震荡,没有任何下降趋势,看起来像是模型根本没学到东西。
原因:单类别检测任务相对简单,模型需要学习的特征维度较少。如果学习率设置过低(比如从官方默认的0.01直接改成0.0001),梯度更新幅度太小,cat和kite的类别特征差异迟迟无法被区分。
解决:先按官方默认学习率跑,跑到第50轮时观察loss是否下降。如果下降速度过慢,把学习率调回默认值或加倍,并且同时调整warm-up轮数。我自己试验时,通常把初始学习率设为0.005,第100轮引入cosine衰减,整个训练过程会比较平滑。
4.4 避坑:数据集图片中有无人机拍摄背景干扰
现象:验证集上mAP达到了88%,但实际部署时在复杂天空背景下误检率很高,把云层边缘当成了风筝。
原因:训练集的图片背景相对单纯,蓝天白云偏多;而实际测试场景中可能有电线、树枝、其他飞行物,模型在这些look-a-like目标上过拟合了。
解决:这个问题的根源在于数据多样性,不是一个训练参数能解决的。建议在现有数据基础上做数据增强:随机裁剪、HSV扰动、mosiac增强,特别是random_perspective参数调高到0.5,让模型看到更多透视变换后的形态。如果增强之后效果还不够,再去网上补充一些复杂背景的风筝图片,扩到4000张再重训。
5. 训练YOLOv8模型的完整命令与关键参数设置
5.1 选择哪个检测框架
对于一份包含2260张图片、8790个框的单类别数据集,YOLOv8和YOLOv5都是好选择。两者的差距不在于精度,而在于工程便利性:YOLOv5的社区资料更多,排障经验好找;YOLOv8的生态更新,推理速度更快。我个人通常用YOLOv8来快速验证数据集质量,因为它的默认参数更稳健,不需要调太多东西就能跑出一个baseline。
# 安装ultralytics pip install ultralytics # 开始训练 yolo detect train data=kite_dataset/data.yaml model=yolov8s.pt epochs=200 imgsz=640 batch=16 workers=8 device=0训练命令中的几个参数值得展开分析:
model=yolov8s.pt:表示在官方预训练权重上微调。选s而不是n是因为s的精度和速度平衡更好,风筝目标在天空背景下不算小,s足够。如果希望更快的迭代速度,可以先用n跑一轮确认数据没问题,再换s正式训练。epochs=200:单类别数据集的训练收敛速度比多类别快,通常100轮就能看到明显的loss下降。200轮是为了让cosine衰减把学习率逐步降到一个稳定低位,输出更稳的框。batch=16:显存不够就把这个值调小到8或4,同时检查workers是否过高。数据加载进程过多时,windows系统容易出现死锁。imgsz=640:官方默认就是640。图片输入尺寸增大到960或1280可以提高对小目标的检测精度,但训练时间也成倍增加。风筝在大部分图片里占的比例并不小,640是性价比最高的选择。
5.2 训练过程中的监控指标
训练启动后,不要干等结果。要观察两个指标来快速判断数据集是否正常:
box_loss:这个loss如果在前50轮内从0.1降到0.06以下,说明框回归的拟合进行得不错。如果一直降不下去,大概率是标注框本身就有问题——部分框没有紧贴目标。cls_loss:单类别分类任务的cls_loss通常很低,因为不需要区分多个类别。如果cls_loss居高不下,检查txt里的class_id是否都是0,是否存在多类别混杂。
# 训练完成后查看所有训练指标曲线 yolo detect val data=kite_dataset/data.yaml model=runs/detect/train/weights/best.pt模型在runs/detect/train/目录下会生成weights/best.pt和weights/last.pt,前者是根据验证集结果挑选的最优权重。训练结束后一定要用验证集跑一次独立的评估,确认mAP50和mAP50-95这两个指标符合预期。
5.3 数据集的整体训练结果预期
根据标注框数为8790、类别数为1、图片数为2260的条件,正常训练后验证集mAP50应该能达到85%以上。如果低于这个数字,优先检查数据集划分时是否存在图片泄露——也就是验证集里出现了训练集的原图或高度相似的重复图片。单类别检测的翻车点往往不在算法,而在数据处理环节。
# 用训练好的权重对单张图片做推理 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict('test_images/kite_001.jpg', conf=0.25, save=True)conf=0.25表示置信度阈值设为25%,这是一个常见的平衡点。如果部署场景要求低误报率,把这个值调到0.5以上;如果要求高召回率,下调到0.15也不是不行。单类别检测任务可以放宽置信度阈值,只要模型本身的精度可靠,多框几次可以靠后处理去重,漏检反而是更麻烦的问题。
6. 标注框的置信度与可视化验证:让模型告诉你数据集哪里有问题
模型训练完成后,不要急着部署,也不要用训练时的验证集指标来衡量最终效果。更可靠的验证方式是把推理结果可视化出来,直接看模型在未知图片上的表现。打开一张测试图,如果发现预测框经常偏离目标中心或者框大小不对,问题往往出在标注框本身,而非模型参数。
# 批量推理并保存可视化结果 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict( source='kite_dataset/images/test', conf=0.3, save=True, project='runs/inference', name='test_viz' )这个可视化脚本的价值远不止“看图”。我习惯把推理结果中置信度低于0.4的样本单独筛出来,逐个查看原图和标注框,找到模型“犹豫”的原因。常见原因有:远处风筝目标太小、分辨率不够;风筝与背景颜色接近导致特征不清晰;标注框不完整导致模型学到的目标形状是被截断的。这些情况通过看图片比看数据指标容易发现得多。
我自己在类似项目上的习惯做法是:寻找数据集中面积特别小或者目标数量特别多的图片,单独统计选取几张,在推理结果上叠加绘制预测框,然后打印出每个框的置信度。对于单类别检测模型,置信度差异可以当作一个简单的数据质量指标。如果某一张图里所有目标的置信度普遍低于其他图,那么这张图的标注质量大概率有问题。
# 按图片统计检测结果的平均置信度,定位低质量标注 from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') img_dir = 'kite_dataset/images/test' results = model.predict(img_dir, conf=0.01) for result in results: if len(result.boxes) == 0: continue conf_list = result.boxes.conf.cpu().numpy() avg_conf = conf_list.mean() if avg_conf < 0.3: print(f'{result.path} 平均置信度{avg_conf:.3f}')把这个脚本跑一遍,得到的低置信度图片清单,就是整个数据集里最值得人工复查的对象。标注质量提升一个点,比模型调参提升两个点还明显。这也是我为什么一直强调:花在数据清洗上的时间,永远比花在训练参数上的时间回报率高。
有一次我处理一个单类别的航拍目标检测项目,用官方数据集跑出来的权重在自己采集的验证图上效果不佳,后来一查原因发现是标注文件里有大约5%的框只框住了目标的一部分,模型学到的目标特征是被截断的风筝形体。从那以后我每次拿到新数据集,都会用训练好的模型反向筛一遍标注,把平均置信度偏低的图全部导出复查,再决定要不要修改标注。这套流程看起来多花半天时间,实际上能省下后面好几周的踩坑时间。希望帮到你。
本文还有配套的精品资源,点击获取