简介:该小番茄目标检测数据集面向农业智能化与计算机视觉方向的学习者、算法工程师及科研人员,以真实环境下的小番茄为检测对象,解决果实定位、成熟度判别等实际问题。压缩包共包含1790个文件,其中895张png格式原始图像与895个xml格式标注文件一一对应,xml文件内记录每个小番茄的边界框坐标与类别标签,图片覆盖不同角度与光照场景,整体大小约180.33MB,足以支撑YOLO系列模型的训练、验证与推理测试。目前已有79人学习下载,既能帮助新手完整走通从数据加载、模型训练到评估的检测流程,也为研究者提供了迁移学习、数据增强等方向的可操作样本。利用这份数据集,可快速搭建小番茄检测基线,并延伸至自动采摘、产量统计等农业应用场景,有效降低数据采集与标注成本,是一份兼顾教学与实用的小规模视觉数据集。
1. 小番茄目标检测数据集:从 rar 压缩包到 YOLO 训练的完整路径
拿到“图片+xml格式标签”的小番茄数据集,先别急着解压。xml 是 Pascal VOC 风格标注,而 YOLO 训练要的是每张图一个同名 txt、类别整数编号、坐标归一化的格式,中间缺一次格式转换。
小番茄果实小、密集、遮挡多,是练小目标检测的合适样本。这个 rar 的核心价值在图片质量与标注一致性,解压只是开始,后续所有工作都围绕标签展开。
落地路径分四段:核对 xml 标签字段、批量转 txt 并划分 train/val、配置 data.yaml 跑通 yolov8 训练、按尺寸分层验证漏检。全部步骤不需要 GPU 也能完成训练前的准备。
2. 解压 rar 后先读 xml:小番茄数据集的标签字段与目录核对
2.1 用 unrar 解压并按图片与 xml 一一对应核对
解压看起来没有技术含量,但这类型的数据集压缩包内部并不总是规整的:图片和 xml 可能散落在多个子目录,可能混入损坏文件,也可能出现图片与标签后缀不一致的情况。常见做法是解压后先跑一遍文件名配对,确认“每张图有标签、每个标签有图”再继续,否则转换出来的 YOLO 标签会静默缺失某几张图,训练时 mAP 难看且不好排查。
# Debian/Ubuntu 安装 unrar(Windows 下直接用 7-Zip 或 Bandizip 右键解压) sudo apt install unrar unrar x -o+ tomato_dataset.rar ./dataset_src # 统计 xml 和图片数量,注意 find 的 -o 优先级要用括号括起来 find ./dataset_src -name "*.xml" | wc -l find ./dataset_src \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | wc -lunrar 参数说明:x表示保留压缩包内完整路径解压,-o+表示覆盖已存在文件,避免第二次解压时交互式提问卡住流程。如果 rar 包设置了密码,直接找发布方索要口令,不建议碰任何绕过工具。两个 wc 数量差异较大时,说明标注或图片有缺失,优先补齐再进转换,别把脏数据带进训练。
数量对上了,还要确认文件名能对应上,用一段短脚本做集合差运算:
from pathlib import Path src = Path("./dataset_src") imgs = {p.stem: p for p in src.rglob("*") if p.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp")} xmls = {p.stem: p for p in src.rglob("*.xml")} print("图片:", len(imgs), " xml:", len(xmls)) print("缺 xml 的图片:", len(set(imgs) - set(xmls))) print("缺图片的 xml:", len(set(xmls) - set(imgs)))这段脚本以文件主名(stem)为键建字典,set 差集一次能算出两个方向的缺失。输出为 0 时再往下一步;如果缺的是图片,检查是否混入了 tiff、rgb 等非常见格式,把后缀补进集合即可。
2.2 bndbox、difficult、truncated 在 YOLO 转换中的去留
VOC 风格的 xml 典型结构如下,小番茄数据集如果标注规范,字段基本就是这些:
<annotation> <folder>tomato</folder> <filename>IMG_0021.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>tomato</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>612</xmin> <ymin>402</ymin> <xmax>736</xmax> <ymax>531</ymax> </bndbox> </object> </annotation>字段含义与去留策略:
| 字段 | 含义 | 转换策略 |
|---|---|---|
| name | 目标类别名 | 映射成整数编号,编号顺序必须与后续 data.yaml 的 names 一致 |
| bndbox | 左上角与右下角像素坐标 | 换算为归一化的中心点坐标与宽高 |
| truncated | 目标被图像边界截断 | 建议保留参与训练,能提升遮挡鲁棒性 |
| difficult | 难例标记 | 转 YOLO 时跳过或单独归档,避免难例当正样本造成损失噪声 |
为什么 difficult 要特殊处理:YOLO 训练管线的 txt 里每一行只解析 class_id、cx、cy、w、h 五个数字,没有预留难例位。若把 difficult=1 的框当成普通正样本,模型会去硬拟合这些极难甚至标注模糊的目标;完全丢掉又浪费信息。折中的做法是先统计这类框的占比,低于 5% 直接丢弃,高于 5% 保留并在训练后按漏检分析决定是否清洗。
2.3 统计类别与框面积分布,预判是否属于小目标检测问题
小目标检测的难点在于特征在下采样过程中丢失:一个 32x32 像素的目标,在 1920 宽原图缩放到 imgsz=640 后约剩 10x10 像素,经过 YOLOv8 的第 5 层下采样只剩一两个特征点。所以训练前先看框的面积分布,能直接决定 imgsz 和 mosaic 的配置方向。
import xml.etree.ElementTree as ET from pathlib import Path import numpy as np areas, classes = [], {} for xml_path in Path("./dataset_src").rglob("*.xml"): root = ET.parse(xml_path).getroot() W = float(root.find("size/width").text) H = float(root.find("size/height").text) for obj in root.findall("object"): name = obj.find("name").text.strip() classes[name] = classes.get(name, 0) + 1 b = obj.find("bndbox") w = float(b.find("xmax").text) - float(b.find("xmin").text) h = float(b.find("ymax").text) - float(b.find("ymin").text) areas.append(w * h / (W * H)) # 归一化面积,跨分辨率可比 areas = np.array(areas) print("类别统计:", classes) print("归一化面积中位数:", np.median(areas)) print("小于 0.002 的框占比:", round((areas < 0.002).mean() * 100, 1), "%")说明:归一化面积 0.002 约等于 640x640 下的 28x28 像素,占比超过 30% 就应该按小目标数据集处理,训练时优先提高 imgsz 或开启 mosaic。另外类别统计如果出现 tomato、Tomato 两种写法,说明标注大小写不统一,转换脚本里要先做 lower(),否则同一个目标会被当成两个类别。
3. xml 转 YOLO txt 的批量脚本:类别映射、坐标归一化与 train/val 划分
3.1 YOLO 标签格式与换算公式
YOLO 的标签文件与图片同名、后缀为 txt,每行表示一个目标:class_id cx cy w h。cx 和 cy 是归一化后的目标中心坐标,w 和 h 是归一化后的框宽高,全部落在 [0,1] 区间。从 VOC 的 bndbox 换算的公式:
| 目标值 | 计算公式 |
|---|---|
| cx | (xmin + xmax) / 2 / W |
| cy | (ymin + ymax) / 2 / H |
| w | (xmax - xmin) / W |
| h | (ymax - ymin) / H |
对应的目录结构是 images/ 与 labels/ 平行,下面再分 train、val,两个目录里的文件名保持一致。data.yaml 里只需要写 images/train 和 images/val,ultralytics 会自动到 labels/ 同路径下读取标签。
3.2 一个可直接用的 VOC 转 YOLO 脚本
#!/usr/bin/env python3 import shutil import xml.etree.ElementTree as ET from pathlib import Path SRC = Path("dataset_src") # 第 2 章解压后的目录 DST = Path("datasets/tomato") # YOLO 工作目录 CLASSES = ["tomato"] # 由 2.3 的类别统计决定 cls_id = {n: i for i, n in enumerate(CLASSES)} IMG_SUF = (".jpg", ".jpeg", ".png", ".bmp") def convert_one(xml_path: Path) -> list[str]: root = ET.parse(xml_path).getroot() W = float(root.find("size/width").text) H = float(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip().lower() # 统一小写防错位 if name not in cls_id: continue b = obj.find("bndbox") x1, y1 = float(b.find("xmin").text), float(b.find("ymin").text) x2, y2 = float(b.find("xmax").text), float(b.find("ymax").text) if x2 <= x1 or y2 <= y1 or x1 < 0 or y1 < 0 or x2 > W or y2 > H: print("跳过异常框:", xml_path.name, name) # 脏数据直接丢弃 continue cx = ((x1 + x2) / 2) / W cy = ((y1 + y2) / 2) / H bw = (x2 - x1) / W bh = (y2 - y1) / H cx, cy, bw, bh = min(cx, 1), min(cy, 1), min(bw, 1), min(bh, 1) lines.append(f"{cls_id[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") return lines for xml_path in sorted(SRC.rglob("*.xml")): stem = xml_path.stem img = None for s in IMG_SUF: cand = xml_path.with_suffix(s) if cand.exists(): img = cand break if img is None: print("缺图:", xml_path) continue lines = convert_one(xml_path) if not lines: print("无有效目标:", stem) continue (DST / "images" / "all").mkdir(parents=True, exist_ok=True) (DST / "labels" / "all").mkdir(parents=True, exist_ok=True) shutil.copy2(img, DST / "images" / "all" / img.name) (DST / "labels" / "all" / f"{stem}.txt").write_text("\n".join(lines) + "\n") print("转换完成")脚本里的关键设计:CLASSES 列表的顺序直接决定类别编号,训练时 data.yaml 的 names 必须和它完全一致;对 name 做 lower() 是为了修复前面提到的大小写不一致问题;异常框丢弃而不是强行转换,否则宽高为 0 的框会让损失变成 nan;数值 min 夹取是防御浮点误差导致坐标超过 1。图片用 copy2 复制而不是移动,避免后续增强环节污染原始文件。
如果后面要补标注,CVAT 导出时可以直接选择 YOLO 格式,比再走一遍 xml 中转省事,导出后把 txt 放进 labels 目录即可。
3.3 按 8:2 划分 train/val 并可视化抽查
划分要按图片为单位,不能按框为单位,否则同一张图被同时分到训练和验证,指标虚高且没有参考意义。固定随机种子保证每次划分结果一致:
import random from pathlib import Path random.seed(42) # 固定种子保证可复现 all_imgs = sorted((DST / "images" / "all").glob("*")) random.shuffle(all_imgs) n_val = int(len(all_imgs) * 0.2) for split in ("train", "val"): (DST / "images" / split).mkdir(parents=True, exist_ok=True) (DST / "labels" / split).mkdir(parents=True, exist_ok=True) for i, img in enumerate(all_imgs): split = "val" if i < n_val else "train" label = DST / "labels" / "all" / f"{img.stem}.txt" img.replace(DST / "images" / split / img.name) label.replace(DST / "labels" / split / label.name) print("train:", len(all_imgs) - n_val, "val:", n_val)如果原始数据按生长阶段或光照条件分目录采集,建议改成按目录分组再抽验证集,避免同一批照片的强相关性让验证指标虚高。划分完成后用 OpenCV 抽查几张验证图:
import cv2 img = cv2.imread(str(DST / "images/val" / "IMG_0021.jpg")) H, W = img.shape[:2] for line in open(DST / "labels/val/IMG_0021.txt"): _, cx, cy, w, h = map(float, line.split()) x1, y1 = int((cx - w / 2) * W), int((cy - h / 2) * H) x2, y2 = int((cx + w / 2) * W), int((cy + h / 2) * H) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, "tomato", (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite("check_visual.jpg", img)这里的逻辑是直接读 txt 画框,等于把转换结果从头校验一遍:框整体偏移通常是 cx 公式里漏除了 W,多个框挤在同一点多半是 lines 拼接时丢了换行。图像确认没问题后,train/val 两个目录就可以交给训练命令了。
4. 配置 data.yaml 并跑通 YOLOv8 训练自己的小番茄数据集
4.1 环境安装与最小的 data.yaml
先装 ultralytics 并确认命令可用:
pip install -U ultralytics yolo --version环境装好后,YOLO 目标检测流程正式进入数据配置环节。需要说明的是,AMD 显卡用户通常要装 ROCm 对应版本的 PyTorch 再装 ultralytics,CPU 也能训练,只是速度慢一个数量级。接着在工作目录写 data.yaml:
# datasets/tomato/tomato.yaml path: datasets/tomato train: images/train val: images/val names: 0: tomato参数说明:path 是相对你运行 yolo 命令的工作目录,亦可写绝对路径;train 和 val 指向 images 下的子目录,ultralytics 会自动用 labels 替换 images 去读标签;names 字典的编号必须和转换脚本里的 CLASSES 顺序一致,这是错位率最高的地方。
4.2 第一次训练的命令与关键超参数
yolo detect train \ data=datasets/tomato/tomato.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=100 \ batch=16 \ patience=20 \ lr0=0.01 \ mosaic=1.0 \ close_mosaic=10 \ project=runs/tomato \ name=s_base参数表:
| 参数 | 取值 | 设置逻辑 |
|---|---|---|
| model | yolov8s.pt | 小数据集先用 s 跑通流程,再根据指标换 m 或 l |
| imgsz | 640 | 小框占比高时优先试 768,同时 batch 减半 |
| epochs | 100 | 单类别小数据集 100 轮足够,配合 patience 自动早停 |
| batch | 16 | 以显存上限为基准,OOM 就减半,不要硬撑 |
| patience | 20 | 验证集 mAP 连续 20 轮不升即停 |
| lr0 | 0.01 | 带预训练权重时的常规起点;从头训练建议降到 0.001 |
| mosaic | 1.0 | 小目标尺度多样性靠它,开启即可 |
| close_mosaic | 10 | 最后 10 轮关闭 mosaic,让尺度分布回归真实 |
batch 与 imgsz 是显存敏感的联动参数:imgsz 从 640 提到 768,单张图计算量按面积接近增加 44%,batch 不降显存必然爆。对小番茄这类小目标数据集,优先保 imgsz 而不是保 batch,目标框变小后模型在低分辨率下学不到细粒度特征。
4.3 训练中的损失曲线怎么看
训练输出在 runs/tomato/s_base/ 下,核心文件是 results.csv。YOLOv8 的损失函数包含三个头:box_loss 管边框回归、cls_loss 管分类、dfl_loss 管位置分布,三个头都同时计算 train 和 val。用命令行快速看最近几轮的趋势:
head -1 runs/tomato/s_base/results.csv | cut -d, -f3-5,10-12 tail -5 runs/tomato/s_base/results.csv | cut -d, -f3-5,10-12三种典型的失败形态:
- val 损失回升而 train 持续下降:过拟合,优先检查 train/val 是否混入同一批图片,再考虑增强强度。
- 损失正常但 mAP50 长期低于 0.3:检查类别映射是否错位,ultralytics 启动时会打印每个类别的标签数量,和数据集总框数对一下。
- loss 出现 nan:回头查是否漏掉异常框过滤,或 lr0 设得过高。
单类别数据集的错位问题最隐蔽:类名映射错了不会出现跨类别串框,而是表现为某个类别的样本数异常多或异常少,且 mAP 曲线整体偏低,启动日志里对比即可发现。
4.4 训练完成后立刻跑一次验证
yolo detect val \ model=runs/tomato/s_base/weights/best.pt \ data=datasets/tomato/tomato.yaml \ imgsz=640验证命令说明:默认加载 best.pt,输出 mAP50、mAP50-95 和 precision/recall,同时生成混淆矩阵和 PR 曲线。小目标检测场景下 mAP50 的参考价值大于 mAP50-95,因为小框和真值框在 IoU 0.75 以上的匹配非常苛刻,mAP50-95 会被这一点拉低,不代表模型完全不可用。
5. 收尾技巧:close_mosaic 恢复训练与按目标尺寸分层查漏检
5.1 继续训练的微调配方
基础训练结束后,如果 mAP 差强人意,先别急着换大模型。一个低成本动作是接着 best.pt 再做一轮 30 轮的恢复训练,学习率降到 0.001,最后 5 轮关闭 mosaic。对小目标数据集来说,mosaic 在收敛后期会拖后腿:四张图缩拼让目标尺度偏离真实单图分布,close_mosaic 让模型把最后几轮花在适应真实尺度上。
yolo detect train \ data=datasets/tomato/tomato.yaml \ model=runs/tomato/s_base/weights/best.pt \ imgsz=640 \ epochs=30 \ lr0=0.001 \ close_mosaic=5 \ project=runs/tomato \ name=finetune提示:close_mosaic 的轮数要小于总轮数,并且保证模型在关闭 mosaic 前已经收敛,否则尺度还没学稳就提前切换,收益不明显。
5.2 用预测标签与真值标签做分尺寸漏检统计
想知道模型漏的是大目标还是小目标,可以用 predict 输出 txt,再和验证集真值做一次 IoU 匹配。先推理并保存标签:
yolo detect predict \ model=runs/tomato/finetune/weights/best.pt \ source=datasets/tomato/images/val \ conf=0.25 \ iou=0.6 \ save_txt=True \ project=runs/tomato \ name=predict_valfrom pathlib import Path gt_dir = Path("datasets/tomato/labels/val") pred_dir = Path("runs/tomato/predict_val/labels") SMALL = 0.002 # 与 2.3 节的面积阈值保持一致 def box_iou(a, b): x1, y1 = max(a[0]-a[2]/2, b[0]-b[2]/2), max(a[1]-a[3]/2, b[1]-b[3]/2) x2, y2 = min(a[0]+a[2]/2, b[0]+b[2]/2), min(a[1]+a[3]/2, b[1]+b[3]/2) inter = max(0.0, x2-x1) * max(0.0, y2-y1) return inter / (a[2]*a[3] + b[2]*b[3] - inter) miss, total = {}, {} for gt_file in gt_dir.glob("*.txt"): pf = pred_dir / gt_file.name preds = [] if pf.exists(): for line in pf.read_text().splitlines(): preds.append(list(map(float, line.split()))[1:5]) # 去掉类别和 conf for line in gt_file.read_text().splitlines(): parts = list(map(float, line.split())) box = parts[1:5] # cx cy w h key = "small" if box[2]*box[3] < SMALL else "mid" total[key] = total.get(key, 0) + 1 if not any(box_iou(box, p) >= 0.5 for p in preds): miss[key] = miss.get(key, 0) + 1 for k in total: print(k, "漏检:", miss.get(k, 0), "/", total[k], "=", round(miss.get(k, 0)/total[k]*100, 1), "%")统计逻辑说明:predict 输出的 txt 每行是 class、cx、cy、w、h、conf 六列,切片 [1:5] 取出坐标;真值文件本身就是五列,box 也是 [1:5]。box_iou 在归一化坐标系里直接算 IoU,阈值取 0.5,对应 mAP50 的匹配口径。分尺寸的结果如果显示小目标漏检率远高于中等目标,下一步该提高 imgsz 或补充小目标样本,而不是盲目换更大模型。
本文还有配套的精品资源,点击获取