简介面向计算机视觉与智能农业应用场景这份数据集提供了西红柿与大番茄两类作物的真实图像样本及对应标注适用于YOLO系列目标检测模型的训练、验证与算法对比也能迁移至Faster R-CNN、SSD等常见检测框架适合从事目标检测研究、农业智能化开发或相关课程实践的学习者使用。包内共279个文件其中277张jpg图片覆盖多种生长阶段、拍摄角度及光照条件包含不同程度的背景干扰与复杂遮挡有助于提升算法在真实环境中的鲁棒性另附2个json格式标注文件对每张图片中的目标位置与类别进行了精确记录为训练高质量模型提供了可靠标签基础。压缩包整体约162.54MB结构简洁紧凑。截至目前已有134人学习使用可作为农情监测、果实计数、成熟度判别及智能采摘等方向的数据支撑同时也可作为深度学习教学与竞赛训练的实操素材。1. 这份西红柿大番茄数据集到底能干什么先摸清边界再动手做目标检测的人最缺的往往不是模型而是干净、能直接用的数据。这份「YOLO目标检测-西红柿大番茄检测数据集图片json格式标签.rar」提供的正是训练阶段最关键的原料真实拍摄的西红柿和大番茄图片以及对应的JSON格式标签。无论你要做采摘机器人的视觉识别、温室里的果实计数还是果蔬分拣线上的大小分级这份数据都能省掉你大量采集和人工标注的时间。JSON标签的最大好处是它不绑定某个训练框架你可以把边界框提取出来转成YOLO、Darknet、MMDetection等任意格式。但也正因为“JSON”只是一个统称收到压缩包后不能直接就开训。我建议你按“解压检查 → 格式转换 → 试训验证”三步走每一步都有容易踩的坑。这篇文章会把每一步需要看的、需要改的命令和参数全部拆开讲最后让你拿到手就能复现一套完整训练流程。2. 解压后的数据长什么样图片与JSON标签的两种常见组织方式2.1 图片文件命名与目录结构别被中文文件名坑到解压一个rar数据集后常见目录结构是images/下放所有jpg或png图片annotations/下放JSON标签。有些数据集会把图片和JSON放在同一级目录图片叫“番茄_0001.jpg”同名JSON叫“番茄_0001.json”也有把所有标注汇总成一个大JSON文件放在annotations/下的情况。先说命名问题。中文文件名在Windows上看起来没问题但一旦把数据集传到Linux服务器或者挂载到docker容器里很容易遇到字符编码错乱OpenCV读图片直接返回None训练进程卡在读图阶段。我一般会先做一次全量重命名改成“tomato_0001.jpg”这类纯英文小写文件名。与此同时如果JSON里的imagePath字段引用了旧文件名重命名后必须同步修改这个字段否则图片和标签就对不上了。另一个常见问题是子目录。图片可能放在images/train/和images/val/下而JSON全部集中在annotations/下。转换格式时不能简单取文件名拼接路径必须先理清每个JSON对应的图片在哪个子目录否则后面做数据集划分时会漏掉一批图片。2.2 JSON标签的两种格式COCO还是LabelMe不要认错JSON标签有好几种方言但农业数据集里最常见的是COCO格式和LabelMe格式。它们都能表达“西红柿在哪里”但数据结构完全不一样转换脚本也不同。对比项COCO JSONLabelMe JSON文件组织多个图片的标注汇总在一个json里每个图片对应一个同名json关键字段images、annotations、categoriesshapes、imagePath、imageData框的表达annotations里的bbox为[x, y, width, height]shapes里的points为多边形坐标[[x1,y1],[x2,y2]]坐标单位像素值像素值类别来源categories数组里的nameshapes里每个元素的label字段当前任务可用性直接可用取bbox即可需要从多边形点计算外接矩形取min/max即可拿到JSON后不要靠猜。先看第一层有没有annotations键有就是COCO风格如果第一层有shapes键那就是LabelMe风格。两个都有的话优先用COCO的bbox因为LabelMe里的多边形可能是高压标注反而容易引入噪声。有一点要注意即使用了COCO格式bbox也可能是[x1, y1, x2, y2]而不是[x, y, w, h]。这种“两点式”框在目标检测数据集里不算少见。保险起见转换前随机抽一个注释打印它的bbox长度和数值大小判断是左上角宽高还是左上角右下角。2.3 用Python把JSON标签做一次“数据体检”这一步是开训前最关键的事。不要急着转换格式先把数据集从整体上扫一遍图片数、标注数、类别分布、空标签、缺失图片。这个体检脚本可以直接对COCO和LabelMe两种格式复用。import json import glob import os from collections import Counter # 假设解压在 dataset/ 目录下 img_dir dataset/images label_dir dataset/annotations # 情况一COCO格式所有标签在一个instances.json里 coco_file glob.glob(f{label_dir}/*.json) if len(coco_file) 1 and images in json.load(open(coco_file[0], encodingutf-8)): with open(coco_file[0], encodingutf-8) as f: coco json.load(f) print(图片数:, len(coco[images])) print(标注框数:, len(coco[annotations])) cat_map {cat[id]: cat[name] for cat in coco[categories]} counter Counter() for ann in coco[annotations]: counter[cat_map[ann[category_id]]] 1 print(类别分布:, dict(counter)) # 检查空标注图片 img_ids {img[id] for img in coco[images]} ann_img_ids {ann[image_id] for ann in coco[annotations]} print(无标注图片数:, len(img_ids - ann_img_ids)) # 情况二LabelMe格式每图一个json labelme_jsons glob.glob(f{label_dir}/*.json) if labelme_jsons and shapes in json.load(open(labelme_jsons[0], encodingutf-8)): empty_count 0 missing_img 0 label_counter Counter() for jf in labelme_jsons: with open(jf, encodingutf-8) as f: data json.load(f) if not data[shapes]: empty_count 1 img_path os.path.join(img_dir, data.get(imagePath, )) if not os.path.exists(img_path): missing_img 1 for shape in data[shapes]: label_counter[shape[label]] 1 print(空标签json数:, empty_count) print(图片缺失数:, missing_img) print(类别分布:, dict(label_counter))这个脚本的作用是先建立“数据集黑匣子”的白盒视图。假如出现“图片数2000标注框数只有500”说明大量图片没有标注训练时会把这些图当成背景图严重干扰模型收敛。出现这种情况要么删掉这些空图要么重新检查JSON导出过程。类别分布也要重点看。目标是两个类西红柿和大番茄。如果打印出来多了一个“Tool”或者“小番茄”说明标注软件里混入了其他类别标签。YOLO训练只认你给的classes.txt多出来的类别要么合并要么删掉不能直接忽略。3. 把JSON标签转成YOLO能用的txt转换脚本与四个边界坑3.1 YOLO的txt坐标为什么是“归一化的中心点”——先理解再转换YOLO系列模型训练时每张图片对应一个同名txt文件每行格式是class x_center y_center width height。这四个坐标都是相对图像宽高的比例取值在0到1之间。比如图片宽度是1920一个框左边x等于480框宽是960那么归一化后的x_center就是(480 960/2) / 1920 0.5。这种归一化设计是有原因的模型在训练时会对输入图片做resize无论原图是 1920×1080 还是 640×480只要标签也是归一化比例resize后标签依然有效不需要重新计算坐标。这也是JSON里的像素坐标不能直接给YOLO用的根本原因。所以转换的核心就是一套换算像素坐标除以图像宽高得到比例左上角坐标加上半宽得到中心点。另外还要记住一个反向关系推理时输出的也是归一化坐标画框时反过来乘以图像宽高就能还原成像素框。3.2 转换脚本同时兼容COCO和LabelMe实际数据处理中一个数据集的来源经常不止一种标注工具。我习惯写一个同时兼容COCO和LabelMe的转换函数这样无论rar里是哪一种JSON都能一步到位。import json import os import glob # 定义类别顺序后面YOLO就按这个顺序输出 # 这里固定为 [tomato_small, tomato_big]即 0 和 1 categories [tomato_small, tomato_big] def coco_to_yolo(coco_json, out_dir): with open(coco_json, encodingutf-8) as f: coco json.load(f) cat_id_map {} for cat in coco[categories]: name cat[name] # 如果类别名是中文先映射到标准英文 if name 西红柿: cat_id_map[cat[id]] 0 elif name in (大番茄, tomato_big, big_tomato): cat_id_map[cat[id]] 1 else: # 其他类别按出现顺序补位但这里建议合并进已有类 cat_id_map[cat[id]] len(categories) os.makedirs(out_dir, exist_okTrue) for img in coco[images]: width img[width] height img[height] lines [] for ann in coco[annotations]: if ann[image_id] ! img[id]: continue x, y, w, h ann[bbox] # 过滤异常框宽高为0或负值 if w 0 or h 0: continue # 转为归一化中心点坐标 x_center (x w / 2) / width y_center (y h / 2) / height w_norm w / width h_norm h / height # 防止边界溢出裁剪到[0,1] x_center max(0, min(1, x_center)) y_center max(0, min(1, y_center)) w_norm max(0, min(1, w_norm)) h_norm max(0, min(1, h_norm)) lines.append(f{cat_id_map[ann[category_id]]} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) txt_name os.path.splitext(os.path.basename(img[file_name]))[0] .txt with open(os.path.join(out_dir, txt_name), w, encodingutf-8) as f: f.writelines(lines) print(COCO转换完成输出目录:, out_dir) def labelme_to_yolo(json_dir, out_dir, img_root): jsons glob.glob(os.path.join(json_dir, *.json)) os.makedirs(out_dir, exist_okTrue) for jf in jsons: with open(jf, encodingutf-8) as f: data json.load(f) img_rel data[imagePath] img_path os.path.abspath(os.path.join(img_root, img_rel)) # 获取图片宽高 import cv2 img cv2.imread(img_path) if img is None: print(警告: 图片读取失败跳过, img_path) continue height, width img.shape[:2] lines [] for shape in data[shapes]: label shape[label] # LabelMe用的是多边形点取外接矩形 pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x1 min(xs) y1 min(ys) x2 max(xs) y2 max(ys) w x2 - x1 h y2 - y1 if w 0 or h 0: continue x_center (x1 w / 2) / width y_center (y1 h / 2) / height if label 西红柿: cls_id 0 elif label in (大番茄, tomato_big): cls_id 1 else: continue # 未知标签直接丢弃 lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w/width:.6f} {h/height:.6f}\n) txt_name os.path.splitext(os.path.basename(jf))[0] .txt with open(os.path.join(out_dir, txt_name), w, encodingutf-8) as f: f.writelines(lines) print(LabelMe转换完成共处理, len(jsons), 个文件)脚本里有几个参数值得单独说明categories列表的顺序就是YOLO训练时的类别id顺序一旦定了就不要在训练途中改动cat_id_map的存在是为了处理COCO原始category_id不连续的情况w_norm和h_norm用max/min裁剪看起来多余但在一些标注不规范的数据集里框会超出图像边界不裁剪的话训练时会让模型学到错误的位置信息。3.3 类别映射与train/val划分先定类别再切分转换工作完成后下一步是划分训练集和验证集。划分的一个大原则是先定类别顺序再切分文件。因为YOLO的dataset.yaml里names顺序必须和txt里的数字一一对应如果后面发现类别顺序错了整个训练集都要重新转。我一般会这样划分把所有图片和txt文件放在一起用随机种子打乱顺序然后取80%到images/train和labels/train20%到images/val和labels/val。注意不能用简单的“前80个文件归训练后20个归验证”因为数据集的原始排列往往按采集时间、地块或光照条件分组会引入分布偏差。import random import os import shutil # train_val_split.py random.seed(42) samples [f.replace(.jpg, ) for f in os.listdir(dataset/images) if f.endswith(.jpg)] random.shuffle(samples) split_idx int(len(samples) * 0.8) image_src dataset/images label_src dataset/labels_yolo os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) for name in samples[:split_idx]: shutil.move(f{image_src}/{name}.jpg, dataset/images/train/) shutil.move(f{label_src}/{name}.txt, dataset/labels/train/) for name in samples[split_idx:]: shutil.move(f{image_src}/{name}.jpg, dataset/images/val/) shutil.move(f{label_src}/{name}.txt, dataset/labels/val/)这个脚本用随机种子保证了可复现。如果你用多卡训练或者后续要调参固定seed非常重要否则每次划分的验证集都不一样模型对比就没有意义。划分后建议顺手打印train/val里每个类别的框数量确保没有出现“验证集里全是大番茄训练集里全是西红柿”这种极端分布。3.4 验证转换结果把txt画的框画回原图比看坐标更直观坐标数值看起来对不代表框就贴对了。我见过不少转换后“换算公式错了但数值恰好不报错”的情况比如把x和y互换了、把宽高当成了中心点。与其肉眼盯着txt不如直接画图。import cv2 import os def draw_yolo_txt(image_path, txt_path, out_path): img cv2.imread(image_path) if img is None: return with open(txt_path, encodingutf-8) as f: lines f.readlines() for line in lines: parts line.strip().split() cls_id int(parts[0]) x_c, y_c, w_n, h_n map(float, parts[1:]) h, w img.shape[:2] x1 int((x_c - w_n / 2) * w) y1 int((y_c - h_n / 2) * h) x2 int((x_c w_n / 2) * w) y2 int((y_c h_n / 2) * h) color (0, 255, 0) if cls_id 0 else (0, 0, 255) label tomato_small if cls_id 0 else tomato_big cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, label, (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1, cv2.LINE_AA) cv2.imwrite(out_path, img) print(已保存:, out_path) # 随机抽5张验证 import glob import random samples glob.glob(dataset/images/val/*.jpg)[:5] for s in samples: name os.path.basename(s).replace(.jpg, ) txt fdataset/labels/val/{name}.txt draw_yolo_txt(s, txt, fdebug/{name}_check.jpg)这一步能看到两类框的颜色区分。绿色是tomato_small红色是tomato_big。如果发现红框位置偏了优先检查JSON里bbox是[x,y,w,h]还是[x1,y1,x2,y2]如果发现所有框都偏移了半个图那多半是像素坐标除以了错误的宽高或者原始json里的图片尺寸字段和实际图片尺寸不一致。很多数据集的问题在这一步就暴露了千万不要跳过。4. 用YOLOv8训练西红柿大番茄检测模型命令与参数全部给到4.1 准备dataset.yaml数据集的“门牌地址”YOLOv8训练时不会自己去扫描目录它需要一个yaml文件告诉它“图片和标签在哪、类别是什么”。这个yaml是训练前最后一道配置关。# dataset.yaml path: /data/tomato_dataset # 数据集根目录写绝对路径最保险 train: images/train # 相对于path的训练图片目录 val: images/val # 验证图片目录 nc: 2 # 类别数这里必须是2 names: 0: tomato_small 1: tomato_bigpath这一项是最容易出错的。如果写的是相对路径不同目录下执行训练命令结果可能完全不一样我习惯在yaml里直接写死绝对路径并在训练前确认这个目录存在。train和val指向的目录里应该只有图片文件YOLO会自动在同级目录的labels子目录下寻找同名txt文件。也就是说images/train/1.jpg对应labels/train/1.txt这两个目录必须把名字匹配好。names的顺序就是前面转换脚本里categories的顺序。如果你在3.2里用的是[tomato_small, tomato_big]这里就必须保持同样顺序否则模型输出和标注对不上。4.2 训练命令选型模型尺寸、batch、epochs怎么定配置好yaml后直接用YOLOv8的CLI命令开始训练。常见做法是用官方预训练权重做微调不要从随机权重开始。因为西红柿检测属于相对简单的视觉任务预训练模型已经学会了基础纹理和形状特征微调几十个epoch就能收敛。yolo detect train \ data/data/tomato_dataset/dataset.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ workers8 \ seed42这里的参数我按重要性逐个说modelyolov8n.pt模型大小。n是nano最轻量适合两类目标和不算太小的果实检测。如果你的图片里有很多小番茄或者目标占比很小换成yolov8s.pt甚至yolov8m.pt会更稳但显存占用和训练时间都会上升。imgsz640训练输入尺寸。原图如果是1920×1080直接缩到640会让小目标信息流失。可以先跑一版640看mAP如果小番茄漏检明显再调到960或1280。batch16按显存定。8GB卡建议816GB卡可以16到32。batch太大不提升精度太小则loss抖动厉害。epochs100对微调来说足够。YOLOv8默认early stopping patience是100意思是连续100个epoch验证集mAP没提升就自动停。实际训练经常在第50到80轮就停了。seed42固定随机种子保证每次训练可复现。训练过程中终端会打印每轮的box_loss、cls_loss、mAP50等指标。第一轮mAP可能很低不要急着停看完整周期再说。4.3 训练过程看什么loss曲线、mAP、PR曲线训练结束后训练目录下会生成results.png、confusion_matrix.png、PR_curve.png等文件。这些图不是摆设我是按下面的顺序来判断模型是否合格看results.png里train/box_loss和val/box_loss是否整体下降。如果val loss下降到一半又反弹说明学习率太高或者数据有问题。看mAP50是否稳定在一个阈值以上。西红柿这类目标大、类别少的数据集mAP50达到0.9以上才算合格如果只有0.7说明类别混淆或标注噪声严重。看confusion_matrix.png的最后一列background背景。如果背景误检率高说明模型把很多非目标物体当成了番茄常见原因是空标注图片没有清理。这里想再强调一次训练参数不是固定的。imgsz和batch是强相关的两个变量如果以后你的显卡变了这两个参数需要重新匹配不要照抄一个配置就期望在所有硬件上得到一致结果。5. 避坑训练西红柿大番茄检测最常见的5个翻车点5.1 类别标签错位大番茄全被认成西红柿现象训练完mAP很高但跑实际图片时所有大番茄都被框成西红柿类小番茄却几乎不识别。原因转换时直接沿用了JSON里原始的category_id但原始id可能是1和3YOLO只认0和1导致类别数字发生了位移。解决在转换脚本里必须显式建立一个重新映射的字典比如cat_id_map {1: 0, 3: 1}而不是简单取ann[category_id]。同时用3.4的画框脚本可视化几十张确认“绿色框是西红柿、红色框是大番茄”后再训练。5.2 JSON里隐藏的“空框”和“重复框”现象训练时某几张图的loss异常大loss曲线出现尖刺。检查发现一张图上居然有50多个框但图片上只有3个番茄。原因数据标注软件在导出时把多次编辑的历史框重复保留了还有部分框宽高为0或负值转换脚本没有过滤。解决转换前统一过滤宽高小于等于0的框对同一张图里IoU大于0.9的重复框只保留面积更大的那个。这样做的逻辑是重复框来自同一目标训练时相当于给这个目标加了成倍权重会破坏类别平衡。5.3 imgsz与图片尺寸不匹配导致小番茄漏检现象训练时mAP50还行但实际部署到采摘相机上远处的番茄漏检严重框出来的都是近处的。原因数据原图是1920×1080缩放到640×640后长期处于画面边缘的番茄可能只有8像素宽模型根本学不到这个特征。解决先把训练参数里的imgsz调到1280跑一版对比mAP。如果显存不够退而求其次把原图按中心裁剪成多个1024×1024 tiles再训练推理时也按tile拼图预测。对农业场景切图比拉高分辨率更实用。5.4 数据集划分不随机验证集变成“单一光照专场”现象训练集loss下降到0.05验证集mAP却不到0.6而且每次重启训练验证集结果都不稳定。原因图片文件在目录里按采摘当天的顺序排列前80%全是晴天温室后20%全是阴天大棚。如果你按文件顺序切train/val验证集就等于只在阴天图片上测试。解决用随机种子打乱文件列表后再按8:2划分。进阶做法是按“采集地点或大棚编号”分组同一个大棚的图片只进训练集或验证集避免模型因为记住了大棚环境特征而虚高。5.5 增强参数太凶西红柿颜色完全失真现象训练过程mAP不断升高但可视化验证集预测结果时明明是大红色的西红柿被框成偏蓝色模型好像分不清颜色了。原因YOLOv8默认开启的HSV增强会把色相、饱和度拉得很大原本用于通用检测的颜色扰动对果蔬这类颜色高度敏感的目标反而有害。解决训练时显式限制增强参数在命令后加上hsv_h0.01 hsv_s0.3 hsv_v0.3甚至全部设为0。果蔬检测里形状和纹理是主要判别线索颜色增强可以做但必须温柔否则模型学到的“西红柿是红的”这个基本规律会被破坏。6. 最后一步把模型导出并做一次端到端验证6.1 用最佳权重在测试集上做“交付验收”训练完成后runs/detect/train/weights/best.pt就是验证集表现最好的权重。在正式用之前我习惯先跑一遍测试集把结果落成可检查的图片而不是只看终端指标。yolo detect predict \ modelruns/detect/train/weights/best.pt \ sourcedataset/images/test \ conf0.25 \ save_txtTrue \ save_confTrue \ projectruns/verify \ namefinal_check这里的conf0.25是置信度阈值低于0.25的预测框会被过滤掉。测试集里如果有很多遮挡严重的番茄可以降到0.1再看效果但部署时还是建议回到0.25减少误检。save_txtTrue会输出每个预测框的txt结果方便用脚本统计检测数量和类别分布比肉眼翻图更可靠。做完这一步才算对模型质量有一个明确的认知。如果测试集mAP和目标场景差距大优先回头检查数据划分和标注噪声而不是调整训练参数。6.2 导出ONNX并写一个最小推理Demo部署阶段我一般会把best.pt导出成ONNX格式这样既能脱离训练框架推理也能在边缘设备上跑。yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640 dynamicTrue用ONNX做推理时输出张量是一个[1, 6, 8400]或[1, 84, 8400]形状需要自己补充NMS后处理。下面是一个最小可运行的Python推理示例import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name image cv2.imread(test.jpg) img_h, img_w image.shape[:2] resized cv2.resize(image, (640, 640)).astype(np.float32) / 255.0 blob np.transpose(resized, (2, 0, 1))[None] outs session.run(None, {input_name: blob})[0] # [1, 84, 8400] # 取第0张去掉前4个坐标和类别数 preds np.transpose(outs[0]) boxes preds[:, :4] scores preds[:, 4:6] # 两个类别的置信度 box boxes * np.array([img_w, img_h, img_w, img_h]) # 反归一化 cv2.rectangle(image, (int(box[0,0]), int(box[0,1])), (int(box[0,2]), int(box[0,3])), (0,255,0), 2) cv2.imwrite(result.jpg, image)这里只演示了单框输出实际需要先对8400个候选框做NMS再取每个类别的最高分框。推理时如果发现框整体偏移多半是letterbox预处理没有还原坐标务必把缩放和padding的偏移量补回来。我现在的经验习惯是拿到任何封成压缩包的数据集第一件事绝不是开训而是先做数据体检和真实框可视化确认图片和JSON真的对得上。如果这一步最开始不做之后所有mAP数字都是不可靠的。希望这篇能帮你把这套流程跑顺也让番茄检测少走我走过的那些弯路。希望帮到你。本文还有配套的精品资源点击获取