基于深度学习的生菜周期检测系统:全流程复盘与工程实践

📅 发布时间:2026/10/21 2:50:02
基于深度学习的生菜周期检测系统:全流程复盘与工程实践
先说结论吧这套“基于深度学习的生菜周期检测系统”本质上就是一条目标检测的标准流水线——数据集做成YOLO格式用YOLOv10训练模型再套一个Python写的UI界面。但真正把它从“能跑通”变成“能在棚里用”中间藏着大量标注标准、参数调优和界面设计的细节。这篇文章我把整个项目从数据准备到模型训练再到界面落地的过程完整复盘一遍里面的坑和取舍思路都会讲到对想用视觉方案做农业检测的开发者或者正在做类似毕业设计的朋友应该都能少走点弯路。1. 生菜周期检测到底在解决什么问题1.1 大棚管理里被忽略的真实痛点最开始找我做这套系统的朋友管理着一个大概三千盆生菜的大棚。他说得直白管理员每天要沿着种植区走一圈弯腰看每盆生菜的长势判断是不是到了采收窗口。旺季人手不够的时候一个人看一千盆看下来眼睛都是花的。更麻烦的是不同管理员对“能不能收”的判断标准不统一有人觉得再等两天更好有人觉得现在就该收采收时间前后能差出一两天。这一两天在蔬菜生产里不是小事。收早了单棵重量不够产量直接吃亏收晚了生菜纤维变粗、运输损耗增加卖相也受影响。而大棚里最缺的就是一个客观、统一的判断工具。我当时给他的方案很明确用摄像头把整片种植区拍下来对画面里的每一棵生菜做目标检测输出它属于哪个生长周期——苗期、团棵期、快速生长期还是成熟期。管理员只需要打开界面看一眼统计结果就能知道全棚有多少棵到了采收标准而不必再去一棵一棵辨叶片。这套系统的核心价值就在这里把靠个人经验的主观判断转成可复现、可量化的视觉检测结果。只要模型训练到位它的标准是稳定的不会因为换了个值班的人就变来变去。1.2 从“图像分类”到“目标检测”的选型依据最初讨论方案时朋友的第一反应是“用图像分类不就行了吗把每棵生菜拍一张照片然后判断它是哪个周期”。这个思路在理想情况下当然成立但实际大棚里根本拍不到“一棵生菜一张照片”这种干净画面。你拿手机或者普通监控相机往棚里一拍一张图上全是密密麻麻的叶子一盆挨一盆目标之间还有遮挡。分类模型只能告诉整张图“看起来像”什么类别完全没法回答“这图里有几棵、分别在哪个位置、各自处于什么周期”这些管理上真正需要的问题。目标检测模型输出的是一组“边界框 类别 置信度”这跟你棚里的真实需求是天然匹配的。一张全景图进去模型给你返回每一棵生菜的位置框并告诉每个框里是哪个周期。而且从标注成本来看检测框也比实例分割轻量得多。生菜这玩意儿边界本来就外扩、叶片交叠你让标注员去画像素级轮廓效率和一致性都会崩一个矩形框框住整个叶丛信息量已经足够判断周期。至于为什么选YOLO而不是别的检测框架后面会专门聊。这里先记住一个重要结论做农业场景的视觉检测优先考虑目标检测而不是图像分类因为种植管理的核心诉求是“哪里有、是什么、有多少”这三个问题只有检测模型能一次性回答。1.3 方案总览数据、模型、界面三条线整个项目我拆成了三条工作线每条线都相对独立但又有先后依赖关系第一条线是数据集。要跑通一套检测系统最核心的原料是带标注的图片。我最后整理出来的数据集有四个类别、上千张标注图片全部转成了YOLO格式。这条线耗时最长前后占掉了整个项目大约六成的时间。第二条线是模型。数据集准备完毕之后用YOLOv10的预训练权重做迁移学习训练、评估、导出最后得到了一份针对生菜周期检测的best.pt模型文件。这条线技术性最强但实际操作时间反而不长训练一个晚上加一个白天基本就收敛了。第三条线是UI界面。用Python写了一个桌面应用底层调用训练好的模型做推理界面上显示实时画面、检测框、类别统计和数据导出。这条线的目标是让不懂技术的大棚管理员也能直接用。下面我按这条流水线把每条线的关键细节和踩坑过程展开说。2. 一张“能训练”的生菜数据集是怎么来的2.1 YOLO数据集的目录组织与标注格式数据集的质量决定了模型的性能上限。我先明确一下YOLO格式长什么样因为很多新手在这块栽跟头。YOLO格式的标注不是单独一个JSON或XML文件而是每张图片对应一个同名的txt文件里面的每一行代表一个目标格式是class cx cy width height其中class是整数类别编号cx cy width height是目标的中心点x、y坐标和宽、高全部归一化到0到1之间。也就是说坐标值跟图片绝对像素无关除以了图片本身的宽度和高度。目录结构我建议按官方惯例组织训练、验证、测试的图片和标签分开存放lettuce-dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml是给训练脚本看的配置文件告诉它类别数量和名称。我用的内容很简单path: /data/lettuce-dataset train: images/train val: images/val test: images/test nc: 4 names: [seedling, rosette, rapid_growth, mature]标注工具方面我用的是一款开源图形标注工具直接导出YOLO格式在线标注平台也可以团队协作时更方便。这里给一个实操建议画框的时候要紧贴生菜最外缘叶片但不要把花盆边缘、周围的基质土框进去。我自己标注完第一版后检查发现很多框把花盆的土和旁边的基质都圈进去了导致模型学到的特征混入了背景信息。还有一个很关键的原则不要试图在标注阶段过滤“难样本”。有的图片里生菜被遮挡严重、有的光线很差但真实场景中最有问题的恰恰就是这些难样本。把它们保留在数据集里模型才能在实拍的时候不露怯。2.2 生菜周期的类别定义与标注标准分类标准如果不提前定死标注员各标各的后面模型就会学出一堆矛盾。我审了一圈数据质量报告之后就把类别定义固化成了一张表发给每个参与标注的人。类别特征描述叶片直径参考范围苗期真叶刚刚展开叶丛明显平铺贴地植株矮小约5到10厘米团棵期叶片数量明显增多叶丛呈紧凑的莲座状约10到20厘米快速生长期叶丛外展、个体迅速增大叶片有明显展开趋势约20到30厘米成熟期叶球紧实或叶丛达到商品采收标准外缘叶片饱满约30厘米以上你可能会觉得生菜的周期不是连续的渐变过程吗怎么能硬分成几个离散类确实两个相邻周期之间有明显的过渡带。我的处理原则是过渡期内目标特征不明显、连人都很难判断的样本直接删掉不在数据集里硬标。与其让模型学到一个模糊标签不如让它只学习特征清晰的类别。实践证明这样训练出来的模型在真实场景里的预测反而更稳定。另外每个类别的样本量要尽量均衡。我踩过的坑是苗期样本严重不足因为苗小、拍摄距离远拍出来的目标在图像里往往只有几十个像素标注难度也大。后面专门补拍了一轮苗期特写镜头才把样本量拉上来。2.3 数据增强不只是旋转和翻转数据集采集的时候我按不同时间段、不同光照条件拍了三批照片早晨侧光、中午顶光、傍晚弱光。这样做的原因是大棚里的光照变化非常剧烈如果模型只在一种光照下见过生菜换到阴天或者遮阳网开启的时候它就“不认识”了。YOLOv10的训练脚本内置了丰富的数据增强参数我通常会在配置里做针对性调整。几个用得上的关键参数如下# 训练增强相关参数YOLOv10命令行或yaml配置 hsv_h: 0.015 # 色调扰动不宜过大 hsv_s: 0.7 # 饱和度扰动 hsv_v: 0.5 # 亮度扰动农业场景建议适当调大 fliplr: 0.5 # 水平翻转 mosaic: 1.0 # 马赛克拼接 scale: 0.5 # 随机缩放增强参数不是越大越好。色调扰动如果开太大绿色生菜可能变成紫色生菜模型反而学歪了。我自己实际测试下来hsv_v亮度扰动比默认值稍大一点对棚内多变的补光环境很有帮助。另外mosaic增强会把四张图拼起来训练很大程度上缓解了密集种植场景下目标太小、上下文信息不足的问题。这里提醒一句数据增强是训练阶段的事推理阶段不要做同样强度的增强你加载模型预测时只需要正常resize到640尺寸就够了。2.4 类别不均衡的处理方法第一版模型训练完我看了每个类别的召回率苗期只有0.62左右明显低于其他类别。原因并不难找苗期样本数量只有成熟期的一半左右而且目标在画面里非常小模型对它的特征学得不够充分。我做了两件事来解决第一把苗期样本做了一次定向复制扩充保留原始图的同时把苗期出现较多的那批图片做了水平翻转、微缩放后重新加入训练集相当于给模型“加餐”第二把苗期的目标框在标注阶段做得更精细确保每一棵小苗都被完整标到一个不漏。做了这两件事之后苗期的召回率拉到了0.84虽然还略低于成熟期的0.94但已经在可接受范围内。这张类别的“跷跷板”现象其实在农业目标检测里很常见大目标好标、样本多小目标难标、样本少模型天然偏向前者。优化思路就是定向补数据而不是盲目给所有类别加样本。3. YOLOv10的训练流程与模型评估要点3.1 为什么选YOLOv10这个版本我先聊聊技术选型。YOLOv10是端到端的检测框架最大的变化是去掉了NMS后处理输出直接就是最终检测结果。这一改动有两个实际好处一是推理流程更简洁、延迟更低二是误检框的抑制由模型内部完成部署的时候少了一道手工调NMS参数的工序对不熟悉后处理的开发者很友好。在生菜这个数据集上我对比过同量级的YOLOv8s和YOLOv10s两者的mAP接近但推理速度YOLOv10s有明显优势。我当时跑出来的参考数据大致是这样模型输入尺寸mAP50-95单张推理耗时同硬件YOLOv8s640×6400.72约12毫秒GPUYOLOv10s640×6400.73约9毫秒GPU当然这个数据跟你自己的数据集分布、硬件环境有关不必迷信但在农业场景的密集小目标任务里YOLOv10这个方向是值得尝试的。如果你面对的是超大图或者极高分辨率需求也可以往下调模型规模我这里用的是s版原因是大棚管理人员实际运行时可能只用CPUs版在CPU上的推理时间还能控制在几百毫秒内不卡顿。3.2 训练环境与关键超参数训练环境这块我用的是Python 3.10、PyTorch 2.1版本GPU是常见的消费级显卡显存8GB左右训练YOLOv10s完全够用。依赖安装不多说重点讲超参数设置。我训练时用的关键参数如下imgsz640常规输入尺寸兼顾精度和速度。epochs150初始设定。如果没有早停实际到80epoch左右就已经收敛。batch168GB显存下的一个安全值。patience20连续20个epoch验证集指标不提升就提前停止。lr00.01初始学习率适合迁移学习场景。训练命令并不复杂yolo detect train \ datalettuce-data.yaml \ modelyolov10s.pt \ epochs150 \ imgsz640 \ batch16 \ patience20 \ projectruns \ namelettuce_yolov10s这里有一个新手容易忽略的细节一定要用预训练权重yolov10s.pt做起点不要从零训练。COCO预训练模型已经把通用目标的纹理、边缘、形状特征学得差不多了迁移到生菜这种特定目标上只需要在高层特征上做微调收敛速度更快、最终精度也更高。我见过有人直接随机初始化从头训练结果训练了两百个epochmAP还不到0.5白白浪费算力。3.3 从损失曲线和PR曲线看训练是否正常训练过程中不要只盯着进度条要定期看两组图损失曲线和PR曲线。第一轮训练时我注意到训练集box_loss一直下降但验证集的box_loss降到某个点之后开始反弹。这是一个典型的过拟合信号——模型把训练集里的盆底纹理、棚架背景都背下来了而不是真正学到了生菜本身的特征。处理办法是增加数据增强强度、减小模型复杂度或者提前早停。我把hsv_v从0.3调到了0.5同时把patience设成20让训练在验证指标不再提升时自动停掉后面就没有再出现过拟合。PR曲线的解读更直接每一类有一条曲线曲线越靠近右上角说明这一类在准确率和召回率上越平衡。我看见成熟期曲线几乎贴着右上角苗期曲线明显低一截就知道问题主要在苗期。这跟2.4节说的类别不均衡是对应上的。3.4 模型评估别只盯着mAP很多人看到mAP 0.7就觉得模型可以了但实际落地时mAP只是一个参考。我在棚里做了好几轮实测发现有两类错误最需要关注第一类是车间背景误检。棚里的黑色遮阳网、蓝色肥料袋跟生菜叶子的色差很大按理不该误检但模型偶尔会把肥料袋边缘识别成成熟期生菜。这类问题的根因是训练集里几乎没有“没有任何生菜”的负样本。我当时在数据集里额外加入了一批只有棚区背景、没有生菜的照片并标注成空图后重新训练误检率明显下降。第二类是相邻生菜的并框问题。两棵生菜长得又大又挨得近模型可能输出一个把两棵都框进去的大框而不是两个框。这个问题留在第5章详细讲因为它涉及数据标注和推理参数的联动调整。评估阶段我还会重点看三件事每个类别的precision、recall曲线混淆矩阵的具体误差方向以及推理耗时。综合这几项才能判断一个模型是不是真的适合部署。4. 从模型到UI界面推理流程与交互设计4.1 选择UI技术栈的取舍模型训练完成后管理器需要把模型“端”给管理人员用。UI技术栈我对比过几种方案最终选的是PyQt5方案优点缺点适用场景PyQt5/PySide桌面应用、显示自由、可打包exe样式开发稍繁琐单机部署、本地面向管理员Streamlit开发极快、交互组件丰富运行需要浏览器快速原型、远程展示Flask/网页可远程访问、灵活前端工作量更大多客户端、Web化Tkinter内置无需安装控件和美化很弱简单小工具我选PyQt5最核心的原因是大棚管理员的操作场景是固定的工作站电脑桌面程序打开即用不需要起服务、开浏览器而且PyQt5的绘图功能可以自由绘制检测框、状态文字和统计图配合QThread做异步推理交互流畅度足够。如果你希望多台电脑或者手机也能看那建议改成Flask来包一层Web界面但需要多写不少前端代码工作量是两个方向。4.2 推理线程与核心代码组织UI界面的核心难点不在界面本身而是别让推理卡住界面。我第一次做的时候很天真直接在按钮点击的回调里执行模型预测结果摄像头画面一到UI线程就冻结拖动窗口都费劲。正确的做法是把模型推理放在单独的工作线程里用信号把结果传回界面主线程。推理函数本身很简单from ultralytics import YOLO model YOLO(best.pt) def detect_frame(frame): results model.predict( frame, imgsz640, conf0.35, iou0.45, verboseFalse ) detections [] for r in results: for box in r.boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) xyxy box.xyxy[0].cpu().numpy() detections.append((cls_id, conf, xyxy)) return detections注意model.predict在推理时会加载计算图重复调用会导致内存累积。更稳妥的写法是在程序启动时加载一次模型之后每次都复用同一个模型对象只对新帧做inference。另外conf0.35这个阈值不是固定死的做成界面上的滑动条会让调试方便很多——我实际使用中发现棚内光照不好时把阈值降到0.3能找回不少漏检室内光照稳定时再升到0.4可以把背景误检压下去。4.3 UI界面该展示什么信息才能让管理员真正用起来界面设计上我没有做成“实验室风格”的密密麻麻坐标输出而是按照管理员的真实工作习惯来排布。左边是实时画面或者导入图片的预览区所有检测框都画在画面上类别不同颜色不同右边是类别统计面板显示苗期多少棵、团棵期多少棵、快速生长期多少棵、成熟期多少棵以及成熟期占比。最下面是一个当天的周期分布柱状图方便一眼看出整个棚的整体状态。这里有个很关键的产品细节成熟期生菜用暖色高亮因为这代表“可以准备采收”管理员看到红色区块增多就能安排人手苗期则用绿色表示“还在长”。这种视觉编码比让用户去查表格效率高得多。数据导出我做成了一键生成CSV报表每天记录所有检测结果。别小看这个功能它让管理者积累了按天、按批次的生长数据。后面第6章会讲这些累积数据怎么变成更有价值的趋势分析。5. 实拍场景里的坑小目标、遮挡与光照5.1 叶子遮挡导致漏检或并框前面提到两棵快速生长期生菜挨在一起时叶片外扩互相叠压模型很容易把它们识别成一个目标输出一个大框。我在棚里实测的第一周这种情况占错误检测的比例最高。这个问题的根因有两层。第一层在数据端标注时如果两棵生菜靠得太近标注员图省事就给了一个框模型自然也就学成了一个框。解决方法是严格按照“一棵一框”的原则标注即使叶片交叠也尽量用两个框分别框住各自的外缘。第二层在推理端把NMS的iou阈值稍微调低比如从0.5改成0.4让相邻重叠的框不那么容易被合并。需要注意iou调低可能会增加重复框得在现场多试几个值。另外我后来还用了copy-paste增强从其他图片里把单棵生菜目标剪切出来、随机粘贴到训练图上。这个方法对“目标之间互相靠近”的密集场景非常有效训练出来的模型对重叠目标有更好的区分能力。5.2 光照变化导致的误检大棚的光照是个大变量。晴天正午太阳直射叶片叶子表面会有高光反光阴天整个棚里光线偏暗叶片对比度降低早晚还有斜射光照出的长长阴影。这些变化如果不处理好模型的表现会时好时坏。我遇到过比较典型的一次误检把棚里一块蓝色的防潮塑料布识别成了成熟期生菜。为什么会这样因为那块塑料布在正午强光下的颜色和纹理在模型眼里跟高光下的成熟生菜叶子很相似。训练数据里蓝色背景太少模型就没学出“这种东西不是生菜”的边界。解决办法有几个层面第一采集阶段多跑几个时段专门把早晨、正午、傍晚、阴天的照片都收进数据集第二训练时把hsv_v亮度扰动调高一点模拟不同光照第三推理之前先对图像做简单的直方图均衡化拉一拉对比度减少光照绝对强度的影响。从实测结果看补了阴天样本之后阴天当天的误检率下降了将近一半。5.3 密集种植与小目标从imgsz到切片推理苗期生菜在常规拍摄距离下图像里往往只有二三十个像素的直径。即便YOLOv10已经用了多尺度特征融合对这种超小目标仍然容易漏检。我在正式部署前做了几个对比实验按效果从低到高排列最直接的办法是把imgsz从640提高到960让输入图放大小目标的特征更明确。代价是推理时间增加约一倍在CPU设备上会慢不少。再进一步是tiling切片推理把大图切成若干块每块单独送进模型最后再把检测框映射回原图坐标。这种方法对“一张图里几十个目标”的密集场景非常有效我最后在实机部署时就是这么跑的成熟期和苗期的召回率都有了明显提升。更高阶的做法是用SAHI这种切片推理库它能把重叠切片的结果做去重融合基本不用自己处理边界框拼接的逻辑。唯一要记住的是切片尺寸必须大于模型的有效感受野建议至少是imgsz的1.2倍否则目标会在切片的边缘附近被截断检测率反而下降。6. 项目打包与后续扩展6.1 模型导出与桌面部署模型训练完再导出成推理格式这一步主要为了部署体积和效率。我把best.pt导出成了ONNX格式再用ONNX Runtime做CPU推理yolo export modelbest.pt formatonnx imgsz640 opset12为什么不用PyTorch直接打包因为PyTorch的库文件体积太大一个包含CUDA的打包环境动辄几个GB给管理员装起来很麻烦。换成ONNX Runtime以后整个打包体积能小到几百MB运行也不需要额外的深度学习环境。这里分享一个打包踩坑第一次打包出exe之后我拿到另一台没有显卡的电脑上跑程序能启动但一加载模型就崩溃。排查了半天发现是推理代码里默认用了CUDA而目标机器根本没有CUDA驱动。后来统一改成CPU推理并明确指定设备之后问题就消失了。所以如果你要给不特定的设备部署打包之前先把代码里的设备选择逻辑写死成CPU或者做成可配置项避免运行时崩溃。6.2 把“单张检测”升级成“周期统计报表”模型和界面都稳定之后我回去跟朋友聊使用反馈。他说得很有启发性每天单独看一张照片的检测结果只能知道“今天有多少棵成熟了”但这个信息其实不够他想看的是“这批生菜从定植到现在各周期的数量到底是怎么变化的”。于是我在原系统上加了一个定时采集模块每天固定时间拍一张棚区全景自动跑一次检测把各类别数量追加到当天的统计记录里。这个功能看似简单但价值非常大——管理者终于可以拿数据回答“这批菜长得快不快”“什么时候是采收高峰”这些问题了。我还在UI里加了一个简单的趋势折线图把最近十天的成熟期数量变化画出来管理员扫一眼就知道要不要提前安排采收人手。6.3 后续可以扩展的方向这个项目做到现在已经能稳定运行但它的架子还能继续往外扩。我梳理过几个实际可行的方向按投入产出比排序一是把检测框升级成实例分割用分割结果去估算单棵叶面积进而估算单株产量。叶子面积和生物量之间有很强的相关性这一步做出来之后系统就从“看周期”升级成了“估产量”。二是融合环境数据。大棚里通常有温湿度传感器、光照传感器把这些数据和视觉检测结果放到一起可以训练一个简单的生长速率预测模型提前几天预测采收时间比纯视觉判断更稳。三是最有价值的同一个数据集和训练管线换一套标注就能复用到其他叶菜上。生菜、青菜、油麦菜在形态上有差异但检测框架、UI界面、统计报表模块完全可以复用。我之前帮朋友验证过往系统里加一个新类别只需要补这一类的标注图片再训练一轮不需要重写任何代码。回看整个项目我最大的体会是这个系统的瓶颈从来不在算法而在数据。YOLOv10的训练现在已经非常成熟跑起来基本不会出错UI界面的编写也不难一套Qt代码半天就能搭完。真正决定模型能不能在棚里落地的是你有没有花时间去定义清楚标注标准有没有把不同光照、不同密度、不同时期的样本都采集到位。如果你正准备复现这个项目我的建议是先别急着跑代码花一周时间去棚里认认真真拍照片、做标注把数据底子打好后面所有环节都会轻松很多。