简介:面向农业科研人员与计算机视觉开发者,这份PDF文档系统讲解基于YOLOv11的多作物叶片计数与生长状态评估完整方案,可有效缓解传统农业表型分析中目标检测效率低、人工成本高的痛点。文档共48页,单个PDF约2.26MB,支持目录章节跳转与书签定位,阅读检索十分便捷。内容从YOLO系列算法演进切入,逐一拆解YOLOv11的骨干网络、颈部网络、检测头设计,以及损失函数与训练策略;随后围绕数据集的采集、标注、增强和归一化,梳理模型训练、量化剪枝与评估流程。针对叶片计数,详细说明边界框解码、非极大值抑制、类别过滤等后处理步骤,并讨论叶片重叠、光照变化、多作物混合等复杂场景的应对策略;生长状态评估部分则覆盖生理指标选取、形态特征考量与环境因素关联。目前已有73人学习使用,对希望将深度学习落地于智慧农业场景的读者颇具参考价值。
1. 农业表型分析里的YOLOv11:叶片计数不是数人头那么简单
在农业表型分析里,“数叶片”和“测生长状态”是最容易低估难度的两个任务。人眼数单株叶片可以很准,但换成小区试验的几百个小区、每个小区几十株作物,人工效率就完全撑不住。机器视觉方案里,传统图像处理对均匀背景下的离体叶片有效,一到田间就败给光照变化和叶片重叠。YOLOv11这类深度学习检测器的价值在于,它把“找到叶片”从手动设计特征变成了数据驱动:标记几千个框,模型就能学会区分水稻、玉米、番茄等不同形态的叶片。但真正落地时,训练模型只占一半工作量,另一半在数据集组织、推理后处理和计数误差控制上。这篇内容面向打算用YOLOv11做多作物叶片计数和生长状态评估的开发者,从数据准备、训练、推理到小目标优化,给出一套可以直接跟着跑通的方案。
2. 训练前的数据准备:为YOLOv11构建多作物叶片检测数据集
2.1 数据采集与标注:YOLO格式下叶片框的边界要画到哪
很多刚接触YOLOv11的人会直接拿网上现成的通用数据集训练,结果在田间表现很差。原因不是模型不行,而是叶片检测的标注标准和通用物体差别很大。通用检测框喜欢把完整物体包进去,叶片在田间往往是倾斜、卷曲、互相遮挡的,如果每片叶子的框都标准地贴住外接矩形,模型学到的可能是“矩形区域内的纹理”,而不是“叶片边界”。我一般建议两套标准:对于完全展开的叶片,框要紧凑贴合叶片最外侧像素,不包含相邻叶片和茎秆;对于被遮挡超过30%的叶片,仍要标记,但框只覆盖可视部分。这样模型输出的每个框代表一片可见叶片的证据,后续计数时还能根据置信度处理遮挡。
数据采集时不要只拍俯视正射图,也要加入斜视角度、不同时间段的侧光、湿润和干燥叶片、土壤和地膜背景。多作物不是单纯把几种植物的图片混在一起,每种作物要有独立类别,否则YOLOv11学到的只是“叶片共有的绿色纹理”。类别设计建议使用下表这样的粒度:
| 类别名 | 目标作物 | 形态难点 | 建议最小标注数 |
|---|---|---|---|
| rice_leaf | 水稻/小麦等禾本科 | 狭长、弯曲、分蘖重叠 | 2000 |
| maize_leaf | 玉米/高粱 | 宽大、展开面积大 | 1500 |
| tomato_leaf | 番茄/黄瓜等茄果类 | 复叶、彼此遮挡 | 2500 |
| soybean_leaf | 大豆/花生等豆科 | 三出复叶、顶部新叶小 | 1500 |
这个表格不仅决定类别数,也决定了后面data.yaml中的nc和names。如果试验只测单一作物,类别就是1,但工程上我仍然建议保留“leaf”外的背景类别,因为YOLOv11会把田间杂草和工具上的绿色漆面误判为叶片。
标注工具可以用LabelImg或Roboflow,导出为YOLO格式即可。一个关键点是:YOLO格式的txt每行是class x_center y_center width height,坐标都归一化到0到1。Roboflow导出时如果选择了COCO格式,还要转一下;如果直接用LabelImg保存YOLO格式,就省去这步。
提示:不要为了赶进度而把所有图像直接拖进训练。留出一部分同种植物的后期生长阶段图像做验证集,否则模型只见过幼苗期,到了拔节期计数就会崩。
2.2 data.yaml与数据集划分:先定类别再跑脚本
YOLOv11训练自己的模型时,数据集入口是data.yaml。它并不关心图片是放在一个目录还是多个目录,只关心train、val和names是否匹配。常见的目录组织方式是把所有jpg和txt放在同一层,用拍摄时间或传感器ID作为文件名前缀,再用一个txt文件记录每个阶段的文件列表。下面这个脚本可以按比例切分训练和验证集,并处理YOLO格式的标注是否缺失:
import random from pathlib import Path root = Path("leaf_dataset") images = sorted([p for p in root.rglob("*.jpg") if p.with_suffix(".txt").exists()]) random.seed(42) random.shuffle(images) train_num = int(len(images) * 0.8) train_imgs = images[:train_num] val_imgs = images[train_num:] def write_lines(img_list, split_name): with open(root / f"{split_name}.txt", "w") as f: for img in img_list: f.write(str(img.resolve()) + "\n") write_lines(train_imgs, "train.txt") write_lines(val_imgs, "val.txt") print(f"train={len(train_imgs)}, val={len(val_imgs)}")脚本只把存在txt的图片计入,避免出现全黑或相机抖动导致的空标注文件混入训练集。random.seed(42)保证每次运行划分结果一致,方便复现。如果你有多个重复的试验小区,需要按小区而不是按图片划分,否则同一株作物会在训练和验证里各出现一次,val指标会虚高。
然后是data.yaml:
path: /data/leaf_dataset train: train.txt val: val.txt nc: 4 names: ["rice_leaf", "maize_leaf", "tomato_leaf", "soybean_leaf"]这里path是绝对路径,train和val可以是相对path的路径,也可以是绝对路径。注意names的顺序必须与标注txt里的class id一致,样本不足的类别可以填写去重后的优先级。检查数据是否对齐,最简单的命令是生成一个随机采样可视化脚本,把标注框画回原图看边界是否错位。这一步如果不做,YOLOv11训练时不会报错,但mAP会一直上不去,排查时浪费半天。
3. YOLOv11环境配置与训练:用自己的叶片数据跑通模型
3.1 环境搭建与预训练权重选择
YOLOv11依赖的ultralytics包同时支持训练、验证、导出和推理,环境配置比老版本YOLOv5简单很多。建议用conda创建独立环境,避免和其他项目的PyTorch版本打架。命令行如下:
conda create -n leaf_yolo python=3.10 -y conda activate leaf_yolo pip install ultralytics yolo version如果电脑有NVIDIA GPU,需要先确认CUDA版本,一般pip安装ultralytics会自动拉取匹配的PyTorch。没有GPU也能用小batch训练,但叶片是小目标,imgsz通常要640以上,CPU训练一个epoch会慢到难以接受。确认环境正常后,可以直接下载官方预训练权重,YOLOv11系列的模型权重名称在ultralytics中是yolov11n.pt对应nano,如果你用的包版本较新也可能叫yolo11n.pt,以yolo version输出的提示为准。对于多作物叶片计数,我一般从yolov11n.pt或yolov11s.pt起步。nano体积小、适合移动端部署,s在召回率上会好一点,特别是番茄复叶这种容易被漏检的类别。
注意:不要一上来就选yolov11m或更大。叶片数据量通常只有几千张,大模型容易过拟合,而且推理慢会拖累后面的计数管线。先把nano跑通,再看验证集哪些类别的AP低,决定要不要换大模型。
3.2 训练命令与关键参数:imgsz、batch和epochs怎么配
用命令行训练自己的数据集是最快的路径,下面命令可以直接在leaf_yolo环境里执行:
yolo train model=yolov11n.pt data=leaf.yaml \ epochs=200 imgsz=640 batch=16 device=0 \ project=leaf_exp name=baseline \ optimizer=AdamW lr0=0.001 \ close_mosaic=10 cache=True这里每个参数都值得单独说。imgsz表示训练时图像短边会被缩放到640,长边按比例缩放。如果原始图是2000x2000,收缩后叶片可能只有十几个像素,这就是后面要提高imgsz或做切图推理的原因。batch=16受显存限制,3060 12G一般能跑yolov11n,16G以上可以尝试32。epochs=200看起来多,但ultralytics自带early stopping,patience默认100轮,模型在验证集上不再提升就会自动停。close_mosaic=10表示最后10个epoch关闭马赛克增强,防止模型被拼出来的假叶片干扰。cache=True把图像缓存到内存,小数据集能明显加快epoch时间。
下面是几个需要调节的参数速查表:
| 参数 | 影响 | 建议值 |
|---|---|---|
| imgsz | 小目标召回率 | 640起步,小目标多改1024 |
| lr0 | 收敛速度与稳定性 | SGD用0.01,AdamW用0.001 |
| batch | 梯度噪声与显存占用 | nano用16~32 |
| mosaic | 重叠叶片模拟 | 0.5~1.0,病害实验用0.3 |
| patience | early stopping轮数 | 50~100 |
| weight_decay | 过拟合控制 | 默认即可 |
训练过程中要看的不是训练loss,而是验证集的mAP50和mAP50-95。田间叶片计数关注“有没有找到”多于“框得多准”,所以mAP50更重要,一般能到0.85以上说明模型可用。如果mAP50反复在0.6附近震荡,优先检查标注是否漏标,而不是调参。
3.3 验证与导出:用val命令看模型有没有学歪
训练结束后runs/leaf_exp/baseline/weights/下会有best.pt和last.pt。接下来先用val命令验证一下:
yolo val model=leaf_exp/baseline/weights/best.pt data=leaf.yaml split=val验证集指标会输出到results.csv,包括每类的precision、recall和mAP。这时候要看一个容易被忽略的点:验证集图片如果拍摄时间与训练集相近,模型可能会“背”下光照条件。可以单独准备一个异地或不同日期的图像文件夹,用predict跑一遍,看看计数结果跟人工数差多少。这是后续所有调优的基准。
同时建议把模型导出为ONNX,方便后面的推理后处理脚本脱离ultralytics环境运行,或者部署到Jetson这样的边缘设备:
yolo export model=leaf_exp/baseline/weights/best.pt format=onnx imgsz=640导出为ONNX后可以用onnxruntime做推理,推理速度比PyTorch快不少。但ONNX导出时会把NMS嵌入图中,如果你的后处理需要原始框信息,可以在export命令里加上nms=False,把NMS放到后处理里自己控制。
4. 推理后处理:实现叶片计数与生长状态评估的关键算子
4.1 用predict保存推理结果并提取检测框
训练完成后,最常用的操作就是对一张高分辨率田间照片预测并保存结果。YOLOv11的predict接口会把带框的图保存到runs/detect/下,还可以把每张图的检测明细存成txt。命令中最容易忽略的是imgsz要与训练时保持接近,否则模型在低分辨率输入下对叶片的响应明显下降。下面是同时完成预测保存与结果提取的脚本:
from ultralytics import YOLO model = YOLO("leaf_exp/baseline/weights/best.pt") results = model.predict( source="field_plot_001.jpg", conf=0.35, iou=0.45, imgsz=1024, save=True, # 保存画框后的图像到runs/detect save_txt=True, # 保存标注结果txt save_conf=True, # 同时在txt里写置信度 project="leaf_pred", name="plot_001", )conf和iou是决定性参数。conf太高会漏掉被遮挡但可见的叶片,太低会把背景纹理当成叶片。iou用于去重,YOLOv11自带的NMS已经处理过同一叶片的多个候选框,一般不需要调。枝叶密集的田块可以把conf降到0.25,再配合第5章提到的切图推理提升召回。save_txt生成的文件每行是class conf x1 y1 x2 y2,后面所有统计都可以从这个txt出发,避免每次重复跑推理。
4.2 重叠叶片去重与计数
即使有NMS,当同一株作物的叶片在图像上相互覆盖,模型有时会把一个叶片拆成两个相邻的框。最直接的判断方法是用每个框的中心点做聚类,距离过近的框合并成一个叶片。下面代码在predict结果基础上做中心点DBSCAN:
import numpy as np from sklearn.cluster import DBSCAN boxes = results[0].boxes.xyxy.cpu().numpy() conf = results[0].boxes.conf.cpu().numpy() cls = results[0].boxes.cls.cpu().numpy() centers = np.column_stack(((boxes[:, 0] + boxes[:, 2]) / 2, (boxes[:, 1] + boxes[:, 3]) / 2)) # eps用图像宽度的1%左右比较合适 eps = results[0].orig_shape[1] * 0.01 clusters = DBSCAN(eps=eps, min_samples=1).fit(centers) leaf_count = len(set(clusters.labels_))eps是判断两个中心点是否属于同一叶片的最大距离,通常取图像宽度的1%到2%。如果同一叶子被切成上下两半,中心点距离接近,聚类后合并;min_samples=1保证任何框都不被当作噪声丢弃。这一步做完得到的才是叶片数。不同类别如果都代表叶片,需要把counts和leaf_count分别统计,比如番茄复叶上经常同时检出成叶和新叶,两个类别都算作叶片数量,而杂草类别要排除。
提示:DBSCAN里的eps不是固定值。相机高度改变后叶片在图像中的实际尺寸会变,最好根据当天畸变校正后的像素尺寸标定一次,不要直接照抄别人的参数。
4.3 生长状态评估:从框面积到叶面积指数
生长状态评估可以看作计数的高阶输出。最常见的做法是用检测框面积除以图像总面积得到“相对叶面积”,再结合叶片数量判断处于哪个生长阶段。下面代码把每个类别的叶片数和归一化叶面积汇总:
image_area = results[0].orig_shape[0] * results[0].orig_shape[1] for r in results: boxes = r.boxes.xyxy.cpu().numpy() cls = r.boxes.cls.cpu().numpy().astype(int) names = model.names counts = {} leaf_area = 0.0 for box, c in zip(boxes, cls): x1, y1, x2, y2 = box areas = (x2 - x1) * (y2 - y1) leaf_area += areas name = names[c] counts[name] = counts.get(name, 0) + 1 print(f"叶片数={sum(counts.values())} {counts}") print(f"归一化叶面积={leaf_area / image_area:.4f}")归一化叶面积在幼苗期通常在0.05以下,旺盛期会涨到0.2以上。但它只能作为相对指标,因为检测框是外接矩形,无法反映叶片卷曲或镂空。如果想进一步区分生长状态,可以加一个“叶片紧凑度”特征,即框的宽高比的中位数:展开叶片的宽高比接近1,卷曲叶片的宽高比会明显偏离1。把这个值与面积结合,可以做成简单的健康度分级。实际项目中还可以把多天的统计结果写入CSV,按时间序列看生长曲线,但这需要跟踪同一小区的位置对齐,这就引出了YOLOv11目标跟踪的作用。对固定摄像头视频流,可以使用model.track()替换model.predict(),配合ByteTrack给每个叶片分配ID,从而避免帧间重复计数。这一部分在农业表型里属于高级方案,但原理上与单帧计数一致。
5. 小目标优化与结果验证:田间场景下YOLOv11的进阶调参
5.1 提高分辨率与SAHI切图
田间图像动辄2000万像素,直接缩到1024会让小叶片丢失大量细节。最直接的小目标优化是把预测时的imgsz提升到1280,但显存占用会急剧上升。另一种更可控的办法是切图推理:把大图切成512x512的小块分别送入YOLOv11,再把结果坐标映射回原图。可以用SAHI实现:
pip install sahifrom sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction det_model = AutoDetectionModel.from_pretrained( model_type="ultralytics", model_path="best.pt", confidence_threshold=0.35, image_size=640, ) result = get_sliced_prediction( "high_res.tif", det_model, slice_height=512, slice_width=512, overlap_height_ratio=0.2, overlap_width_ratio=0.2, )切片尺寸建议取训练imgsz的0.8到1倍,太小会让模型看不到完整叶片的上下文,太大又失去切图意义。overlap取0.2可以缓解叶片被切在边缘导致的重复计算,SAHI内部会做NMS合并。切图后小目标的mAP一般能提升5到10个百分点,但推理时间也会成倍增加。
5.2 注意力机制与CARAFE的取舍
当常规调参已经到瓶颈时,才会考虑修改YOLOv11的网络结构。常见做法是在backbone输出后添加自注意力机制,例如让最后一层C3k2模块变成C2PSA,这样模型能更关注叶片与背景的语义关系,对光照变化更鲁棒。但自注意力机制会增加参数量和推理耗时,叶片数据量少时反而容易过拟合。另一个热门改进是把上采样从最近邻插值换成CARAFE,它让上采样过程学习如何根据像素内容重建特征,对狭长叶片边缘的检测有改善。做修改前,建议先确认瓶颈确实是特征融合而不是数据。判断方法很简单:把验证集里漏检的图拿出来看,如果漏检的都是小于10像素的叶片,说明分辨率问题占主导,先做切图;如果漏检的是被泥土覆盖的叶片,才值得改网络结构。
5.3 用计数误差指标验证
改进是否有效,最终要看计数误差而不是mAP。叶片计数的评价指标与检测指标不同,通常用人工数出的叶片数作为真值,与模型计数比较。推荐用MAE和相对误差:
import numpy as np manual_counts = np.array([35, 42, 28, 51, 60]) auto_counts = np.array([34, 44, 29, 48, 58]) mae = np.mean(np.abs(manual_counts - auto_counts)) rmse = np.sqrt(np.mean((manual_counts - auto_counts) ** 2)) relative_error = mae / manual_counts.mean() * 100 print(f"MAE={mae:.2f} leaves, RMSE={rmse:.2f}, relative error={relative_error:.1f}%")如果MAE已经小于人工计数本身的重复测量误差,通常两次人工计数相差2到3片,就说明模型计数已经达到可用水平。对多作物场景,还应分别统计每个类别的MAE,找出拖后腿的作物。比如水稻分蘖期叶片密集,计数误差往往最大,这时可以回到第2章补充难例标注,或者单独训练一个水稻专用模型。最终建议是每次试验都保存一张带检测框的预测图和一个CSV结果表,误差指标能压到多少,直接决定这套YOLOv11方案能不能从试验田推向生产。
本文还有配套的精品资源,点击获取