简介面向YOLO目标检测的小番茄检测数据集聚焦农业生产中的果实识别与成熟度判断为计算机视觉方向的开发者和农业自动化研究者提供可直接使用的数据基础。压缩包共1790个文件包含895张png原始图像与895个xml标签文件整体约180.33MBxml中标注了边界框坐标和类别信息可无缝对接YOLO及其衍生模型的训练与验证流程。图片覆盖不同拍摄角度和光照环境有利于提高模型在实际农田场景中的泛化能力同时数据集可切分为训练集与测试集方便对照不同网络结构、损失函数以及学习率等参数调优实验。目前已有79人学习使用这份数据既能辅助新手快速掌握目标检测的数据组织方式和标注规范也可用于采摘机器人视觉系统、成熟度自动分级、产量预估等应用的前期验证同时可作为迁移学习、数据增强等改进策略的基准测试集能显著降低从采集到标注的重复投入。1. 小番茄目标检测数据集YOLO训练前先想清楚xml标签怎么进pipeline一个小番茄在1280×720的画面里往往只占几十个像素叶子挡一半、果实挤成一串这种场景恰恰是YOLO最容易丢检的对象。标题里这份「图片xml格式标签」的数据集叠加上YOLO训练真正的门槛不在跑通训练代码而在把Pascal VOC风格的xml标注正确转成YOLO能读的txt格式再处理小目标带来的正样本稀疏问题。很多人在第一步就翻车xml里class_name和图片文件名对不上、坐标归一化算错、数据集没按目录结构划分导致训练出来的模型mAP看着还行一测实拍视频全是漏检。这篇文章就从数据集本身讲起把标签解析、格式转换、训练配置到小目标调参这条链路逐个拆开适合刚拿到标注数据准备训YOLOv5/v8的工程师也适合想把手头VOC数据迁到YOLO流程的人。2. 解析Pascal VOC的xml标签从文件结构到YOLO txt的转换链路2.1 xml里到底存了什么object、bndbox与filename的对应关系Pascal VOC格式的标注文件是一个xml核心信息围绕annotation根节点展开。典型的小番茄标注xml长这样annotation folderJPEGImages/folder filenametomato_001.jpg/filename size width1280/width height720/height depth3/depth /size object nametomato/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin356/xmin ymin188/ymin xmax402/xmax ymax224/ymax /bndbox /object /annotation这段结构里filename是图片名size里的宽高直接决定坐标归一化时的分母bndbox四个值是绝对像素坐标。小番茄数据集里一张图常常有十几个object节点每个都对应一个果实实例这些果实可能互相紧挨着所以xmin/ymin/xmax/ymax的差值经常只有几十像素——这是小目标的典型特征。解析时要注意的坑有两个。第一name的值必须和后续YOLO训练用的class name完全一致比如数据里写的是tomato那yaml文件里names就得是[tomato]大小写和空格都不能差。第二size里存的是原图尺寸不是标注工具预览窗口的尺寸转换脚本必须以这个宽高做归一化否则框的位置整体偏移。很多转换脚本报坐标越界问题就出在用cv2读图后没有校验读到的宽高和xml里的width/height是否一致。写解析脚本前建议先用一分钟跑一个统计脚本确认所有xml都能被正确解析并且class分布均匀。下面这段代码可以快速扫描整个标注目录输出每个xml里的object数量和class频率用于提前发现空标注或类别写错的样本import xml.etree.ElementTree as ET from pathlib import Path xml_dir Path(annotations) class_counter {} total_boxes 0 for xml_file in xml_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() objects root.findall(object) if len(objects) 0: print(f[warn] {xml_file.name} 没有任何object节点) for obj in objects: name obj.find(name).text class_counter[name] class_counter.get(name, 0) 1 total_boxes 1 print(类别统计:, class_counter) print(总标注框数:, total_boxes)这段代码用xml.etree.ElementTree遍历所有xmlfindall(object)拿到当前图所有目标空标注文件会直接打印警告。类别统计结果里如果出现tomato之外的意外类别说明标注软件或数据导出时混入了脏数据需要先清洗。对YOLO训练来说类别错乱比框画歪更致命因为类别错误会让模型在推理阶段产生困惑。2.2 写一个VOC转YOLO的脚本归一化坐标必须用原图尺寸YOLO系列v5/v6/v8等统一的标签格式是txt每一行代表一个目标class_id x_center y_center width height其中中心点坐标和宽高全部归一化到[0,1]区间且width/height除以的是图片宽高而不是归一化后的小数。转换逻辑就是读xml的bndbox取四个角的像素坐标换算成中心点和宽高。一个能直接用于训练的转换脚本如下import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_path, class_map): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) with open(out_path, w) as f: for obj in root.findall(object): name obj.find(name).text if name not in class_map: print(f[skip] 未知类别 {name} in {xml_path.name}) continue cls_id class_map[name] xmin float(obj.find(bndbox/xmin).text) ymin float(obj.find(bndbox/ymin).text) xmax float(obj.find(bndbox/xmax).text) ymax float(obj.find(bndbox/ymax).text) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}\n) class_map {tomato: 0} xml_dir Path(annotations) label_dir Path(labels) label_dir.mkdir(exist_okTrue) for xml_path in xml_dir.glob(*.xml): out_txt label_dir / (xml_path.stem .txt) voc_to_yolo(xml_path, out_txt, class_map)代码里有几个地方值得留意。root.find(size/width)用XPath径取size节点下的width比连续两次find更简洁class_map字典把类别名映射成从0开始的整数id这个映射和之后yaml里的names顺序必须一致写出时用f{x_center:.6f}保留6位小数精度完全足够训练也避免文件膨胀。.6f这种格式化不是死规矩但太少精度比如2位会让小目标框产生几像素的抖动太多精度没有实际收益。转换完成后建议把labels/下生成的txt和JPEGImages/下的图片文件名做一次差集比对属于“标注了但没图”或“有图但没标注”的样本直接用脚本剔除并记录到一个missing.txt备用。这一步能在训练前就拦住数据划分时的崩溃比训练到一半报FileNotFoundError好处理得多。2.3 转换后校验画框预览和标签分布统计不能省很多人转完格式直接开训结果训练时loss正常下降跑验证集却发现mAP很低。多数情况不是模型问题而是转换后标签坐标算错但YOLO训练过程不会主动报坐标错误。所以转换后必须做两件事可视化校验和分布统计。可视化校验就是拿原图把txt里的归一化坐标转回像素坐标画框肉眼确认框的位置没有系统性偏移。小番茄数据集里果实边界往往带绿色萼片如果标注框是紧贴红色果实的那说明标得偏紧如果框把萼片也包进去则偏松。YOLO训练对这两种都能容忍但混合存在时会导致anchor匹配不稳定。写一个快速画框脚本import cv2 img cv2.imread(JPEGImages/tomato_001.jpg) h, w img.shape[:2] with open(labels/tomato_001.txt) as f: for line in f: cls_id, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, fcls{int(cls_id)}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(check_tomato_001.jpg, img)画框后只抽三五张看还不够要分别挑单果图、密集果图、遮挡图各看一张。分布统计则是看所有框的宽高分布小番茄数据集的框宽高大多落在原图尺寸的2%-8%区间。如果统计发现大量框的宽度小于原图宽度的1%这么大的目标用常规YOLO默认anchor几乎无法匹配后面就要专门讲小目标的处理策略这也解释了为什么这个数据集不能拿YOLO默认参数直接训。校验项通过标准不通过时的常见原因画框位置框中心落在果实中心xml坐标与size宽高不一致类别名称只有tomato一种标注软件误选类别空标签文件占比小于1%漏标或转换脚本跳过框宽高分布大部分在2%-8%小目标密集需调anchor3. 用YOLOv8训练小番茄数据集yaml配置、目录划分与训练命令3.1 数据集目录怎么摆train/val划分和yaml文件写法把转换好的txt标签和图片整理成YOLO标准目录结构是训练前最后一步。常见做法是放在同一个数据集根目录下用train和val两个子目录分别存图片label文件放到和图片同级的labels子目录里。目录结构如下dataset/ ├── images/ │ ├── train/ │ │ ├── tomato_001.jpg │ │ └── ... │ └── val/ │ ├── tomato_050.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── tomato_001.txt │ │ └── ... │ └── val/ │ ├── tomato_050.txt │ └── ... └── data.yaml划分比例常见做法是8:1:1的变体但小目标数据集建议把验证集比例提高到15%-20%因为小目标样本中“难检”的比例高验证集太小会导致mAP指标波动剧烈。划分时要注意不能简单随机切分如果同一串番茄有多张连续帧图片要按场景分组划分否则训练集和验证集出现重复背景mAP虚高。这个坑在大目标数据集里不明显在果实类小目标数据集里非常常见。data.yaml是YOLOv8训练入口内容如下path: /absolute/path/to/dataset train: images/train val: images/val names: 0: tomatopath字段建议写绝对路径写相对路径时YOLOv8会基于当前工作目录拼接换机器跑就容易找不到数据。train和val字段填的是相对于path的目录路径注意只写到images那层YOLOv8会自动去对应的labels目录找同名txt。names的映射必须和转换脚本里class_map保持一致顺序错一个就全错。3.2 训练命令与关键参数imgsz、batch、epochs怎么设YOLOv8的训练命令是CLI风格一个常用的小目标数据集训练命令如下yolo detect train \ modelyolov8n.pt \ datadata.yaml \ imgsz640 \ batch16 \ epochs100 \ patience20 \ device0 \ workers4 \ projectruns/tomato \ nameexp1 \ pretrainedTrue逐项解释参数modelyolov8n.pt使用nano规模的预训练权重对小番茄这种单类且目标小的任务nano和s的区别主要在推理速度和精度上限先拿nano跑通流程再换大的更稳妥imgsz640是训练分辨率这个值直接关系到小目标能不能被模型看见后面会展开讲batch16受显存限制12GB显存跑nano配batch16没问题如果显存不够就减半而不是降低分辨率patience20表示验证集mAP连续20个epoch不提升就早停小数据集训练通常在40-60个epoch收敛patience设太大会浪费算力设太小又可能提前停掉有潜力的训练。这里设为20是个折中。训练过程中的几个输出指标要盯住看。box_loss下降说明定位在收敛cls_loss下降说明分类在收敛mAP50是IoU阈值0.5下的平均精度对小目标数据集来说mAP50比mAP50-95更有参考价值因为小目标框和真值框的IoU很难超过0.7mAP50-95会被小目标整体拉低。这属于小目标任务的正常现象不代表模型不行。还有一个容易忽略的参数是rectTrue它让YOLO按图片宽高比分组batching减少padding浪费。小番茄数据集如果图片分辨率统一不需要开如果混合了多种分辨率开启后训练速度会明显提升。3.3 训练中断与继续训练resume的正确打开方式训练跑到一半机器重启或显存溢出是常态。YOLOv8断点续训很简单不需要重新传所有参数yolo detect train resume modelruns/tomato/exp1/weights/last.pt需要说明的是resume时会自动读取训练时的data.yaml、imgsz等配置不需要再手动指定。如果你是手动中断的训练结束后last.pt就是最新权重如果是显存溢出崩掉的last.pt也是最后一次保存的权重不会被破坏。恢复训练后建议先看一眼loss曲线是否连续如果恢复后第一个epoch的loss断崖式跳动大概率是学习率和优化器状态没恢复好这种情况直接重新训比纠结续训更省时间。4. 小目标检测的调参技巧用小番茄数据集的三个实际决策4.1 小番茄为什么难检像素占比决定了anchor匹配难度一张1280×720的图里一个直径40像素的小番茄约占整张图面积的0.17%。YOLOv8的下采样倍率是32倍意味着特征图上一个网格对应原图32×32像素40像素的目标在特征图上只有约1.2个网格大小。如果目标中心恰好落在网格边界附近两个网格都只能看到半个目标正样本匹配的IoU就会很低导致这个目标干脆没被分配为正样本。这就是小目标检测精度低的根本原因不是模型不够强是特征图分辨率天然限制了小目标的表征能力。4.2 三步调参法提分辨率、改anchor、加Mosaic增强针对这个问题最有效的调整依次是提高imgsz、用自动anchor、调Mosaic概率。第一步是提高训练分辨率。把imgsz640改到imgsz960或1280小番茄在特征图上的像素占比直接提升1.5到2倍mAP提升通常立竿见影。代价是显存占用变大训练时间变长。实测在nano模型上960分辨率、batch8,一张12GB卡能跑超过就会OOM。第二步是让YOLOv8自动计算anchor。YOLOv8默认会在训练前对标签做anchor聚类你可以显式跑一次检查anchor分布yolo detect train modelyolov8n.pt datadata.yaml imgsz1280 epochs0epochs0表示不实际训练只跑数据加载和anchor计算。终端里会输出anchor的宽高分布比如0.02-0.05这个区间占比很高说明你的标签普遍是小框与模型预设anchor匹配度不够此时可以调整anchor_t这个阈值参数默认是4.0意思是一个目标的宽高比超过4倍就算“不匹配”。小番茄这类宽高比接近1的圆形目标不需要改但如果数据里出现长条形果实串可以把anchor_t适度调低到3.0让anchor匹配更严格减少低质量正样本进入训练。第三步是数据增强策略。YOLOv8默认开启Mosaic但小目标密集场景下Mosaic把4张图拼在一起后每个小番茄变得更小反而可能让模型更难学。可以调整mosaic0.5降低Mosaic概率同时调大scale0.3让目标放大的概率更高等效于让模型多看到“更大”的小番茄。这两个超参都在ultralytics的配置里直接改命令行写法如下yolo detect train \ modelyolov8n.pt \ datadata.yaml \ imgsz960 \ batch8 \ epochs100 \ mosaic0.5 \ scale0.5 \ fliplr0.5scale0.5表示每次随机缩放的幅度按0.5-1.5倍扰动原始目标在这个范围内有概率被放大这在小目标数据集上比用复制粘贴的小目标增强更稳。fliplr水平翻转对番茄这类对称目标无害可以保留但不要开flipud因为农业机器人视角下果实不会倒置。4.3 验证模型有没有真正学会错误分析看三类case训练完以后不要只看mAP一个数用小番茄数据集验证模型要分别看三类case单独果实、密集串果、遮挡果实。写一个推理脚本把验证集全部跑完把置信度低于0.25但GT框存在的地方单独保存成图片from ultralytics import YOLO import cv2 model YOLO(runs/tomato/exp1/weights/best.pt) results model.predict(sourcedataset/images/val, conf0.25, saveFalse) for r in results: boxes r.boxes.xyxy.cpu().numpy() if len(boxes) 0: print(f{r.path} 完全未检出)输出文件名连续多张“完全未检出”时先看这些图里的小番茄是否偏小如果偏小就回到4.2第一步提高分辨率重训。如果漏检集中在密集串果说明NMS把相邻的框合并掉了此时调整max_det和NMS的iou阈值把iou0.6改低到0.45让相邻框更容易被保留。如果漏检集中在遮挡果则要提高conf阈值再观察因为低置信度预测多为遮挡目标这个平衡只能靠验证集反复试。把这三类的表现列成一张简单的对照表基本就能定位模型还差在哪也就能决定下一步是补数据还是调参。本文还有配套的精品资源点击获取