简介:这份PDF文档面向智慧农业从业者、农业工程与计算机视觉方向的学习者,围绕YOLOv11在作物生长阶段识别与精准施肥决策中的落地实践展开,帮助读者理解如何将单阶段目标检测算法应用于农业场景,解决传统识别效率低、施肥决策粗放的问题。文档共37页,为单一PDF文件,压缩包约2.3MB,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,查阅体验完整流畅。内容从智慧农业背景、YOLOv11核心架构与创新点讲起,依次覆盖作物生长阶段数据集构建、模型训练与优化、精准施肥决策算法设计、系统集成开发,并配有实践案例分析与未来展望,目录层次分明、条理清晰。目前已有54人学习,适合希望系统掌握YOLOv11农业应用、对照目录快速定位知识点的读者参考,文档仅供学习使用。
1. 智慧农业实践:YOLOv11作物生长阶段识别与精准施肥决策
去年夏天我在一个番茄大棚里蹲了三天,就为了搞清楚一件事:为什么农户明明看着叶子发黄了才追肥,产量还是上不去。后来发现根子不在施肥量,而在施肥时机——作物从营养生长转向生殖生长的窗口期只有短短几天,错过就是错过,后期补多少肥都救不回来。人工巡棚靠肉眼判断,一个人管几十亩地,根本盯不过来。这就是YOLOv11作物生长阶段识别与精准施肥决策这套方案要解决的问题:用目标检测模型自动识别作物当前处于哪个生长阶段,再根据阶段映射到施肥策略,把“看天吃饭”变成“看数据决策”。适合有基本深度学习基础、想把手里的检测模型落到真实农业场景的工程师,也适合农业物联网方向的开发者参考整套链路怎么搭。
2. 从图像到决策:YOLOv11识别作物生长阶段的完整链路
2.1 为什么选YOLOv11而不是分类网络
很多人第一反应是:识别生长阶段不就是个图像分类问题吗,ResNet、EfficientNet不香吗?我一开始也这么想,直到在实际大棚里跑了一轮才发现问题。分类网络的前提是“一张图里只有一个主体”,但大棚场景下,一帧画面里可能有十几株作物,每株处于不同生长阶段,你需要的不是给整张图打一个标签,而是定位到每一株并分别判断它的阶段。这就变成了检测问题。
YOLOv11相比前代在几个关键点上对农业场景更友好。一是C3k2模块替换了部分C2f结构,参数量更少但特征提取能力不降,这对边缘设备部署很关键。二是SPPF后面的注意力机制改进,在小目标密集场景下召回率有明显提升——幼苗期的作物在画面里就是典型小目标。三是解耦头的设计让分类和回归任务互不干扰,生长阶段识别本质上分类头要输出的是阶段类别,回归头负责框准位置,解耦之后两边都更稳。
选YOLOv11的另一个理由是生态。Ultralytics的框架封装程度高,从训练到导出到部署一条龙,你不需要自己去写NMS、写数据增强pipeline。对于农业这种非计算机视觉主战场的领域,工程效率比模型极限性能更重要。
2.2 数据集构建:生长阶段标注的四个关键决策
数据集这块我踩过的坑最多,先说结论:不要试图用一个模型识别所有作物的所有阶段,先锁定一种作物、一套阶段划分标准。
阶段怎么划分?以番茄为例,我一般分为苗期、开花期、坐果期、成熟期四个阶段。但这里有个容易翻车的地方:开花期和坐果期在图像上差异很小,花谢了刚冒出小果的那几天,肉眼都容易混淆。我的做法是在标注规范里加一条——以“可见果实直径是否超过1cm”作为坐果期的判定标准,而不是靠花的状态。这样标注一致性从最初的70%多提升到了90%以上。
标注格式用YOLO格式,每张图对应一个txt文件,每行是class_id x_center y_center width height,坐标归一化到0-1。目录结构如下:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml的内容:
path: ./dataset train: images/train val: images/val test: images/test nc: 4 names: ['seedling', 'flowering', 'fruiting', 'mature']这里nc=4对应四个生长阶段。注意names的顺序要和标注时的class_id严格对应,我见过有人标的时候用0代表苗期,写yaml的时候把顺序换了,训练出来的模型把所有阶段都认反了。
数据量方面,每个阶段至少准备300-500张有效标注图,四个阶段加起来1500-2000张起步。如果某些阶段样本少,用Albumentations做离线增强,重点加随机裁剪、亮度抖动和旋转。不要用mosaic增强来扩充阶段样本——mosaic会把四张图拼在一起,生长阶段的上下文信息全乱了。
2.3 训练配置:从预训练权重到超参调优
环境配置这块,Python 3.9以上、PyTorch 2.0以上、CUDA 11.8,这些是基线。安装Ultralytics:
pip install ultralytics如果要用TensorRT加速推理,还需要装tensorrt和pycuda,这个后面部署章节再说。
训练脚本我一般写成这样:
from ultralytics import YOLO model = YOLO('yolo11m.pt') # 加载预训练权重 results = model.train( data='data.yaml', epochs=150, imgsz=640, batch=16, device=0, workers=8, optimizer='AdamW', lr0=0.001, lrf=0.01, warmup_epochs=5, cos_lr=True, patience=30, augment=True, mosaic=0.5, mixup=0.1, copy_paste=0.1, degrees=15.0, translate=0.1, scale=0.3, fliplr=0.5, hsv_h=0.015, hsv_s=0.5, hsv_v=0.3, project='runs/train', name='crop_stage_v1', exist_ok=True, pretrained=True, verbose=True )逐项说下关键参数。yolo11m.pt是中等规模模型,在精度和速度之间比较平衡,如果部署到Jetson Nano这种设备上,建议换成yolo11n.pt。imgsz=640是标准输入尺寸,如果作物在画面里占比很小,可以提到1280,但显存占用会翻倍。optimizer='AdamW'在农业数据集上通常比SGD收敛更快,因为农业图像的特征分布不像COCO那么多样,AdamW的自适应学习率更合适。cos_lr=True开启余弦退火,配合lrf=0.01让学习率从0.001平滑降到0.00001。patience=30是早停耐心值,30个epoch验证集指标不提升就停,省时间。
数据增强参数里,mosaic=0.5表示50%的概率做mosaic增强,比默认的1.0低,原因前面说了。mixup=0.1和copy_paste=0.1都是轻度使用,主要是增加样本多样性。degrees=15.0控制旋转角度,农业场景下相机基本是固定角度拍的,旋转太大会引入不真实的样本。hsv_h=0.015、hsv_s=0.5、hsv_v=0.3是色调、饱和度、亮度抖动,模拟不同光照条件,这个在大棚里特别重要,因为棚膜透光率会随天气和使用时间变化。
训练过程中重点看三个指标:metrics/mAP50、metrics/mAP50-95和每个类别的metrics/precision和metrics/recall。如果某个阶段的recall特别低,大概率是那个阶段的样本太少或者标注质量有问题。我遇到过一次开花期recall只有0.6,查了半天发现是标注时把一些刚谢花的样本标成了开花期,模型学混了。
2.4 推理与结果保存:把检测框变成施肥建议
训练完之后,推理和保存结果的代码:
from ultralytics import YOLO import cv2 import json model = YOLO('runs/train/crop_stage_v1/weights/best.pt') # 阶段到施肥策略的映射 fertilizer_map = { 'seedling': {'N': 20, 'P': 10, 'K': 10, 'note': '促根壮苗,氮肥为主'}, 'flowering': {'N': 10, 'P': 15, 'K': 15, 'note': '控氮增磷钾,保花保果'}, 'fruiting': {'N': 8, 'P': 8, 'K': 20, 'note': '钾肥为主,促进果实膨大'}, 'mature': {'N': 5, 'P': 5, 'K': 10, 'note': '减量施肥,控水控肥'} } results = model.predict( source='test_images/', conf=0.5, iou=0.45, imgsz=640, save=True, save_txt=True, save_conf=True, project='runs/predict', name='field_test' ) for r in results: boxes = r.boxes for i, box in enumerate(boxes): cls_id = int(box.cls[0]) cls_name = model.names[cls_id] conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() strategy = fertilizer_map.get(cls_name, {}) print(f"检测到 {cls_name} (置信度 {conf:.2f}) " f"位置 {xyxy} -> 建议: {strategy.get('note', '未知')}")conf=0.5是置信度阈值,低于0.5的检测框丢弃。这个值在农业场景下可以适当降低到0.4,因为漏检一个需要施肥的植株比误检的代价更大。iou=0.45是NMS的IoU阈值,控制重叠框的合并程度。save=True保存可视化结果图,save_txt=True保存检测框的txt文件,save_conf=True在txt里附带置信度。
施肥决策的逻辑很直接:每个检测框对应一株作物,根据它的生长阶段查表得到施肥建议。实际落地时,这个映射表应该由农艺师根据当地土壤检测结果来定,我上面给的数值只是示例。更精细的做法是把检测结果按区域聚合,比如把画面分成几个网格,统计每个网格里各阶段的数量,然后按比例计算整个区域的施肥配方。
3. 避坑指南:作物生长阶段识别落地时最容易翻车的五个地方
3.1 光照变化导致模型“失明”
现象:模型在训练集上mAP50能到0.9,拉到棚里实测直接掉到0.6,尤其是中午强光和傍晚弱光下,检测框要么消失要么乱飞。
原因:训练数据基本都是在同一种光照条件下采集的,模型学到了“光照”这个虚假特征而不是作物的真实形态特征。大棚里光照条件变化极大,棚膜老化、阴天、补光灯都会改变图像分布。
解决:采集数据时强制覆盖早中晚三个时段,每个时段至少占总数据的20%。增强参数里hsv_v可以提到0.4,模拟亮度变化。如果条件允许,在推理端加一个自适应直方图均衡化(CLAHE)预处理:
import cv2 def preprocess(img_path): img = cv2.imread(img_path) lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) lab[:,:,0] = clahe.apply(lab[:,:,0]) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)3.2 小目标漏检:幼苗期植株在画面里只有几十个像素
现象:苗期检测recall只有0.5左右,大量幼苗被漏掉。
原因:YOLOv11默认的P3特征图 stride=8,对应80x80的网格,对于640输入下小于16x16像素的目标,特征响应很弱。
解决:三个方向。一是提高输入分辨率到1280,让幼苗占更多像素。二是修改模型结构,在P2层加一个检测头,stride=4,专门抓小目标。Ultralytics支持自定义yaml,在head部分加一行P2的输出。三是用SAHI切片推理,把大图切成小图分别检测再合并:
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type='yolov11', model_path='best.pt', confidence_threshold=0.4, device='cuda:0' ) result = get_sliced_prediction( 'field.jpg', detection_model, slice_height=320, slice_width=320, overlap_height_ratio=0.2, overlap_width_ratio=0.2 )3.3 阶段边界样本标注不一致
现象:开花期和坐果期的混淆矩阵一团糟,两个类互相误判的比例超过30%。
原因:标注人员对“什么时候算坐果”理解不一致,有人看到花谢了就标坐果,有人要等果实明显膨大才标。
解决:写一份标注规范文档,用图像示例明确每个阶段的判定标准。对于模糊样本,单独放一个ambiguous文件夹,训练时不使用。另外可以在损失函数里对边界样本降权,但更根本的还是标注规范要统一。
3.4 部署到Jetson Nano后帧率暴跌
现象:在服务器上推理一张图20ms,导出到Jetson Nano上变成200ms,完全达不到实时要求。
原因:Jetson Nano的GPU算力只有472 GFLOPS,而且默认的PyTorch模型没有做TensorRT优化,大量算力浪费在框架开销上。
解决:导出TensorRT引擎:
yolo export model=best.pt format=engine device=0 half=True imgsz=640然后在Jetson上用TensorRT推理。注意Jetson Nano只支持FP16和INT8,不支持FP32的TensorRT加速。如果还嫌慢,把模型换成yolo11n,输入降到416,帧率能到15-20FPS。详细步骤后面第5章展开。
3.5 施肥决策表没有随土壤数据更新
现象:模型识别没问题,但施肥建议被农艺师吐槽“不准”,因为同样的生长阶段在不同地块的施肥需求不一样。
原因:施肥决策表是静态的,没有考虑土壤本底养分、pH值、有机质含量这些因素。
解决:把决策表做成可配置的JSON文件,每个地块对应一套参数。检测结果输出后,结合该地块的土壤检测数据做二次修正。比如土壤速效钾含量高的地块,坐果期的钾肥推荐量可以下调20%。这个逻辑不复杂,但需要和农艺师一起把规则定清楚。
4. 精准施肥决策的工程化:从检测结果到变量施肥指令
4.1 检测结果的空间聚合与网格化
单张图片的检测结果只能告诉你“这一帧里有几株开花期作物”,但施肥决策需要的是“这片区域里各阶段的占比是多少”。所以第一步是把检测框映射到地理坐标或者网格坐标。
如果相机是固定安装的,可以预先标定每个像素对应的实际面积。更简单的做法是把画面分成N×M的网格,统计每个网格里各阶段的数量:
import numpy as np def grid_aggregate(boxes, img_shape, grid_size=(4, 4)): h, w = img_shape[:2] gh, gw = grid_size cell_h, cell_w = h // gh, w // gw grid = np.zeros((gh, gw, 4), dtype=int) # 4个阶段 for box in boxes: cls_id = int(box.cls[0]) x_center = float((box.xyxy[0][0] + box.xyxy[0][2]) / 2) y_center = float((box.xyxy[0][1] + box.xyxy[0][3]) / 2) row = min(int(y_center // cell_h), gh - 1) col = min(int(x_center // cell_w), gw - 1) grid[row, col, cls_id] += 1 return grid这个网格矩阵就是后续施肥决策的输入。每个格子里的阶段分布决定了这个格子的施肥配方。比如某个格子里坐果期占70%,那这个格子就按坐果期的配方来,钾肥比例调高。
4.2 施肥量计算公式与变量施肥机对接
从阶段分布到具体施肥量,需要一个换算公式。我一般用加权平均的方式:
def calculate_fertilizer(grid, fertilizer_map, area_per_cell): """ grid: (gh, gw, 4) 每个格子的阶段计数 fertilizer_map: 阶段到施肥量的映射 area_per_cell: 每个格子对应的实际面积(平方米) """ gh, gw, _ = grid.shape total_n, total_p, total_k = 0, 0, 0 for i in range(gh): for j in range(gw): cell = grid[i, j] total_plants = cell.sum() if total_plants == 0: continue # 加权计算该格子的施肥量 n = sum(cell[k] * fertilizer_map[list(fertilizer_map.keys())[k]]['N'] for k in range(4)) / total_plants p = sum(cell[k] * fertilizer_map[list(fertilizer_map.keys())[k]]['P'] for k in range(4)) / total_plants k_val = sum(cell[k] * fertilizer_map[list(fertilizer_map.keys())[k]]['K'] for k in range(4)) / total_plants total_n += n * area_per_cell total_p += p * area_per_cell total_k += k_val * area_per_cell return {'N': round(total_n, 2), 'P': round(total_p, 2), 'K': round(total_k, 2)}这个函数的输出是整片区域的总施肥量。如果要对接变量施肥机,需要输出每个格子的施肥量,格式通常是Shapefile或者GeoJSON,施肥机的控制器按网格读取配方。
4.3 决策结果的验证与闭环
施肥决策做完不能就这么算了,得有验证。最直接的方式是留几块对照区,一块按模型推荐施肥,一块按农户经验施肥,记录产量和品质指标。我一般会记录这几个数据:单株果实数、平均单果重、可溶性固形物含量、施肥总量。
如果模型推荐的施肥量比农户经验少但产量不降,说明决策是有效的。如果产量降了,就要回头检查是阶段识别错了还是施肥配方不对。这个闭环跑上两三个生长季,决策表就能调得比较准了。
5. Jetson Nano部署YOLOv11的实操细节与推理加速
5.1 环境配置:JetPack版本与依赖安装
Jetson Nano的坑从刷机就开始了。必须用JetPack 4.6以上版本,对应的Ubuntu 18.04,CUDA 10.2,cuDNN 8.2。不要试图在Jetson Nano上装最新版的PyTorch,官方只支持到1.10左右。安装命令:
# 先装系统依赖 sudo apt-get update sudo apt-get install -y python3-pip libopenblas-base libopenmpi-dev # 安装PyTorch(Jetson专用wheel) wget https://nvidia.box.com/shared/static/fjtbno0vpo676a25cgvuqc1wty0fkkg6.whl -O torch-1.10.0-cp36-cp36m-linux_aarch64.whl pip3 install torch-1.10.0-cp36-cp36m-linux_aarch64.whl # 安装torchvision sudo apt-get install -y libjpeg-dev zlib1g-dev git clone --branch v0.11.1 https://github.com/pytorch/vision torchvision cd torchvision export BUILD_VERSION=0.11.1 python3 setup.py install --user装完之后验证:
import torch print(torch.__version__) print(torch.cuda.is_available())如果cuda.is_available()返回False,大概率是CUDA路径没配好,检查/usr/local/cuda是否存在,以及LD_LIBRARY_PATH是否包含/usr/local/cuda/lib64。
5.2 TensorRT加速:从ONNX到engine
PyTorch模型在Jetson Nano上跑不快,必须转TensorRT。步骤分两步:先导出ONNX,再转engine。
导出ONNX:
yolo export model=best.pt format=onnx opset=12 simplify=True imgsz=640然后在Jetson上用trtexec转engine:
/usr/src/tensorrt/bin/trtexec \ --onnx=best.onnx \ --saveEngine=best.engine \ --fp16 \ --workspace=1024 \ --verbose--fp16开启半精度,Jetson Nano的GPU对FP16有原生支持,速度比FP32快一倍左右。--workspace=1024是分配给TensorRT的工作空间大小,单位MB,Jetson Nano只有4GB内存,不要设太大。
转完之后用Python加载engine推理:
import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np class TRTInference: def __init__(self, engine_path): self.logger = trt.Logger(trt.Logger.WARNING) with open(engine_path, 'rb') as f, trt.Runtime(self.logger) as runtime: self.engine = runtime.deserialize_cuda_engine(f.read()) self.context = self.engine.create_execution_context() self.stream = cuda.Stream() # 分配输入输出内存 self.inputs = [] self.outputs = [] self.bindings = [] for binding in self.engine: size = trt.volume(self.engine.get_binding_shape(binding)) dtype = trt.nptype(self.engine.get_binding_dtype(binding)) host_mem = cuda.pagelocked_empty(size, dtype) device_mem = cuda.mem_alloc(host_mem.nbytes) self.bindings.append(int(device_mem)) if self.engine.binding_is_input(binding): self.inputs.append({'host': host_mem, 'device': device_mem}) else: self.outputs.append({'host': host_mem, 'device': device_mem}) def infer(self, img): np.copyto(self.inputs[0]['host'], img.ravel()) cuda.memcpy_htod_async(self.inputs[0]['device'], self.inputs[0]['host'], self.stream) self.context.execute_async_v2(bindings=self.bindings, stream_handle=self.stream.handle) for out in self.outputs: cuda.memcpy_dtoh_async(out['host'], out['device'], self.stream) self.stream.synchronize() return [out['host'] for out in self.outputs]这段代码是TensorRT推理的骨架,实际用的时候还要加上预处理(resize、归一化)和后处理(NMS、坐标还原)。预处理用OpenCV的cv2.dnn.blobFromImage就行,后处理可以复用Ultralytics的NMS实现。
5.3 帧率优化:从10FPS到25FPS的调参记录
Jetson Nano上跑yolo11m的TensorRT FP16引擎,初始帧率大概10FPS。我做了几轮优化:
第一轮,把输入从640降到416,帧率提到15FPS,但小目标召回率掉了8个百分点。权衡之后决定保持640,因为幼苗漏检的代价太大。
第二轮,把模型从yolo11m换成yolo11n,帧率直接到22FPS,mAP50只掉了2个点。这个交换很划算,最终用的就是yolo11n。
第三轮,优化预处理。原来用OpenCV的resize,改成CUDA加速的resize kernel,省了3ms左右。
第四轮,把后处理的NMS从CPU挪到GPU,用TensorRT的EfficientNMS插件,又省了5ms。
最终稳定在25FPS左右,满足实时检测需求。如果还要更快,只能上INT8量化,但INT8需要校准数据集,而且精度损失在农业场景下不太好控制,我一般不建议。
5.4 一个容易忽略的细节:相机标定与像素当量
部署的时候很多人只关注模型推理速度,忽略了相机标定。同样一个检测框,在不同高度、不同角度拍出来的实际面积是不一样的。如果不做标定,施肥量计算就是错的。
标定方法很简单:在田间放一个已知尺寸的参照物(比如一个30cm×30cm的白色标定板),拍一张照,量出标定板在图像里的像素宽度,算出像素当量(cm/pixel)。然后根据相机安装高度和角度,用相似三角形算出每个像素对应的实际面积。
def pixel_to_area(pixel_width, pixel_height, pixel_equivalent): """ pixel_equivalent: 每个像素对应的实际长度(cm/pixel) """ real_width = pixel_width * pixel_equivalent real_height = pixel_height * pixel_equivalent return real_width * real_height # 平方厘米这个函数在施肥量计算时对每个检测框调用一次,把像素面积转成实际面积,再乘以单位面积施肥量。
这套方案我从数据采集到部署跑通花了大概两个月,中间翻车最多的不是模型本身,而是数据标注的一致性和部署环境的兼容性。如果你也想在农业场景落地YOLOv11,我的建议是先把数据规范定死,再动手训模型,不然后面返工的成本远大于前期多花的那几天。希望帮到你。
本文还有配套的精品资源,点击获取