简介:风筝检测数据集是一份可供直接训练的目标检测数据资源,同时提供Pascal VOC与YOLO两种常用标注格式。压缩包内共包含2260张JPG图片,全部标注为单一类别kite(风筝),累计有8790个矩形框标注。所有标注均使用labelImg工具完成,格式规范统一,适用于训练YOLO系列、Faster R-CNN、SSD等常见检测模型,也可用于模型性能对比或数据格式转换练习。该资源包共2000个文件,以XML标注文件为主体,另附TXT说明文档,压缩后大小约268MB。文件命名规律清晰,图片与对应标注一一对应,便于用户自行划分训练集、验证集与测试集。数据集原始来源明确,标注文件与图片同名且一一匹配,省去自行对齐的麻烦,可快速接入现有训练流程。目前已有76人学习,适合计算机视觉初学者快速了解VOC/YOLO数据集的组织结构,也适合算法工程师在无人机巡检、户外监控等实际业务中补充风筝目标检测样本。类别简单、样本量适中,整体易于上手,能有效节省数据收集和标注时间。
1. 拿到“风筝检测数据集VOC+YOLO格式2260张1类别.7z”,先别急着解压跑训练
这个标题拆开看其实信息很足:图片2260张,标注类别只有风筝一个,压缩格式是7z,同时给了VOC和YOLO两种标注格式。常见做法是同一批图片导出了两份标注,VOC格式方便看和改,YOLO格式直接喂给yolo系列训练。适合谁?准备用YOLO做定制检测的入门者、缺起步数据做无人机或景区巡检demo的人,以及想搞明白两种标注格式到底怎么互换的新手。我的结论很直接:能用,但直接解压就训练和先做一轮校验再训练,效果差得不是一点半点。接下来我按自己处理这类小样本单类数据集的流程,把解压、格式换算、训练参数和踩坑点一次讲完。
2. VOC与YOLO双格式:目录结构、坐标换算和2260张的划分逻辑
2.1 VOC侧先看什么:JPEGImages、Annotations和ImageSets里的三个关键文件
拿到手如果先解压,你会发现VOC格式那部分的目录结构基本是固定的,常见做法是VOCdevkit/VOC20xx/下面挂几个子目录:JPEGImages放原图,Annotations放xml标注,ImageSets/Main放划分索引。这是从PASCAL VOC延续下来的习惯,很多数据集打包时即使年份不写,目录骨架也会照这个来。
xml标注里最核心的是object节点,风筝数据集只有1个类别,正常情况下每个xml里只会出现name为kite的对象。一个典型的标注片段长这样:
<annotation> <filename>kite_00123.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>kite</name> <bndbox> <xmin>100</xmin> <ymin>200</ymin> <xmax>500</xmax> <ymax>800</ymax> </bndbox> </object> </annotation>filename要和JPEGImages里对应,size里的宽高决定后面归一化坐标的分母。这里有个容易忽略的点:xml里写的width和height未必和图片实际尺寸一致,某些标注工具会写错,后面校验章节我会专门说。
ImageSets/Main里的train.txt、val.txt是VOC官方的数据划分索引,每行一个图片文件名(不带扩展名)。很多二手数据集打包时漏掉这个目录,那就需要自己按比例切分。另外补充一点训练习惯:新建了VOC数据集后,即便原包有ImageSets/Main,我也建议重新生成一遍,因为原划分未必符合你的验证需求。
2.2 YOLO侧怎么对应:images与labels同名配对,类别号从0开始
YOLO格式这边相对简洁,常见做法是用两个平级目录images/和labels/,两个目录下文件名一一对应,图片是jpg或png,标注是对应的同名txt。每行代表一个目标,一行五个数,顺序是类别id x_center y_center width height,其中坐标全部归一化到0~1,四个坐标量都以图片宽高作为分母。
拿上面那个xml框举例,在YOLO标签文件里会写成:
0 0.15625 0.462962 0.208333 0.555555这四个小数怎么来的,第2.3节我列换算表。需要特别记住的是:类别id从0开始计数,既然整个数据集只有风筝这一个类别,那这个id只能写0。好多人在这一步翻车,把类别id写成1,结果训练时目标直接被丢掉,损失函数怎么降都学不到东西。
还有一点新手容易困惑:images/和labels/一定要保持同名前缀,比如kite_00123.jpg对应kite_00123.txt。Ultralytics YOLOv8在训练时会拿图片路径去同级labels/目录下找同名txt,如果你把图片放在images/train、标签放在labels/train,相对位置对上就行,不需要额外写索引文件。
如果你看到压缩包内只有一层目录、图片和txt混在一起,也别慌,那只是省掉了images/labels两级目录;后面我会给整理脚本,把它规整成YOLO能直接吃的结构。
2.3 坐标换算:VOC的xmin/ymin/xmax/ymax转YOLO的cx,cy,w,h
VOC和YOLO的坐标体系是两个极端:VOC记录的是左上角xmin、ymin和右下角xmax、ymax,单位是像素绝对值;YOLO记录的是目标框中心的归一化坐标cx, cy以及归一化宽高w, h。两者互换公式如下:
| 转换方向 | 公式 |
|---|---|
| VOC转YOLO,x_center | (xmin + xmax) / 2.0 / image_width |
| VOC转YOLO,y_center | (ymin + ymax) / 2.0 / image_height |
| VOC转YOLO,box_width | (xmax - xmin) / image_width |
| VOC转YOLO,box_height | (ymax - ymin) / image_height |
| YOLO转VOC,xmin | (x_center - width / 2.0) * image_width |
| YOLO转VOC,ymin | (y_center - height / 2.0) * image_height |
| YOLO转VOC,xmax | (x_center + width / 2.0) * image_width |
| YOLO转VOC,ymax | (y_center + height / 2.0) * image_height |
我在前面用的是0.15625和0.462962,现在反过来用公式验算一遍更直观:图宽1920,(100+500)/2/1920 = 0.15625;图高1080,(200+800)/2/1080 = 0.46296;框宽(500-100)/1920 = 0.20833;框高(800-200)/1080 = 0.55556。对得上就说明两种格式血缘一致。
这里有一个必须注意的边界:如果原始标注里xmax或ymax越过了图片边界,比如xmax=1925而图宽只有1920,直接换算会让归一化后的宽度大于1,训练时很多框架会报警甚至把框丢掉。正确的做法是先做一次夹紧(clip)再换算。
2.4 2260张怎么切分:train/val比例、单类别训练的现实预期
2260张对于一个单类检测任务来说不算大,但也不是小到没法用。我一般会把train/val按8:2或者7:3切,也就是约1800张训练、450张验证。如果你后面还要调超参、选模型,可以再单独切一小部分test,但数据量本来就紧张,我更倾向只切train和val,用交叉验证或者靠早停来控制过拟合。
单类别数据集的现实预期要先建立起来:因为类别少,分类上的难度低,模型主要学的是“风筝长什么样”和“背景怎么排除”。风筝检测的难点反而不在类别数上,而在目标尺度上——天空中的风筝经常很小,有的可能只有几十个像素;还有逆光、云层背景、细线干扰这些情况。如果验证集里小目标占比高,mAP50可能会被拉得很难看,这时候别急着怀疑数据集有问题,先按第4章的imgsz思路调。
切分时还要顺手做一件事:统计每张图里到底有几个目标。单类数据集里经常出现大量空标签图,也就是图片里没有目标或是负样本。YOLO训练允许空标签存在,但如果空图比例超过20%,背景损失会压制前景学习,训练出来的模型会偏向“什么都不检”。我会在第3章末尾给统计脚本,这一步建议不要跳过。
3. 解压、校验与格式互转:7z解压和三步坐标血缘验证
3.1 先看清单再解压:Linux下7z命令与Windows下的对应操作
压缩格式是7z,这在Windows上双击就能用,但如果你在Linux服务器上训练,就得先处理解压工具。很多新手直接在PyCharm里想办法“添加7z支持”,其实完全不用折腾IDE,命令行解压比图形界面快得多,还能顺带做完整性检查。
先看压缩包内部清单,不解压:
7z l 风筝检测数据集VOC+YOLO格式2260张1类别.7z7z l只列出压缩包里的文件目录和大小。我拿到任何标注数据集的第一步都是这个命令,不是急着解压。看什么?一是看是不是真的有Annotations、labels这些目录,二是看文件名是否规整,有没有中文名或者空格,三是看文件总数和2260对应的图片数能不能对上。
确认没问题后解压:
7z x 风筝检测数据集VOC+YOLO格式2260张1类别.7z7z x会保留压缩包内的目录结构,这点和7z e不同,后者会把所有文件平铺到当前目录,同名文件会互相覆盖,不建议用。Windows下用7-Zip右键解压到同名文件夹即可。如果提示命令找不到,在Ubuntu/Debian上装一下:
apt install p7zip-fullCentOS/RHEL系用yum install p7zip。解压完成后,建议跑一次7z t校验CRC,确认没有传输损坏。这一步很便宜,但能省掉后面训练到一半发现图片损坏的大麻烦。
3.2 从VOC转到YOLO:一个不依赖标注工具的Python转换脚本
如果压缩包同时带了两种格式,转换脚本可以当校验工具用;如果以后你自己采集数据、只标了VOC格式,这个脚本也能直接改改路径复用。我一般会用PIL读取图片真实尺寸,而不是完全信任xml里的size节点——这个习惯帮我抓到过好几次标注尺寸和实际不符的错位。
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for xml_path in os.listdir(xml_dir): if not xml_path.endswith('.xml'): continue tree = ET.parse(os.path.join(xml_dir, xml_path)) root = tree.getroot() # 图片名取自xml里的filename节点 filename = root.find('filename').text img_path = os.path.join(img_dir, filename) with Image.open(img_path) as img: w, h = img.size yolo_lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text # 单类别数据集:不管name是什么,统一映射到0 cls_id = 0 box = obj.find('bndbox') xmin = int(box.find('xmin').text) ymin = int(box.find('ymin').text) xmax = int(box.find('xmax').text) ymax = int(box.find('ymax').text) # 夹紧越界坐标,再算归一化坐标 xmin = max(0, min(xmin, w - 1)) xmax = max(0, min(xmax, w - 1)) ymin = max(0, min(ymin, h - 1)) ymax = max(0, min(ymax, h - 1)) if xmax <= xmin or ymax <= ymin: print(f'skip invalid box: {xml_path}') continue x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h # 格式化时保留6位小数,避免浮点误差积累 yolo_lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}') if yolo_lines: txt_name = filename.rsplit('.', 1)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write('\n'.join(yolo_lines)) if __name__ == '__main__': voc_to_yolo('Annotations', 'JPEGImages', 'labels')代码里几个关键参数说明:xml_dir是VOC标注所在目录,img_dir是图片目录,out_dir是YOLO标签输出目录。脚本里所有除法都是浮点运算,Python 3默认没问题,别在Python 2下跑。坐标夹紧用的w-1而不是w,是因为像素坐标从0开始,右边界最大是w-1。输出保留了6位小数,足够YOLO训练使用,格式化得过于夸张反而会引入精度噪声。
3.3 反过来校验:把YOLO的txt读回坐标,和VOC做IoU比对
转换脚本能跑通不代表没出错,真正的血缘验证是把YOLO的txt读回绝对坐标,再和VOC原始坐标做IoU比对。如果两份标注来源一致,IoU应该无限接近1。下面这个脚本会遍历同一张图的两种标注,输出IoU低于0.99的样例:
import os import xml.etree.ElementTree as ET def parse_voc_boxes(xml_path): tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.findall('object'): box = obj.find('bndbox') boxes.append((int(box.find('xmin').text), int(box.find('ymin').text), int(box.find('xmax').text), int(box.find('ymax').text))) return boxes def parse_yolo_boxes(txt_path, img_w, img_h): boxes = [] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue _, cx, cy, bw, bh = parts cx = float(cx) * img_w cy = float(cy) * img_h bw = float(bw) * img_w bh = float(bh) * img_h xmin = cx - bw / 2 ymin = cy - bh / 2 xmax = cx + bw / 2 ymax = cy + bh / 2 boxes.append((xmin, ymin, xmax, ymax)) return boxes def iou(a, b): ax1, ay1, ax2, ay2 = a bx1, by1, bx2, by2 = b ix1, iy1 = max(ax1, bx1), max(ay1, by1) ix2, iy2 = min(ax2, bx2), min(ay2, by2) inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) area_a = (ax2 - ax1) * (ay2 - ay1) area_b = (bx2 - bx1) * (by2 - by1) union = area_a + area_b - inter return inter / union if union > 0 else 0 # 假设图宽高已知,按你的数据集改 img_w, img_h = 1920, 1080 for txt_name in os.listdir('labels'): if not txt_name.endswith('.txt'): continue base = txt_name.rsplit('.', 1)[0] xml_path = os.path.join('Annotations', base + '.xml') yolo_path = os.path.join('labels', txt_name) voc_boxes = parse_voc_boxes(xml_path) yolo_boxes = parse_yolo_boxes(yolo_path, img_w, img_h) if len(voc_boxes) != len(yolo_boxes): print(f'count mismatch: {base}') continue for v, y in zip(voc_boxes, yolo_boxes): if iou(v, y) < 0.99: print(f'low iou: {base}, {iou(v, y):.4f}')这个脚本的输出很有价值:如果报出一堆count mismatch,说明两份标注本身对不上,可能是打包时导错了图;如果只有少量low iou,通常是坐标精度丢失或边界夹紧造成的,可以接受。把这段代码跑完再进训练,能少走很多弯路。
3.4 空标签和漏标:统计每张图的目标数,确认“1类别”背后的正样本量
最后一步统计标签分布,看这个“1类别”后面到底有多少正样本,有没有一些图根本没目标。方法很简单,遍历labels目录下所有txt,统计每个文件的行数分布:
import os from collections import Counter counter = Counter() empty_files = [] for txt_name in os.listdir('labels'): if not txt_name.endswith('.txt'): continue with open(os.path.join('labels', txt_name), 'r') as f: lines = [l for l in f.read().strip().splitlines() if l.strip()] counter[len(lines)] += 1 if len(lines) == 0: empty_files.append(txt_name) print('每个文件目标数分布:', dict(sorted(counter.items()))) print('空标签文件数量:', len(empty_files))我通常会注意两个数:一是单张图目标数特别高的那些图,比如9个以上,说明是风筝集中出现的场景;二是空标签文件数量。空标签比例如果超过15%,我会建议训练时把空图单独留着做背景验证,不要全混进训练集。另一个连带检查是排查漏标:随机抽50张图,肉眼扫一遍图里有没有显然的风筝但txt里没有框。这个工作在像素层面没有捷径,只能抽检,但做一次能避免整个数据集带病训练。
4. 把2260张风筝数据跑进YOLOv8:data.yaml配置、训练参数和第一个验证结果
4.1 组建训练目录:按YOLO格式把images和labels放进train/val两个文件夹
Ultralytics YOLOv8训练时并不需要VOC那种train.txt索引文件,它靠目录结构自动匹配。最省事的目录组织方式是这样:
/data/kite ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── kite.yaml如果压缩包解压后是图片和标签平铺在一起,或者虽然分了images和labels但没有切train/val,那就自己动手切。假设原包是images/和labels/平级且未划分,划分脚本如下:
mkdir -p images/train images/val labels/train labels/val # 按文件名哈希粗分8:2,避免列表顺序偏差 ls images | sort | awk 'NR % 10 < 8 {print $0}' > train_list.txt ls images | sort | awk 'NR % 10 >= 8 {print $0}' > val_list.txt while read f; do mv "images/$f" "images/train/$f" base="${f%.jpg}.txt" mv "labels/$base" "labels/train/$base" done < train_list.txt while read f; do mv "images/$f" "images/val/$f" base="${f%.jpg}.txt" mv "labels/$base" "labels/val/$base" done < val_list.txt这段bash里按NR % 10来分配,而不是取前80%,是为了让数据分布更均匀,避免前面一批图都是同一个时间段拍的。如果你知道数据集本身是随机排列的,也可以直接head -n按行数切。移动文件时注意后缀匹配,有时图片是png或jpeg,对应标签后缀永远是txt,用base="${f%.*}"更稳妥。
4.2 data.yaml必须写对的四行:path、train、val、nc、names
Ultralytics的data.yaml配置非常直白,但新手总在nc和names上出错。这个数据集只有1个类别,那么nc必须是1,names列表只放一个字符串,类别id天然就是0。下面是可用的配置:
path: /data/kite train: images/train val: images/val nc: 1 names: 0: kitepath是数据集根目录的绝对路径,train和val是相对于path的路径,框架会自动把图片路径中的images替换成labels去找对应标注文件。如果你沿用VOC格式的原始目录,可以写train: VOCdevkit/VOC20xx/ImageSets/Main/train.txt,但那样不如转换成images/labels结构省心。
名字这里有个常见误区:如果写成names: ['kite', 'background'],nc就变成2了,模型会把背景当一个类别来学,这跟我们要的单类检测完全不是一回事。还有个容易踩的坑是YAML缩进,names底下如果写0: kite,冒号后面必须有空格,否则解析报错。训练启动时报data相关错误,先回来看yaml。
4.3 训练参数选择:imgsz、batch、epochs和预训练权重怎么给定
我自己训练同类小数据集的基础命令是这样:
yolo detect train \ data=/data/kite/kite.yaml \ model=yolov8s.pt \ imgsz=640 \ batch=16 \ epochs=100 \ patience=15 \ project=runs/kite \ name=exp1几个关键参数逐个说。model=yolov8s.pt用的是COCO预训练权重,风筝不在COCO类别里,但预训练权重的底层特征仍然有效,尤其对边缘、纹理的抽象能力,这比从头训练收敛快得多。如果你显存有限,把model=yolov8n.pt换成nano版本;想要更高精度且显存够,就换yolov8m.pt,但2260张单类数据用m模型容易过拟合,我试过s是性价比最高的档。
imgsz=640是权衡速度与精度的默认值,但风筝检测有个特殊性:很多目标在图片里占比很小。如果验证集里大量目标的框宽或高小于30像素,建议把imgsz提到1280,代价是显存占用翻四倍左右。T4这种16G显存的卡跑1280分辨率加batch 8基本到顶。
epochs=100配合patience=15的意思是:连续15个epoch验证指标不涨就早停。单类小数据集一般30到60个epoch就收敛了,设100只是给个上限,实际训练不用跑满。batch=16按显存调节,先看nvidia-smi,如果OOM就把batch降到8或4。
4.4 训练完看什么:PR曲线、混淆矩阵和val文件夹里的漏检样例
训练结束后,结果在runs/kite/exp1/下,第一优先级看weights/best.pt。验证命令:
yolo detect val \ model=runs/kite/exp1/weights/best.pt \ data=/data/kite/kite.yaml这一步会重新跑一遍验证集,输出F1_curve.png、PR_curve.png、confusion_matrix.png等图表。对单类风筝检测,我主要看两样:一是PR曲线(Precision-Recall curve)的形状,如果曲线在召回率0.6之后断崖式下跌,说明模型对小目标或遮挡目标把握不足,这时候反推imgsz不够;二是混淆矩阵,注意看背景列有没有大量被误判成风筝——天空中的云、远处屋顶、甚至鸟群都可能是误检来源。
val_batch*.jpg是可视化标注结果,红框是预测,蓝框是真实值。我习惯按文件名翻几十张,专门找“图片里有风筝但框没打出来”的漏检图。数据集的真实水平往往不在mAP数字上,而在这些肉眼可见的漏检里。
5. 风筝检测数据集避坑指南:格式、坐标和训练的5条踩坑记录
5.1 现象:loss正常下降,mAP50却是0,训练像没学一样
训练日志里box loss和cls loss都在降,但验证集mAP50一直还是0。原因:标注的类别id不对。1类别的数据集标注里只能出现0,如果原始txt中类别id写成了1,虽然不会报错,但模型学到的目标类别和验证标签对不上,验证时把所有预测都当误检。解决:训练前写一行命令检查所有txt里第一列的最大值,不超过0才是正常的。我用的是:
awk '{print $1}' labels/train/*.txt | sort -n | tail -1如果输出是1或更大,需要批量把id改回0。别嫌这一步麻烦,这个坑我见过有人卡了整整两天。
5.2 现象:Windows解压后部分图片打不开,训练中途报image not found或读图失败
原因:.7z文件在传输过程中损坏,或者解压路径中带中文和空格,导致部分文件名被截断或乱码。这种情况在Windows上尤其常见,尤其是压缩包本身用中文命名时。解决:解压前先跑7z t做完整性测试,如果报错说明压缩包本身就有问题,得重新下载或重新打包;解压路径建议改成纯英文,比如D:/dataset/kite。在PyCharm里不要纠结怎么给IDE装7z插件,命令行校验和解压更可靠。
5.3 现象:VOC转YOLO后目标中心点跑到图片外,训练直接丢框
原因:xml里的xmax或ymax大于图片真实尺寸,或者图片有EXIF旋转信息,PIL读到的宽高和xml里记录的不一致。我用脚本转换时就遇到过一张宽高反了的图,xml写的1920x1080,实际PIL读出来是1080x1920,所有框全错位。解决:转换脚本里以Image.open(img_path).size为准,不要用xml里的size节点;越界坐标先clip再归一化。如果已经生成错了,重新跑一遍转换脚本覆盖labels目录即可,不用重新解压。
5.4 现象:明明训练自己的数据集,日志里却显示80个类别
原因:训练命令没带data参数,或者参数写错路径,ultralytics回退到了默认的COCO配置。我见过有人写data=kite.yaml时目录不对,但命令没有报错,只是悄悄用了默认数据。解决:训练日志第一行会打印用的data路径,每次开局瞄一眼。确认方式很简单,在训练命令里显式写data=/data/kite/kite.yaml,关闭shell历史展开,别用相对路径,服务器上相对路径太容易飘。
5.5 现象:验证集mAP有0.8,实际跑视频时小风筝疯狂漏检
原因:验证集里大目标占比高,拉高了整体mAP;小风筝在640分辨率下被下采样到只有十几个像素,模型丢失了目标特征。mAP这个指标被大目标主导,这在单类检测里是常态。解决:先统计验证集里目标框的像素尺度,如果小目标占比高,训练时把imgsz改1280;或者推理时用切片推理,把大图切块分别检测再合并。漏检比误检更让风筝检测场景难受,要考虑把置信度阈值调低到0.1左右再上应用。
6. 小样本风筝检测的进阶验证:从PR曲线找置信度阈值,再走一遍部署验证
数据集训练完不是终点,我每次都会多做一步阈值校准。YOLOv8推理默认置信度阈值是0.25,但单类检测场景里,漏检风筝比误检的代价高得多,所以阈值应该向召回率倾斜。做法是把验证集预测结果导成CSV,然后画PR曲线,看准召平衡点:
yolo detect val \ model=runs/kite/exp1/weights/best.pt \ data=/data/kite/kite.yaml \ save_json=True这段命令会生成predictions.json,然后可以用Python脚本按不同置信度阈值统计精确率和召回率,找到适合你场景的阈值。比如我调风筝阈值时发现0.25下召回率只有0.72,降到0.12后召回率到0.9,误检增加得很有限,那就说明阈值该降。
之后如果要做上线部署,顺手把模型导出成ONNX或TensorRT格式验证一下速度:
yolo export model=runs/kite/exp1/weights/best.pt format=onnx imgsz=640做了这么多年的检测项目,我养成一个习惯:拿到新数据集先跑统计和校验,再决定训练参数,这个流程帮我避开了不少夜里才发作的翻车现场。风筝检测这种单类小数据集,成败往往不在模型选择上,而在格式转换时坐标有没有错位、类别id写得对不对、目标尺度匹配不匹配输入分辨率。把这几点摸透,2260张数据也能训出能用的模型。希望帮到你。
本文还有配套的精品资源,点击获取