☰
YOLO11西红柿检测实战:从数据集划分到PyQt可视化全流程解析
2026/9/26 7:21:56 网站建设 项目流程

简介:面向Python与深度学习初学者及目标检测实践者,这包资源提供基于YOLO11的西红柿检测完整方案,涵盖标注数据集、训练脚本、推理脚本与可视化界面,可帮助快速掌握YOLO11在农业视觉场景中的落地流程。压缩包共1322个文件,约143.79MB,包含656张jpg原图、326个txt标签、321个xml标注、3个yaml配置文件、3个Python脚本以及3个pt权重文件,同时附有训练日志与results.csv指标记录,既可直接调用已训练模型完成识别,也可依据流程重新训练。资源已有102人学习,脚本分工为数据划分、模型训练和PyQt图形界面检测,界面支持加载图片并输出识别结果,操作直观。此外包内自带依赖清单与目录说明,便于环境配置与二次开发,适合作为YOLO系列目标检测入门及西红柿识别任务的参考资源。

1. yolo11目标检测做西红柿检测:这套源码包到底能跑出什么

做农业视觉项目,最怕的不是模型选型,而是找到一个“数据集、训练脚本、可视化界面都齐了”的现成工程。这套基于 python + pytorch 的 YOLO11 西红柿检测资源,就是干这个用的:它自带标注好的西红柿图像数据集,通过01划分数据集.py把图片转成 YOLO 格式的 txt 标注并生成data.yaml,02train.py完成训练,最后03pyqt.py拉起一个 PyQt5 可视化界面,鼠标点一下就完成单张图片的检测与框选展示。适合正在做采摘机器人、果实产量估算、农学类毕业设计目标检测方向的人,或者其他目标检测项目想拿“小数据集 + 完整 pipeline”当模板的开发者。西红柿在拍摄时存在果实重叠、叶片遮挡、光照不均三类干扰,恰好能把 yolo11 目标检测的泛化能力逼出来。

2. 资源包内部结构:tfevents、labels.cache、results.csv 分别是什么

2.1 三个 py 文件的分工与 requirement.txt 的定位

解压后不要急着跑代码,先把目录里的文件分成三类看。第一类是三个 Python 脚本,命名已经说明用途:01划分数据集.py负责把原始图片整理成 YOLO 训练要的目录结构和标签格式,02train.py负责训练,03pyqt.py负责加载训练好的权重做可视化识别。第二类是环境文件requirement.txt,里面列的是 pytorch、ultralytics、opencv-python、PyQt5 这一串依赖,安装顺序建议先装 PyTorch(按自己的 CUDA 版本选命令),再pip install -r requirement.txt装其余部分。第三类是训练痕迹文件:几张 jpg 是样本图,events.out.tfevents.1733742028.zzg.7648.0是 TensorBoard 日志,labels.cache是标签缓存,results.csv是每一轮训练指标。前两个很多人当垃圾文件删掉,其实它们是判断训练是否正常的关键证据。

2.2 labels.cache 不是垃圾文件:标签缓存与失效机制

labels.cache这个文件值得单独说,因为它最容易引发“改了数据集但训练没变化”的玄学问题。ultralytics 系的目标检测训练流程里,第一次读取数据集时会扫描images目录下的所有图片、校验对应labels目录里的 txt 是否存在、标签坐标是否越界,把校验结果打包成一个缓存文件。第二次再跑训练时,模型直接读缓存,不再全量扫描,启动速度快很多。

import pickle # 读取训练时生成的 labels.cache,看看里面到底存了什么 with open('labels.cache', 'rb') as f: cache = pickle.load(f) print(type(cache)) # dict print(cache.keys()) # 通常包含 images、labels 等键 # labels 键对应的值就是每个图片路径对应的标注信息

这段代码把缓存文件反序列化出来,你就能看到它本质上是一个 Python 字典,记录着每一张图片的路径、尺寸、标注框、是否损坏等信息。关键坑在“缓存失效”上:如果你在训练之后手动增删了图片,或者改了某个 txt 标签,labels.cache里的信息还是旧的,训练时就会跳过新图片、沿用旧标签。解决方式很直接——每次调整数据集后删掉这个文件再重新训练,或者训练指令里加cache=False强制不走缓存。我在项目里习惯写个小命令rm -rf labels.cache放在训练脚本开头,宁可每次多花十几秒扫描,也不赌缓存一定是最新的。

2.3 results.csv 与 events.out.tfevents:训练结果怎么看

results.csv是训练过程的“成绩单”,每一行是一个 epoch 的指标。YOLO11 训练时标准列大致是:epoch、train/box_loss、train/cls_loss、train/dfl_loss、metrics/precision(B)、metrics/recall(B)、metrics/mAP50(B)、metrics/mAP50-95(B)、val/box_loss、lr/pg0这些。训练完不用等脚本打印,直接拿 pandas 读这个文件就能画出曲线。

import pandas as pd df = pd.read_csv('results.csv') # 列名里可能带空格,先清理 df.columns = [c.strip() for c in df.columns] # 看最后 5 轮的 mAP50-95 变化 print(df[['epoch', 'metrics/mAP50(B)', 'metrics/mAP50-95(B)']].tail(5))

训练中断后想恢复,也要靠这个文件判断之前跑到哪个 epoch。events.out.tfevents.*是 TensorBoard 的事件文件,训练代码里一般已经埋了tensorboard回调,想可视化的话在项目目录下跑tensorboard --logdir .,浏览器打开http://localhost:6006就能看到 loss 曲线和 mAP 曲线。我一般只用它看一个东西:train/box_loss和val/box_loss之间的剪刀差——训练 loss 一直降、验证 loss 拐头上升,就是过拟合信号,这时候再调数据增强比调模型结构管用。

3. 把西红柿数据集转成 YOLO 格式:01划分数据集.py 的划分逻辑与 data.yaml 生成

3.1 YOLO txt 标签格式与数据集目录约定

YOLO 系列训练不是直接读 xml 或 json 标注,它要求每张图片对应一个同名 txt,txt 每行描述一个目标框,格式是五个数字:类别id x_center y_center width height。注意后四个值全部是归一化坐标,也就是用像素值除以图片宽高,范围在 0 到 1 之间。这套西红柿数据集里已经有标注文件了,但原始标注长什么样不重要,重要的是01划分数据集.py会统一转成这种 txt。

目录约定建议按照 ultralytics 默认结构来:

tomato_dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── train.txt ├── val.txt └── data.yaml

images/train放训练图片,labels/train放对应 txt,val 同理。train.txt和val.txt是图片路径列表,训练时按这个名单找图。data.yaml 告诉模型“类别有几个、叫什么、训练集在哪”。

3.2 划分脚本的核心逻辑:shuffle、比例、路径统一

01划分数据集.py做的事可以拆成四步:扫描所有图片、打乱顺序、按比例切分训练集和验证集、把切分结果写进 txt。下面这段是这类脚本最常见的最小实现,逻辑和资源里的脚本等价,你可以打开自己的 01 文件对照着看。

import os import random from sklearn.model_selection import train_test_split # 你的数据根目录,改成实际路径 root = 'tomato_dataset' image_dir = os.path.join(root, 'images_all') # 未划分前的图片总目录 # 1. 收集所有图片路径 all_images = [] for img_name in os.listdir(image_dir): if img_name.lower().endswith(('.jpg', '.jpeg', '.png')): all_images.append(os.path.join(image_dir, img_name)) # 2. 切分:常见比例是 8:2,样本少就 9:1 train_imgs, val_imgs = train_test_split( all_images, test_size=0.2, random_state=42 ) # 3. 写 train.txt / val.txt,注意写的是绝对路径 with open(os.path.join(root, 'train.txt'), 'w') as f: f.write('\n'.join(train_imgs)) with open(os.path.join(root, 'val.txt'), 'w') as f: f.write('\n'.join(val_imgs)) print(f'train: {len(train_imgs)}, val: {len(val_imgs)}')

train_test_split的test_size=0.2就是验证集占 20%,西红柿这类单一类别小数据集,验证集比例太高会导致 mAP 曲线抖得厉害,我一般会压到 15%。random_state=42是随机种子,固定它保证每次划分结果一致,否则跑两次训练数据分布不一样,对比实验就不公平。写 txt 时建议写绝对路径,YOLO 训练时对相对路径的容错很差,报AssertionError的概率很高。

3.3 data.yaml、train.txt、val.txt 如何串起来

划分脚本生成data.yaml后,训练时它就是唯一的“地图”。一个单类别西红柿检测的 data.yaml 长这样:

# data.yaml train: /absolute/path/to/tomato_dataset/train.txt val: /absolute/path/to/tomato_dataset/val.txt nc: 1 names: ['tomato']

train和val指向的是 txt 文件本身,而不是图片目录。这是很多新手写 yaml 最容易搞错的地方——写成train: /path/to/images/train会让模型认为每个子目录是一类,训练直接报错。nc是类别数量,西红柿只有一类就写 1;names用列表按 id 顺序排列,第 0 个就是tomato。如果以后想加“未成熟西红柿”这个类,就改成nc: 2、names: ['tomato', 'green_tomato'],同时所有标签 txt 里的类别 id 要对应修改。

3.4 从 VOC xml 或杂乱文件夹迁移时怎么改

这套资源自带的数据集已经组织好了,但很多人手头是另一批数据,比如从网上下的是 VOC 格式 xml 标注,或者图片散落在一个大文件夹里没分类。这时候 01 脚本不能直接跑,要加一步格式转换。常见做法是用xml.etree.ElementTree解析 xml,把<bndbox>里的xmin, ymin, xmax, ymax换算成 YOLO 需要的中心点加宽高公式:

# VOC 坐标转 YOLO 坐标 x_center = ((xmin + xmax) / 2) / img_width y_center = ((ymin + ymax) / 2) / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height

换算完的四个值必须都在 0 到 1 之间,超出说明标注框越界了,训练时会被过滤掉,直接表现就是“训练集图片数量对得上,但有效标注少了一半”。我踩过一次这种坑:一批西红柿图片是从高处俯拍的,部分果实被叶片完全遮住,标注员把遮挡区域也框进去,导致 width 或 height 接近 1,归一化后越界。解决办法不是删图,而是跑一遍标签校验脚本,打印出所有越界标注的文件名,一个个手动修。这种脏数据在labels.cache里其实已经标出来了,但只有用前面那段 pickle 脚本才能看到,UI 界面上不显示。

4. 训练自己的西红柿检测模型:02train.py 的参数设置与日志解读

4.1 训练启动前要确认的四个环节

跑02train.py之前,先按顺序确认四件事,能省下半天排错时间。第一,确认data.yaml里的路径是绝对路径且真实存在,train.txt里第一行路径用cat train.txt | head -1看一眼,能访问到再继续。第二,确认显卡可用,命令行跑python -c "import torch; print(torch.cuda.is_available())",输出True才走 GPU,输出False说明装的 pytorch 是 CPU 版,得按 CUDA 版本重装。第三,确认labels.cache已经删除,尤其在你动过数据集之后。第四,看一眼requirement.txt里 ultralytics 的版本,YOLO11 是较新的模型,太老的 ultralytics 根本读不到yolo11n.pt这个权重文件。

4.2 训练参数怎么给:epochs、batch、imgsz、device 的选择

02train.py内部通常就是一行 ultralytics 的训练调用,核心参数全部是 API 的参数。常见写法如下:

from ultralytics import YOLO model = YOLO('yolo11n.pt') # 从预训练权重开始,收敛快 model.train( data='tomato_dataset/data.yaml', epochs=100, # 训练轮数 batch=16, # 每批图片数 imgsz=640, # 输入尺寸 device=0, # 0 表示第一张显卡,CPU 训练写 'cpu' workers=4, # 数据加载线程数 patience=20, # mAP 连续 20 轮不涨就早停 optimizer='AdamW', # 优化器 lr0=0.001, # 初始学习率 seed=42, # 固定随机种子 cache=False, # 强制不走 labels.cache )

参数关键点逐个说。epochs不要迷信越大越好,西红柿检测场景下 100 轮足够,配合patience=20早停机制,实际可能 50 轮就停了,这样能省至少一半时间。batch纯看显存,16G 显存跑 yolo11n 可以给 32,8G 就给 8 或 16,爆显存时报错信息是CUDA out of memory,下面的避坑章细说。imgsz=640是性价比最高的输入尺寸,西红柿这种中等大小的目标 640 完全够;想要更高精度可以试 960,但训练时间会多一半以上,推理速度也会下降。lr0=0.001是 AdamW 下比较稳的初始值,SGD 的话建议0.01起步。

4.3 训练日志与 results.csv:正常收敛长什么样

训练开始后终端会每轮打印一张表格,包含box_loss、cls_loss、dfl_loss、precision、recall、mAP50、mAP50-95这些列。西红柿检测场景,正常的收敛曲线应该是这样的:前 10 轮box_loss从 0.1 量级快速掉到 0.04 左右,mAP50 从 0.2 涨到 0.7;中间 20 到 60 轮进入平台期,mAP50 在 0.85 上下波动;如果后段出现训练 loss 继续降、验证 loss 反弹,说明过拟合,回退到验证 loss 最低的那个 epoch 用它的权重。

比较隐蔽的问题出现在“收敛太快”上。西红柿数据集如果只有几百张图、背景又比较单一(比如都是大棚里拍的),前 10 轮 mAP50 可能直接冲到 0.95,看起来很爽,但换一批光照不同的图片立刻掉到 0.5 以下。判断模型是不是“背题”了,最直接的办法就是看results.csv里训练集和验证集的 mAP 差距:验证集 mAP 比训练集低 10 个点以上,基本可以断定过拟合了。这时候优先做两件事,一是增加mosaic=0.5之类的数据增强参数,二是提升val集图片的场景多样性,而不是继续加 epochs。

4.4 中断训练、改参数继续跑的后悔药

训练到一半老板说“效果不错换个方式再跑”,或者机房断电训练中断,这两件事都有后悔药。ultralytics 的train()支持断点续训:先找到上次训练保存的last.pt权重,然后加载它继续跑。

model = YOLO('runs/detect/train3/weights/last.pt') model.train( data='tomato_dataset/data.yaml', epochs=100, # 这里的 epochs 是总轮数,不是剩余轮数 resume=True, # 自动读取 last.pt 对应训练状态 )

resume=True会自动续上之前的状态、学习率调度和优化器参数,不用手动指定起始 epoch。需要注意的是epochs要填总轮数,代码会自己跳过已完成的轮数。想改数据增强或者学习率再训练,同样是加载last.pt,但不加resume=True,相当于基于已有权重继续微调。从这里开始你的results.csv会接着追加新训练结果,原文件会保留第一段曲线,对比着看就能发现哪次参数更有效。我习惯把每一次实验的results.csv拷贝一份带时间戳的副本,比如results_20250116_epoch50.csv,等项目做完回看这些文件就是完整的调参轨迹。

5. 三个 py 文件连跑的常见问题排查:从 labels.cache 到 PyQt 界面的坑

5.1 现象:01 脚本跑完,train.txt 和 val.txt 是空文件

原因基本只有一个:源图片目录路径写错了。01 脚本里通常硬编码了一个图片根目录,比如source_images或者raw_data,如果你的数据集解压后目录名对不上,os.listdir扫不到任何图片,train_test_split拿到空列表,写出的 txt 自然为零字节。还有一个隐蔽情况是图片扩展名大小写混用,集合里写了.jpg但目录里全是.JPG,Windows 下能扫到、Linux 下全部漏掉。

解决:在扫描图片那段代码后面加一行打印print(len(all_images)),先确认扫到了几张图再往下走。如果路径没问题但数字是 0,改成遍历所有文件再按扩展名过滤的写法:

for f in os.listdir(img_dir): if os.path.isfile(os.path.join(img_dir, f)) and f.split('.')[-1].lower() in ['jpg', 'jpeg', 'png']: all_images.append(os.path.join(img_dir, f))

5.2 现象:02train.py 一启动就报 CUDA out of memory

这是 yolo11 训练最常见问题,原因不一定真是显存不够,更多是batch和workers设置不合理。yolo11n 在 640 分辨率下显存占用约 4GB 每 16 张图,8GB 显存跑batch=16已经到极限了。另一个被忽略的因素是workers线程数,开太多会额外占显存做数据预处理。

解决:先把batch降到 8,workers降到 2,能跑通再逐步加。如果必须要大 batch,在 train 参数里加device=0指定单卡,避免 ultralytics 默认尝试多卡并行导致显存翻倍。还有一招是开启梯度累积,ultralytics 支持batch=-1自动按显存选择最大 batch,虽然会慢一点,但至少不会中途崩掉。

5.3 现象:训练到一半 loss 变成 NaN,然后 mAP 全变 0

训练 loss 变 NaN 的原因比较集中:学习率太大、标签里有空 txt 或全零坐标、数据里有损坏图片。西红柿数据集里偶尔会有完全被遮挡的果实,标注员可能只点了几个像素,归一化后 x_center 和 width 都很小,接近 0,这类极端的框容易在损失计算时产生数值溢出。

解决:先删掉labels.cache重新扫描一遍,看有没有图片找不到标签的警告;再检查labels目录下 txt 文件是否有空文件,find labels/ -size 0 | head直接列出来。学习率方面,lr0从0.001降到0.0005重跑一次。如果前 10 轮就 NaN,九成是数据问题,不是参数问题。

5.4 现象:03pyqt.py 加载图片后程序直接卡死或闪退

PyQt 界面卡死,大概率不是检测模型的问题,而是图片路径问题。Windows 下如果图片路径包含中文(比如C:\Users\张三\Desktop\tomato.jpg),OpenCV 的imread会读不到图像,返回一个空对象,后续画框操作直接报空指针错。另一类是图片分辨率过大,读取后没做缩放直接送进模型,推理耗时几十秒,界面自然像“死机”了。

解决:代码里读取图片后、推理前,强制做一次尺寸规整。用统一转换路径的方式规避中文问题:

import cv2 import numpy as np def read_image(path): # 用 numpy 读字节流,绕开 cv2.imread 的中文路径问题 data = np.fromfile(path, dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR) return img img = read_image('你的图片路径.jpg') img = cv2.resize(img, (640, 640)) # 与训练 imgsz 保持一致

界面里再兜底加一层判断:if img is None: return,弹个对话框提示用户换张图,而不是让程序闪退。

5.5 现象:检测框太多误检,把叶子和土壤也框出来了

这个现象只发生在推理阶段,训练指标一切正常。原因不是模型坏了,而是推理时的置信度阈值太低。YOLO 默认conf=0.25,西红柿在自然光下和叶片颜色接近时,模型会给出大量 0.1 到 0.3 的预测框,默认阈值全被当成目标了。

解决:跑推理时手动调高置信度。面对大棚场景,我用conf=0.35起步,叶片遮挡多再提到0.45。想保留低置信度框但控制数量,就同时调iou=0.5做更激进的 NMS 合并。03pyqt.py 里model.predict(source=img, conf=0.4, iou=0.5)改成这两个值,误检会明显下降,代价是极个别被严重遮挡的西红柿会漏检,这属于权衡,没有两全的参数组合。

6. 03pyqt.py 的进阶技巧:把单图检测升级成批量推理与 CSV 导出

PyQt 界面点一张图测一张,验证模型没问题,但真要统计一棚西红柿的产量,这样效率太低。我拿到这套资源后的第一步改动,就是给 03pyqt.py 加一个批量处理入口:选一个文件夹,遍历所有图片跑推理,把每张图的检测框数量、平均置信度、所有框坐标写进 CSV。这样不仅能算出整批图片的果实总数,还能顺带验证模型的泛化能力——如果某个批次的平均置信度明显低于其他批次,说明这批图片的拍摄条件跟训练集差异大。

核心改动是在检测按钮的槽函数后面加一个批量分支:

import os import csv import time from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') def batch_inference(folder_path, csv_path): rows = [] for img_name in os.listdir(folder_path): if not img_name.lower().endswith(('.jpg', '.png', '.jpeg')): continue img_path = os.path.join(folder_path, img_name) t0 = time.time() results = model.predict( source=img_path, conf=0.4, iou=0.5, imgsz=640, verbose=False ) infer_ms = (time.time() - t0) * 1000 r = results[0] boxes = r.boxes.xyxy.cpu().numpy() # 左上右下坐标 confs = r.boxes.conf.cpu().numpy() # 置信度 rows.append([ img_name, len(boxes), round(float(confs.mean()), 4) if len(confs) else 0, round(infer_ms, 1) ]) with open(csv_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['图片名', '检测框数', '平均置信度', '推理耗时ms']) writer.writerows(rows)

这里有个细节值得注意:boxes.xyxy返回的是 CPU 张量,要先转 numpy 再取数,直接对 CUDA 张量做循环会拖着数据在 GPU 和 CPU 之间反复搬运,批量跑的时候会慢得明显。另外每张图都单独调一次predict是有开销的,更快的做法是把整个文件夹路径直接传给model.predict(source=folder_path),让 ultralytics 内部批量推理,但对 PyQt 界面来说逐张调用更便于更新进度条,两难之下我选逐张处理,因为一套西红柿图也就几百张,时间差在可接受范围内。

跑完批量推理后,用time.time()包住推理那一段,还能顺手验证自己的硬件水平:yolo11n 在 1080Ti 上跑 640 分辨率大概每张 25 到 35ms,在纯 CPU 上大概是 800 到 1500ms。如果发现 CPU 推理慢到不可用,说明这份资源更适合“先离线训练、再在带显卡的机器上做识别”的部署路径,不要幻想笔记本 CPU 能实时跑视频流。

从那以后我每次拿到目标检测项目,都强制自己先跑一遍批量推理并导出 CSV,而不是在界面上点十张图凭肉眼判断“效果不错”。检测框数量分布、置信度分布、单张耗时,这三个数字比截图更能说明模型真实水平。希望帮到你。

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

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

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

立即咨询