资讯动态

小番茄数据集YOLO训练全流程:XML标注转TXT及避坑指南

发布时间:2026/10/1 12:26:09 来源:尧图企业网站定制
简介面向农业视觉与目标检测学习者的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说明存在带透明通道的PNGYOLO训练时一般要统一转成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原版规定difficult1表示这个目标很难辨认训练时一般直接忽略。但很多第三方标注工具生成的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 fcheck_{idx:02d}.jpg cv2.imwrite(out, img) print(fsaved {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_okTrue) 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): # 跳过difficult1的样本 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(funknown 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(fskip {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用错尺寸整张图的标签全部偏掉。归一化时先算(xminxmax)/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(fmissing 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), fdataset/images/{split}/{img_name}) shutil.copy(os.path.join(label_dir, label_name), fdataset/labels/{split}/{label_name}) print(ftotal {n}, train {train_split}, val {val_split - train_split}, test {n - val_split}) EOF注意这里复制而不是移动文件是为了保留一份完整的原始包后续想重新划分不用解压重新来。固定随机种子是必要条件否则每次跑结果都不同训练时改过几次增强参数后指标变化就分不清是增强引起的还是划分差异引起的。划分完成后最好手动抽查验证集里是否有和训练集高度相似的图比如同一角度同一串果的连拍发现就手动挪走。任何标注类项目验证集泄漏都是最隐蔽的指标虚高来源尤其是农业视频抽帧类数据集几乎必中。4. 避坑小番茄数据集训练YOLO的五个翻车现场4.1 漏检集中在后景小果原因在输入分辨率而非模型现象训练完跑验证召回率看着还可以但把模型搬到实拍视频上一测后景里那些只有几十个像素的小番茄全部漏检前景大果倒是全检出来了。原因这套数据集里目标尺度分布很宽后景果实的像素面积可能只有前景果实的几十分之一默认的imgsz640训练把整张图缩到640小果实际只剩下十几像素特征基本丢失。解决先把训练输入分辨率提到960或1280同时把batch相应调小保住显存如果小果还是漏先检查小目标框的数量占比再把Mosaic增强改到0.5以下因为Mosaic拼接后小果在拼接图里会被进一步缩小。我一般会先统计一下所有框宽高像素的分布再决定要不要切图训练。4.2 类别名大小写不一致导致训练中断现象数据划分时类的classes.txt里写的tomatoYOLO训练跑了几个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)批量转存。同时我习惯在数据集配置文件里把cacheFalse先跑第一个epoch确认数据管道稳定后再改成cacheram加速否则崩的时候根本分不清是内存问题还是坏图问题。4.4 模型在光照稍变的果园就失效数据增强参数不是越大越好现象训练集里测mAP有0.9拿到另一个大棚、另一个时间段实拍检测框乱飘红色误检率明显上升。原因采集图的背景以绿色叶片为主但不同光照下叶片颜色会偏移模型可能学到了用颜色先验找红色区域而不是用形状特征。解决在YOLO的数据增强参数里hsv_h不建议开大小番茄的颜色是重要特征色调偏移过大会把果实改成橙色甚至黄色训练出来的模型对颜色的敏感度反而下降我一般设hsv_h0.01、hsv_s0.4、hsv_v0.4只微调色饱和度和明度。同时打开flipud0.5增加上下翻转因为果园相机视角通常固定在下方向上下翻转能打破模型对叶片纹理方向的依赖。遇到红色误检时优先调hsv_s而不是加负样本。4.5 验证集指标好看但实地用不了检查划分泄漏现象mAP50到了0.92大家都说可以上设备了结果搭建到实地小车上一跑框的位置总是慢半拍且重复框。原因验证集和训练集来自同一段视频的相邻帧模型本质上对这段视频过拟合了验证时前后帧高度相似指标当然好看。解决划分数据集前先按文件名前缀或时间戳分组同一组内的图只能进同一份数据集如果原始信息不够没法分组就降低划分随机性改成按文件名的序号区间切块划分比如前80%序号进训练集后20%进验证集这至少能减少相邻帧被拆散的概率。验证集指标只能作为参考最终判断必须靠一段从未参与训练的视频逐帧检测。5. 训练完怎么验mAP只是门槛漏检率和视频实测才是真相5.1 用val命令看指标先确认mAP50是否过基线YOLOv8训练完成后直接跑验证命令yolo detect val modelruns/detect/train/weights/best.pt datadata.yaml batch16 imgsz640关注输出里的三列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 modelruns/detect/train/weights/best.pt sourceorchard_video.mp4 imgsz1280 conf0.25 saveTrue跑完后不要只看保存的视频要重点做两件事一是记录每秒的检测框数是否稳定如果某一秒框数骤降基本就是镜头转到逆光方向或运动模糊严重的片段说明模型对模糊和极端光照的鲁棒性不够二是用日志里的推理耗时换算帧率如果硬件上跑不到设备需要的帧率就先把imgsz降到960再考虑换更小的模型变体。从那以后我每换一个目标检测数据集都强制自己先跑完第2章的坏图和标注检查、再跑第3章的格式转换与反向画框验证最后才允许训练脚本挂机验证环节也坚持用一段训练集之外的视频收尾而不是只盯着一行mAP数字。这套流程多花两小时能省掉后面好几天排查玄学问题的日子。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑