拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蛇类目标检测数据集:112张VOC/YOLO双格式标注图与YOLOv8训练实战

蛇类目标检测数据集:112张VOC/YOLO双格式标注图与YOLOv8训练实战

简介:蛇类目标检测数据集,同时提供VOC与YOLO两种标注格式,适用于Fast R-CNN、SSD、YOLO等常见目标检测模型的训练与效果验证。包内共337个文件,含112张jpg图片、112个xml标注文件、113个txt文件,压缩包18.71MB;jpg图像大小约1-500KB,xml与txt分别对应VOC和YOLO格式,可直接供主流检测框架读取使用。数据集由labelImg工具标注完成,目标类别统一为snake,并遵循精确框选边界、完整标注全部目标、一致性检查等标注规范,有助于减少二次清洗成本。压缩包为rar格式,无需解压密码,内含图片、VOC标注、YOLO标注三个独立文件夹,结构清晰便于查阅。目前已有74人浏览学习,适合计算机视觉初学者或需要蛇类样本的算法工程师快速获取可用的标注数据,用于模型调参、精度对比或数据扩充。

1. 蛇数据集 VOC 和 YOLO 双格式:112 张标注图能把检测模型推到哪一步

做蛇类目标检测的人最头疼的不是模型,是数据。公开数据集里蛇的标注图要么混在几十个类里,要么标注框框得随心所欲,想拿来做单类检测总得自己重新清洗一遍。这份数据集把最费时的活做完了:112 张左右蛇类 jpg 原图,VOC 格式的 xml 和 YOLO 格式的 txt 各一套,类别统一为 snake,用 labelImg 标注,压缩包解压后三个文件夹直接躺好。它能解决的是目标检测的第一公里——不用爬图、不用清洗、不用从零标注,整理目录、配好 data.yaml 就能喂给 YOLO 训练。适合两类人:拿 YOLOv8/YOLOv5 做课程设计或毕设、需要一份干净数据快速跑通流程的学生;以及想验证蛇类识别方案可行性的工程师,先用这 112 张把评估脚本和训练链路跑起来,再决定要不要自己扩数据。

2. 先看懂两张标注格式:VOC 的 xml 与 YOLO 的 txt 到底差在哪

2.1 VOC 的 xml 里存了什么:从 filename 到 bndbox 逐字段拆解

用 labelImg 打开一张 snake_81.jpg,框出那条蛇,按 Ctrl+S 存盘,得到的就是一个 PascalVOC 格式的 xml。这套格式最早是 PASCAL VOC 挑战赛定的,后来几乎所有检测框架都兼容它。xml 是纯文本,文本编辑器就能打开,结构是树状的,我拆给你看。

根节点 annotation 下面挂着几类信息:folder 写图片所在文件夹,filename 写图片文件名,path 写保存时刻的绝对路径,这三个字段和模型训练基本无关,最要紧的是 size 和 object。size 里有 width、height、depth 三个子节点,宽度、高度是像素值,depth 是通道数,jpg 基本都是 3。object 节点才是标注本体,每个 object 表示图里的一个目标,这张数据集里一张图可能只有一条蛇,但复杂场景下会有多个 object 并列。

object 下面最常用的字段是 name 和 bndbox,偶尔有 truncated 和 difficult。name 就是类别名,这份数据集统一写 snake。bndbox 是矩形框坐标,包含 xmin、ymin、xmax、ymax 四个子节点,表示框的左上角和右下角。注意这套坐标是像素绝对坐标,原点在图片左上角,x 向右增大,y 向下增大,单位是像素不是比例。比如 xmin=214 就是框左边界离图片左缘 214 个像素。数据集说明里提到的那三条标注规范——边界准确、目标标全、一致性检查——对应到 xml 上,就是 bndbox 大多贴着蛇身轮廓,而不是把蛇所在的整块草地都圈进去,这对训练是好事。

2.2 切换到 YOLO 后坐标为什么变了:归一化与中心点换算

YOLO 格式的标注不是 xml,而是每个图片一个同名的 txt 文件,每行代表一个目标,一共五个数:类别编号、目标中心 x、目标中心 y、目标宽、目标高。后四个都是 0 到 1 的归一化小数,要除以图片的宽和高。比如把一份 xml 转成 txt,核心换算长这样:

import xml.etree.ElementTree as ET tree = ET.parse('labels_voc/snake_81.xml') root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) obj = root.find('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) x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height print(f"snake 0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}")

这段代码做的事情就是把 xml 里的像素框转成 YOLO 格式。逻辑分三步:先从 size 节点读宽高做分母,再从 bndbox 读四个角点,最后用中心点加宽高的方式重写坐标。中心点公式是 (xmin+xmax)/2 再除以图片宽,宽度是 (xmax-xmin) 除以图片宽,高度同理。为什么要除以宽高?因为训练时 YOLO 会把图片缩放到固定尺寸比如 640×640,像素坐标在缩放后就变了,归一化坐标不会变,模型只看相对比例,这也是同一条蛇从手机拍的和从监控截图的能共用一套标注的原因。

输出里的snake 0 0.512345 0.345678 0.234567 0.156789,第一个是类别名(给人看的说明),后面五个数才是真正写进 txt 的内容。注意类别编号从 0 开始,不是从 1 开始,这是 YOLO 的老规矩,新手最容易在这里栽跟头。

对比项VOC(xml)YOLO(txt)
文件格式xml 树状结构纯文本,每行一个目标
坐标基准像素绝对坐标0~1 归一化比例
框表示左上角 xmin/ymin + 右下角 xmax/ymax中心点 x/y + 宽高
类别表示name 字符串从 0 开始的整数编号
多个目标多个 object 节点多行文本

2.3 labelImg 双格式导出的设置要点

这份数据集的两个标注格式来源就是 labelImg 的两种保存模式。labelImg 是目标检测圈最常见的开源标注工具,界面左边竖着一排按钮,画框用 W,调框用鼠标拖动,底部有 PascalVOC 和 YOLO 的切换按钮,点 YOLO 模式后会弹窗让你选一个 classes.txt 文件,这个文件决定了 YOLO 模式下的类别列表和编号顺序。

一个常见做法是新建一个 classes.txt,里面只写一行 snake,保存到项目根目录。然后切到 YOLO 模式继续标注,之后 Ctrl+S 存出来的就是 txt。反过来切回 PascalVOC 模式,存出的又是 xml。这解释了为什么这份数据集的 xml 和 txt 内容完全对应——它们是同一批图片用 labelImg 分别以两种模式各存了一遍,或者由其中一种批量转换得出。拿到数据集后我自己也会做一次一致性抽检,具体方法在第 6 章。

3. 把数据集跑起来:目录整理、配对校验与 YOLOv8 首轮训练

3.1 解压后的目录结构:三个文件夹该怎么组织

压缩包里是三个文件夹:一个装 jpg 原图,一个装 VOC 的 xml,一个装 YOLO 的 txt,文件名一一对应。这个状态适合「用 labelImg 打开看图」和「人工核对」,但不适合直接喂给训练框架。训练框架期望的典型结构是 images 和 labels 两个平级目录,jpg 和 txt 分开放,文件名相同。我一般会把它整理成这样:

snake_dataset/ ├── images/ # 112张左右jpg原图 ├── labels/ # YOLO格式txt,与images同名 └── labels_voc/ # VOC格式xml,保留作转换源和备份

这样分层有三个好处:images 和 labels 是训练框架的标准输入,不需要改框架配置;labels_voc 单独留一份是因为 xml 可读性好,后续要改类别名或重新统计标注情况直接在 xml 上改,再批量转回 txt;三个目录平级,增删图片时只需要同名增删文件,不会把原始标注搞乱。如果你后面要自己扩标注,labelImg 打开 images 目录直接画就行。

3.2 用脚本快速校验图片与标注文件配对情况

整理完目录千万别急着训练,先跑配对校验。小数据集最常见的坑就是解压丢包、重命名后扩展名不一致、哪张图漏了标注文件——这类问题训练时会静默跳过或报错,排查成本远高于提前校验。我一般会保存一个 check_dataset.py 在项目里,每个数据集都用它过一遍。

from pathlib import Path img_dir = Path('images') txt_dir = Path('labels') img_files = {p.stem: p for p in img_dir.glob('*.jpg')} txt_files = {p.stem: p for p in txt_dir.glob('*.txt')} for stem in txt_files: if stem not in img_files: print(f"[txt没有对应jpg] {stem}") for stem in img_files: if stem not in txt_files: print(f"[jpg没有对应txt] {stem}") txt = txt_dir / f"{stem}.txt" if txt.stat().st_size == 0: print(f"[空标注] {stem} 需要回labelImg补标")

逻辑只有三步:用 Path.glob 收集两个目录里所有同扩展名文件,以去掉扩展名的 stem 当主键;对比两边的键是否一一对应;顺带检查 txt 文件大小,0 字节说明这张图没有任何标注。跑完输出为空说明配对没问题。112 张全量扫一遍耗时不到一秒,我会把它挂在训练命令之前当成强制前置步骤。当初我偷懒跳过这步,结果训练到一半才发现有 13 张图漏了标注,白跑两轮,血泪经验。

3.3 在 YOLOv8 上完成第一次训练:参数表与常见误区

配对没问题就可以训练了,这里以 YOLOv8 为例。先建 data.yaml:

path: snake_dataset train: images val: images names: 0: snake

train 和 val 暂时都指向 images,是因为 112 张的规模再分 train/val 会让每份都太薄,常见做法是先全量训练看 loss 曲线是否正常,后面扩到几百张再正经划分。如果非得分,按 9:1 切,val 留 12 张左右足够观察趋势,不要在 112 张里再抠 20 张当 test。

然后跑训练:

yolo detect train model=yolov8n.pt data=snake.yaml epochs=100 imgsz=640 batch=4 patience=20

几个参数说清楚。model 用 yolov8n.pt 预训练权重起步,不要从 yolov8m 或 yolov8x 开始,112 张数据根本不支持大模型,n 系列参数量最小但在这种单类小目标场景完全够用,而且收敛快、方便试错。imgsz 默认 640,如果你的原图里蛇占面积大、边框很满,可以降到 512 甚至 416,减小缩放带来的形变。batch 看显存,8G 显存跑 batch=4 比较稳。epochs 给 100 够了,配合 patience=20 做早停,实际训练到 final 之前就会停。第一次跑建议开着 runs/detect 目录盯 val 曲线,正常走势是 mAP50 在前 20 个 epoch 快速上涨后变缓,class loss 平稳下降;如果 train loss 和 val loss 差距越拉越大,就是过拟合信号,第 5 章展开。

参数取值说明
modelyolov8n.pt小模型预训练权重,112张首选
imgsz640原图画幅大建议降到512
batch48G显存稳妥值,显存大可加
epochs100配合早停,不需要手动改
patience2020轮val无提升即停

4. 格式转换与二次加工:写一个 xml→txt 转换脚本

4.1 为什么要自己写转换脚本而不是手动另存

有读者会问:数据集的 xml 和 txt 不是都给了吗?为什么还要自己转换?两个原因。一是增量:你后面自己加了 20 张图,用 labelImg 默认存成 xml,这时候手头就有一套 VOC 一套 YOLO 混着,训练前必须把新增的 xml 转成 txt。二是改标注:你想把类别从 snake 拆成 snake_class1、snake_class2,或者把越界的框修正,在 xml 文本上操作比在 txt 上操作安全得多,改完再批量转。labelImg 虽然也能直接从 xml 切到 YOLO 再逐张另存,但上百张图点几百次鼠标纯属浪费时间,写个 40 行的脚本一次跑完才是工程做法。

4.2 xml_to_yolo.py:全量代码与逐段逻辑

完整脚本如下,保存成 xml_to_yolo.py 放到项目根目录运行。

import os import glob import xml.etree.ElementTree as ET # 类别列表:顺序必须和训练时 data.yaml 的 names 一致 CLASSES = ['snake'] def xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in CLASSES: print(f"[跳过未知类别 {name}] {xml_path}") continue cls_id = CLASSES.index(name) 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 / width y_center = (ymin + ymax) / 2.0 / height w = (xmax - xmin) / width h = (ymax - ymin) / height # 越界防御:坐标必须落在 [0,1] x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") img_name = os.path.basename(xml_path).replace('.xml', '.txt') out_path = os.path.join(out_dir, img_name) with open(out_path, 'w') as f: f.write('\n'.join(lines)) if __name__ == '__main__': xml_dir = 'labels_voc' out_dir = 'labels' os.makedirs(out_dir, exist_ok=True) for xml_file in glob.glob(os.path.join(xml_dir, '*.xml')): xml_to_yolo(xml_file, out_dir) print(f"[已转换] {xml_file}")

几个关键点拆开说。CLASSES 列表决定类别编号,它的顺序必须和训练配置里 names 的顺序严格一致,YOLO 训练不认类别名,只看 txt 第一列的数字和 data.yaml 里 names 的索引,两者错一位整个模型就废了。:.6f保留六位小数是 YOLO 训练的常见约定,精度再高没意义,再低会影响小目标的小框精度。clamp 那一行不是可有可无,labelImg 里画框拖出图片边界的情况时有发生,xml 里就会出现负数或超过宽高的坐标,不夹取的话训练时会产生 NaN 或越界框,mAP 直接崩。

跑法很简单:

python xml_to_yolo.py

输出会一路打印已转换的文件名。转换完以后,labels 目录里应该出现和 images 目录一一对应的 txt 文件。注意脚本里对未知类别是 print 后跳过,如果跳过的数量很多,说明 xml 里有你没见过的类别名,先打印出来核对一遍,再决定是加入 CLASSES 还是删掉那些框。数据集的 xml 和 txt 是同源标注,脚本跑出来的结果应该和压缩包里自带的 txt 完全一致,这份数据本身可以作为脚本正确性的对照基准。

4.3 转换后验证:把归一化坐标还原回像素框

转换不是跑完脚本就结束了,还要验证输出。验证思路是反着来:把生成的 txt 里五个数还原成像素坐标,和 xml 里的 bndbox 比对,误差应该为 0。单独写一段反向验证:

# 还原yolo坐标回像素坐标,用于和xml的bndbox核对 w = 640 # 改成该图实际宽度 h = 480 # 改成该图实际高度 x_center, y_center, bw, bh = 0.512345, 0.345678, 0.234567, 0.156789 xmin = int(round((x_center - bw / 2) * w)) ymin = int(round((y_center - bh / 2) * h)) xmax = int(round((x_center + bw / 2) * w)) ymax = int(round((y_center + bh / 2) * h)) print(xmin, ymin, xmax, ymax)

这个脚本看着短,但它是整个转换链路的照妖镜。xmin 由中心减半宽再乘宽度得到,因为归一化时是除法,还原时就是乘法,完全可逆。如果还原出的 xmin 和 xml 里的 xmin 对不上,要么是源 xml 被改过,要么是脚本里 width/height 读错。配合第 6 章的可视化脚本一起用,一张图就能看出所有问题。另外要注意:xml 里如果没有 object 节点,lines 就是空列表,写出的 txt 是空文件,训练时会跳过这张图,配对校验脚本会把这个情况单独标出来,不要忽略它。

5. 避坑记录:112 张数据集训练最容易翻车的五个实测点

112 张的规模,模型架构基本不会出问题,真正的坑全在数据侧。以下五条是我拿小数据集训练时反复踩过的,每一条按现象、原因、解决拆开写,后面照方抓药。

5.1 图片读取失败或路径乱码

现象:训练一开始就报 FileNotFoundError,或者跑着跑着提示图片打开失败,日志里的 image path 是一长串乱码。

原因:压缩包解压路径或项目路径里有中文和空格。labelImg 在 Windows 下生成的 xml 可能是 GBK 编码,而 YOLO 框架默认用 UTF-8 读文件路径,编码对不上就炸。

解决:项目根路径改成纯英文目录,比如 D:\snake_work\,不要出现「蛇数据集」这种中文文件夹名;图片文件名保持 snake_81.jpg 这种原始命名,别手动重命名成「蛇_81.jpg」。路径规范是第一坑,新数据集拿到手先统一文件名和路径再动训练,能省掉后面一大半莫名其妙的报错。

5.2 类别编号错位,mAP 一直为零

现象:训练正常跑完,val 曲线里 mAP50 全程为 0,box loss 也在降,但怎么等都不涨,看起来像模型没学到东西。

原因:txt 第一列的编号和 data.yaml 里 names 的索引对不上。比如 labelImg 的 classes.txt 里写了 snake,但你在 data.yaml 里写成了1: snake,或者你改了类别列表让编号从 1 开始,但转换脚本固定输出 0,模型按 0 去查 names 查不到对应类别,评估自然全 0。

解决:先拿一张图的 txt 和 xml 手工对一遍,txt 第一列应该是 0(单类数据集必然是 0)。然后检查 data.yaml,单类的 names 必须从 0 开始编号。这条我检查过好几个人的项目,十次有八次死在这。

5.3 标注框越界导致 loss 爆掉或 BN 崩溃

现象:训练到第几个 epoch 时 train loss 突然跳到巨大值,之后一路 NaN,日志里 mAP 直接消失,有说法是 BN 崩了。

原因:xml 里的 bndbox 坐标有负数或超出图片宽高,例如 xmax 标成了 741,但图片宽度只有 640。YOLO 对越界框的容忍度很低,归一化后出现大于 1 的坐标,反向传播时梯度异常,表现为 BN 统计量乱掉,最终全链路崩掉。BN 崩溃只是现象,根因多半在标注框。

解决:转换脚本里加上 clamp 夹取(第 4 章的脚本已经内置),训练前再跑一次配对校验脚本,把 txt 里所有坐标扫一遍,任何一个数大于 1 或小于 0 就先回 labelImg 修框。从那以后我拿到任何数据集都会先扫一遍 txt 里的异常数值,这个习惯救了我好几次。

5.4 数据集太小,val 分数虚高看着像过拟合

现象:训练到 40 个 epoch,train loss 还在降,val mAP 已经到 0.9x 甚至 1.0,但换一张没见过的蛇图实测就漏检。

原因:112 张的 val 集可能只有 10 张左右,样本太少,模型把训练图背下来了,遇到新背景立刻露馅,val 分数是在「刷熟脸」而不是「考能力」。

解决:val 分数只能当参考,关键是拿没参与训练的图做盲测。常见做法是从总数据里抽 3~5 张「打死不进训练」的图,训练完单独跑 predict 用肉眼看检测框。数据增强参数调大一点也有帮助,第 6 章给具体方案。

5.5 xml 缺一张、txt 多一张,模型静默跳过样本

现象:训练日志里显示的 images 数量比实际少了,或者某张图的 loss 对不上原图,排查半天发现是标注文件没配对。

原因:解压时丢包、编辑时误删 xml、增量标注时只导出了部分文件。YOLO 训练时如果一张图没有对应 txt,会静默跳过这张图,不报错,所以数量不对很难被发现。

解决:把 3.2 的校验脚本设为训练前置步骤,train 之前强制跑一次,输出为空才允许继续。小数据集手动检查也行,但脚本可以复用,成本为零,靠人眼逐个数是靠不住的。

6. 112 张用出 500 张的效果:数据增强与标注质量终检

6.1 针对蛇的增强方案

112 张跑完首轮,常见瓶颈是背景单一、光照分布窄。用 albumentations 做离线增强是最省事的方案,先存增强后的图和对应的标注。

import albumentations as A transform = A.Compose([ A.RandomRotate90(p=0.5), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.6), A.RandomScale(scale_limit=0.2, p=0.4), ], bbox_params=A.BboxParams(format='yolo', label_fields=['labels']))

参数说明:蛇体细长,RandomRotate90 比任意角度旋转更稳,避免把蛇旋成「拧麻花」的畸形样本;RandomScale 的 0.2 控制框缩放幅度,兼顾模拟不同拍摄距离;BboxParams 的 format='yolo' 表示接受的就是 txt 里那种归一化坐标,增强后 bbox 会跟着图一起变换,不需要自己重算。扩完一轮,112 张变 300 多张,训练时再用 YOLOv8 自带的 mosaic 增强,效果比裸跑明显好。

6.2 标注质量终检:把框画回图上看

最后一道工序永远是可视化终检。脚本思路是把每张图的标注框画回原图,随机抽 30 张左右肉眼过一遍:

import cv2 img = cv2.imread('images/snake_81.jpg') h, w = img.shape[:2] # 从txt读第一条标注 with open('labels/snake_81.txt') as f: line = f.readline().strip().split() cls_id, x_c, y_c, bw, bh = int(line[0]), float(line[1]), float(line[2]), float(line[3]), float(line[4]) xmin = int((x_c - bw / 2) * w) ymin = int((y_c - bh / 2) * h) xmax = int((x_c + bw / 2) * w) ymax = int((y_c + bh / 2) * h) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.imwrite('check_snake_81.jpg', img)

抽检时重点看三类:框是否贴住蛇身轮廓、有没有框到旁边的树根或石块、蛇身细长弯折时框是否把中间空档也包了。这类问题不会报错,但直接影响模型学到的特征。从那以后,我每次拿到别人标注的数据集,都强制先跑一遍可视化终检再进训练,这张蛇数据集我也按这个流程过了,至少省下两轮「训练完才发现标注有问题」的白跑调试。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表