简介:这份蝴蝶目标检测数据集面向计算机视觉入门与进阶开发者,适用于分类、检测模型的训练与验证场景。资源以VOC和YOLO双格式提供,共1425张jpg图片,每张均配有对应的xml与txt标注文件,标注类别统一为butterfly,使用labelImg完成,遵循边界框选准确、目标不遗漏、多轮一致性检查等规范,可直接接入主流检测框架。压缩包为rar格式,无需解压密码,内含图片、xml、txt三个文件夹,解压后即可用标注软件查看核对。包内文件总数2000个,其中xml文件1425个、txt文件575个,整体约60.54MB,图片大小在1-500KB之间,轻量易传输。目前已有235人学习下载,适合需要快速获取高质量蝴蝶标注数据、开展模型训练或算法对比的读者使用。
1. 蝴蝶数据集 VOC 与 YOLO 双格式:1425 张标注到底怎么用
拿到一个标注好的蝴蝶数据集,第一反应往往不是“太好了”,而是“这 1425 张到底能不能直接喂进我的训练脚本”。我见过太多人卡在这一步:压缩包解压出来是Annotations、JPEGImages、ImageSets三个文件夹,兴冲冲写了个data.yaml指向images/train,结果训练一启动就报No labels found。问题不在数据,在于 VOC 和 YOLO 是两套完全不同的标注组织方式,前者用 XML 描述绝对坐标,后者用归一化后的 txt 描述相对坐标,目录结构、坐标体系、类别映射三处全不一样。
这个蝴蝶数据集同时提供 VOC 和 YOLO 两种格式,本质上是把“标注结果”和“训练输入”之间的转换工作提前做掉了。VOC 格式适合做数据审查、可视化、跨框架迁移,YOLO 格式适合直接进 Ultralytics 系训练管线。1425 张的体量不算大,属于典型的小样本目标检测场景,蝴蝶本身又存在姿态多变、翅膀纹理复杂、背景干扰强的问题,所以这份数据真正的价值不在于“有多少张”,而在于“标注质量是否一致、两种格式是否严格对齐”。接下来我会按“先验格式、再跑通训练、最后处理坑”的顺序,把这份数据从解压到出第一版权重讲清楚,适合刚拿到数据集的新手照着走,也适合熟手直接跳到参数和排查部分。
2. 先看清 VOC 与 YOLO 的目录和坐标差异
2.1 VOC 格式的目录结构与 XML 字段含义
VOC 格式的核心是Annotations目录下的 XML 文件,每个 XML 对应JPEGImages里一张同名图片。一个典型的蝴蝶标注 XML 长这样:
<annotation> <folder>JPEGImages</folder> <filename>butterfly_0001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>butterfly</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>112</xmin> <ymin>88</ymin> <xmax>305</xmax> <ymax>276</ymax> </bndbox> </object> </annotation>这里要重点看四个字段:size里的宽高决定坐标是否越界,name是类别名,bndbox是绝对像素坐标,difficult标记难样本。蝴蝶数据集如果只有butterfly一个类,那name应该全一致;如果出现butterfly和moth混标,后面类别映射就会出问题。difficult=1的框在评估时通常会被忽略,但训练时是否保留要看你的策略,小样本场景我一般保留,因为 1425 张本来就少,丢掉难样本会让模型在复杂背景上更弱。
ImageSets/Main下的 txt 文件是划分清单,常见有train.txt、val.txt、trainval.txt、test.txt,每行一个不含扩展名的文件名。很多人忽略这个目录,自己重新随机划分,结果和原始标注的难易分布不一致,验证指标忽高忽低。建议先读一遍这些 txt,确认官方划分是否存在,存在就优先用。
2.2 YOLO 格式的归一化坐标与类别索引
YOLO 格式每个图片对应一个同名 txt,每行一个目标,格式是:
class_id x_center y_center width height五个值全部归一化到 0~1,class_id从 0 开始。同样一张图,VOC 的xmin=112, ymin=88, xmax=305, ymax=276,图片 640×480,转成 YOLO 就是:
0 0.325781 0.379167 0.301563 0.391667计算方式是x_center=(112+305)/2/640,width=(305-112)/640,y 方向同理。这里最容易翻车的是归一化时用了错误的宽高——有人拿size里的值,有人拿 PIL 读出来的实际尺寸,如果图片被预处理过(比如统一 resize 到 640×640),两者会不一致。我的习惯是转换前先用脚本校验一遍所有图片的实际尺寸和 XML 里的size是否一致,不一致的单独列出来。
YOLO 格式的目录通常是images/train、images/val、labels/train、labels/val,图片和标签同名不同扩展名,靠路径对应而不是靠文件名里的编号。如果标签缺失,训练时不会报错,只会静默跳过,所以转换后必须做一次“图片数 vs 标签数”的核对。
2.3 两种格式的类别映射与文件对应关系
VOC 用类别名,YOLO 用类别索引,中间必须有一张映射表。蝴蝶数据集如果只有一个类,映射就是{'butterfly': 0};如果有多类,顺序必须固定,且要和data.yaml里的names列表严格一致。我见过最典型的错误是转换脚本里按set()去重后排序,结果两次运行类别顺序不同,训练出来的模型类别全乱。
文件对应关系上,VOC 靠filename字段,YOLO 靠同名文件。转换时建议保留原始文件名,不要重命名成000001.jpg这种,否则一旦中间某步失败,很难回溯是哪张图。1425 张的规模,用文件名直接对应完全够用,没必要引入额外索引。
3. 把 VOC 转成 YOLO:脚本、校验与划分
3.1 转换脚本的完整实现与参数说明
下面这个脚本我用了很多次,核心逻辑是读 XML、算归一化坐标、写 txt,同时做边界裁剪和尺寸校验:
import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射,必须和 data.yaml 的 names 顺序一致 CLASS_MAP = {'butterfly': 0} def convert_voc_to_yolo(xml_dir, img_dir, out_label_dir): os.makedirs(out_label_dir, exist_ok=True) skipped = [] for xml_file in os.listdir(xml_dir): if not xml_file.endswith('.xml'): continue xml_path = os.path.join(xml_dir, xml_file) tree = ET.parse(xml_path) root = tree.getroot() filename = root.find('filename').text img_path = os.path.join(img_dir, filename) if not os.path.exists(img_path): skipped.append((xml_file, 'image missing')) continue # 用实际图片尺寸,而不是 XML 里的 size with Image.open(img_path) as im: w, h = im.size lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CLASS_MAP: skipped.append((xml_file, f'unknown class {name}')) continue cls_id = CLASS_MAP[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) # 裁剪到图片范围内,防止越界坐标 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: skipped.append((xml_file, 'invalid box')) continue x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(filename)[0] + '.txt' with open(os.path.join(out_label_dir, out_name), 'w') as f: f.write('\n'.join(lines)) print(f'skipped: {len(skipped)}') for s in skipped[:20]: print(s) convert_voc_to_yolo('Annotations', 'JPEGImages', 'labels_all')几个参数要说明:CLASS_MAP必须和后续data.yaml的names顺序一致,这是硬约束;坐标裁剪用w-1和h-1是因为像素索引从 0 开始,越界框在 YOLO 里会导致 loss 异常;:.6f保留六位小数,足够表达 1425 张图的精度,再多没必要。脚本最后打印跳过项,这是排查的关键,不要省。
3.2 转换后的三项校验:数量、坐标、可视化
转换完不能直接训练,先做三项校验。第一项是数量核对:
echo "images: $(ls JPEGImages | wc -l)" echo "labels: $(ls labels_all | wc -l)"两个数应该相等,如果 labels 少,说明有 XML 被跳过,回去看脚本输出的 skipped 列表。第二项是坐标范围校验:
import os bad = [] for f in os.listdir('labels_all'): with open(os.path.join('labels_all', f)) as fp: for i, line in enumerate(fp): parts = line.strip().split() if len(parts) != 5: bad.append((f, i, 'field count')) continue vals = [float(x) for x in parts[1:]] if any(v < 0 or v > 1 for v in vals): bad.append((f, i, 'out of range')) if vals[2] <= 0 or vals[3] <= 0: bad.append((f, i, 'zero size')) print(f'bad lines: {len(bad)}') for b in bad[:20]: print(b)第三项是可视化,把 YOLO 框画回图片上,肉眼确认框的位置和蝴蝶对齐:
import cv2 import os img_dir = 'JPEGImages' label_dir = 'labels_all' out_dir = 'vis' os.makedirs(out_dir, exist_ok=True) for f in os.listdir(label_dir)[:50]: # 先看 50 张 img_name = os.path.splitext(f)[0] + '.jpg' img = cv2.imread(os.path.join(img_dir, img_name)) h, w = img.shape[:2] with open(os.path.join(label_dir, f)) as fp: for line in fp: c, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(os.path.join(out_dir, img_name), img)这三项做完,数据基本可信。可视化这一步别偷懒,我遇到过 XML 里xmin和xmax写反的情况,数值校验查不出来,画出来一眼就能看到框是负宽度。
3.3 训练集验证集划分的两种策略
划分策略有两种:跟随 VOC 的ImageSets/Main,或者自己按比例随机划分。如果原始train.txt和val.txt存在且合理,优先跟随,因为官方划分通常考虑了场景分布。自己划分的话,1425 张按 8:2 是 1140 训练、285 验证,按 9:1 是 1282 和 143。小样本场景我倾向 8:2,验证集太小指标波动大。
划分脚本要保证图片和标签同步移动,且用固定随机种子:
import os import random import shutil random.seed(42) files = [os.path.splitext(f)[0] for f in os.listdir('labels_all')] random.shuffle(files) split = int(len(files) * 0.8) train, val = files[:split], files[split:] for subset, names in [('train', train), ('val', val)]: os.makedirs(f'images/{subset}', exist_ok=True) os.makedirs(f'labels/{subset}', exist_ok=True) for n in names: shutil.copy(f'JPEGImages/{n}.jpg', f'images/{subset}/{n}.jpg') shutil.copy(f'labels_all/{n}.txt', f'labels/{subset}/{n}.txt')random.seed(42)是为了可复现,换种子会得到不同划分,对比实验时不要改。划分完再核对一次每个子集的图片数和标签数是否相等。
4. 用这份数据跑通 YOLO 训练:配置与首轮结果
4.1 data.yaml 的字段与路径陷阱
YOLO 训练靠data.yaml描述数据位置和类别,最小配置如下:
path: /home/user/butterfly train: images/train val: images/val names: 0: butterfly四个字段里path是根目录,train和val是相对路径,names是类别索引到名称的映射。最常见的坑是path用了相对路径,而训练脚本的工作目录和你以为的不一样,导致找不到图片。我的习惯是写绝对路径,或者训练前cd到数据集根目录。另一个坑是names写成列表['butterfly'],新版本 Ultralytics 支持,但老版本只认字典,混用会报类别数不匹配。
如果数据集有多个类,names的顺序必须和转换脚本里的CLASS_MAP完全一致,索引从 0 连续。蝴蝶数据集如果只有一类,nc就是 1,训练日志里Class那一列只会出现一个类别。
4.2 训练命令与关键超参设置
一条能跑通的训练命令:
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/butterfly \ name=exp1参数逐个说:model=yolov8n.pt是最小的预训练权重,1425 张用 nano 足够,换 s 或 m 容易过拟合;imgsz=640是输入尺寸,蝴蝶目标通常占图比例中等,640 够用,显存紧张可以降到 512;batch=16在 8G 显存上比较稳,显存小就降到 8 并配合accumulate;lr0=0.01是初始学习率,小样本可以降到 0.005 减少震荡;patience=20是早停,验证指标 20 轮不升就停,省时间。
训练启动后重点看三行日志:train: Scanning后面的图片数是否等于你的训练集大小,val: Scanning同理,以及第一个 epoch 的box_loss是否在下降。如果Scanning出来的数字是 0,说明路径错了;如果box_loss一直是nan,多半是标签里有非法值,回去跑 3.2 的坐标校验。
4.3 首轮结果解读:mAP、混淆矩阵与过拟合信号
100 轮跑完,看runs/butterfly/exp1/results.png和confusion_matrix.png。小样本蝴蝶检测,mAP50 能到 0.7 以上算正常,低于 0.5 要查标注质量。混淆矩阵如果出现大量背景被误判为蝴蝶,说明背景干扰强,可以加负样本或提高box的置信度阈值。如果训练 loss 持续下降但验证 mAP 早早停滞,就是过拟合,减模型容量或加数据增强。
yolo detect val可以单独跑验证:
yolo detect val model=runs/butterfly/exp1/weights/best.pt data=data.yaml输出里mAP50和mAP50-95两个指标,前者宽松后者严格,小样本场景重点看前者。如果mAP50高但mAP50-95很低,说明框的位置不够准,可以检查标注框是否偏大或偏小。
5. 蝴蝶数据集训练避坑:5 个真实踩坑记录
5.1 坑一:图片和标签数量对不上,训练静默跳过
现象:训练日志里Scanning出来的标签数比图片数少,但训练不报错,只是 mAP 偏低。原因:YOLO 对缺失标签的图片不报错,直接当负样本处理,1425 张里少几十个标签,模型就学不到那部分目标。解决:训练前跑一次数量核对,images/train和labels/train的文件名集合必须完全一致,不一致的列出来补标或删除。
5.2 坑二:类别索引错位,模型把蝴蝶学成背景
现象:训练 loss 正常下降,但推理时框的位置对、类别全错,或者置信度极低。原因:转换脚本里CLASS_MAP和data.yaml的names顺序不一致,比如脚本里butterfly:0,yaml 里0: moth。解决:把两处的类别映射打印出来逐行对比,多类场景建议用同一份 JSON 配置驱动转换和训练,避免手写两遍。
5.3 坑三:XML 坐标越界导致 loss 变 nan
现象:第一个 epoch 的box_loss直接是nan,训练无法继续。原因:XML 里xmax超过图片实际宽度,归一化后大于 1,YOLO 的 loss 计算出现非法值。解决:转换脚本里做坐标裁剪(3.1 脚本已包含),并在转换后跑坐标范围校验,把所有越界行打印出来人工确认。
5.4 坑四:验证集划分泄漏,指标虚高
现象:验证 mAP 高得离谱,换一批新图推理效果却很差。原因:划分时用了随机划分但没固定种子,或者同一张图的不同增强版本同时进了训练和验证。解决:固定random.seed,划分前先去重,确保同一原始图片只出现在一个子集。1425 张规模不大,划分后人工抽查几张验证集图片,确认没在训练集里见过。
5.5 坑五:直接拿 VOC 的 ImageSets 当 YOLO 划分用
现象:按train.txt里的文件名去labels目录找标签,找不到,因为 VOC 的 txt 只有文件名没有路径,而 YOLO 的标签在另一个目录结构下。原因:VOC 和 YOLO 的划分文件格式不同,不能直接复用。解决:读 VOC 的 txt 拿到文件名列表,再按这个列表去复制对应的图片和标签到 YOLO 的images/train和labels/train,而不是直接把 txt 丢给 YOLO。
6. 小样本蝴蝶检测的进阶技巧:从 1425 张里再榨出几个点
1425 张在目标检测里属于小样本,想再提点,靠的不是换更大的模型,而是把数据增强和验证策略做细。我一般会先做一轮强增强实验:在训练命令里加mosaic=1.0、mixup=0.1、degrees=15、translate=0.1、scale=0.5,蝴蝶姿态多变,旋转和缩放增强收益明显。但mixup别开太大,0.1 到 0.2 之间,开大了小样本容易欠拟合。
第二个技巧是分层验证。把验证集按背景复杂度分成“纯色背景”和“自然背景”两组,分别跑yolo detect val,看模型在哪一组掉点。如果自然背景组 mAP 明显低,说明背景干扰是瓶颈,可以针对性补充自然背景的负样本,或者用copy-paste增强把蝴蝶贴到复杂背景上。这个操作不需要额外标注,用现有框就能做。
第三个技巧是模型集成。训练 3 个不同种子的yolov8n,推理时对框做加权融合,小样本场景通常能涨 1 到 2 个点。代价是推理变慢,看你的部署场景是否接受。如果只想要单模型,可以把yolov8n换成yolov8s但配合更强的正则,比如dropout=0.1和weight_decay=0.001,我试过在蝴蝶数据上比直接换大模型更稳。
最后一个习惯:每次改完增强或超参,固定用同一份验证集和同一个yolo detect val命令对比,不要凭训练 loss 判断好坏。我早期吃过亏,训练 loss 降得很漂亮,验证 mAP 反而掉了,后来只认验证指标。这份 1425 张的蝴蝶数据集,把格式转换、校验、训练、排查四步走完,基本能拿到一个可用的基线,剩下的提升就是在这个基线上做增强和集成的微调。希望帮到你。
本文还有配套的精品资源,点击获取