先说结论吧:这套“基于深度学习的生菜周期检测系统”,本质上就是一条目标检测的标准流水线——数据集做成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毫秒(GPU)YOLOv10s640×6400.73约9毫秒(GPU)当然这个数据跟你自己的数据集分布、硬件环境有关,不必迷信,但在农业场景的密集小目标任务里,YOLOv10这个方向是值得尝试的。如果你面对的是超大图或者极高分辨率需求,也可以往下调模型规模;我这里用的是s版,原因是大棚管理人员实际运行时可能只用CPU,s版在CPU上的推理时间还能控制在几百毫秒内,不卡顿。3.2 训练环境与关键超参数训练环境这块,我用的是Python 3.10、PyTorch 2.1版本,GPU是常见的消费级显卡,显存8GB左右,训练YOLOv10s完全够用。依赖安装不多说,重点讲超参数设置。我训练时用的关键参数如下:imgsz=640:常规输入尺寸,兼顾精度和速度。epochs=150:初始设定。如果没有早停,实际到80epoch左右就已经收敛。batch=16:8GB显存下的一个安全值。patience=20:连续20个epoch验证集指标不提升就提前停止。lr0=0.01:初始学习率,适合迁移学习场景。训练命令并不复杂:yolo detect train \ data=lettuce-data.yaml \ model=yolov10s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ project=runs \ name=lettuce_yolov10s这里有一个新手容易忽略的细节:一定要用预训练权重yolov10s.pt做起点,不要从零训练。COCO预训练模型已经把通用目标的纹理、边缘、形状特征学得差不多了,迁移到生菜这种特定目标上,只需要在高层特征上做微调,收敛速度更快、最终精度也更高。我见过有人直接随机初始化从头训练,结果训练了两百个epoch,mAP还不到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, imgsz=640, conf=0.35, iou=0.45, verbose=False ) 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。另外conf=0.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 model=best.pt format=onnx imgsz=640 opset=12为什么不用PyTorch直接打包?因为PyTorch的库文件体积太大,一个包含CUDA的打包环境动辄几个GB,给管理员装起来很麻烦。换成ONNX Runtime以后,整个打包体积能小到几百MB,运行也不需要额外的深度学习环境。这里分享一个打包踩坑:第一次打包出exe之后,我拿到另一台没有显卡的电脑上跑,程序能启动,但一加载模型就崩溃。排查了半天,发现是推理代码里默认用了CUDA,而目标机器根本没有CUDA驱动。后来统一改成CPU推理并明确指定设备之后,问题就消失了。所以如果你要给不特定的设备部署,打包之前先把代码里的设备选择逻辑写死成CPU,或者做成可配置项,避免运行时崩溃。6.2 把“单张检测”升级成“周期统计报表”模型和界面都稳定之后,我回去跟朋友聊使用反馈。他说得很有启发性:每天单独看一张照片的检测结果,只能知道“今天有多少棵成熟了”,但这个信息其实不够;他想看的是“这批生菜从定植到现在,各周期的数量到底是怎么变化的”。于是我在原系统上加了一个定时采集模块:每天固定时间拍一张棚区全景,自动跑一次检测,把各类别数量追加到当天的统计记录里。这个功能看似简单,但价值非常大——管理者终于可以拿数据回答“这批菜长得快不快”“什么时候是采收高峰”这些问题了。我还在UI里加了一个简单的趋势折线图,把最近十天的成熟期数量变化画出来,管理员扫一眼就知道要不要提前安排采收人手。6.3 后续可以扩展的方向这个项目做到现在已经能稳定运行,但它的架子还能继续往外扩。我梳理过几个实际可行的方向,按投入产出比排序:一是把检测框升级成实例分割,用分割结果去估算单棵叶面积,进而估算单株产量。叶子面积和生物量之间有很强的相关性,这一步做出来之后,系统就从“看周期”升级成了“估产量”。二是融合环境数据。大棚里通常有温湿度传感器、光照传感器,把这些数据和视觉检测结果放到一起,可以训练一个简单的生长速率预测模型,提前几天预测采收时间,比纯视觉判断更稳。三是最有价值的:同一个数据集和训练管线,换一套标注就能复用到其他叶菜上。生菜、青菜、油麦菜在形态上有差异,但检测框架、UI界面、统计报表模块完全可以复用。我之前帮朋友验证过,往系统里加一个新类别只需要补这一类的标注图片,再训练一轮,不需要重写任何代码。回看整个项目,我最大的体会是:这个系统的瓶颈从来不在算法,而在数据。YOLOv10的训练现在已经非常成熟,跑起来基本不会出错;UI界面的编写也不难,一套Qt代码半天就能搭完。真正决定模型能不能在棚里落地的是,你有没有花时间去定义清楚标注标准,有没有把不同光照、不同密度、不同时期的样本都采集到位。如果你正准备复现这个项目,我的建议是,先别急着跑代码,花一周时间去棚里认认真真拍照片、做标注,把数据底子打好,后面所有环节都会轻松很多。
先说结论吧:这套“基于深度学习的生菜周期检测系统”,本质上就是一条目标检测的标准流水线——数据集做成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毫秒(GPU)YOLOv10s640×6400.73约9毫秒(GPU)当然这个数据跟你自己的数据集分布、硬件环境有关,不必迷信,但在农业场景的密集小目标任务里,YOLOv10这个方向是值得尝试的。如果你面对的是超大图或者极高分辨率需求,也可以往下调模型规模;我这里用的是s版,原因是大棚管理人员实际运行时可能只用CPU,s版在CPU上的推理时间还能控制在几百毫秒内,不卡顿。3.2 训练环境与关键超参数训练环境这块,我用的是Python 3.10、PyTorch 2.1版本,GPU是常见的消费级显卡,显存8GB左右,训练YOLOv10s完全够用。依赖安装不多说,重点讲超参数设置。我训练时用的关键参数如下:imgsz=640:常规输入尺寸,兼顾精度和速度。epochs=150:初始设定。如果没有早停,实际到80epoch左右就已经收敛。batch=16:8GB显存下的一个安全值。patience=20:连续20个epoch验证集指标不提升就提前停止。lr0=0.01:初始学习率,适合迁移学习场景。训练命令并不复杂:yolo detect train \ data=lettuce-data.yaml \ model=yolov10s.pt \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ project=runs \ name=lettuce_yolov10s这里有一个新手容易忽略的细节:一定要用预训练权重yolov10s.pt做起点,不要从零训练。COCO预训练模型已经把通用目标的纹理、边缘、形状特征学得差不多了,迁移到生菜这种特定目标上,只需要在高层特征上做微调,收敛速度更快、最终精度也更高。我见过有人直接随机初始化从头训练,结果训练了两百个epoch,mAP还不到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, imgsz=640, conf=0.35, iou=0.45, verbose=False ) 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。另外conf=0.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 model=best.pt format=onnx imgsz=640 opset=12为什么不用PyTorch直接打包?因为PyTorch的库文件体积太大,一个包含CUDA的打包环境动辄几个GB,给管理员装起来很麻烦。换成ONNX Runtime以后,整个打包体积能小到几百MB,运行也不需要额外的深度学习环境。这里分享一个打包踩坑:第一次打包出exe之后,我拿到另一台没有显卡的电脑上跑,程序能启动,但一加载模型就崩溃。排查了半天,发现是推理代码里默认用了CUDA,而目标机器根本没有CUDA驱动。后来统一改成CPU推理并明确指定设备之后,问题就消失了。所以如果你要给不特定的设备部署,打包之前先把代码里的设备选择逻辑写死成CPU,或者做成可配置项,避免运行时崩溃。6.2 把“单张检测”升级成“周期统计报表”模型和界面都稳定之后,我回去跟朋友聊使用反馈。他说得很有启发性:每天单独看一张照片的检测结果,只能知道“今天有多少棵成熟了”,但这个信息其实不够;他想看的是“这批生菜从定植到现在,各周期的数量到底是怎么变化的”。于是我在原系统上加了一个定时采集模块:每天固定时间拍一张棚区全景,自动跑一次检测,把各类别数量追加到当天的统计记录里。这个功能看似简单,但价值非常大——管理者终于可以拿数据回答“这批菜长得快不快”“什么时候是采收高峰”这些问题了。我还在UI里加了一个简单的趋势折线图,把最近十天的成熟期数量变化画出来,管理员扫一眼就知道要不要提前安排采收人手。6.3 后续可以扩展的方向这个项目做到现在已经能稳定运行,但它的架子还能继续往外扩。我梳理过几个实际可行的方向,按投入产出比排序:一是把检测框升级成实例分割,用分割结果去估算单棵叶面积,进而估算单株产量。叶子面积和生物量之间有很强的相关性,这一步做出来之后,系统就从“看周期”升级成了“估产量”。二是融合环境数据。大棚里通常有温湿度传感器、光照传感器,把这些数据和视觉检测结果放到一起,可以训练一个简单的生长速率预测模型,提前几天预测采收时间,比纯视觉判断更稳。三是最有价值的:同一个数据集和训练管线,换一套标注就能复用到其他叶菜上。生菜、青菜、油麦菜在形态上有差异,但检测框架、UI界面、统计报表模块完全可以复用。我之前帮朋友验证过,往系统里加一个新类别只需要补这一类的标注图片,再训练一轮,不需要重写任何代码。回看整个项目,我最大的体会是:这个系统的瓶颈从来不在算法,而在数据。YOLOv10的训练现在已经非常成熟,跑起来基本不会出错;UI界面的编写也不难,一套Qt代码半天就能搭完。真正决定模型能不能在棚里落地的是,你有没有花时间去定义清楚标注标准,有没有把不同光照、不同密度、不同时期的样本都采集到位。如果你正准备复现这个项目,我的建议是,先别急着跑代码,花一周时间去棚里认认真真拍照片、做标注,把数据底子打好,后面所有环节都会轻松很多。