简介:胡萝卜检测数据集面向目标检测开发者和YOLO算法学习者,从COCO2017数据集中提取全部胡萝卜相关样本,并完成标签格式统一转换,类别名称为carrot,可直接用于YOLO系列模型训练。整个压缩包共2000个文件,内含1683张jpg原始图像、1683个xml标签及1684个txt标签,分别对应VOC格式与YOLO格式,标签与图像一一对应,用户无需二次标注即可投入训练;资源包大小约270.13MB,文件结构清晰,便于按需取用。目前已有215人学习下载,适合农业视觉检测、蔬菜识别等场景的模型验证与算法练手。借助这份整理好的数据,既能快速完成数据加载、类别配置和训练流程测试,也省去从COCO全量数据中筛选样本、手工转换标签的繁琐步骤,有助于将更多精力放在网络调参与检测效果优化上。
1. 胡萝卜检测数据集为什么要用VOC格式:先把细长目标标注的底子铺对
做农产品分拣的朋友常跟我抱怨:胡萝卜这种细长条目标,标起来比圆滚滚的苹果麻烦得多,稍微标注随意一点,模型训练出来就是漏检一大片。胡萝卜检测数据集解决的就是这个具体问题——把图像里每一根胡萝卜的位置和类别用VOC格式的XML标签记录下来,供YOLO、SSD这类检测模型做监督训练。VOC格式是目标检测领域流传最广、工具链最完整的标注格式之一,适合正在做农产品分拣、田间长势统计、采收机器人视觉方案的人。新人能照着步骤把数据集从零建出来,熟手也能在格式转换和质检环节少踩几个已知的坑。
2. VOC格式的目录结构与XML标签:把标注格式的底裤扒干净
VOC格式全称PASCAL VOC,最早来自PASCAL VOC挑战赛确立的标注规范,后来被整个目标检测生态沿用。它不绑定任何具体算法,只规定“图片放哪、标签放哪、标签里写什么”,所以成了数据集交换的通用语言。很多公开数据集、预训练权重和评测脚本都以VOC格式为基准,这也是选它而不是自造格式的原因。
2.1 JPEGImages、Annotations、ImageSets:三个目录各管哪一块
VOC格式的标准目录布局长这样:
VOCdevkit/ ├── VOC2007/ # 数据集版本目录,自定义名称即可 │ ├── JPEGImages/ # 存放原始图片,统一为.jpg │ ├── Annotations/ # 存放每张图片对应的XML标签文件 │ └── ImageSets/ │ └── Main/ # 存放训练/验证集的划分文件 │ ├── train.txt │ ├── val.txt │ └── trainval.txtJPEGImages要求图片文件名和XML标签文件名一一对应,比如carrot_001.jpg对应carrot_001.xml。图片格式统一成jpg能省很多麻烦,png带透明通道在训练时反而容易导致数据加载报错。Annotions目录下每个XML对应一张图片,里面记录这张图里所有胡萝卜的位置。ImageSets/Main下是txt文件,每行一个不带扩展名的图片名,用来告诉训练脚本哪些图进训练集、哪些进验证集。
我一般会把原始图和标注都留一份在VOCdevkit里不动,转换出的其他格式放到单独目录。这样一旦转换脚本出问题或训练需求变化,随时能从原始标注重新生成,算是给自己留的后悔药。
2.2 bndbox、truncated、difficult:XML字段在胡萝卜场景里怎么用
一份典型的VOC标注XML长这样:
<annotation> <folder>JPEGImages</folder> <filename>carrot_001.jpg</filename> <path>/data/carrot_dataset/JPEGImages/carrot_001.jpg</path> <source> <database>Carrot Detection Dataset</database> </source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <segmented>0</segmented> <object> <name>carrot</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>356</xmin> <ymin>512</ymin> <xmax>482</xmax> <ymax>889</ymax> </bndbox> </object> </annotation>size里记录图片宽度和高度,单位是像素,转换坐标时必须靠它做归一化。object里关键字段是name和bndbox,前者是类别名,后者是左上角和右下角坐标。truncated表示目标有没有被图片边界截断,胡萝卜从画面边缘伸出来时这个字段要置1。difficult表示目标是否难以辨认,比如严重遮挡或模糊的胡萝卜,置1后多数训练脚本会直接忽略它,不参与损失计算。
胡萝卜这种细长目标,bndbox四个坐标的精度直接影响检测效果。x方向偏差几个像素问题不大,但y方向如果框短了一截,模型学到的是“只检测胡萝卜中间一段”,后期召回率上不去。标注规范里应该明确:框必须包住胡萝卜完整可见部分,包括叶子,除非叶子被截断到画面外。
2.3 标注工具选型:为什么最后停在labelImg
做过标注的人都知道,标注工具的选择直接影响生产效率。VOC格式的标注工具里,labelImg是社区最常用的,因为它就是为VOC格式设计的。快捷键、自动保存、类别列表管理都够用,没有多余功能。操作流程是:打开图片目录,框选目标,选类别,按Ctrl+S保存XML,切换到下一张。
我一般会先建好一个classes.txt,里面按行写好类别名,比如carrot和spoiled_carrot,labelImg会自动读取并生成下拉列表。这样能避免每个标注员手打类别名导致的大小写不一致。需要提醒的是,labelImg默认保存路径可能和原图不一致,启动前要把默认保存目录设到Annotations,不然XML会散落到各处,后期整理到你想哭。
还有一种情况是标注员用labelme标多边形,然后转VOC矩形框。对胡萝卜这种近似的柱状目标,多边形转矩形会带来多余的背景区域,反而不如直接用矩形框标。除非后续要做分割任务,否则没必要绕这一步。
3. 从零构建胡萝卜检测数据集:采集、清洗、标注的完整流程
光懂VOC格式还远远不够,数据集的质量决定了训练结果的上限。这一章把从拿到原始图像到产出合格VOC标注的完整流程走一遍。
3.1 采集与清洗:分拣线和田间两条路径,先定分辨率再定目标尺寸
胡萝卜图像的采集场景可以粗略分两条路径。第一条是工业分拣线,相机固定安装,在传送带上方俯拍或侧拍,光照相对稳定,胡萝卜姿态比较统一。这种场景建议用硬触发或按固定频率抽帧,每隔几帧保存一张,避免相邻图片过于相似导致训练集冗余。第二条是田间地头,胡萝卜可能埋在土里半露不露,或者堆在货筐里互相遮挡,光照从强日光到阴天变化很大。
清洗阶段的标准要提前定死,否则标注员会在模糊图上浪费大量时间。我常用的标准是:运动模糊和对焦模糊直接删掉,网上下载的带水印或严重压缩的图删掉,胡萝卜占比小于图片面积5%的删掉。分辨率建议不低于960×540,因为胡萝卜是细长目标,像素不够时标注框本身就不准确,模型能学到的东西也有限。清洗完成后的图片统一重命名,用carrot_0001.jpg这种等宽编号,方便后续脚本处理和文件排序。
3.2 类别体系设计:一根胡萝卜不是只有carrot一个标签
很多新手把类别简单设成一个carrot,这在前早期能跑通,一旦遇到实际业务就会出问题。分拣线上坏果和好果必须分开,否则模型学会了“检测胡萝卜”,但不知道哪些该剔除。我建议最少分两个类别:carrot是好果,spoiled_carrot是开裂、发黑、腐烂的。
类别设计表可以参考这样:
| 类别名 | 含义 | 标注规则 |
|---|---|---|
| carrot | 完好胡萝卜 | 整根可见或截断但主体完整 |
| spoiled_carrot | 缺陷胡萝卜 | 明显开裂、黑斑、腐烂面积超过20% |
| 不标 | 背景中的残叶、泥土块 | 不建框,保持背景纯净 |
类别条数不要一上来就搞十几个,细长目标本身标注难度就高,每一类样本量不够时模型会严重偏向样本多的类别。先建2到3个类别把流程跑通,后续再扩充是常见做法。标注规范里还要写清楚:同一根胡萝卜被货筐边缘挡住、可见部分不足三分之二时,按truncated=1处理;完全看不出形态的就用difficult=1跳过,别硬标。
3.3 标注质检:用一段脚本把标注和图片逐一对齐
标注完成后的质检经常被跳过,但这恰恰是数据集踩坑的重灾区。人工抽检只能发现明显错误,像坐标越界、类别名不一致、XML语法错误这类问题,必须用脚本全量过一遍。下面这段脚本就是干这个的,我每次标注完都会跑一遍:
import os import xml.etree.ElementTree as ET from PIL import Image JPEG_DIR = "JPEGImages" ANNO_DIR = "Annotations" ALLOWED_CLASSES = {"carrot", "spoiled_carrot"} def check_annotation(xml_path): tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") size = root.find("size") img_width = int(size.findtext("width")) img_height = int(size.findtext("height")) # 这张图片是否真实存在 img_path = os.path.join(JPEG_DIR, filename) if not os.path.exists(img_path): return f"图片缺失: {filename}" # 图片实际尺寸与XML记录的尺寸是否一致 with Image.open(img_path) as img: actual_w, actual_h = img.size if (actual_w, actual_h) != (img_width, img_height): return f"尺寸不一致: {filename} XML={img_width}x{img_height} 实际={actual_w}x{actual_h}" for obj in root.findall("object"): name = obj.findtext("name") if name not in ALLOWED_CLASSES: return f"未知类别: {filename} -> {name}" bndbox = obj.find("bndbox") xmin = int(bndbox.findtext("xmin")) ymin = int(bndbox.findtext("ymin")) xmax = int(bndbox.findtext("xmax")) ymax = int(bndbox.findtext("ymax")) # 坐标是否越界、是否有效 if xmin < 0 or ymin < 0 or xmax > img_width or ymax > img_height: return f"坐标越界: {filename} ({xmin},{ymin},{xmax},{ymax})" if xmax <= xmin or ymax <= ymin: return f"坐标无效: {filename} ({xmin},{ymin},{xmax},{ymax})" return None errors = [] for xml_name in sorted(os.listdir(ANNO_DIR)): if not xml_name.endswith(".xml"): continue err = check_annotation(os.path.join(ANNO_DIR, xml_name)) if err: errors.append(err) if errors: print("质检失败,共 %d 个问题:" % len(errors)) for e in errors: print(e) else: print("质检通过,共 %d 个标注文件" % len(os.listdir(ANNO_DIR)))这段脚本做的事很简单:检查图片是否存在、XML记录的尺寸是否和真实图片一致、类别名是否在允许列表内、坐标是否越界或非法。图片尺寸不一致这个问题特别隐蔽,我遇到过一次是后期对图片做了压缩但没有同步更新XML里的width和height,转换出来的YOLO格式坐标全部错位,训练了十几个小时才发现。脚本跑完没问题,再抽5%的图片人工看一眼标注框贴合度,就可以进入转换环节了。
4. VOC转YOLO训练格式:转换脚本与三个必调参数
现在到了训练前的最后一步:把VOC格式的XML标签转换成YOLO训练需要的txt格式。YOLO系模型(YOLOv5、YOLOv8)默认用归一化的中心点坐标加宽高表示目标,每个txt文件对应一张图片,每行格式是“类别索引 x_center y_center width height”,全部除以图片宽高缩放到0到1之间。
4.1 从XML到txt的转换脚本:归一化坐标里的w、h顺序坑
转换脚本只需要用到XML里的size和每个object的bndbox。直接上代码:
import os import xml.etree.ElementTree as ET CLASSES = ["carrot", "spoiled_carrot"] # 顺序一旦确定,后面训练配置必须保持一致 def convert_xml_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.findtext("width")) img_h = int(size.findtext("height")) filename = root.findtext("filename") base_name = os.path.splitext(filename)[0] out_path = os.path.join(output_dir, base_name + ".txt") lines = [] for obj in root.findall("object"): # 跳过difficult=1的难例,它们会让训练损失变得很不稳定 if obj.findtext("difficult") == "1": continue name = obj.findtext("name") if name not in CLASSES: print(f"警告: 跳过未知类别 {name} in {filename}") continue cls_id = CLASSES.index(name) bndbox = obj.find("bndbox") xmin = float(bndbox.findtext("xmin")) ymin = float(bndbox.findtext("ymin")) xmax = float(bndbox.findtext("xmax")) ymax = float(bndbox.findtext("ymax")) # 坐标边界裁剪,防止标注时手抖越界几个像素 xmin = max(0, min(xmin, img_w - 1)) xmax = max(0, min(xmax, img_w - 1)) ymin = max(0, min(ymin, img_h - 1)) ymax = max(0, min(ymax, img_h - 1)) # 中心点坐标和宽高都除以图片宽高做归一化 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 图片没有任何目标时,写上空文件即可,训练时该图会被自动跳过 with open(out_path, "w") as f: f.write("\n".join(lines)) os.makedirs("labels/train", exist_ok=True) for xml_name in sorted(os.listdir("Annotations")): if xml_name.endswith(".xml"): convert_xml_to_yolo(os.path.join("Annotations", xml_name), "labels/train")三个必调参数要注意。第一个是CLASSES列表的顺序,转换脚本里第几个位置,训练配置里就必须是第几个类别,顺序错了模型训练不报错但检测结果完全对不上。第二个是归一化的分母,必须用size里的原始宽高,不能用图片读出来的实际尺寸替代——如果图片本身被处理过而XML没更新,这里读到的就是旧尺寸。第三个是difficult过滤,如果你希望模型学习这些难例,就放开过滤,但我建议前期训练先跳过,让模型在干净样本上稳定收敛后再加入难例微调。
4.2 训练集划分:直接读ImageSets/Main还是重新shuffle
VOC数据集的ImageSets/Main里已经有train.txt和val.txt了,转换时可以直接按这份划分来组织YOLO格式的图片列表。我的做法是:先按VOC的划分生成train.txt和val.txt,每行写训练图片的绝对路径:
# 读ImageSets/Main里的划分文件,生成YOLO需要的图片列表 for split in train val; do while read name; do echo "$(pwd)/images/$name.jpg" >> ${split}.txt done < ImageSets/Main/${split}.txt done这里有个容易踩的点:VOC划分文件里的一行只有一个文件名,不带.jpg后缀,拼接路径时不要重复加。如果不想用现有划分,也可以手动shuffle,但要保证训练集和验证集的类别分布大致一致,不然验证集里都是同一类缺陷,mAP指标会虚高。我习惯按7:2:1分train/val/test,test部分严格不参与训练和验证,留到最后做一次真实效果评估。
4.3 给YOLOv8的data.yaml:类别索引顺序必须和转换脚本锁死
YOLOv8训练自己的数据集时,核心配置文件是data.yaml,里面指定了训练/验证图片路径和类别名。写法如下:
# data.yaml train: ./train.txt # 训练图片列表 val: ./val.txt # 验证图片列表 nc: 2 # 类别数量,和CLASSES列表长度一致 names: ["carrot", "spoiled_carrot"] # 顺序必须和转换脚本里的CLASSES完全一致这里的names顺序是训练参数里最容易被忽视的一个。很多翻车现场都是训练脚本里写的是["carrot", "spoiled_carrot"],而转换脚本跑出来的txt里却是反的,检测结果显示胡萝卜被判成坏果,坏果判成好果。每次训练前花十秒钟检查一下第一个txt文件第一行的类别索引,就能省一晚的无效训练时间。
5. 胡萝卜检测数据集标注避坑:5个最常翻车的地方
数据标注环节的坑,绝大多数不会在训练时报错,而是让模型在错误的方向上悄悄收敛。这一章把我一路走过来最常遇到的5个问题列出来,每条都是现象、原因、解决的完整链路。
5.1 坐标越界:训练loss正常但mAP很怪
现象:转换后的txt文件里出现大于1的坐标值,训练时loss能降下去,但验证mAP一直上不去,检测框偏移严重。
原因:标注员在框选贴近图片边缘的胡萝卜时,鼠标拖出了图片边界,labelImg保存时没有做裁剪,XML里的xmax或ymax超出图片实际宽高。归一化后这个值大于1,模型学到的边界框坐标是错的。
解决:转换脚本里做坐标边界裁剪,就是上一章代码里的max/min那几行,这属于防御性编程,成本极低但效果明显。另外在质检脚本里也加一条坐标越界检查,标注阶段就拦截掉,比转换时裁剪更好,因为裁剪只是生成了正确输送,原始XML的错误仍然留在那。
5.2 filename和实际文件名对不上:训练时大量图片被跳过
现象:训练日志里出现大量File not found警告,数据量明明很大但每个epoch实际上只用了一半图片。
原因:图片批量重命名后没有同步更新XML里的filename字段,或者标注工具保存时把路径带上了特殊字符。XML里记录的filename是carrot (1).jpg,实际文件叫carrot_1.jpg,匹配不上。
解决:质检脚本里检查XML的filename对应图片是否存在,不匹配时直接报错。更彻底的办法是统一用脚本重命名图片和XML,再重新生成XML里的filename和path字段,不要手动改。
5.3 类别名大小写和拼写不一致:一个类别被拆成好几个
现象:训练出的模型对同一类物体有两个检测框,置信度都很低,验证集里该类别的AP值偏低。
原因:标注工具里的类别列表没固定,有的标员输入Carrot,有的输入carrot,有的手滑拼成carrot_,XML里的name字段五花八门。转YOLO格式时脚本直接把它们当成不同类别处理。
解决:质检脚本里加ALLOWED_CLASSES集合校验,不在列表里的类别直接报错。labelImg启动前把classes.txt准备好,别让标注员手打类别名。发现错误类别名后,用脚本批量替换XML里的text节点,不要在标注工具里逐张改。
5.4 胡萝卜横截面的小目标被系统性漏标
现象:验证集里检测到大目标胡萝卜效果不错,但切段的横截面圆形目标几乎全部漏检,召回率惨淡。
原因:标注员注意力被大目标吸引,小目标漏标严重。横截面胡萝卜在图片中占比很小,只有几十个像素,标注时容易被忽略。被漏标的目标在训练中成了背景负样本,模型学到的是“这些区域没有目标”,所以越训练越检测不出来。
解决:标注规范里明确所有胡萝卜目标都要标,包括切段的横截面区域。如果小目标确实占比很大,可以在清洗阶段直接过滤掉横截面图片,让模型专注在整根胡萝卜检测上,否则既要检测细长条又要检测接近圆形的小块,对模型负担不小。实在要保留,训练时对含小目标的图片做过采样,多出现几次。
5.5 数据分布偏移:训练集都是单根胡萝卜,验证集全是堆叠场景
现象:训练集loss和mAP都很漂亮,一到实际场景或测试集就崩溃,误检漏检严重。
原因:采集时相机固定,传送带上一次只过一根胡萝卜,收集的数据全是单目标、干净背景的图片。但实际场景里胡萝卜堆在一起,互相遮挡,背景信息完全不同,模型没见过这种分布。
解决:构建数据集时就要有意识覆盖多样性:不同光照、不同角度、不同遮挡程度、单根和堆叠都要有。用difficult=1标注遮挡严重的堆叠场景目标,把它们放进验证集,让评测指标反映真实场景的难度。这个坑只能靠数据层面解决,增强和调参都补不回来。
6. 数据增强与验证技巧:让模型真正读懂胡萝卜
训练前的最后一道工序,是数据增强策略和验证流程。胡萝卜是细长目标,通用的增强参数未必合适,这里分享几个我常用的配置。
6.1 先验证再训练:转换后抽三张图画框
开始训练之前,建议先抽三张转换后的txt对应图片,用脚本把归一化坐标还原成像素坐标并画框。这个习惯能发现80%的格式转换问题,包括类别顺序错乱、坐标飘移、归一化错误。画框脚本很简单,用PIL的draw接口即可,把归一化坐标乘以图片宽高画矩形,肉眼看一眼框是否贴合目标。不要跳过这一步,格式错了训练多久都是白费。
6.2 增强策略:针对细长目标的旋转和色域调整
训练YOLOv8时,我通常关掉mosaic增强改用中等强度的旋转和色域变换。胡萝卜这个品类直线感强,旋转超过30度会让长条目标畸形,模型反而学到错误的形态。常用配置是degrees=15,hsv_h=0.02,hsv_s=0.5,hsv_v=0.4,基本只动颜色不动几何。亮度调整对胡萝卜尤其重要,因为泥土背景色和胡萝卜橙色接近,色域增强能帮模型把前景和背景解耦。
验证时看两类指标:整体mAP@0.5和分尺寸的AP。细长目标往往在小目标维度上AP偏低,如果加了增强后小目标AP仍低于0.5,优先回去查漏标和清洗,而不是盲目堆训练轮数。我自己每次微调训练前,都会从转换后的训练集里随机抽三张图,画框看一遍,确认坐标没漂移再开训,这个习惯帮我避开过好几次格式错位的血泪事故。做检测数据集没有捷径,把VOC格式吃透、把质检脚本跑顺、把边界条件焊死,这个方向值得投入,也愿你少走我之前绕过的弯路,希望帮到你。
本文还有配套的精品资源,点击获取