1. 从“青果被摘”说起:成熟度识别的本质是决策问题
去年在一个番茄采摘机器人项目调试现场,我遇到过一件特别尴尬的事。模型在测试集上跑出了0.97的mAP@0.5,看起来已经相当能打,结果机械臂伸出去,当着客户的面摘了一颗青果回来。当时所有人的表情都凝固了。
后来排查发现,问题根本不在模型,而在方案设计——我们一开始就把番茄成熟度识别当成一个“图像分类任务”来处理:给整张图贴一个标签,成熟、半熟、未熟,完事。但真实大棚里的场景根本不是分类题,而是“定位+分级+抓取决策”的组合题。果实是离散分布的,同一帧画面里可能同时有十几颗果,有的被叶子挡住一半,有的藏在串果后面,有的光照强烈到反光发白。你让分类模型去判断“这画面里有没有成熟番茄”,它能给你输出概率,可是它没法告诉你“成熟果在第几个像素位置、直径大概多少、当前抓取点应该落在哪里”。
这就是智慧农业视觉方案里最容易被新手忽略的一件事:成熟度识别的最终目标不是识别,而是采摘执行。你的算法输出必须能直接对接机械臂的轨迹规划,否则再高的mAP都只是学术指标,不是落地指标。
番茄成熟度本身还是一种模糊概念。国内大棚里常用的分级方法是按着色面积划分:绿熟期(果面全绿)、转色期(开始出现少量红晕)、半熟期(着色面积30%-60%)、坚熟期(着色面积60%-90%)、完熟期(全红)。不同品种、不同季节、不同棚膜透光率下,同一颗果的颜色特征差异很大。而且采摘决策和视觉分类并不是同一个标准——采摘机器人可以摘半熟果拿去长途运输,却不一定该摘完熟果(太软易碰伤)。所以视觉模型要输出的不是“它是什么颜色”,而是“它该不该摘、从哪个点摘”,这两个问题的边界必须靠标注口径来定义。
我在这个项目里的核心思路是这样:用目标检测模型输出果实的位置框和成熟度类别,再做后处理计算出可执行的抓取点坐标与置信度,最后传给机械臂。这套方案把“成熟度识别”和“采摘点识别”两个问题解耦,模型的职责更单一,后处理逻辑也可以反复迭代不用重训。接下来的几个章节,我会按完整流程走一遍,包括数据怎么标、模型怎么选、针对大棚场景做了哪些改进、抓取点后处理怎么设计,以及最后部署到边缘设备上时踩过的坑。
2. 数据采集与标注口径:先定“摘果规则”再谈模型
2.1 采数据不能只在天晴中午去
很多农业AI项目一上来就找公开数据集,或者雇人去棚里拍几百张照片就开标。番茄这个品类还好,COCO里有一类“fresh fruit”的通用数据,但真要用来做成熟度识别,公开数据直接废掉——大棚自然环境下番茄的形态、光照、遮挡变化太复杂,公开数据集根本覆盖不了。
我这边最终的数据采集策略是按“时间×天气×角度×距离”四个维度铺开的。采集时间是早上开棚后、正午、下午盖棚前三个时段,分别对应低照度、强直射光、暖色散射光三种光照环境。天气至少要覆盖晴天、阴天、雨天棚膜起雾三种状态。拍摄角度除了正面平视,还包括仰拍、俯拍、侧向穿叶缝拍,因为机械臂的相机视角往往不是正对果实。距离上按0.3m、0.5m、0.8m三档采集,用于模拟机械臂靠近过程的不同工作距离。
有个容易忽略的点是:同一颗果不只要拍一次。番茄从转色期到完熟期通常有5-7天的窗口期,我专门对几垄番茄做了连续跟踪拍摄,每天固定角度固定位置给同一颗果拍几张。这样训出来的模型才能学到“同一颗果在成熟过程中颜色渐变的连续性”,而不是把不同品种的色差当成成熟度差异。
2.2 标注口径比标注精度更关键
标注阶段,团队内部最容易吵起来的就是“半熟到底算不算能摘”。如果口径不统一,标注员A把10%红晕的果标成半熟,标注员B要等到40%红晕才标半熟,模型学到的边界就会漂移。
我在项目里定了一套明确的标注规则,全部按着色面积比例划分:
| 类别 | 着色面积 | 采摘决策 | 标注建议 |
|---|---|---|---|
| unripe(未熟) | 0-10% | 禁止采摘 | 整体绿色,果面光泽较暗 |
| semi-ripe(半熟) | 10%-50% | 可摘但需谨慎 | 红晕明显可见,但未变成全红 |
| ripe(成熟) | 50%-100% | 优先采摘 | 果面大部分转红,适合即采即售 |
这套口径的逻辑是:采摘机器人的误判成本不对称。把未熟果当成成熟果摘下来,果实不能卖,属于直接损失;把成熟果当成未熟果漏掉,最多是效率损失,下一轮巡检还能补采。所以标注时,宁可把边界样本往“半熟”偏,也不要往“未熟”偏。
实际操作里还有两个细节。第一个是“被遮挡的果怎么标”:我们的规则是只要可见面积大于整体的40%,就正常标框;小于40%的果实直接不标,不参与训练。第二个是“串果怎么标”:番茄经常一串上挂5-8颗,严重重叠。我们的做法是每颗果标独立框,重叠部分允许边界框相互穿透,但标签必须区分清楚,不能把两颗果框成一个框。这个标准在标注平台里写进规范文档,出图前做二次审核,不合格的打回重标。
2.3 半熟样本永远不够,需要针对性补齐
第一轮数据回来,我统计了一下类别分布:未熟果占45%,成熟果占40%,半熟果只占15%。这个分布根本不是模型的问题,是大棚里的自然规律——农户不会在一个棚里同时等比例种三个成熟度的果,采摘前一大片都是半熟往成熟过渡的状态,但未熟和已熟都相对集中。
我的处理方式是“过采样+合成”。半熟果样本不足,就把所有半熟果的图像单独挑出来,配合Mosaic增强、随机HSV扰动、亮度扰动扩出4-6倍。特别注意模拟“红晕不均匀”的情况——有些果是向阳面先红,背阴面还是绿的,这类果实视觉上最容易误判成半熟或未熟,原始数据里天然占比低,但实际采摘时占比不低。这些增强不是乱加的,都是对照棚内实拍照片做了参数验证后才定下来的。
最终训练集一共8700张图,包含超过4万个标注框。半熟类经过增强后占35%,基本拉平了分布。数据集的每一次迭代我都保留版本记录,因为农业场景的数据清洗是永无止境的,后面加新品种、换大棚、改采摘标准,数据版本都要能回溯。
3. 模型选型:为什么我最后选了YOLO系检测模型
3.1 先否掉“分类模型+滑窗”这条老路
做成熟度识别,网上能搜到很多论文方案,看起来百花齐放,实际落地时大多数都不适合棚内部署。最早期我考虑过两条路线:
第一条是纯分类路线。用ResNet或者EfficientNet把果实图片分类成三个成熟度等级,然后检测阶段用传统视觉方式(颜色阈值分割、边缘检测)找果实位置。听起来轻巧,实际做起来特别脆弱——番茄的颜色受光照影响太大,同一个成熟度的果在直射光和散射光下颜色差异比类别差异还大,颜色阈值分割在反光、穿叶缝隙、串果重叠面前根本扛不住。
第二条是实例分割路线。Mask R-CNN、Cascade Mask R-CNN这类模型没错,分割精度高,能给出果实的像素级轮廓,对抓取点识别很有帮助。但代价是速度和部署复杂度。结论是半熟的边框其实是能严重重叠的,单果分割的边界在密集场景下也会彼此粘连,后处理反而更麻烦。后来又试了SOLO、YOLACT这些,精度和速度各有利弊,但都意味着更大的显存占用和更复杂的动态库依赖。
3.2 检测模型的对比实测
绕了一圈之后,我把重点放在目标检测模型上。当时手里对比了YOLOv5、YOLOv8、RT-DETR、DETR增量训练四个方向,直接在我自己的番茄数据上做了A/B测试。测试集是固定不动的874张棚内实拍图,用mAP@0.5和mAP@0.5:0.95两个指标衡量,同时记录推理耗时。
| 模型 | 输入分辨率 | mAP@0.5 | mAP@0.5:0.95 | 推理耗时(边缘设备) | 备注 |
|---|---|---|---|---|---|
| YOLOv5s | 640 | 0.923 | 0.711 | 17.2ms | 稳定但技术偏旧 |
| YOLOv8s | 640 | 0.941 | 0.748 | 15.8ms | 均衡,生态好 |
| YOLOv8s | 1280 | 0.962 | 0.793 | 46.3ms | 精度高但边缘设备吃力 |
| RT-DETR-R18 | 640 | 0.958 | 0.776 | 36.5ms | 精度好但速度不够 |
| RT-DETR-R50 | 640 | 0.964 | 0.785 | 62.8ms | 精度最高,速度太慢 |
RT-DETR在精度上的确略微胜出,但在边缘设备上的推理耗时几乎翻倍。采摘机器人的视觉链路不能只跑一次模型——它需要实时跟踪、多帧融合、抓取点更新,如果单帧检测就要60ms以上,整个控制周期会变得很紧张。所以我最终选了YOLOv8s作为主干,这个决定不是因为“YOLO最强”,而是因为它在精度、速度、部署生态之间做到了最适合当前项目的平衡点。
3.3 为什么不用更花哨的优化结构
还有个实际原因让我不敢碰太新的模型:采摘机器人项目的维护周期往往是2-3年,模型结构越复杂,后续换设备、改接口、升级推理引擎时踩坑的概率越高。YOLO家族的部署资料最多,转ONNX、TensorRT、量化、Python/C++接口都有成熟方案,团队里任何一个工程师接手都能快速上手。而像一些需要特定算子库支持的新模型,在边缘端可能因为一个算子不支持就得回退版本,这种隐性成本在农业项目里尤其致命。
农业场景还有一个特点:果实目标相对固定,类别只有三个,背景复杂但变化可预期。这样一个中等复杂度任务,选一个成熟稳定的检测模型完全够用,没必要为了刷论文指标引入部署负担。
4. 输入端的针对性改进:小目标、遮挡与光照
4.1 加P2检测头,把小目标从“漏检区”捞出来
第一版YOLOv8s模型跑下来,最大的badcase是远处的小目标果实。棚内相机挂在高处往下拍,或者机械臂还在移动过程中提前拍摄,果实在画面里往往只占几十个像素。YOLOv8的默认结构是P3/P4/P5三个检测头,分别对应80、40、20倍下采样,对于50像素以下的小目标,P3层特征已经不够精细。
我的做法是增加一个P2检测头,也就是把特征金字塔向下延伸到160倍下采样层。这个改动在YOLOv8里不算困难,需要调整模型配置和对应的anchor尺寸。加了P2头之后,小目标的召回率从0.87提升到0.93,mAP@0.5涨了1.6个百分点。代价是推理速度慢了大约4-5ms,在边缘设备上完全可接受。如果你的项目里果实距离相机视角普遍较远,这个改动几乎是性价比最高的精度提升手段。
4.2 可变形卷积和注意力模块怎么加才不翻车
针对叶子遮挡和串果重叠,我在C2f模块里试过把标准卷积替换成可变形卷积DCNv3,理论上能让卷积核的采样位置适应果实的形状变化,更好地感知被叶子遮住一半的轮廓。实测下来,DCNv3在串果重叠场景的mAP提升明显,尤其是semi-ripe和ripe之间的边界样本,但训练时要小心:DCNv3的收敛比普通卷积慢,学习率降低到原来的0.7倍,训练轮数增加到250轮才稳定。
注意力模块我加在检测头的Neck部分,用的是比较轻量的CoordAtt(坐标注意力),没有用SE或者CBAM。SE主要是通道注意力,对目标空间位置帮助有限;CBAM虽然加了空间注意力,但在小目标上增益一般。CoordAtt能同时编码水平和垂直方向的位置信息,对“果实在图像左上角/右下角”这类空间位置感知有帮助。实测提升约0.8个点,推理耗时几乎可以忽略。
有一个坑要提醒:注意力模块不是加得越多越好。我试过在Backbone每个C2f后面都挂一个注意力,结果mAP没涨,训练时间长了40%,还容易出现训练震荡。最终只加在Neck端的三层特征上,效果最好。
4.3 训练数据增强:专门针对棚内光照写规则
光照是农业视觉绕不开的话题。大棚膜的滤光效果、中午直射光过曝、傍晚暖光色偏,都会让同一个成熟度的果实呈现完全不同的颜色。我在数据增强管线里做的不是通用增强,而是模仿自然光照变化的针对性增强:
- HSV的色调偏移控制在±15度,饱和度和明度偏移±25%,不能太大,否则会把正常果实的颜色也扭曲了。
- 加入随机gamma变换,范围0.7-1.4,模拟棚膜老化、灰尘遮挡后的透光率变化。
- 训练中期开启Mosaic和MixUp,但到训练最后30轮关闭Mosaic,只保留局部增强,帮助模型稳定收敛。
这个增强策略是跟组织内一位做农业视觉的前辈反复对比验证过的。他说的一句话我一直记着:农业模型最怕的不是环境变化,而是让模型见过太多“不存在的世界”。过度增强会让模型学到的不是番茄,而是番茄的抽象涂鸦。所以增强参数要克制,对标真实的棚内场景再动手。
5. 从检测框到机械臂:抓取点识别与坐标输出
5.1 检测框的中心点不能直接当抓取点
很多毕业设计做到“检测出成熟番茄”就结束了,但实际机器人项目里,检测框只是第一步。机械臂需要的是一个三维抓取点坐标,或者至少是一个像素坐标加深度值。最粗暴的做法是把检测框中心点作为抓取点,这在单果俯视图里可能够用,但放到串果重叠、遮挡、果柄干涉的场景里就会出问题。
举一个实际案例:一串番茄从侧面看,三颗果几乎并排,检测框中心点可能落在两颗果的缝隙里,机械臂伸过去会撞到果柄或者推倒整串果。更危险的是,在那些被叶子遮挡了一半的果实上,检测框的中心点实际上对应的是叶片,而不是果面。所以抓取点要绕开框中心,往安全的果实暴露区域偏移。
我的做法是:调用模型的检测输出后,取检测框内置信度最高的热力图响应位置作为候选抓取点,再用靠近图像下方的边界做一个偏移修正。这个策略源自一个朴素的观察——俯视视角下,能被采摘的果实往往暴露在下半区域,而上半区域大概率被果柄或叶片占据。
5.2 重叠果实的过滤规则
棚内很多番茄是串型生长,检测后处理阶段同时输出的三个类别框会大量重叠。如果不去重,机械臂会把同一颗果理解成好几个抓取目标,轨迹规划直接乱掉。
我写了一个简单的后处理规则,放在模型推理之后:
- 对所有检测框按置信度从高到低排序。
- 对于类别为ripe的框,如果与置信度更高的框IoU大于0.4,直接丢弃。
- 对于semi-ripe类,IoU阈值放宽到0.5,因为半熟果经常和ripe果紧挨着,属于可以采摘的边界情况。
- 对unripe类,只要与任何可采摘框有重叠,即使IoU只有0.3,也保留原框并抑制可采摘框——未熟果不能被误摘,这是优先级最高的规则。
这个规则的代码实现很短,大概十几行,但它直接影响了后来机械臂在棚内连续采摘的成功率。我建议每个做采摘后处理的项目都先画一张“采摘优先级决策表”,再写代码,不要跳步骤。
5.3 输出格式对接机械臂控制
模型和后处理的最终输出是JSON格式,每个果实一个条目:
{ "fruit_id": 17, "category": "ripe", "confidence": 0.91, "bbox": [214, 156, 286, 231], "pixel_grip_point": [245, 203], "depth_mm": 724.5 }深度值来自相机对齐后的深度图。对抓取点像素坐标做中值滤波,避免单帧深度噪声导致机械臂抖动。如果深度值缺失或超过机械臂工作范围,直接过滤掉该目标,宁可不摘也不乱摘。
这一节再补充一个小点:如果做的是多品类农场,比如同时识别番茄和核桃,后处理逻辑需要每个品类单独配置。核桃的采摘点和番茄完全不同,核桃通常要识别果柄位置而不是果实质心。模型本身可以共用主干,但抓取点后处理规则必须各自独立。
6. 部署到采摘机器人上的调优实录
6.1 边缘设备的真实速度不是跑一次模型那么简单
项目部署在边缘计算设备上,起步方案用的是TensorRT FP16精度,YOLOv8s加P2头,640输入分辨率。单看模型推理耗时理想值只有15ms左右,但在真实控制链路里远不止这个数。相机取流、图像预处理、模型推理、后处理、坐标转换、机械臂指令下发、结果回传,整条链路跑下来要58-65ms。如果还要做连续帧的跟踪和滑动窗口异常检测,周期只会更长。
我的建议是:前期做模型选型时,不要把单模型推理时间当作系统延迟预算。至少预留30%-40%的余量给前后处理和时间同步。YOLOv8s在FP32转FP16时精度损失很小,mAP@0.5基本不掉点,属于最理想的部署状态。INT8量化时mAP掉了2.3个百分点,主要损失集中在semi-ripe和unripe的边界样本上,这个项目里不可接受,所以最终没有采用INT8。
6.2 部署过程中最容易出问题的是环境一致性
农业项目里,边缘设备的训练环境、开发环境、现场环境经常是分离的。我踩过一个典型的坑:开发机是PC环境,TensorRT版本和边缘设备不匹配,导出的engine文件在现场直接加载失败。排查了快半天,最后发现是CUDA、cuDNN和TensorRT版本三者的兼容矩阵没对齐。
后来我总结了一套应对流程:所有推理代码的依赖锁到版本,模型导出和序列化必须在目标设备上完成一次。具体的做法是先在边缘设备上装好环境,然后让模型在目标设备上重新转一次engine。哪怕开发机跑得再好,现场第一件事永远是拿20张实拍图跑通端到端,确认输出格式、置信度阈值、深度映射都符合预期,再让机械臂动起来。
另外,边缘设备的内存带宽和CPU占用会影响预处理环节。我在现场试过开Jetson的MAX时钟模式,推理延迟确实下降,但整机温度明显上升,长时间运行会触发降频。最终的稳定方案是锁在中高性能模式,以牺牲一点跑分换取长时间运行的稳定性。这个取舍在采摘季连轴转的场景里非常重要。
6.3 一个容易被忽略的稳定性问题:模型输出抖动
模型不是每次都对同一颗果输出完全相同的框和置信度。尤其在光照快速变化(云遮日、棚膜晃动)时,相邻两帧的检测结果可能差异很大。如果机械臂直接按最新帧的坐标去抓,会出现来回抖动甚至抓空。
我的处理是加了一个轻量级的“置信度记忆”模块:对同一跟踪目标,维护最近5帧的检测结果,只有当连续3帧都输出同类别且置信度高于阈值时,才把目标放进可采摘队列。抓取点坐标使用最近3帧的中值而不是均值,这样能有效剔除离群帧。这个机制在测试集里让误摘率从2.1%降到了0.8%。
这一套部署下来,整体链条虽然还有不少可以优化的地方,但至少已经从一个“演示项目”变成了能在棚里连续工作一个采摘季的系统。平时跟同行交流,经常有人说深度学习模型效果好,落地难。我的体会是:模型只解决“看到什么”的问题,而“怎么摘、摘了稳不稳”需要整个链路一起负责。农业视觉的复杂度不在于某一个算法,而在于把视觉、控制、机械、环境四者的不确定性同时管住,这才是这类项目真正有趣的地方。