风筝数据集VOC转YOLO格式与YOLOv8训练实践全解析
发布时间:2026/10/14 15:09:09 锦皓数字建站

简介一套面向风筝目标检测的高质量数据集包含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 filenamekite_xyxr_30.jpg/filename size width1920/width height1080/height depth3/depth /size object namekite/name bndbox xmin312/xmin ymin184/ymin xmax857/xmax ymax623/ymax /bndbox /object /annotationfilename字段看起来是记录图片名的但很多标注工具存在一个翻车点如果标注时图片改名了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检测到第{i1}个目标的坐标偏差: {diff}) return False print(fxml和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第一列却写成了1import 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__里开启autoTrue让系统自动计算合适的填充尺寸。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 datakite_dataset/data.yaml modelyolov8s.pt epochs200 imgsz640 batch16 workers8 device0训练命令中的几个参数值得展开分析modelyolov8s.pt表示在官方预训练权重上微调。选s而不是n是因为s的精度和速度平衡更好风筝目标在天空背景下不算小s足够。如果希望更快的迭代速度可以先用n跑一轮确认数据没问题再换s正式训练。epochs200单类别数据集的训练收敛速度比多类别快通常100轮就能看到明显的loss下降。200轮是为了让cosine衰减把学习率逐步降到一个稳定低位输出更稳的框。batch16显存不够就把这个值调小到8或4同时检查workers是否过高。数据加载进程过多时windows系统容易出现死锁。imgsz640官方默认就是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 datakite_dataset/data.yaml modelruns/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, conf0.25, saveTrue)conf0.25表示置信度阈值设为25%这是一个常见的平衡点。如果部署场景要求低误报率把这个值调到0.5以上如果要求高召回率下调到0.15也不是不行。单类别检测任务可以放宽置信度阈值只要模型本身的精度可靠多框几次可以靠后处理去重漏检反而是更麻烦的问题。6. 标注框的置信度与可视化验证让模型告诉你数据集哪里有问题模型训练完成后不要急着部署也不要用训练时的验证集指标来衡量最终效果。更可靠的验证方式是把推理结果可视化出来直接看模型在未知图片上的表现。打开一张测试图如果发现预测框经常偏离目标中心或者框大小不对问题往往出在标注框本身而非模型参数。# 批量推理并保存可视化结果 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcekite_dataset/images/test, conf0.3, saveTrue, projectruns/inference, nametest_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, conf0.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%的框只框住了目标的一部分模型学到的目标特征是被截断的风筝形体。从那以后我每次拿到新数据集都会用训练好的模型反向筛一遍标注把平均置信度偏低的图全部导出复查再决定要不要修改标注。这套流程看起来多花半天时间实际上能省下后面好几周的踩坑时间。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。