ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv11的生菜生长周期自动识别系统设计与实现

基于YOLOv11的生菜生长周期自动识别系统设计与实现

生菜生长周期识别,我用YOLOv11搭了一套带界面的检测系统

农业种植里有个很实际的需求:生菜从出苗到采收,不同生长阶段的管理动作完全不一样——苗期要控水,快速生长期要追肥,到了成熟期得赶紧采收,晚了就容易抽薹、纤维化、掉价。但靠人眼判断生长周期,既依赖经验又费时间,一个人盯几十亩大棚,一圈转下来眼睛都花了。所以做一个能自动识别生菜生长周期的系统,在智慧农业场景里非常落地。我这次用YOLOv11做目标检测,配合自建的YOLO数据集,训练了一个能区分生菜不同生长阶段的模型,再封装成带登录注册界面的桌面UI,完整跑通了一套从数据到部署的流程。

这套系统解决的核心问题就是:给一张生菜图像,自动判断它当前处于哪个生长周期。它适合正在入门深度学习视觉方向的开发者、做智慧农业项目的学生,以及想把自己的YOLO模型包装成完整产品的小团队。下面我把整个项目的设计思路、数据集构建、训练调参和UI集成的过程全部拆开讲,每一步都给出实际可复现的方案。

1. 整体设计:一个生菜检测系统应该拆成哪几块

1.1 需求拆解与边界确认

做项目第一件事不是写代码,而是想清楚系统要干什么、做到什么程度。生菜生长周期检测,习惯上分成三个阶段:幼苗期、快速生长期和成熟期。幼苗期叶片小而少,株型紧凑;快速生长期叶片数量明显增加,株型扩张,但还没形成紧实的叶球;成熟期叶球紧实,外观圆整,是采收的最佳窗口。有些需求方还会要求区分种子萌芽期或者抽薹期,但这两个阶段在实际生产里意义不大——萌芽期本质上就是土壤里的一粒种子,视觉特征太弱;抽薹期意味着已经采收晚了,识别出来只能用来做损耗统计。我第一次和需求方聊的时候,他们要求七个类别,后来我建议压缩到三类,准确率提升了十几个点,模型的实用性反而更强。

系统边界也要划清楚:输入端是单张图像或者摄像头实时画面,输出端是类别标签、置信度分数和可视化标注框。简单说,这就是一个单阶段目标检测任务,不是图像分类。区别在于,一张图里可能同时有多个生长阶段的生菜苗,比如移栽后同一片地里,补苗的区域和正常区域会有时期差异,所以用检测框逐个圈出每一株生菜、分别判断它的生长状态,比整图分类更贴近真实场景。

1.2 技术选型和各个模块的协作关系

整个系统按功能拆成三层:数据层负责图像的采集、清洗和标注,模型层负责训练和推理,应用层负责UI交互和结果展示。数据层我用LabelImg标注工具,格式转成YOLO的txt标注格式;模型层用YOLOv11这个最新的YOLO系列模型;应用层用PyQt5做桌面界面。

选YOLOv11不是因为它最新所以最酷,而是它的检测头设计做了一些调整,在同等算力下比YOLOv8有更好的mAP表现,尤其是小目标检测能力有提升。生菜幼苗期的叶片目标不算大,小目标检测能力直接关系到整体识别率。而且Ultralytics框架的封装对新手极其友好,几行代码就能跑通训练和推理,部署落地非常快。如果你用的是老一点的显卡,YOLOv8也完全能胜任这个任务,不一定非要追新版本,关键是数据集质量。

PyQt5做界面是因为Python生态里,它做桌面端交互是最成熟的方案之一。登录注册界面可以用SQLite存用户信息,检测界面用QLabel显示图像,检测结果用QTableWidget或者直接在图像上绘制标注框,这些组件都能快速实现。整体架构图不复杂:用户登录后,选择图片或者打开摄像头,推理线程把图像传给YOLO模型,模型返回检测结果,界面渲染框体和文字信息。

2. 数据集构建:模型效果的上限,在标注这一步就决定了

2.1 数据采集与清洗

很多人跑深度学习项目,恨不能第一周就把模型训出来,结果被数据集狠狠教育了。YOLO系列模型虽然泛化能力不错,但训练数据如果不行,后面再怎么调参都是白搭。这次生菜数据集我前后花了两周时间采集和整理,总共攒下约4200张图像,分为幼苗期、快速生长期、成熟期三个类别。

采集渠道主要有两个:一是去周边的农业种植基地实拍,涵盖不同光照条件、不同品种的生菜,这部分数据最宝贵,模型最终要能在真实大棚环境里跑;二是从公开的农业图像数据集中筛选,进行数据补充。实拍的要特别留意拍摄角度——不要只拍俯视图,模型在部署时可能面对不同安装高度的摄像头,所以平视角度、45度俯角、正上方视角都要覆盖。

清洗是很多人容易偷懒的环节。我拿到原始图像后做了三步筛选:模糊图像直接删掉,因为运动模糊或者对焦不准的图会让模型学到错误的纹理特征;严重遮挡的图要谨慎处理,一株生菜被另一株挡住超过40%就删除,因为标注框里含有大量无关像素;光照严重失衡的图只保留一部分,用于模拟恶劣环境。最后筛选出约3600张有效图像,这个数据量对于三分类检测任务来说属于中等偏上水平,完全够用。

2.2 标注规范:细节定了,后面返工就少了

标注用的工具是LabelImg,这是最经典的YOLO标注工具,操作简单、易上手。安装方式很简单:

pip install labelImg

打开后界面左侧有一排工具按钮,常用的就是画框(Create RectBox)和保存(Save)。标注前先把图片格式统一成JPEG或PNG,标注输出格式选YOLO,这样会生成一个同名txt文件,里面每一行代表一个目标框。

标注的核心规范有三个,我因为前期没定清楚,返工了整整一个下午:

第一,框要贴着生菜的叶片边缘走,但不用精确到每一片叶子的锯齿状轮廓,检测框毕竟是矩形,框住整株生菜的主体部分就行,留一点边缘余量是允许的,目的是让模型关注整体形态而不是局部纹理。

第二,生菜只有露出土面的部分需要标注,根部盖着地膜或者土壤的部分不要框进去,否则大量背景信息会被当成生菜特征学进去。

第三,边界遮挡的个体要单独处理。如果一株生菜在图像边缘只露出一半,建议直接不标,因为这类半截目标只会给模型带来标注噪声。如果是两株生菜挨得很近,各自完整可辨,就分别标框,框的边缘可以挨着,但不要重叠超过20%。

标注文件长这样,每行对应一个目标,分别是类别ID和归一化后的中心坐标、宽高:

2 0.482143 0.516304 0.284821 0.351936 0 0.215625 0.710870 0.103571 0.121803

数值计算方式很直接:中心坐标x等于框的左上角x加上框宽的一半,再除以图像宽度,得到0到1之间的归一化值,宽高同理。

2.3 数据增强与划分

3800张图像训一个三分类检测模型,直接训练也能出效果,但鲁棒性会差一些。我做了在线数据增强,也就是训练阶段实时对图像做变换,不额外占用磁盘空间。主要用的增强手段包括:水平翻转(概率0.5,这对生菜检测非常有效,因为生菜没有方向性特征)、小幅旋转(正负15度以内,模拟摄像头安装偏差)、亮度对比度调整(覆盖早晚不同光照条件)、轻微模糊(增强模型对镜头脏污的鲁棒性)。

数据集划分方面,我按6:2:2切成训练集、验证集和测试集。划分的时候注意一个坑,如果图像序列里有连续拍的几帧,要防止它们被分到不同集合里,否则会造成验证集和训练集高度相似,评估结果虚高。我用视频抽帧方式采集的部分,先按视频片段分组,再整体分配,保证同一片段的帧不会横跨训练集和验证集。

划分完毕后,目录结构整理成YOLO默认的数据集格式,然后写一个data.yaml文件,指向三个目录的路径,并声明类别名称:

train: datasets/lettuce/train/images val: datasets/lettuce/val/images nc: 3 names: ['seedling', 'growing', 'mature']

3. 模型训练与优化:参数怎么定、指标怎么看、坑怎么避

3.1 环境准备与训练参数选择

训练环境用的是单张主流消费级显卡,显存12GB,操作系统是Ubuntu 20.04,CUDA和cuDNN按官方文档配置好。Ultralytics框架的安装非常简单:

pip install ultralytics

装好之后,检测框架是否正常可以用一行命令验证:

from ultralytics import YOLO model = YOLO('yolo11n.pt') results = model.train(data='datasets/lettuce/data.yaml', epochs=100, imgsz=640)

我用的是YOLOv11n,n代表nano,是系列里参数量最小的版本。为什么不用s或者m?因为生菜检测帧率很关键,最终部署要考虑实时性,nano模型推理速度快,而且对单目标类别的检测任务来说精度足够撑起整个项目。当然如果你的算力充裕,用yolo11s.pt起步,mAP一般会高1到2个点。

关键参数我按下面这组来跑:

参数设置值说明
imgsz640输入图像尺寸,越大精度越高但显存占用上升
batch16显存12GB时刚好放得下
epochs120配合早停机制,防止过拟合
optimizerSGD数据量中等时SGD比较稳
lr00.01初始学习率,跑起来如果loss震荡就降到0.005
weight_decay0.0005常规L2正则项
patience1515轮没改善就自动停

这里有个容易踩的坑:数据量不是特别大的时候,很多人喜欢把epochs直接拉到300以上,但这会让模型在后期把训练集的特征背下来,验证集mAP反而下降。我设120轮,配合patience早停机制,实际在80轮左右就收敛了,节省了大量训练时间。

3.2 训练过程观测与指标解读

训练日志里重点盯两个东西:loss曲线和验证集mAP曲线。loss曲线如果持续下降且没有明显反弹,说明模型在学习;mAP50是IoU阈值0.5下的平均精度,这个指标对检测任务最直观,生菜这类目标框定位要求不像自动驾驶那么高,mAP50达到90以上就算很理想了。

我这次训练的结果,三类类别的mAP50分别是:幼苗期0.91、快速生长期0.88、成熟期0.94,综合mAP50约0.90。mAP50-95(更严格的综合指标)在0.62左右,属于中等水平,说明模型的框定位还有提升空间,但对于农业场景来说已经够用,因为农业检测框不需要像素级精确。

有一个实践经验:生菜不同生长阶段的标注框尺寸差异很大。幼苗期框小,成熟期框大,模型在预测时对小目标的回归能力往往弱一些。如果发现幼苗期的置信度明显低于其他两类,可以尝试在训练时开启多尺度训练参数,让模型在不同输入尺寸下都能学到目标特征:

model.train(..., scale=0.5)

表示训练时随机缩放到原始尺寸的50%到150%之间,强制模型适应不同尺度的目标,对提升小目标检测有明显帮助。

3.3 模型导出与格式转换

训练完成后,best.pt保存在runs/detect/train目录下。部署到桌面UI里有两种方式:一种直接加载PyTorch权重,简单方便,但推理速度不是最优;另一种转成ONNX格式再推理,可以获得更快的速度。对于这个项目,我直接用PyTorch权重,因为在PyQt界面里,单张图片推理几十毫秒,用户根本感觉不到差异。

导出ONNX的命令:

yolo export model=best.pt format=onnx imgsz=640

导出后可以顺手用ONNX Runtime做一个简单的速度测试,实测下来在CPU上单张640x640图像推理时间大约120ms,GPU上大约15ms,完全满足实时检测要求。

4. PyQt5界面:从登录到检测结果,一把梭到底

4.1 界面整体布局与功能梳理

UI设计上参考了常见的教务管理系统布局,左侧是功能导航,右侧是操作区域。整个应用分两个界面:登录注册界面和主检测界面。

登录注册界面是系统的第一道门。用户输入用户名和密码,登录成功后才能进入主界面。这部分的数据存储我用SQLite,不需要额外安装数据库服务,轻量可靠,一个文件搞定。密码存储用哈希加密的方式,明文存储密码不仅是懒,而且是很严重的安全隐患。我用hashlib库的sha256加盐处理,虽然对于本地小应用来说有点大材小用,但养成好习惯没坏处。

主界面包含三个核心功能区:图像来源区、模型参数区、结果展示区。图像来源区提供两个按钮,一个打开本地图片,一个打开摄像头实时检测。模型参数区可以调整置信度阈值,默认0.5,这个参数决定了系统对低置信度预测的容忍度。调低会漏掉一些低置信度的真实目标,调高则可能放过一些模糊目标,实际使用时要根据现场摄像头清晰度灵活调整。结果展示区左侧显示原图或带框标注的检测图,右侧用表格展示检测到的每个目标框的类别、置信度和坐标位置。

4.2 登录注册模块实现

登录注册模块的代码结构很清楚,我用一个用户表存用户名和密码hash,注册时先检查用户名是否已存在,不存在则插入新记录。实际写的时候要注意SQLite在桌面应用里的并发问题,虽然本地单用户场景基本不会出现两个线程同时写库,但保险起见,登录和注册函数都用with语句确保连接正常关闭:

import sqlite3 import hashlib def hash_password(password): salt = 'lettuce_project' return hashlib.sha256((password + salt).encode('utf-8')).hexdigest() def register_user(username, password): conn = sqlite3.connect('users.db') cursor = conn.cursor() cursor.execute('CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT)') cursor.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, hash_password(password))) conn.commit() cursor.close() conn.close()

注册时要处理用户名重复、密码长度校验。我这里的逻辑是用户名不少于3个字符,密码不少于6位,并且注册成功后自动跳转到登录页,省去重新输入的麻烦。

登录时查询数据库比对hash值。这里有个细节,比对要用hash对hash,不要在数据库和内存之间传明文密码。整个流程完成后,进入主界面并传当前用户名给主窗口,让状态栏显示“当前用户:xxx”。

4.3 检测流程与结果可视化

主界面的检测逻辑是核心,完整的调用链是这样的:

用户点击打开图片,弹窗选择文件路径,路径赋值给image_path变量;点击开始检测按钮后,调用model.predict()得到检测结果,从结果对象里取出boxes(框坐标)、names(类别名)、conf(置信度);最后把标注信息画到图像上,显示在左侧的QLabel组件里。

检测的核心代码:

from ultralytics import YOLO model = YOLO('best.pt') def detect_image(image_path, conf_threshold=0.5): results = model.predict(source=image_path, conf=conf_threshold, imgsz=640) detections = [] for r in results: for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf[0]) cls = int(box.cls[0]) detections.append({ 'bbox': [x1, y1, x2, y2], 'confidence': round(conf, 3), 'class': model.names[cls] }) return detections

绘制结果我用OpenCV,在原始图像上用cv2.rectangle绘制矩形框,cv2.putText在框上方写出类别名和置信度,比如“seedling 0.82”。不同类别用不同颜色区分:幼苗期用绿色,快速生长期用蓝色,成熟期用橙色。这个细节看似不重要,但对使用者的体验影响挺大——颜色区分让他们不用看文字就能一眼分辨当前画面里各个阶段的分布,提高棚内巡查效率。

摄像头检测的逻辑类似,只是图片来源从图片路径换成了cv2.VideoCapture(0)的逐帧读取。为了让界面不卡死,摄像头帧读取和推理要放到独立线程里,通过信号槽机制把结果传回主线程更新UI。这里用QThread实现,涉及跨线程通信,一个常见的坑是直接在子线程里操作UI组件,导致程序崩潢,正确做法是子线程只计算结果,主线程做UI更新。

4.4 界面美化与打包部署

PyQt5默认样式比较死板,我用QSS样式表做了一套深色主题,让界面看起来更专业。整体配色以深灰为底、蓝色为主色调,配合白色文字。按钮加了hover效果,鼠标放上去会变亮,这些细节能让用户感觉系统更精致。

QSS的一个简单示例:

QPushButton { background-color: #2E86DE; color: white; border-radius: 5px; padding: 8px 16px; } QPushButton:hover { background-color: #3498DB; }

打包分发用PyInstaller,这是Python桌面应用最成熟的打包方案。打包过程有几个要注意的问题:best.pt模型文件默认不会被自动包含,需要在spec文件里用datas参数手动添加;PyQt5的插件文件也要一并打包,否则运行时会报平台插件缺失。我实际打包后整个应用大约160MB,其中模型文件20MB,PyQt5动态库占了大头,这也是Python桌面应用的常态,可以接受。

打包命令参考:

pyinstaller --onefile --windowed --add-data "best.pt;." --add-data "users.db;." main.py

Windows系统注意分隔符用分号,Linux和macOS用冒号,这是我曾经打包后换平台运行报错才发现的坑。

5. 常见问题排查:训练到部署,我踩过的那些坑

5.1 训练阶段的典型问题

第一个高频问题:loss下降很快但mAP不动。这是典型的过拟合初期表现,模型在训练集上很强大,但验证集上表现平平。解决办法是检查数据是否泄露,也就是验证集中的图像跟训练集太相似;其次降低模型复杂度,从yolo11s换回yolo11n;最后增强正则化,提升weight_decay到0.001试试。

第二个问题:类别不平衡导致某个类别检测效果差。我这次的数据里,快速生长期的图像比其他两类少一些,训练完发现它的mAP50比其他类低将近6个点。解决办法一是数据增强时对这个类别的图像做更多变换,二是人工补充采集这个阶段的图像数据。数据不平衡在农业场景里很常见,尽量从源头补齐数据,少指望算法层面去硬调。

第三个问题:训练时显存不够,报CUDA out of memory。最简单有效的办法是batch size减半,从16降到8,观察显存占用再决定是否继续降。还可以开启梯度累积,Ultralytics框架里配置batch参数为累积次数即可,但新手不建议搞这么复杂,直接减小batch就行。

5.2 部署环节容易忽视的细节

模型训练得很好但部署后效果差,这是最常见的情况之一。原因往往在图像输入尺寸或者格式上。训练时用的imgsz是640,推理时如果用户上传一张1920x1080的大图,框架会自动缩放,但有时候你要手动调用,得明确指定imgsz参数,确保推理尺寸和训练尺寸一致。

还有一种情况是摄像头画面颜色偏色。大棚里有些摄像头带红外补光,或者白平衡不准,导致画面偏绿或偏蓝,影响检测准确率。我在预处理里加了一个可选的白平衡校正开关,用户觉得画面颜色不对时可以手动开启,在UI参数区实现这个功能。

另外要提醒的是UI显示图像时的坐标映射问题。OpenCV画完框后,图像保存为RGB格式没问题,但QLabel显示之前要先把BGR转成RGB。这个我试过没转,结果绿色生菜在界面上变成蓝色,一度以为模型学错了特征,排查半天才发现是这个基础问题:

img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)

5.3 常见问题速查表

问题现象可能原因解决办法
训练loss不降学习率过大,数据格式错误下调lr0到0.005,检查标注文件坐标是否越界
验证集mAP低于60数据量太少,类别不平衡增加数据增强,补充少类别图像
推理速度很慢CPU推理,模型过大换用yolo11n,或用GPU推理
摄像头画面卡顿UI线程被推理阻塞把推理放到独立子线程,UI只做显示
打包后模型加载失败模型文件未随包打包在spec文件中用add-data添加模型文件
QLabel显示图片特别小未调整控件尺寸策略设置setScaledContents(True)并配合resizeEvent重绘

6. 写在最后:这个项目还能往哪些方向扩展

这套生菜生长周期检测系统完整跑下来,数据模块、模型模块、应用模块都已经验证可行。按照现有框架,往生产环境落地时还有几个扩展方向可以做:一是加入定期统计功能,对同一地块连续多天的检测结果做趋势记录,自动生成生长进度报表,这个对种植方案的动态调整很有帮助;二是把UI的本地版改成前后端分离架构,将模型部署到服务器上,通过接口对外提供检测服务,这样手机端也可以调用;三是针对更多蔬菜品种做数据扩充,把模型升级成多品种生长检测,这样一套系统就可以覆盖整个大棚的不同作物区域。

我个人在实际操作中的体会是,目标检测项目想快速出成果,模型选型已经不是最关键的变量,Ultralytics框架把大量工程细节都封装好了,真正的差距来自数据整理和部署整合。把这两块做扎实,项目离落地就只有一步之遥。数据标注这一步,建议至少预留一半的项目时间,哪怕模型训练花了两天,数据标注花十天也是值得的。另外做界面的时候不要贪复杂功能,用户真正高频使用的就那三五个操作,先把主流程打磨顺滑,让普通用户看着界面能猜出怎么操作,比堆一堆花哨控件实在得多。

最后再分享一个小技巧:模型训练完成后,别急着直接部署到UI里,先写一段五到十分钟的真实场景视频,抽帧跑一遍推理,把输出的每一帧都保存下来检查。很多在静态图片上看起来完美的情况,一到连续帧就会暴露问题,比如同一株生菜在前一帧是快速生长期、后一帧突然变成成熟期,这种时序上的抖动会极大损害系统在用户心中的可信度。检测到这类问题后,可以在推理线程加一个简单的帧间平滑策略,对相邻帧的类别判断做多数投票,稳定性和现场体验都会有明显提升。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进