☰
小番茄数据集YOLO训练全流程:XML标注转TXT及避坑指南
2026/10/1 12:26:05 网站建设 项目流程

简介:面向农业视觉与目标检测学习者的YOLO小番茄检测数据集,包含895张不同角度、光照条件下采集的田间果实图片,以及895个配套XML标注文件,适用于训练和验证YOLO系列模型,帮助解决成熟度判断、自动采摘等场景中的果实定位问题。压缩包总大小约180.33MB,共1790个文件,目录以图片与标注一一对应方式组织,XML中提供边框坐标与类别信息,便于直接开展模型训练、迁移学习或数据增强实验。该资源已有85人学习,适合具备一定深度学习基础、希望获取真实作物检测数据进行实战练习的研究者或农业智能化开发者使用。借助这份数据,可以完整走通从数据准备、标注解析到YOLO训练与评估的流程,也可结合R-CNN、SSD等算法对比验证,探索提升小目标检测精度与速度的可行方案。

1. 小番茄目标检测数据集:一包XML标签能省掉你两周标注时间

做农业视觉的人都有同感:采摘机器人或者大棚巡检小车落地时,最难的不是模型选型,而是手里没有一张带标注的图。YOLO目标检测在农业场景里的优势从来不是理论上的,是它能在嵌入式设备上接近实时地跑起来;但想让YOLO动起来,第一步必须有干净、格式统一、类别明确的标注数据。这套小番茄目标检测数据集,就是打包好的图片加XML格式标签,图片命名到tomato784.png,单体小果居多,适合直接验证YOLO在小目标、密集场景下的表现。特别适合两类人:刚入门目标检测、不想把时间耗在手动标注上的新手,以及做采摘机器人、成熟度判断项目,需要快速跑通基线的老手。

2. 拆开数据集的包:从图片命名、XML结构到标注质量初筛

2.1 先从文件名和图片尺寸确认训练原料是否干净

拿到压缩包解压后,第一件事不是看XML,而是先扫一遍图片本身。这个数据集的命名规律是tomato + 序号 + .png,序号不连续是正常的,因为原始采集时可能有删图,但如果程序里写了按文件名顺序循环读图,就可能在中间碰到断号报错。我一般会先跑一个图片完整性检查,顺便把尺寸分布、通道数统计出来。

import os from PIL import Image img_dir = "tomato_dataset/images" sizes = set() channels = set() broken = [] for fname in os.listdir(img_dir): if not fname.lower().endswith((".png", ".jpg", ".jpeg")): continue path = os.path.join(img_dir, fname) try: img = Image.open(path) img.verify() # 只校验文件结构,不加载完整像素 img = Image.open(path) # verify之后需要重新打开才能读属性 sizes.add(img.size) channels.add(len(img.getbands())) except Exception: broken.append(fname) print("出现过的尺寸:", sizes) print("通道数:", channels) print("损坏图片:", broken)

这段脚本里,img.verify()是快速校验文件是否损坏的关键调用,它不会把整张图读进内存,对几百张图的包跑下来很快。注意PIL的verify机制是一次性的,校验完必须重新Image.open才能读宽高和通道,否则会报"image file is truncated"之类的错。如果channels里出现4,说明存在带透明通道的PNG,YOLO训练时一般要统一转成RGB三通道,否则某些框架在加载时会对通道数不一致报错。sizes里如果出现两种以上分辨率,后续转标注时要特别留意,因为XML里的坐标是像素值,依赖原始分辨率,统一缩放图片时必须同步缩放标注框。

2.2 读懂XML标签:bounding box、类别名与difficult标记

这个数据集用的是Pascal VOC风格的XML标签,每个XML文件名与图片名一一对应。核心信息全在<object>节点里:<name>是类别名,<bndbox>里是xmin、ymin、xmax、ymax四个像素坐标。别小看这个结构,训练脚本对类别名大小写敏感,tomato和Tomato会被当成两个类,后面避坑章节会专门讲。我建议训练前先把所有XML里的类别名拉出来做个统计,确认只有一个类、拼写一致。

import xml.etree.ElementTree as ET import os xml_dir = "tomato_dataset/annotations" class_counter = {} dim_error = [] for fname in os.listdir(xml_dir): if not fname.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, fname)) root = tree.getroot() # 记录图片尺寸信息 size = root.find("size") if size is not None: w = int(size.find("width").text) h = int(size.find("height").text) for obj in root.iter("object"): name = obj.find("name").text.strip() cls = obj.find("name").text class_counter[cls] = class_counter.get(cls, 0) + 1 bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) if xmax <= xmin or ymax <= ymin: dim_error.append(fname) print("类别统计:", class_counter) print("坐标异常的标注:", dim_error)

这里有一个容易被忽略的字段叫<difficult>,值通常是0或1。VOC原版规定difficult=1表示这个目标很难辨认,训练时一般直接忽略。但很多第三方标注工具生成的XML里根本没有这个字段,或者写死了0;当你不确定来源时,转换脚本里要显式处理difficult不存在的情况,否则用obj.find("difficult").text会抛NoneType异常直接中断转换。另外,有些工具会把xmin这类坐标写成整数,有些写成带一位小数的浮点,统一转成float再处理最稳妥。如果dim_error列表非空,说明存在标注框坐标倒挂的脏数据,这种样本要么删掉要么手动修,绝不能进训练集。

2.3 标注质量初筛:用可视化脚本找出错标和漏标

看统计数字永远不如直接看图直观。我会把XML标注直接画回原图,随机抽20张左右人工过一遍,重点看三类问题:框是不是贴住果实边缘、有没有把两三个重叠的果标成一个大框、有没有漏掉后景里的小果。这一步是后面所有训练的信任基础,建议不要省。

import cv2 import random import xml.etree.ElementTree as ET img_dir = "tomato_dataset/images" xml_dir = "tomato_dataset/annotations" sample = random.sample([f for f in os.listdir(xml_dir) if f.endswith(".xml")], 20) for idx, xml_name in enumerate(sample): img_path = os.path.join(img_dir, xml_name.replace(".xml", ".png")) img = cv2.imread(img_path) if img is None: # 有的图可能是jpg,这里做一次兜底 img_path = os.path.join(img_dir, xml_name.replace(".xml", ".jpg")) img = cv2.imread(img_path) tree = ET.parse(os.path.join(xml_dir, xml_name)) for obj in tree.getroot().iter("object"): bndbox = obj.find("bndbox") xmin = int(float(bndbox.find("xmin").text)) ymin = int(float(bndbox.find("ymin").text)) xmax = int(float(bndbox.find("xmax").text)) ymax = int(float(bndbox.find("ymax").text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, obj.find("name").text, (xmin, max(0, ymin - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) out = f"check_{idx:02d}.jpg" cv2.imwrite(out, img) print(f"saved {out}, boxes drawn over: {len(tree.getroot().findall('object'))}")

这段脚本的实用点在于容错:先按.png尝试读图,读不到再尝试.jpg,能避免图片和XML扩展名不一致导致的偶发中断。框和类别名直接画在图上,输出成check_xx.jpg,一个人快速翻一遍比看任何统计指标都有效。对密集小果场景,尤其要注意红色框是不是把相邻两果框在一起,如果大面积出现这种问题,说明原始标注采用的是轮廓或外接圆转矩形的方式,转换时丢了很多边缘信息;这种情况我会劝你警惕,因为框的质量决定了YOLO回归分支的上限,框偏大一圈的标注训出来的模型,预测框普遍也会偏大。

3. XML转YOLO TXT:可直接复用的转换脚本与数据集划分

3.1 为什么必须转格式:VOC像素坐标与YOLO归一化坐标的本质差异

YOLO系列训练时读的标签不是XML,而是每个图片对应的一个.txt文件,格式是class_id x_center y_center width height,而且四个坐标值必须归一化到0到1之间。很多新手第一次跑训练报"label format not correct",就是因为直接把XML扔给了YOLO训练脚本。这里的本质差异在于:XML存的是像素绝对值,YOLO存的是相对值;XML允许xmax、ymax超出图片边界,YOLO标签里只要有一个值落到0~1范围之外,训练时就会被当成坏样本丢弃。所以转换时不只是格式变化,还要做边界裁剪。

顺便提一下,如果你用的标注工具导出的坐标系是COCO的x_center, y_center, width, height格式,但单位是像素,同样不能直接用,X和宽都要除以图宽,Y和高除以图高。这个数据集是XML格式,对应VOC坐标,所以核心是先把xmin/ymin/xmax/ymax换算成中心点和宽高,再分别做归一化。

3.2 转换脚本:从XML到TXT的完整实现

下面这套转换脚本是我平常用的版本,做了三件XML转YOLO必须做的事:过滤difficult样本、坐标归一化、边界值裁剪。同时会生成一份classes.txt供YOLO配置文件引用。

import os import xml.etree.ElementTree as ET from PIL import Image img_dir = "tomato_dataset/images" xml_dir = "tomato_dataset/annotations" label_dir = "tomato_dataset/labels" os.makedirs(label_dir, exist_ok=True) classes = ["tomato"] # 与XML里的name字段严格一致 def xml_to_yolo(xml_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): # 跳过difficult=1的样本 diff = obj.find("difficult") if diff is not None and int(diff.text) == 1: continue cls_name = obj.find("name").text.strip() if cls_name not in classes: print(f"unknown class {cls_name} in {xml_path}") continue cls_id = classes.index(cls_name) bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 计算中心点与宽高,并归一化 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 # 边界裁剪,防止出现负数或大于1的值 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return lines for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue xml_path = os.path.join(xml_dir, xml_name) img_name = xml_name.replace(".xml", ".png") img_path = os.path.join(img_dir, img_name) # 图片扩展名兜底 if not os.path.exists(img_path): img_name = xml_name.replace(".xml", ".jpg") img_path = os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f"skip {xml_name}: no image found") continue with Image.open(img_path) as img: img_w, img_h = img.size lines = xml_to_yolo(xml_path, img_w, img_h) if lines: txt_path = os.path.join(label_dir, img_name.rsplit(".", 1)[0] + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines) + "\n") with open(os.path.join(label_dir, "classes.txt"), "w") as f: f.write("\n".join(classes) + "\n") print("conversion done.")

参数说明:img_w和img_h必须从图片实际读取,不要用XML里<size>节点的值,因为部分标注工具在图片被二次压缩后不会同步更新<size>,用错尺寸整张图的标签全部偏掉。归一化时先算(xmin+xmax)/2再除以宽,而不是先分别除宽度再相加除以2,两种写法浮点误差不同,前者的结果是几何中心,后者在极端情况下会多一层舍入误差。6位小数的精度对640分辨率的图足够,宽高误差能控制在0.001像素以内;位数太少会导致小果的框偏移肉眼可见。裁剪那三行是兜底逻辑,正常情况下用不上,但对边界框本来就出界的脏数据,不裁剪YOLO训练时会直接丢弃整个标注。

3.3 按比例划分训练集与验证集:避免验证集泄漏的划分方式

数据集划分看起来简单,但有个隐蔽的坑:如果同一串番茄在不同图片里出现,而且这些图片被随机分进了训练集和验证集,模型其实在验证集里见过目标了,指标会虚高。农业采集场景里,同一个枝串通常会连拍多张,所以划分前先按文件名序号排序再做随机划分没有意义,因为序号相邻的图往往来自同一段视频。更稳妥的做法是按拍摄时间段或者拍摄位置做分组划分;但如果原始数据没记录这些信息,至少不要用纯随机划分,我一般会做带种子的分层抽样,同时保证验证集里每张图和训练集的任何一张图没有高度相似的像素重叠。这里给一个能直接跑的划分脚本:

# 按8:1:1划分train/val/test mkdir -p dataset/images/train dataset/images/val dataset/images/test mkdir -p dataset/labels/train dataset/labels/val dataset/labels/test python - <<'EOF' import os import random import shutil random.seed(42) # 固定种子,保证结果可复现 image_dir = "tomato_dataset/images" label_dir = "tomato_dataset/labels" images = [f for f in os.listdir(image_dir) if f.endswith((".png", ".jpg"))] random.shuffle(images) n = len(images) train_split = int(n * 0.8) val_split = int(n * 0.9) for i, img_name in enumerate(images): label_name = img_name.rsplit(".", 1)[0] + ".txt" if not os.path.exists(os.path.join(label_dir, label_name)): print(f"missing label: {label_name}") continue if i < train_split: split = "train" elif i < val_split: split = "val" else: split = "test" shutil.copy(os.path.join(image_dir, img_name), f"dataset/images/{split}/{img_name}") shutil.copy(os.path.join(label_dir, label_name), f"dataset/labels/{split}/{label_name}") print(f"total {n}, train {train_split}, val {val_split - train_split}, test {n - val_split}") EOF

注意这里复制而不是移动文件,是为了保留一份完整的原始包,后续想重新划分不用解压重新来。固定随机种子是必要条件,否则每次跑结果都不同,训练时改过几次增强参数后指标变化就分不清是增强引起的还是划分差异引起的。划分完成后,最好手动抽查验证集里是否有和训练集高度相似的图,比如同一角度同一串果的连拍,发现就手动挪走。任何标注类项目,验证集泄漏都是最隐蔽的指标虚高来源,尤其是农业视频抽帧类数据集,几乎必中。

4. 避坑:小番茄数据集训练YOLO的五个翻车现场

4.1 漏检集中在后景小果:原因在输入分辨率而非模型

现象:训练完跑验证,召回率看着还可以,但把模型搬到实拍视频上一测,后景里那些只有几十个像素的小番茄全部漏检,前景大果倒是全检出来了。原因:这套数据集里目标尺度分布很宽,后景果实的像素面积可能只有前景果实的几十分之一,默认的imgsz=640训练把整张图缩到640,小果实际只剩下十几像素,特征基本丢失。解决:先把训练输入分辨率提到960或1280,同时把batch相应调小保住显存;如果小果还是漏,先检查小目标框的数量占比,再把Mosaic增强改到0.5以下,因为Mosaic拼接后小果在拼接图里会被进一步缩小。我一般会先统计一下所有框宽高像素的分布,再决定要不要切图训练。

4.2 类别名大小写不一致导致训练中断

现象:数据划分时类的classes.txt里写的tomato,YOLO训练跑了几个epoch直接报IndexError: list index out of range,训练中断。原因:标注XML里大部分<name>是小写tomato,但个别图里被标成Tomato或TOMATO,转换脚本里classes.index(cls_name)没找到就跳过了,理论上不会崩;崩的原因往往是你先自己改过classes.txt,或标注里出现了空字符串,转换时把空串写进了TXT的class位置。解决:转换前先把class_counter统计打印出来,发现拼写不统一就先统一,不要在转换脚本里临时加映射,确保所有标签的类别名和classes.txt严格一致。另外检查TXT首列是否出现非零数字,YOLO数据配置文件里设置的类别数是1,标签里出现1就是异常,说明有第二类混进来了。

4.3 坏图与通道异常让训练中途崩掉

现象:训练跑到某个epoch,日志突然报AssertionError: image not found或者PIL解码异常,几次之后你甚至怀疑是显存问题。原因:数据集包里有若干张损坏的PNG或带Alpha通道的图,YOLO在cache阶段会预加载全部图片,坏图在加载时就崩。解决:训练前先跑2.1节的图片完整性检查,把所有损坏文件直接隔离到一个broken/目录;带Alpha通道的PNG用img.convert("RGB")批量转存。同时我习惯在数据集配置文件里把cache=False先跑第一个epoch,确认数据管道稳定后再改成cache=ram加速,否则崩的时候根本分不清是内存问题还是坏图问题。

4.4 模型在光照稍变的果园就失效:数据增强参数不是越大越好

现象:训练集里测mAP有0.9,拿到另一个大棚、另一个时间段实拍,检测框乱飘,红色误检率明显上升。原因:采集图的背景以绿色叶片为主,但不同光照下叶片颜色会偏移,模型可能学到了用颜色先验找红色区域,而不是用形状特征。解决:在YOLO的数据增强参数里,hsv_h不建议开大,小番茄的颜色是重要特征,色调偏移过大会把果实改成橙色甚至黄色,训练出来的模型对颜色的敏感度反而下降;我一般设hsv_h=0.01、hsv_s=0.4、hsv_v=0.4,只微调色饱和度和明度。同时打开flipud=0.5增加上下翻转,因为果园相机视角通常固定在下方向,上下翻转能打破模型对叶片纹理方向的依赖。遇到红色误检时,优先调hsv_s而不是加负样本。

4.5 验证集指标好看但实地用不了:检查划分泄漏

现象:mAP50到了0.92,大家都说可以上设备了,结果搭建到实地小车上一跑,框的位置总是慢半拍且重复框。原因:验证集和训练集来自同一段视频的相邻帧,模型本质上对这段视频过拟合了,验证时前后帧高度相似,指标当然好看。解决:划分数据集前先按文件名前缀或时间戳分组,同一组内的图只能进同一份数据集;如果原始信息不够没法分组,就降低划分随机性,改成按文件名的序号区间切块划分,比如前80%序号进训练集,后20%进验证集,这至少能减少相邻帧被拆散的概率。验证集指标只能作为参考,最终判断必须靠一段从未参与训练的视频逐帧检测。

5. 训练完怎么验:mAP只是门槛,漏检率和视频实测才是真相

5.1 用val命令看指标,先确认mAP50是否过基线

YOLOv8训练完成后直接跑验证命令:

yolo detect val model=runs/detect/train/weights/best.pt data=data.yaml batch=16 imgsz=640

关注输出里的三列:Precision、Recall、mAP50。对这套小番茄数据,我个人的基线判断是:mAP50要上0.85才算标注质量和训练流程都正常,低于0.7说明大概率有脏标注或训练配置问题;mAP50-95在0.6以上说明框的位置回归得比较准,如果mAP50高但mAP50-95明显偏低,说明框普遍偏大或偏小,需要检查转换脚本的边界裁剪是否把一批框截坏了。

5.2 用混淆矩阵定位误检出在哪

YOLO训练输出目录里的confusion_matrix.png值得放大了看。正常情况是背景到tomato这一格几乎为空,tomato到tomato的对角线深色;如果tomato到背景那一格颜色很深,说明大量果实被漏检,优先检查小目标尺寸分布;如果背景到tomato格很深,说明模型在把叶片、藤蔓误判成番茄,回去调hsv_s和置信度阈值。看混淆矩阵比看一张张测试图快得多,能直接告诉你错误集中在哪一侧。

5.3 实拍视频验证:连续帧统计漏检率

静态验证再准,也得用视频走一遍。我会拿一段实拍视频让模型跑推理,同时统计每一帧的检测框数量、置信度分布和单帧耗时。

yolo detect predict model=runs/detect/train/weights/best.pt source=orchard_video.mp4 imgsz=1280 conf=0.25 save=True

跑完后不要只看保存的视频,要重点做两件事:一是记录每秒的检测框数是否稳定,如果某一秒框数骤降,基本就是镜头转到逆光方向或运动模糊严重的片段,说明模型对模糊和极端光照的鲁棒性不够;二是用日志里的推理耗时换算帧率,如果硬件上跑不到设备需要的帧率,就先把imgsz降到960,再考虑换更小的模型变体。

从那以后,我每换一个目标检测数据集,都强制自己先跑完第2章的坏图和标注检查、再跑第3章的格式转换与反向画框验证,最后才允许训练脚本挂机;验证环节也坚持用一段训练集之外的视频收尾,而不是只盯着一行mAP数字。这套流程多花两小时,能省掉后面好几天排查玄学问题的日子。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询