不少搞农业视觉的朋友一开始找我,第一句话就是"想做一个生菜生长周期检测系统",但聊下去发现大部分需求其实是一套完整的演示系统:既要模型能区分生菜当前长到哪个阶段,又要有能点鼠标操作的界面,还得带登录注册、检测记录这些交互功能。这篇就围绕"基于YOLOv11的生菜生长周期检测系统"这个题目,从数据集准备、模型训练,到UI界面和登录注册模块的落地,完整拆一遍。整个过程适合两类人看:一是拿这个当毕业设计或课程项目的学生,二是想快速搭一套农业CV应用原型、给客户或基地做技术验证的开发者。
这个项目的本质,是把目标检测模型和桌面应用外壳拼在一起。算法层解决的是"生菜在图里哪里、处于什么生长阶段"的问题,应用层解决的是"谁在用、怎么用、结果怎么展示"的问题。前半部分如果把模型训练好,后半部分就算用最简单的Python界面也能撑起来。下面按我实际做过的顺序来写,每一步都会说明为什么这么选、实际会遇到什么坑。
1. 系统整体设计思路与关键选型
1.1 目标场景与检测需求拆解
生菜生长周期检测,核心不是"检测生菜",而是"给生菜的生长阶段分级"。农业上常见的场景有两个:一个是温室或水培基地里定期拍照巡检,管理者需要知道哪些区域已经可以采收、哪些还在幼苗期,从而安排水肥和采收计划;另一个是科研或育种场景,需要记录同一批生菜在不同时间点的生长状态,批量采集图像数据辅助分析。
从目标检测模型的角度看,这个任务属于典型的单阶段检测分类问题:在图像中找到生菜的位置,同时预测它属于哪一个生长阶段。阶段划分没有唯一标准,一般按照种植习惯拆成四类就够了:
| 类别标签 | 中文含义 | 典型外观特征 |
|---|---|---|
| Seedling | 幼苗期 | 子叶或少量真叶,叶片小而窄,植株高度低 |
| Rosette | 莲座期 | 叶片明显增多,呈莲座状向外展开,尚未结球 |
| Heading | 结球期 | 叶片向内抱合,开始形成松散或紧实的叶球 |
| Mature | 成熟采收期 | 叶球紧实度达到采收标准,外叶完整 |
我实际做数据集时,会把类别再简化一点,有些基地不需要区分散叶生菜的结球期,那就合并为Seedling、Growth、Mature三类。类别少了,标注工作量下降,模型也更好收敛。项目标题既然写的是"生长周期检测",建议保留四类,视觉差异更明显,演示效果也更好。
这个任务最大的难点,是相邻阶段之间的外观过渡。莲座后期和结球初期,从图像上看几乎没有明确分界线,人工标注都会有分歧。所以数据集的标注口径必须统一,比如规定"叶片开始明显向内收拢才标Heading",否则类别之间互相污染,模型怎么调精度都上不去。
1.2 为什么选YOLOv11而不是其他模型
我接触过的同类项目里,有人用Faster R-CNN这类两阶段检测器,有人用YOLOv5/YOLOv8,也有人直接上YOLOv11。要我说,这类农业检测项目选YOLOv11是综合考虑训练成本和部署体验后的最省事方案。
首先是训练成本。两阶段检测器虽然精度上限高,但训练时间长,调参细节多,对新手不友好。YOLO系列训练过程透明,loss曲线直观,默认参数基本能出一个不错的结果。YOLOv11作为YOLO家族的新版本,在主干特征提取和颈部融合上做了优化,小目标检测能力比之前版本有提升。生菜幼苗期目标往往是小而密的,这个提升对项目是有实际价值的。
其次是部署体验。YOLOv11可以通过统一的工具库完成训练、验证、导出全流程,模型可以导出为ONNX等格式,方便后面接OpenCV或者别的推理框架。UI界面只需要调用一个预测接口,不需要自己写复杂的后处理逻辑。对开发桌面应用来说,省掉一堆重复造轮子的事。
还有一点是社区生态。YOLO系列的预训练权重很容易获取,资料和踩坑案例也多。真遇到问题,搜索一下基本能定位到原因,这在做项目的时候能省很多时间。对单人开发或者小团队来说,生态成熟比算法理论上的先进性更重要。
提醒:这里并不是说YOLOv11在精度上一定碾压之前所有版本。实际项目里,数据质量对结果的影响远大于模型版本差异。与其纠结用v8还是v11,不如把时间花在统一标注口径和清理脏数据上。
1.3 技术栈与模块划分
整套系统按三层来拆,逻辑会比较清楚:
- 数据层:生菜生长周期图像数据集、标注文件、类别配置文件
- 模型层:YOLOv11模型文件(训练好的权重)、推理封装模块
- 应用层:登录注册界面、主界面、图片/视频检测模块、检测记录展示
技术栈上,Python是必须的,深度学习部分用PyTorch和Ultralytics工具库,界面部分可以用PyQt5或PySide6,数据库用SQLite。SQLite零配置、单文件,适合登录注册这种轻量级用户系统,不需要额外装数据库服务。
我选PySide6而不是Tkinter或者PyQt5的原因很简单:Tkinter做简单工具还行,但要做"能拿得出手"的检测界面,控件美观度和布局灵活度都不够;PyQt5和PySide6功能基本一致,但PySide6是Qt官方支持的Python绑定,更新及时,许可证对个人项目更友好。如果环境受限装不上PySide6,用PyQt5也可以,代码逻辑几乎通用。
2. 数据集准备:这一关决定模型上限
2.1 数据采集规范与数量建议
生菜生长周期检测项目里,我见过太多人先急着装环境跑训练,结果数据集就用网上随便找的几十张图,训练出来的模型一测就翻车。图像质量差、类别不均、背景单一,这些问题靠调参根本救不回来。所以数据集必须认真做。
采集时要注意几个点:一是覆盖不同生长阶段,每个阶段的图片数量尽量均衡,最好每类不少于300张原始图;二是覆盖不同拍摄环境,温室大棚的光照、室内补光灯、自然光都要有,背景包括基质、营养液、地面等;三是不同拍摄距离,既有整畦的大场景,也有单株的近距离图;四是角度多样,俯拍为主,适当加一些斜视角,因为实际使用中摄像头不一定是纯俯拍。
如果实在没有自己的拍摄条件,可以分两步:先用于公开的农业图像数据集或网络图片资源扩充数量,然后自己补拍一部分,让数据里包含目标场景的光照和背景特征。直接用纯公开数据训练出来的模型,往往在自己的基地场景上掉点明显。
采集回来的图片要做清洗。模糊的、严重过曝的、目标被遮挡超过一半的,该扔就扔。还有一类要注意:同一株生菜连续几天拍的照片特征很接近,如果全部放进训练集,模型会记住这株菜,而不是学会判断生长阶段。最好把同一株的图归到同一个采集批次,按批次划分数据集,而不是按单张图随机划分。
2.2 标注工具与YOLO格式细节
标注工具我用过的有三个:LabelImg、Label Studio、X-AnyLabeling。LabelImg是老牌工具,安装简单,标注YOLO格式很方便,但不支持自动检测辅助标注。Label Studio功能强,能做交互式标注,但对YOLO格式的导出稍微绕一点。X-AnyLabeling有模型辅助标注功能,可以先跑一个预训练模型自动框出生菜,人工再修正。
实际效率最高的流程是:先用训练好的YOLO模型对图片做自动预标注,导出YOLO格式,再用LabelImg打开逐张修正。这样比纯人工标注快一倍以上,而且对新手来说,看到预标注框再做调整,也比从零画框容易上手。
YOLO格式的txt标注文件长这样:
3 0.512345 0.389012 0.214578 0.156892每行含义分别是:类别id、归一化后的中心点x、中心点y、宽、高。归一化是指除以图片宽度和高度,所以所有值都在0到1之间。类别id按照 classes.txt 中的顺序对应。
容易踩的坑有三个:一是标注工具导出的路径和图片路径不一致,训练时报错找不到图片;二是中文路径导致读取失败,整个项目路径和图片路径建议不要出现中文;三是类别id写错,比如classes.txt里Seedling是第0类,但标注文件里写成了1,模型训练出来就会把幼苗当成莲座期。
2.3 数据增强策略的取舍
数据增强能显著提升模型的泛化能力,但生菜检测有自己的特殊性。YOLO训练默认打开Mosaic增强,把四张图拼在一起训练,对小目标检测有帮助,但也会让模型看到一些不真实的场景:四张图拼接处会出现天然的框线,生菜被截断成两部分。我的做法是训练前期保留Mosaic,让模型多看不同的组合,训练后期关闭Mosaic,让模型适应完整目标形状。
除了Mosaic,常用的增强还有水平翻转、随机透视、HSV色彩扰动。水平翻转对生菜检测是安全的,因为生菜没有方向性。HSV色彩扰动要适度,农业场景里叶片颜色是很重要的特征,如果H通道扰动量太大,会把黄色、枯叶和正常绿叶搞混。一般Saturation和Exposure扰动设置为0.5以下就够了。
还有一个细节:不推荐做太多随机裁剪和旋转。生菜目标本身形状比较紧凑,旋转90度之后看起来不自然,模型可能学到错误的形状先验。垂直翻转在俯拍场景下可以用,但如果是斜视角拍摄,垂直翻转会改变光源方向,也建议关闭。
2.4 数据集划分的正确姿势
最常见的错误是直接用工具默认的train/val比例随机划分,不管图片之间有没有关联。前面提过,同一株生菜的连续照片特征高度相似,如果它们的兄弟图分别进了训练集和验证集,验证集指标会虚高,真到现场测试就露馅。
更靠谱的做法是:先把所有图片按拍摄日期或批次分组,然后以"组"为单位划分。比如总共10个批次,取8个批次做训练,1个批次做验证,1个批次做测试。这样验证集和训练集之间的重叠度最小,训练出来的模型才能反映真实水平。
划分比例上,我习惯是按图片数量算:训练集75%、验证集15%、测试集10%。测试集平时不参与任何训练和验证,只在最后模型评估的时候拿出来跑一遍,看最终的map、precision、recall指标。
3. 模型训练与调优的完整流程
3.1 环境配置与训练启动
环境配置是整个流程里最没有技术含量、但最容易卡住人的环节。我用的是PyTorch加Ultralytics工具库,Python版本建议3.10及以上,PyTorch根据自己机器的显卡选择CUDA版本。没有独立显卡的话,训练也能跑,就是慢,几百张图的数据集可能要跑几个小时。
一个省心的做法是先创建虚拟环境,再安装依赖:
python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install ultralytics torch torchvisionultralytics库装好之后,torch会作为依赖自动装上,但默认装的是CPU版本。如果想用GPU加速,最好单独安装对应CUDA版本的torch,再装ultralytics。不然跑训练时才发现用的是CPU,时间白白浪费。
训练命令非常简单:
yolo train data=lettuce.yaml model=yolo11n.pt epochs=150 imgsz=640 batch=16这里的lettuce.yaml是数据集配置文件,内容格式大概是:
path: ./dataset train: images/train val: images/val test: images/test nc: 4 names: ["Seedling", "Rosette", "Heading", "Mature"]配置好后直接执行,模型权重yolo11n.pt会自动下载。这里选用yolo11n,是YOLOv11系列里参数量最小的版本,生菜检测任务不需要超大模型,轻量模型推理更快,后续UI界面用起来也更流畅。
3.2 关键训练参数到底该怎么设
训练参数不必死记,但要理解它们各自在干什么。最重要的几个:
- imgsz:输入图像尺寸。默认640,生菜目标不算太小,640够了。如果你的图片里幼苗期目标占比特别小,可以尝试1024,但显存占用会明显上升。我实测640在这个任务里精度和速度都比较均衡。
- batch:批大小。显存允许的情况下越大越好,但batch太大对精度也不全是好事。一般8GB显存建议batch=16,低于这个显存就减半。
- epochs:训练轮数。建议先设150,配合早停机制。如果到100轮左右map不再上升,早停会自动结束,不需要人工干预。
- patience:早停耐心值,建议20。连续20轮验证集指标没有提升,就停止训练,防止过拟合。
- lr0:初始学习率,默认0.01。小数据集上如果loss震荡明显,可以降到0.005试试。
- device:指定训练设备,0表示第一张显卡,cpu表示只用CPU。
还有一个容易忽略的参数是workers,负责数据加载时的并行线程数。Windows下workers设置过大会报错,建议设成4或8。Linux下可以设大一些,加快数据读取速度。
3.3 训练过程中真正要盯的指标
很多人训练时只会看着进度条发呆,等跑完再去看结果。其实训练过程中有几件事是值得看一眼的。
第一个是train/loss和val/loss两条曲线的走向。正常情况是两者一起下降,然后趋于平缓。如果train/loss一直降,但val/loss降一段后开始回升,说明模型开始过拟合了,这时候靠早停截住即可。
第二个是验证集上的mAP50和mAP50-95。mAP50表示IoU阈值0.5下的平均精度,农业检测这种粗粒度任务,看mAP50就够了;mAP50-95是更严格的评价,作为参考看看即可。
第三个是每个类别的precision和recall。我遇到过的情况是整体mAP看起来还不错,但Mature类的precision很低,说明模型经常把结球期生菜误判为成熟期。这时候要回到数据集,检查Mature类的样本是不是太少,或者标注口径和Heading混淆了。
训练结束后,ultralytics会在run/detect/train目录下生成结果文件,包括混淆矩阵、PR曲线、验证集预测图。建议全部翻一遍,特别是混淆矩阵,能直观看到哪些类别之间互相误判。生菜项目里,Rosette和Heading的混淆是最常见的,如果混淆比例高,优先调整标注口径,而不是盲目加训练轮数。
3.4 模型评估与导出
训练完成后,在测试集上跑一次正式的评估:
yolo predict model=runs/detect/train/weights/best.pt source=test_images这个命令会输出每张图的检测结果。不要只看图,要用工具看模型的小时检测统计。我习惯是准备一张测试集的标注文件,然后自己写一段短代码,统计每类目标的检测数量和误检数量,这样才能真实反映模型水平。
评估没问题就可以导出模型,用于UI界面推理。导出ONNX格式的命令:
yolo export model=best.pt format=onnx dynamic=True加了dynamic=True可以让输入尺寸动态变化,但会牺牲一点推理速度。如果UI里固定用640输入,那不加dynamic也行,速度更快。导出的onnx文件可以直接用onnxruntime加载,也可以继续用ultralytics加载原pt文件。我实际在桌面应用里为了省事,直接加载pt文件,因为它自带了NMS后处理,代码最简洁。
4. UI界面与登录注册模块实现
4.1 登录注册模块的设计与实现
做登录注册界面,很多人会直接在一个窗口里画两个输入框,再塞一个按钮,验证逻辑全写在主线程里。这样确实能跑,但离"能用"还差得远。一个合格的登录注册模块至少包含:用户表、密码加密、输入校验、登录状态记录。
用户表用SQLite就够了,表结构可以简单一点:
CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, salt TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP );密码绝对不要明文存储。做法是每个用户生成一个随机salt,然后把"salt + 密码"做哈希,存储哈希值和salt。校验时取出salt,重新计算哈希,对比是否一致。Python内置的hashlib就能做,不用额外引库:
import hashlib, os def hash_password(password, salt=None): if salt is None: salt = os.urandom(16).hex() digest = hashlib.sha256((salt + password).encode()).hexdigest() return salt, digest def verify_password(password, salt, digest): return hash_password(password, salt)[1] == digest注册时校验用户名不能为空、用户名不能重复、密码长度不少于6位。登录时失败要给出统一的提示"用户名或密码错误",不要具体告诉用户是用户名不存在还是密码不对,避免被用来探测注册用户。
登录成功的状态可以用一个全局对象保存,主界面启动时检查这个对象是否存在,不存在就退出或跳转回登录页。简单项目这么做够用,不需要引入复杂的会话管理框架。
4.2 主界面布局设计
主界面采用左右分栏的布局比较实用:左侧是操作区域,右侧是结果展示区域。操作区域放置三个核心控件:图片检测按钮、视频文件检测按钮、摄像头实时检测按钮。结果展示区域用QLabel显示检测后带框的图片,下方用QTextEdit或QTableWidget显示本次检测的统计信息,比如检测到几株生菜、分别属于哪些阶段。
检测记录的维度,可以在界面底部加一个"历史记录"标签页,把每次检测的时间、图片路径、检测结果写入SQLite的另一张表。这样客户演示的时候,可以直接点开历史记录展示之前的检测情况,比现场临时跑图更有说服力。
布局具体怎么摆不追求花哨,但要保证窗口缩放时控件能自适应。PySide6用布局管理器(QVBoxLayout/QHBoxLayout)就能做到,不要用绝对坐标定位,否则窗口一拉大所有控件都挤在角落里。
4.3 图片与视频检测的接入方式
封装一个模型推理类,是UI代码整洁的关键。我习惯建一个单独的detector.py,里面只做两件事:加载模型、对输入图片做推理。这样UI界面的代码就只是调用接口,不掺模型细节。
from ultralytics import YOLO class LettuceDetector: def __init__(self, model_path): self.model = YOLO(model_path) def detect_image(self, image_path): result = self.model.predict(image_path, conf=0.4, imgsz=640, verbose=False)[0] boxes = result.boxes.xyxy.cpu().numpy() classes = result.boxes.cls.cpu().numpy().astype(int) confs = result.boxes.conf.cpu().numpy() return boxes, classes, confs图片检测就是把OpenCV读到的图像传进去,拿到了框和类别,再用OpenCV画框和标签,最后显示在QLabel上。这里要注意OpenCV的BGR通道和Qt的RGB通道显示问题,图片要在显示前转成RGB,否则颜色偏蓝偏绿。
视频检测本质上就是循环读帧,逐帧调用detect_image,再把结果画回去显示。这里有一个很重要的优化:不是每一帧都需要检测,实际项目里可以减少检测频率,比如每3帧检测一次,中间帧直接显示上一帧结果,可以大幅降低CPU占用。生菜生长变化缓慢,实时性要求不高,这个优化完全可行。
4.4 线程模型与界面卡顿处理
桌面应用最容易犯的问题,就是直接在UI主线程里做耗时操作。模型推理一张图可能只要几十毫秒,但视频流检测是持续计算,摄像头读取本身也会阻塞,如果全部放在主线程,界面会卡成一帧一帧的,用户点按钮都没反应。
正确做法是开一个工作线程跑检测循环,通过信号把结果传回主线程更新界面。PySide6里最简单的方案是用QThread配合自定义信号:
class DetectThread(QThread): frame_ready = Signal(object) def run(self): cap = cv2.VideoCapture(self.source) while self.running: ret, frame = cap.read() if not ret: break boxes, classes, confs = detector.detect_image(frame) frame = draw_results(frame, boxes, classes, confs) self.frame_ready.emit(frame) cap.release()界面端连接信号:
thread.frame_ready.connect(self.update_frame) def update_frame(self, frame): qt_img = convert_cv_to_qpixmap(frame) self.image_label.setPixmap(qt_img)这套模式的好处是Qt的信号槽机制自动保证跨线程安全,不需要自己在工作线程里直接操作控件。记住一条红线:任何控件更新都必须发生在主线程,工作线程只负责计算,否则轻则界面闪烁,重则程序直接崩溃。
5. 实战中遇到的典型问题与排查经验
5.1 数据集与训练阶段的高频问题
训练时报错"Class 4 is out of range":这说明标签文件里有类别id为4,但yml配置里nc=4,合法id只能是0到3。检查标注文件,把那类错误框删掉,或者检查classes.txt顺序。
训练完成后所有类别的precision都是0:大概率是训练集和验证集划分有问题,或者标签全部错位。一个快速排查方法是随便打开一张验证集图片,把它的对应txt文件内容打印出来,手动画一下框,看看框是否真的框住了目标。
mAP90多但实际用起来很差:这种基本上都是验证集和训练集样本太相似造成的虚假高分。重新按批次划分数据集,保证验证集的拍摄环境跟训练集有差异。
训练loss曲线一直在高位震荡不下降:先确认数据集是否加载正确,用yolo detect train前先单独跑一次predict在单张图上,看模型是否正常输出。如果正常,再检查学习率是否过大,lr0改为0.003试试。
5.2 模型部署与推理阶段的问题
加载pt模型提示"Error loading best.pt":通常是两个原因,一是模型文件本身下载不完整,重新下载;二是本地代码和训练代码版本不一致,ultralytics版本跨版本加载偶尔会有问题。建议训练和UI推理使用同一个环境。
ONNX模型推理结果和pt模型不一致:这多半是导出时输入尺寸或预处理方式没有对应。如果UI里用的是onnxruntime推理,预处理要和ultralytics保持一致,比如归一化方式、通道顺序。我的建议是:桌面UI直接加载pt文件,省掉这些对比调参的麻烦。
推理速度很慢:检测模型如果还有别的程序在占用GPU,推理会明显变慢。生菜检测任务在普通CPU上也能跑,但如果想流畅视频检测,最好把模型换成yolo11n这样的小模型,并把imgsz固定为640。另外确保推理时设置conf阈值不低于0.3,否则会出现大量低置信度框,增加后处理耗时。
5.3 UI开发遇到的常见问题
界面打开后显示黑色窗口,检测图片不出来:检查OpenCV读取图片是否成功,打印frame是否为空。如果是从文件读取,用os.path.exists先确认路径存在。中文文件名和路径是这里的大坑,尽量用纯英文路径。
点击检测按钮后程序卡死:原因是耗时操作直接放主线程了。按前面介绍的QThread方式重写,千万不要图省事在工作线程里直接调用label.setText之类的方法。
登录注册正常,但登录后主界面打开闪退:优先看SQLite的访问方式。如果登录和主界面同时打开多个数据库连接,并且没有正确关闭,Windows下偶尔会报数据库被锁定。统一的写法是在登录完成后关闭登录窗口的数据库连接,主界面按需重新打开。
多用户登录后历史记录互相串:检测记录表里加一个user_id字段,查询时按user_id过滤。这个需求虽然标题上没有明确写,但买了这套系统的人一定会提,因为每个用户要看自己的巡检记录。
6. 一套能用的生菜检测系统到底长什么样
把前面所有模块串起来,整套系统的典型交互流程是这样的:用户打开程序,进入登录界面,输入正确的账号密码后跳转到主界面。主界面左侧选择图片来源,点"图片检测"按钮,右侧显示带检测框的图片,下方显示统计结果。切换到"视频检测"后,选择本地视频文件,实时看到每帧的检测效果。整个流程下来,用户不需要接触任何模型和代码层面的东西,这就是应用shell存在的意义。
我对这套系统的评价是:算法部分不难,难的是把算法包装成别人能直接上手用的工具。YOLOv11训练模型谁都能跑,但数据集认真不认真、界面设计是否考虑用户体验、登录模块有没有做密码加密,这些细节才是区分"交差项目"和"能演示能落地的系统"的分界线。
如果后续要扩展,可以在现有框架上做三件事:一是增加多品种作物支持,把模型换成多类别检测;二是增加统计报表功能,按日期和阶段统计生菜数量,自动生成生长曲线;三是把检测模块封装成接口,接到服务端做远程巡检。骨架搭好了,往上叠加功能只是工作量问题。真动手做的时候会发现,大部分时间花在处理小细节上,但正是这些细节,决定了一个项目是demo还是产品。