基于YOLO11的蔬菜识别检测系统:数据集处理、模型训练与PyQt5界面开发实战
发布时间:2026/10/2 14:41:02来源:尧图网络
简介基于YOLO11深度学习的蔬菜识别检测系统是一份可直接运行的完整项目资源适合计算机视觉、人工智能及相关专业的在校学生、毕业设计者或企业开发者用于课程作业、课设演示与实践进阶。项目覆盖数据标注、模型训练到界面部署的完整流程内置PyQt5图形界面开箱即用支持西红柿、洋葱、土豆、胡萝卜、大白菜5类常见蔬菜的实时识别与检测。资源包共计2000个文件约47.24MB核心内容包括jpg图片数据集、txt格式标注文件、YOLO11模型权重、Python源码、yaml配置文件、评估指标曲线及演示图片与视频等结构清晰便于按模块查阅和二次开发。目前已吸引168人学习浏览附带的安装使用教程和训练好的模型可显著降低上手门槛适合作为毕设、课设或实战项目的直接参考与扩展基础。1. 基于YOLO11深度学习的蔬菜识别检测系统源码包里最容易被忽视的三个硬门槛如果你正在找基于YOLO11深度学习的蔬菜识别检测系统大概率是被「带GUI界面」「1026张标注好的数据集」「训练好的模型」这几个字吸引来的。这个系统要解决的事情很明确输入一张蔬菜照片或打开摄像头界面里立刻画出每个蔬菜的框、类别名和置信度顺带生成评估指标曲线方便你判断模型到底行不行。适合正在做毕设、课程设计或者农产品分拣演示的人。但「开箱即用」这四个字通常要打八折。我帮人调试过很多类似的YOLO源码包最后卡住的位置几乎不在YOLO11本身而在PyQt5界面启动、数据集标签格式转换、训练环境这三个环节。这篇文章把整条链路拆开先讲为什么YOLO11适合做这个任务再给PyQt5界面的线程写法、标注转换和训练命令最后把拿到这种包最容易翻车的地方一次说清。2. 选型与数据准备YOLO11网络结构改了什么1026张图够不够用2.1 YOLO11 和 YOLOv8 的差异C3k2、C2PSA 对蔬菜目标意味着什么先说结论YOLO11不是YOLOv8的简单换皮而是Ultralytics在v8之后重新整理过的检测模型。网上经常有人把YOLO11和SAM3放在一起比较其实两者任务完全不同SAM3是做分割的YOLO11是做检测的。对蔬菜识别这类应用要的是“这个框是番茄、这个框是黄瓜”YOLO11输出的边界框更直接也更适合接进PyQt5界面做实时绘制。结构上比较明显的三处变化是这样的模块YOLOv8里YOLO11里对蔬菜识别的意义主干基础块C2fC3k2减少冗余计算训练收敛更稳注意力机制基本靠外部插件C2PSA内建提升遮挡、重叠目标的特征响应检测头解耦头解耦头轻量化调整同类目标密集时定位更准C2PSA是YOLO11里值得注意的一个点。它把注意力机制放进瓶颈结构里相当于让网络在提取特征时主动去关注“哪里是番茄轮廓”和“哪里只是叶子纹理”。实际做蔬菜识别时番茄和红色辣椒颜色接近黄瓜经常被藤蔓挡住一半这类视觉混淆不是靠加深网络能解决的注意力结构比单纯堆参数更划算。参数选择上我一般从YOLO11s起步不直接上最大规格。蔬菜识别不是那种需要极致精度的遥感场景1026张图的数据量也不支持你把模型做得很大。s规格在GTX 1660这类老卡上还能跑实时推理训练时间也控制在几小时以内。如果只是验证流程用yolo11n更快把数据跑通再回头换模型都来得及。2.2 1026张标注图够不够用看类别数也看分布这是判断整个系统可不可信的第一道关。1026张图听起来不少但拆到具体类别上差距会非常大。类别总数每类平均图片数我的判断16类每类120张以上配合数据增强够训练出可演示的模型712类每类60100张边缘案例会漏检需要严格划分验证集12类以上每类不足80张会有至少23类AP明显掉队建议补数据我见过最典型的翻车场景包里有1026张图但番茄占了500张胡萝卜只有30张。训练完看总mAP还行一打开GUI识别胡萝卜就漏。原因很简单模型对胡萝卜的“记忆”太少了不是YOLO11不行。数据增强可以缓解但救不了极端失衡。如果class_id分布严重倾斜优先做两件事一是把验证集固定留出10%20%保证每类在验证集里都出现二是把mosaic增强打开。Ultralytics在yolo11训练配置里默认启用了mosaic前期epoch会拼四张图训练这对小目标、遮挡目标很有用。数据集越小越不要用默认的随机划分跑一遍就完事。2.3 把标注文件整理成YOLO格式目录结构、data.yaml和划分脚本拿到源码包后第一件事不是急着跑GUI而是先把数据集目录标准化。YOLO11训练时只认一种目录约定images和labels放同级目录train和val分开每张图片对应一个同名txt。推荐结构如下vegetable/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/配套的data.yaml是模型知道“去哪里读图、类别叫什么”的唯一入口。一个能直接用的data.yaml长这样path: D:/yolo_project/vegetable train: images/train val: images/val test: images/test names: 0: tomato 1: cucumber 2: pepper 3: eggplant 4: carrot注意path要写绝对路径。第一次用Ultralytics的人经常漏了这一项结果训练时报错“找不到images/train”。names的顺序必须和标签txt里的class id一一对应比如类别0是tomatotxt第一列写0不能写1。下面是常用的划分脚本把带标签的图片按比例分到train和val同时把同名标签txt一起复制过去import os import random import shutil IMG_SUFFIX {.jpg, .jpeg, .png, .bmp} SEED 42 VAL_RATIO 0.2 def split_dataset(img_dir, label_dir, out_dir): random.seed(SEED) for part in (train, val): for sub in (images, labels): os.makedirs(os.path.join(out_dir, part, sub), exist_okTrue) images [n for n in os.listdir(img_dir) if os.path.splitext(n)[1].lower() in IMG_SUFFIX] random.shuffle(images) val_count max(1, int(len(images) * VAL_RATIO)) val_names set(images[:val_count]) train_n 0 for name in images: stem os.path.splitext(name)[0] label_path os.path.join(label_dir, stem .txt) if not os.path.exists(label_path): print(f[跳过] 缺少标签: {name}) continue part val if name in val_names else train shutil.copy(os.path.join(img_dir, name), os.path.join(out_dir, part, images, name)) shutil.copy(label_path, os.path.join(out_dir, part, labels, stem .txt)) train_n (part train) print(ftrain: {train_n}, val: {val_count}) if __name__ __main__: split_dataset(raw_images, raw_labels, vegetable)这段脚本做了三件事固定随机种子保证每次划分一致提前跳过没有标签的图片避免模型拿空标签训练只复制同名标签不重命名最大限度保留原包的文件对应关系。参数里VAL_RATIO是验证集比例小数据集我习惯设0.2每类都有样本被留出来做验证而不是全丢进训练集刷mAP。3. PyQt5 界面YOLO11 推理放进 GUI 的线程模型与实时绘制3.1 摄像头识别为什么一开就无响应PyQt5 主线程里千万别跑推理循环拿到带GUI的源码包最常见的翻车是“双击运行后窗口能开一按开始识别就白屏、转圈、未响应”。90%的原因是代码把cv2.VideoCapture的读取循环直接写在了按钮的槽函数里导致Qt主线程被帧读取和模型推理占死。PyQt5界面刷新依赖事件循环而事件循环必须由主线程驱动。如果你在槽函数里写一个while True去读摄像头每一帧的模型推理占几十毫秒界面就没机会重绘表现出来就是“假死”。处理这类问题的标准做法是把推理丢到QThread里线程只发信号、不改控件。下面是我常用的Worker模板from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectWorker(QThread): frame_ready pyqtSignal(object, object) error pyqtSignal(str) def __init__(self, model_path, source0, conf0.25, parentNone): super().__init__(parent) self.model_path model_path self.source source self.conf conf self.running True def run(self): from ultralytics import YOLO try: model YOLO(self.model_path) cap cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ok, frame cap.read() if not ok: break results model.predict(frame, confself.conf, verboseFalse) self.frame_ready.emit(frame, results[0].boxes) cap.release() except Exception as exc: self.error.emit(str(exc)) def stop(self): self.running False self.wait(2000)逻辑说明YOLO模型的加载放在run()里而不是放在主线程创建Worker时这样能避免窗口初始化变慢。每检测一帧就通过frame_ready信号把原始frame和boxes抛出去界面槽函数只负责画框和刷新。predict参数里verboseFalse很关键否则终端被Ultralytics的日志刷爆窗口还是会卡。实际使用时Worker要保存成self.worker之类的实例属性不能写成局部变量。局部变量会在函数结束后被Python垃圾回收线程对象没了界面收不到任何信号看起来就像摄像头没反应。3.2 把检测框画到 QLabel 上BGR 转 RGB、strides 和 .copy() 一个都不能少线程把结果发出来后界面侧要完成三件事画框、把OpenCV的BGR图像转成Qt认识的RGB、塞进QLabel。这一步很多源码包写得潦草导致界面要么颜色偏蓝要么人物和蔬菜框错位。下面这段代码是槽函数的标准实现from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt import cv2 def update_frame(self, frame, boxes): names self.class_names # 从data.yaml读出来的names列表 for box in boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy().astype(int) score float(box.conf[0]) cls_id int(box.cls[0]) label f{names[cls_id]} {score:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, rgb.strides[0], QImage.Format_RGB888).copy() self.label_camera.setPixmap( QPixmap.fromImage(qimg).scaled( self.label_camera.width(), self.label_camera.height(), Qt.KeepAspectRatio))这里有两个容易踩的坑。第一个是坐标Ultralytics返回的xyxy已经是原图坐标模型内部做letterbox缩放后会自动映射回原始分辨率所以不需要自己在PyQt5侧再换算。第二个是QImage如果直接传rgb.data而不调用.copy()图像数据可能被垃圾回收界面会出现花屏或绿屏。QImage构造函数里的rgb.strides[0]是图像行字节数比写死w * 3更稳妥遇到宽度奇数或内存对齐问题时不至于错位。3.3 三种启动方式与关闭清理摄像头、单张图片、批量文件夹一个完整的蔬菜识别GUI通常有多个入口。摄像头用于演示单张图片用于验证批量文件夹用于批量测试。切换入口的常见写法是这样def start_camera(self): if self.worker is not None: self.worker.stop() self.worker DetectWorker(best.pt, source0) self.worker.frame_ready.connect(self.update_frame) self.worker.error.connect(self.show_error) self.worker.start() def open_single_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, , Images (*.jpg *.png *.bmp)) if path: self.run_once(path) def open_folder(self): folder QFileDialog.getExistingDirectory(self, 选择文件夹) if folder: self.run_batch(folder)每次start_camera前先调用stop()是为了避免上一次线程还在跑就重新start导致两个线程同时读摄像头、界面收到乱序信号。关闭窗口时也要在closeEvent里主动停线程def closeEvent(self, event): if self.worker is not None: self.worker.running False self.worker.quit() self.worker.wait(2000) event.accept()不然就会出现“GUI关掉了但摄像头指示灯还亮着进程杀不死”的诡异现象。wait(2000)是给线程两秒收尾避免界面关闭瞬间线程里还在做模型推理导致崩溃。4. 数据集格式转换与训练1026张标注数据如何变成能用的best.pt4.1 labelme转YOLO矩形框和任意多边形两种标注都要兼容很多蔬菜数据集是用labelme标注的labelme默认输出JSON文件而YOLO11训练只认txt格式。如果你拿到的是JSON标注第一个任务就是写转换脚本。转换分两步读出每个多边形或矩形框的外接框然后除以图片宽高做归一化。import json import os def labelme_to_yolo(json_path, out_dir, class_names): with open(json_path, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] txt_path os.path.join( out_dir, os.path.splitext(os.path.basename(json_path))[0] .txt ) lines [] for shape in data[shapes]: label shape[label] if label not in class_names: continue cls_id class_names.index(label) points shape[points] if shape[shape_type] rectangle: (x1, y1), (x2, y2) points else: # polygon xs [p[0] for p in points] ys [p[1] for p in points] x1, y1, x2, y2 min(xs), min(ys), max(xs), max(ys) # 边界保护防止标注点稍微越界导致负宽度 x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) if x2 x1 or y2 y1: continue x_center ((x1 x2) / 2) / img_w y_center ((y1 y2) / 2) / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) if lines: with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))这段脚本兼容rectangle和polygon两种形状polygon的points是一个多顶点列表所以用min/max取外接矩形。所有坐标都以JSON里的imageWidth和imageHeight为分母因为YOLO格式要求0到1之间的相对坐标。边界保护是关键labelme手工标注时偶尔会把点拖出图片边界不裁剪就会写出x_center大于1的标签模型无法从这种样本里学到正确信息。4.2 训练命令与关键参数从预训练权重开始而不是从零开始数据集整理成YOLO格式并写好data.yaml之后训练本身其实只有一条命令。但参数怎么设直接决定你是在“高效迁移学习”还是“浪费时间复训”。yolo detect train \ dataD:/yolo_project/vegetable/vegetable.yaml \ modelyolo11n.pt \ epochs120 \ imgsz640 \ batch16 \ device0 \ patience20 \ projectruns/detect \ namevegetable_exp训练前先确认data.yaml里的path绝对路径存在Windows下尤其不要用中文路径。modelyolo11n.pt表示加载官方预训练权重第一次运行会自动下载到当前目录。预训练权重的作用是让模型从COCO学到的通用特征起步而不是从随机参数开始“硬背”蔬菜特征。这点非常重要蔬菜检测本身不是特别偏门的任务预训练权重能省大量训练时间。epochs我习惯给到120配合patience20做早停如果连续20个epoch验证mAP没有提升训练自动停止。数据量小的时候训练到第60到第90个epoch之间就会收敛硬跑满120反而会过拟合。batch16对应16GB显存如果你的显卡只有8GB改成8摄像头演示用的模型用yolo11n也不丢人。device0表示用第一块GPU不确定环境是否支持CUDA时先用devicecpu跑两个epoch确认代码链路没问题再切回GPU。深度学习训练排错有一个土办法第一次先用一个很小的epoch数跑通比如epochs3成功跑完再改回最终配置。这样能把环境错误和数据路径错误快速筛出去而不是等了两个小时才报错。4.3 评估指标曲线results.csv里到底看哪几列训练跑完后Ultralytics会在runs/detect/vegetable_exp目录下生成一串文件weights/best.pt、weights/last.pt、results.csv、confusion_matrix.png、PR_curve.png、val_batch_pred.jpg等。所谓“评估指标曲线”通常指的就是PR曲线和mAP曲线。很多人只盯着训练结束时的日志看其实更推荐自己把results.csv拉出来画一遍因为训练中途就崩溃的包不会给你看曲线但凡给出曲线你也可以从中判断这模型是“正常过拟合”还是“数据标注有硬伤”。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/detect/vegetable_exp/results.csv) print(df.columns.tolist()) # 先看列名不同版本写法略有差异 plt.figure(figsize(9, 5)) plt.plot(df[epoch], df[metrics/mAP50(B)], labelmAP50) plt.plot(df[epoch], df[metrics/mAP50-95(B)], labelmAP50-95) plt.plot(df[epoch], df[metrics/precision(B)], labelprecision) plt.plot(df[epoch], df[metrics/recall(B)], labelrecall) plt.xlabel(epoch) plt.ylabel(score) plt.grid(True) plt.legend() plt.savefig(vegetable_metrics.png, dpi200)看这条曲线时有一个习惯可以参考mAP50是“框有没有框对”的直接指标蔬菜这类目标IoU要求不算苛刻mAP50能到0.85以上就具备演示价值mAP50-95更严格0.5以上说明模型定位比较准。如果是训练集mAP99、验证集mAP60不要怀疑模型玄学先回去检查验证集里是不是混了难样本或者标注错的框这是过拟合信号不是系统bug。5. 常见问题排查与避坑YOLO11 加 PyQt5 的五个翻车现场5.1 装完环境 import 直接报错先分清是 torch 问题还是 ultralytics 问题现象按教程装完ultralytics后运行训练命令或启动GUI立刻报错内容涉及torch或者CUDA比如“Torch not compiled with CUDA enabled”或导入包时直接ModuleNotFoundError。原因YOLO11依赖PyTorch但pip install ultralytics时会按默认依赖安装torch。Windows环境下默认安装的PyTorch经常是CPU版本或者和你本机CUDA版本不对应。深度学习环境配置不是“装了就能用”模型加载到device0时找不到可用设备就直接崩。解决先跑一行命令确认底子python -c import torch; print(torch.__version__, torch.cuda.is_available())输出里显示torch版本和False说明当前torch根本不支持GPU。如果只是想快速把GUI跑起来看效果把训练和检测命令里的device0改成devicecpu虽然慢但能证明整个链路是通的。如果确实要用GPU卸载torch后从PyTorch官方给的安装命令重新装不要用残缺的版本。这个坑和具体项目无关几乎所有YOLO系列项目都会遇到先排查环境比改代码高效得多。5.2 labelme 与 PyQt5 打架装完标注工具自己的 GUI 打不开了现象为了补标注或者看数据集你在项目同一个环境里pip install labelme之后运行自己的PyQt5界面提示找不到PyQt5.sip或者报“could not load the Qt platform plugin”。原因labelme依赖PyQt5pip在安装时会把本来的PyQt5或PyQt5-Qt5升到它能用的版本结果和PyQt5.sip对不上。这类包之间互相挤版本的问题在Python桌面应用里很常见本质不是YOLO11的问题但确实会卡你半天。解决不要在一个环境里同时搞标注和推理。用conda单独开一个环境给labelme比如conda create -n labelme python3.8然后在里面pip install labelme。主项目环境保持干净只装PyQt5和ultralytics。如果已经搞坏了就先把相关包全卸掉再重装pip uninstall -y PyQt5 PyQt5-sip PyQt5-Qt5 pip install PyQt5重装后先跑一下自己GUI里最简单的窗口确认平台插件正常再继续接模型。偷懒想省这一步的大概率会在深夜被一堆莫名奇妙的Qt报错折磨。5.3 loss 正常但 mAP 一直很低先检查标签坐标是不是“看起来合理”现象训练loss在下降但验证集mAP卡在0.4甚至0.2提不上去。原因最常见的是标签坐标归一化错误。labelme转换脚本里用了错误的图片尺寸做分母或者x_center、w这些值超过[0,1]范围。Ultralytics不会因为这个直接报错而是把越界框当成无效信息模型学不到正确目标。另一个常见原因是class id和names顺序对不上你以为第0类是番茄模型以为第0类是黄瓜训练过程不会报错但mAP会灾难性地低。解决训练前先从train/labels里随机抽样几个txt用脚本把框画回原图肉眼确认一遍import cv2 def check_label(img_path, txt_path, out_path): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cid, xc, yc, bw, bh map(float, parts) 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.imwrite(out_path, img)只画前10张图每张看一眼。框歪了、框太小、框在背景上这些问题一眼就能发现。很多所谓的“模型效果差”最后都是标注数据质量差不是网络结构的问题。5.4 中文路径和中文类别名Windows 上最常见的界面假死现象GUI打开一张图片后完全没有反应或者训练时提示FileNotFoundError。把图片复制到英文目录下又能正常工作。原因cv2.imread在Windows默认编码下读不了带中文的路径这是OpenCV的历史包袱和YOLO11、PyQt5都没关系。另一个类似问题是data.yaml里的names用了中文训练能跑但画图、导出模型、ONNX推理时经常出乱码。解决整个项目目录统一用英文和数字图片文件名也改成英文。万一必须处理中文路径用下面这个函数代替cv2.imreadimport cv2 import numpy as np def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)np.fromfile按二进制把文件读出来再用imdecode解码绕开了OpenCV的文件名编码问题。界面上的中文显示没问题按钮、标签随便用中文但文件路径和类别名尽量别用中文这个习惯能帮你躲掉大量诡异问题。5.5 显存不够batch、imgsz、workers 三个参数的取舍现象训练到一半弹RuntimeError: CUDA out of memory或者在Windows下训练时dataloader worker崩了。原因显存占用和batch大小、输入图分辨率近似二次方相关。imgsz从640加到1280单张图的显存消耗翻四倍。很多人直接把默认参数拉满显存不够就怀疑源码有问题其实只是参数没给够余量。解决把训练命令改成batch8、imgsz640、workers0。workers0在Windows上能避开多进程数据加载的兼容问题速度会慢一点但稳定。同时打开混合精度训练Ultralytics默认开启amp显存占用能省出三分之一。如果只有4GB显存就先把模型规格从yolo11s降到yolo11n再不行就老老实实CPU训练epoch数翻倍反正数据集才1026张不是不能等。6. 让 GUI 真正好用三步现场验证加一个置信度滑条6.1 验证系统的三个步骤按顺序做缺一不可拿到训练好的best.pt之后别急着接GUI。先在命令行跑一次正式验证yolo detect val \ dataD:/yolo_project/vegetable/vegetable.yaml \ modelruns/detect/vegetable_exp/weights/best.pt这个命令会输出每个类别的precision、recall和mAP比单纯看loss可靠得多。第二步是在GUI里打开一张没有进过训练集的现场照片看类别对不对、框紧不紧、有没有把青菜叶子当黄瓜。第三步是开摄像头对准蔬菜连续识别十秒观察帧率和置信度波动。演示视频里截出来的最佳帧不具备参考价值连续识别才是真实水平。6.2 给界面加一个置信度阈值滑条比重新训练更能救急现场环境光照一变化固定置信度阈值很容易翻车阈值太低时桌面背景被框出假目标阈值太高时远处的小番茄直接漏检。最有效的补救不是马上重训而是把conf暴露成界面上的滑条。self.slider_conf.setRange(10, 90) self.slider_conf.setValue(25) self.slider_conf.valueChanged.connect(self.on_conf_changed) def on_conf_changed(self, value): self.conf value / 100.0 self.label_conf.setText(f置信度阈值: {self.conf:.2f})然后在所有调用predict的地方传入confself.conf。这个改动不到十行但能让现场演示灵活很多强光背景复杂时往上拉到0.4夜间暗光时降到0.15。花三分钟加个滑条比花三小时重新训练要划算。我自己的习惯是拿到一个类似的源码包永远先花一个晚上把环境、数据格式、界面线程这三件事全部验证完再谈模型效果。因为这三个地方只要有一个是坏的后面所有优化都无从谈起。希望帮到你少走这一段弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网