ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

目标检测数据集全解析:VOC/COCO/YOLO格式、数据划分与训练避坑指南

目标检测数据集全解析:VOC/COCO/YOLO格式、数据划分与训练避坑指南

简介:这是一套面向YOLO目标检测实战的红蚂蚁数据集,共包含一万张真实场景图片,经专业工具精细标注,场景丰富且标注框质量高,可直接用于算法训练和课程项目。压缩包内共有两千个文件,以一千九百八十六个XML标注文件为主,同时涵盖VOC、COCO、YOLO三种格式标签,另提供Python划分脚本、HTML版环境搭建与训练教程,整体体积约为四百零八MB,目录结构清晰,便于快速定位所需资源。配套脚本支持自由划分训练集、验证集和测试集,教程覆盖Windows与Linux双系统平台,可引导读者从零搭建YOLO目标检测环境并训练自定义数据集,显著降低数据准备和上手指门槛。目前已有一百三十六人学习下载,适合需要高质量真实标注数据与完整训练指引的目标检测初学者及研究者。

1. 红蚂蚁目标检测数据集到底能干什么:从训练到部署的整条链路

朋友接了个农业植保的活,要在田间照片里把红蚂蚁数出来。他第一批图自己手标了三百多张,丢进YOLO训练,验证集里框全是歪的,小蚂蚁和背景糊成一片。后来换用这份红蚂蚁目标检测数据集——10000张真实场景图,VOC、COCO、YOLO三种格式标签全部备齐,划分脚本和训练教程一块打包——三天内就调出一个能用的检测模型。这就是它最直接的用途:不用自己写格式转换脚本,不用一张张核对标注坐标,图片、标签、划分、训练整条链路都是通的。它适合三类人:想跑通YOLO完整流程的新手,做农业视觉项目需要现成底料的一线工程师,以及做算法对比实验、需要一个干净数据集支撑论文的从业者。

2. VOC、COCO、YOLO三套标签怎么选:结构差异与切换成本

拿到一份目标检测数据集,最先要搞清楚的不是模型,而是标签格式。同一个框,在三种格式里写出来完全不一样:VOC是每个目标一个object节点、坐标写绝对像素值;COCO是全部塞进一个json文件,框用[x, y, w, h]表示;YOLO则是一个txt一行一个目标,坐标全部归一化成0到1的小数。选择不对,训练脚本会在数据读取阶段就直接报错,而且报错信息往往很隐晦。下面把这三种格式摊开对比,说清楚每种格式的存储方式和适用场景,再给一个选型建议。

2.1 三种格式的目录与文件组织方式

VOC格式是“目录里套目录”,这套组织方式源自Pascal VOC比赛,后来成了目标检测的通用约定:

VOCdevkit/ ├── JPEGImages/ # 图片,命名如 000001.jpg ├── Annotations/ # 同名XML,命名如 000001.xml └── ImageSets/ └── Main/ # train.txt、val.txt、test.txt,只写不含扩展名的文件名

JPEGImages放原始图片,Annotations放标注文件,ImageSets/Main下放的是划分列表,每个txt里只有文件名,不带.jpg也不带.xml。训练时框架拿着文件名去两个目录里各自找图、找标签。这样的好处是图片和标签物理分离,坏了只要换一张图,不用动标注目录。

COCO格式走的是“大清单”路线,不再用目录结构,而是把所有标注集中到一个json文件里:

dataset/ ├── images/ # 所有图片 └── annotations/ ├── instances_train.json └── instances_val.json

json文件里同时记录图片信息、目标信息和类别信息,一份文件就是一个完整数据集。它的好处是分发方便,坏处是文件大,十万张图配一个json能到几百MB,解析起来比读xml慢。

YOLO格式是近几年的主流,因为YOLO系训练脚本只认这种组织方式:

dataset/ ├── images/ # 图片,命名如 000001.jpg └── labels/ # 同名TXT,命名如 000001.txt

每个txt和图片同名,一行描述一个目标,文件和文件一一对应。训练时脚本看到images下的图,就去labels下找同名txt,找不到就跳过这张图。三种格式的核心差异可以收进一张表:

维度VOCCOCOYOLO
存储形态图片、XML、划分txt分目录图片目录+单个json图片目录+同名txt
坐标形式绝对像素 xmin、ymin、xmax、ymax绝对像素 x、y、w、h归一化 x_center、y_center、w、h
标注单位一张图一个XML整份数据一个json一张图一个TXT
常见框架SSD、早期Faster R-CNNmmdetection、Detectron2YOLO全系列

2.2 什么时候用哪套格式

这个选择其实不复杂,按训练框架倒推就行。如果你打算跑YOLOv5、YOLOv8这类模型,直接用YOLO格式的txt,训练脚本里data.yaml的train和val路径指到images目录,框架会自动去同名labels目录里找标注。这时候你手里如果有VOC或COCO,反而要先转一次才能训。

如果你做的是对比实验,要在一个框架里把Faster R-CNN、Cascade R-CNN、YOLO全跑一遍,我建议统一用COCO格式。mmdetection这类工具箱的coco接口最成熟,VOC接口虽然也有,但很多老接口在类别id处理上有历史包袱。COCO格式里的category_id是显式指定的,不容易出现类别错位。

如果你在跑的是老代码、老权重,比如基于Pascal VOC训练的SSD,那就保持VOC格式别动。老代码往往把VOC的目录结构写死,你改了反而要改代码。一句话总结:新项目优先YOLO或COCO,旧代码用VOC,不折腾。

2.3 同一份数据存三份为什么值得

有些朋友会觉得一份数据存三份是浪费空间,其实不然。标注是整个目标检测链路里最贵的一环,一分钟标一个框,一千张图就是十几个小时的枯燥劳动。转换脚本虽然能自动把VOC转成YOLO,但转换过程中最容易出问题的环节是坐标换算:归一化时除的是宽还是高、中心点坐标是相对于左上角还是右下角,写错一个变量,训练出来就是一批歪框。而你用手在Photoshop或labelimg里重新标注一遍,代价更大。所以一份数据给三份格式,本质上是把最容易出错、最费时间的那一步替你做了。你要做的只是确认文件路径没写错,直接开训。

3. 解析标签文件的真实结构:xml、json、txt里的每个字段

有了基础认知后,直接去读文件字段,是最快建立手感的方式。这一章按VOC到COCO到YOLO的顺序逐个拆,最后给一段“txt反算像素坐标画框”的验证代码,这一步在数据清洗时能救你很多次。

3.1 XML里最关键的object节点

用labelimg标注后导出的VOC XML,结构是固定的,核心长这样:

<annotation> <folder>JPEGImages</folder> <filename>000001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>red_ant</name> <bndbox> <xmin>824</xmin> <ymin>410</ymin> <xmax>942</xmax> <ymax>486</ymax> </bndbox> <difficult>0</difficult> </object> </annotation>

size里的宽高是原图的像素尺寸,bndbox里的四个值是目标框在整张图上的绝对像素坐标,左上角为原点。xmax减去xmin就是框的宽度,ymax减去ymin就是框的高度,这两个数值后面转成YOLO格式时要反复用到。difficult字段表示这个目标是否难以辨认,训练时如果按默认参数,difficult为1的样本会被丢弃或降权,转格式时如果脚本没处理这个字段,容易产生脏数据。

一份数据里如果有多个蚂蚁,XML里就会出现多个<object>节点,每个节点独立一组框。解析XML时用root.findall("object")遍历,别只取第一个。

3.2 COCO的json三个顶层数组

COCO格式的json由三个顶层数组组成,分别是images、annotations、categories。images记录图片的宽高和文件名,annotations记录每个目标框的坐标和类别,categories记录类别id和类别名的对应关系:

{ "images": [ {"file_name": "000001.jpg", "height": 1080, "width": 1920, "id": 1} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [824, 410, 118, 76], "area": 8968, "iscrowd": 0, "segmentation": []} ], "categories": [ {"id": 1, "name": "red_ant"} ] }

bbox四个值分别是左上角x、左上角y、框宽、框高,area是框的面积,数值上等于宽乘高。segmentation在labelimg导出时通常是空列表,因为矩形框没有多边形点集,纯框检测不受影响;但如果你的评估代码要算mask相关的指标,拿不到segmentation就会出问题,这是用labelimg导COCO的两个已知短板之一。另一个短板是iscrowd字段,labelimg默认写0,表示这个目标是否被其他目标遮挡,0就是没有被遮挡。

3.3 txt归一化坐标与像素坐标互转验证

YOLO的txt里只有五个数字,一行一个目标,格式是类别id 中心点x 中心点y 框宽 框高,前四个是归一化到0到1的小数。很多初学的人直接拿这五个数字画框,结果框全聚在图左上角,就是因为没乘回原图宽高。下面的脚本能把txt里的归一化坐标反算成像素坐标并画出来,是验证标注质量最直接的手段:

import cv2 img_path = "images/000001.jpg" txt_path = "labels/000001.txt" img = cv2.imread(img_path) h, w = img.shape[:2] # 注意顺序:shape返回的是高、宽,不是宽、高 with open(txt_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue class_id, x_center, y_center, bw, bh = parts x_center, y_center, bw, bh = map(float, (x_center, y_center, bw, bh)) x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) print(f"共画了 {len(open(txt_path).readlines())} 个框,图片尺寸 {w}x{h}") cv2.imwrite("check_000001.jpg", img)

这段代码的核心是反归一化:txt里的坐标是相对于图片宽高的小数,所以要乘回宽高才能得到像素坐标。计算结果还要做一次int()取整,因为画框需要整数坐标。img.shape[:2]返回的是(高, 宽),如果这里不区分,转置之后框会整体偏移,这是最常见的手滑点。如果画出来的框和蚂蚁轮廓吻合、没有飞出画面边界,说明这份txt标注和图片是配对的。

4. 用划分脚本拆数据:训练/验证/测试三集一步到位

数据集拿到手,默认状态是全部图片放在一起。直接拿整份数据去训练,会在验证阶段吃大亏:模型见过的图片又出现在验证集里,指标虚高。所以划分必须做。这个压缩包里带了三套划分脚本,分别解决不同需求。

4.1 三套脚本分别解决什么问题

脚本名输出内容适用情况
split_train_val生成ImageSets下txt文件划分脚本.py生成ImageSets/Main下的train.txt、val.txt、test.txt走VOC格式的老框架
训练集、验证集划分脚本(图片标签划分写入新文件夹).py把图片和标签复制到train/val两个新目录只分两份,不要测试集
训练集、验证集、测试集划分脚本(图片标签划分写入新文件夹).py把图片和标签复制到train/val/test三个新目录标准三份划分,YOLO训练首选

第一套脚本做的事最轻,它不复制文件,只生成文件名列表,适合数据结构保持VOC原样的场景。第二、三套脚本做的事是物理切分,它会在新目录下创建images和labels子目录,把文件复制进去。YOLO训练推荐用第三套,因为YOLO框架的data.yaml天然期待images/train、images/val、images/test这种目录结构。

4.2 三集划分脚本的逻辑拆解

下面以“训练集、验证集、测试集划分脚本(图片标签划分写入新文件夹).py”为例,讲清楚核心逻辑。这段代码是基于这类脚本的常见写法补全的,你可以直接对照理解:

import os import random import shutil from glob import glob random.seed(42) # 固定随机种子,保证每次运行划分结果一致 image_dir = "JPEGImages" # 图片所在目录 label_dir = "Annotations" # VOC的xml标签目录;如果标签是yolo的txt,改成labels save_root = "split_output" # 输出根目录 ratio = {"train": 0.8, "val": 0.1, "test": 0.1} images = sorted(glob(os.path.join(image_dir, "*.jpg"))) random.shuffle(images) total = len(images) train_n = int(total * ratio["train"]) val_n = int(total * ratio["val"]) for idx, img_path in enumerate(images): if idx < train_n: part = "train" elif idx < train_n + val_n: part = "val" else: part = "test" basename = os.path.splitext(os.path.basename(img_path))[0] img_save_dir = os.path.join(save_root, "images", part) label_save_dir = os.path.join(save_root, "labels", part) os.makedirs(img_save_dir, exist_ok=True) os.makedirs(label_save_dir, exist_ok=True) shutil.copy2(img_path, os.path.join(img_save_dir, os.path.basename(img_path))) xml_path = os.path.join(label_dir, basename + ".xml") if os.path.exists(xml_path): shutil.copy2(xml_path, os.path.join(label_save_dir, basename + ".xml"))

逻辑分五步。先固定随机种子,random.seed(42)这行的作用是把随机序列固定下来,跑两次脚本划分结果一致,不会被“随机”坑到。接着用glob把所有jpg图片路径读进来,shuffle打乱顺序,再按比例切成三段。切分的关键在enumerate循环里:索引小于train_n进训练集,小于train_n + val_n进验证集,剩下的进测试集。复制文件用的是shutil.copy2,它会把文件的修改时间等元数据一起拷走,比shutil.copy更稳。

如果你手里的标签是YOLO的txt而不是VOC的xml,记住两个改动:label_dir改成存放txt的目录,复制目标后缀从.xml改成.txt。这也是脚本目录里同时带了两套划分脚本的原因,VOC走xml逻辑,YOLO走txt逻辑,两者文件配对的基准都是“不带扩展名的文件名”。

4.3 split_train_val脚本:VOC的ImageSets方案

另一套脚本走的是纯列表方案,不复制文件,只生成ImageSets/Main下的txt。逻辑相对更轻:

import os import random from glob import glob random.seed(7) xml_dir = "Annotations" save_main = os.path.join("ImageSets", "Main") os.makedirs(save_main, exist_ok=True) xmls = glob(os.path.join(xml_dir, "*.xml")) names = [os.path.splitext(os.path.basename(x))[0] for x in xmls] random.shuffle(names) total = len(names) train_n = int(total * 0.8) val_n = int(total * 0.1) def write_list(file_path, items): with open(file_path, "w") as f: for name in items: f.write(name + "\n") write_list(os.path.join(save_main, "train.txt"), names[:train_n]) write_list(os.path.join(save_main, "val.txt"), names[train_n:train_n + val_n]) write_list(os.path.join(save_main, "test.txt"), names[train_n + val_n:])

这套方案的思路是:以Annotaions目录下的xml文件为数据源,因为有xml就一定有名配对图片,比直接扫图片目录更可靠。names列表里存的是不带扩展名的文件名,写入txt时每行一个。VOC框架的DataLoader拿到train.txt后,会自己拼出JPEGImages/000001.jpg和Annotations/000001.xml的路径去读取,所以txt里绝不能带扩展名,带了你不仅要改txt还要改代码。

4.4 划分结束后必须做的三项检查

划分完直接开训是翻车高发期,我一般会强制自己走三个检查。第一,对比一下三个子目录的图片总数和原始图片总数,差一个数都要查清楚是漏复制还是路径匹配失败。第二,随机抽验证集的20张图,用第3章的画框脚本确认图片标签配对,重点看有没有“标签复制了但图片没复制”的半空目录。第三,如果用的是生成txt的方案,确认ImageSets/Main里的三个txt和Annotations目录下xml文件名的交集,交集大小应该等于xml总数,少一个都说明有文件没参与划分。

5. 训练前的避坑清单:坏标注、路径错、类别不匹配的排查

数据准备阶段踩过的坑,比模型调参阶段多得多。这里把最容易翻车的四个问题按“现象、原因、解决”写清楚,都是这个数据集场景下高概率碰到的。

5.1 图片和标签文件名配对失败

现象:训练脚本一启动就报No labels found,或者loss一直是0,日志里显示某张图被跳过。

原因:图片目录里是000001.jpg,labels目录里是000001.txt,看起来没问题。但实际文件可能是000001.JPG和000001.txt,后缀大小写不一致,glob("*.jpg")就扫不到大写的JPG。另外如果图片是png,脚本写死只认jpg,同样配对失败。

解决:跑训练前先做一次文件名归一化。操作上我习惯写一个小脚本把图片后缀统一转成小写,然后检查images和labels两个目录下不带扩展名的文件名集合是否完全一致。做交集差集是秒出结果的,别靠肉眼数。

5.2 类别id顺序错位导致预测张冠李戴

现象:训练过程loss正常,但预测时红蚂蚁被识别成背景,或者输出的类别名和实际目标完全对不上。

原因:YOLO的txt里存的是类别id,不是类别名。id为0对应的类别由data.yaml里names列表的顺序决定。如果划分脚本或转换脚本在处理时把xml里的name按字典序排了一遍,而data.yaml里按另一个顺序写,两边就对不上了。红蚂蚁这种单类数据集还好,一旦你往里面加第二类,错位的概率直线上升。

解决:转换脚本跑完,手动打开一个txt看第一行第一个数字,再去data.yaml里数names列表的第几位,确认一致。ctrl+f搜一下训练日志里的类别数量,nc显示几,就要确保全局统计下来真的只有几类。

5.3 验证指标正常但预测框明显偏大

现象:mAP能到0.85,但画出来的框比蚂蚁大一圈,甚至把旁边泥土也框进去;如果蚂蚁密集,框互相重叠。

原因:归一化坐标计算时用错宽高。txt里的宽高是相对于图片宽度的比例,中心点也是。有些转换脚本会写成x_center / height,其实就是除数和被除数搞反了。还有一种常见情况:xml的size里宽和高都记录的是原图尺寸,但脚本先做了resize再取宽高,坐标自然偏移。

解决:用第3.3节的画框脚本抽验证集,把框画出来和原图叠一起看。如果所有框都整体向右下角偏移,就是中心点计算问题;如果框变宽变扁,就是宽高归一化除反了。这类问题靠指标看不出来,只能靠画框目检。

5.4 difficult=1的冗余框拉低训练效果

现象:训练完的模型对某些目标置信度特别低,比如被遮挡一半的蚂蚁框不出来,但单独一张图里的大蚂蚁表现很好。

原因:VOC xml里的difficult=1表示这个目标难以辨认,这类样本不该进训练集。划分脚本复制标签时如果原样复制了difficult,而YOLO训练脚本不认difficult字段,它会把这种低质量框当成普通正样本强行学习,模型学出来的特征自然不稳定。

解决:转成YOLO格式前先把difficult标记的框过滤掉。用xml解析脚本遍历,遇到difficult为1的object节点直接跳过,不写入txt。如果这份红蚂蚁数据集的XML里大部分difficult都是0,可以直接忽略;但如果你要把这套流程复用到别的VOC数据集上,这一步必须检查。

6. 换到自己数据集上的最后一步:改参数、验结果、清理误检

6.1 data.yaml改错是新手最常见的翻车点

训练脚本真正读的配置就一个data.yaml,里面几个字段直接决定成败:

train: split_output/images/train val: split_output/images/val test: split_output/images/test nc: 1 names: ["red_ant"]

train和val写的是图片目录路径,框架会去对应的同级目录labels里找txt。nc是类别总数,红蚂蚁单类就写1。names列表的顺序就是txt里类别id的顺序,id为0对应names[0],id为1对应names[1]。如果你后续加了第二类,names的顺序必须和txt里写入的id顺序严格一致,这是YOLO全家桶最容易踩的隐性地雷。路径建议写绝对路径,尤其是用PyCharm跑的时候,相对路径在不同工作目录下解析结果不一样,我在这里翻过不止一次车。

6.2 用mAP和混淆矩阵验收模型

训练完先看验证集上的mAP,这里有两个指标分开读:mAP_0.5是IoU阈值0.5下的平均精度,主要反应“框得准不准”;mAP_0.5:0.95是多个阈值下的平均,更能反映框的精细程度。红蚂蚁是小目标,如果mAP_0.5高但mAP_0.5:0.95偏低,说明框的位置对、但大小贴合度不够,这时候把训练轮次拉长、或者把输入分辨率从640提到960,通常能追回来。

模型部署到实际场景前,再用测试集跑一次预测,把置信度阈值从默认的0.25往上调到0.4试试。置信度越低,误检越多;蚂蚁相对小且密,阈值太低会把石子、草叶都框进来。调完再看一眼混淆矩阵,如果“背景被预测成red_ant”的比例超过百分之五,说明训练数据里正负样本比例失衡,回去加一些没有蚂蚁的负样本图,而不是继续调参。

讲到这想起自己一次交付模型的教训:当时赶进度,划分完没做检查就训练,测试集里混进了几张和训练集几乎一样的连拍图,甲方验证时发现模型在陌生场景表现远差于测试指标。从那以后我每次划分完数据都强制先跑一遍画框校验脚本,确认标签配对、类别id一致、不存在难例残留,才允许自己碰训练命令。这套习惯一直留到现在。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进