简介:一份约26页的PDF技术文档聚焦YOLOv11在多作物叶片分析及病害识别中的完整落地流程,适合从事精准农业、计算机视觉目标检测的研究者与工程人员。包体为单个PDF文件,大小1.94MB,支持目录章节跳转、左侧大纲显示与快速定位,文字图表显示正常。内容按七章递进组织,涵盖精准农业与病害识别背景、YOLOv11网络结构与训练原理、多作物叶片数据集的收集标注与预处理、模型训练优化、病害识别实现流程,以及小麦、温室蔬菜、果园等实际落地案例,并讨论了数据、模型、应用三方面的挑战与未来方向。文档强调YOLO单阶段检测在速度和精度上的优势,可帮助读者建立从数据准备到模型部署的完整认知。目前已有62人学习。
1. 精准农业里的 YOLOv11:田间病害识别真正难在哪
大田巡检的检测项目里,模型精度常常不是最拖后腿的环节,数据怎么组织、病斑太小查不出来、训练完在设备上推不动,才是让一堆人卡在原地的主要原因。YOLOv11 多作物叶片分析技术与病害识别全流程,就是一套覆盖“多作物叶片数据准备 → 病害识别模型训练 → 小目标优化 → Jetson 边缘部署”的完整做法:一套 YOLOv11 模型同时识别水稻、玉米、番茄等不同作物的叶片区域和病害类别,并输出每一张图的病害框、类别和置信度,最后落地到田间设备上跑推理。这篇笔记写给刚接手农业检测项目、正在评估技术路线的工程师团队。下面的内容全部按我实际操作过的顺序来写,包括脚本、参数和我翻过车的细节。
2. 多作物叶片数据准备:从田间照片到 YOLO 训练集
2.1 多作物叶片分析的数据采集与目录规划
农业检测和公开数据集最大的区别是:一张照片里叶片重叠严重、背景是泥土和杂草、同一个病斑在不同作物上表现差异很大。所以第一步不是急着标数据,而是先规划目录。我一般按作物建子目录,文件名带作物前缀和拍摄日期,例如rice_20240712_001.jpg、corn_20240720_015.jpg。这个命名习惯后面切训练集、验证集时非常关键,能直接按前缀做分层划分,避免某个作物的照片全进了训练集而验证集里全是另一个作物。
目录结构我建议这样建:
datasets/leaf_disease/ ├── json/ # labelme 原始标注 ├── images/ # 原始图片 ├── labels/ # 转换后的 YOLO txt ├── train/images/ ├── train/labels/ ├── val/images/ ├── val/labels/ └── test/images/注意test目录不是必须的,但强烈建议预留。因为农业模型的评估不能只在验证集上看数字,最后要用一批拍新田块的照片来测,那部分数据在训练期间一次都不能碰。
采集照片时有个硬性要求:病斑在画面里至少占 40 像素以上,否则标注出来也没法训练。田间不是实验室,手机、无人机、固定摄像头拍出来的叶片尺度差异极大,同一个模型要同时应对这两种尺度,后续要做第 4 章的切图优化。病害类别划分也要提前统一:是只做“有病/无病”,还是按作物细分具体病害。我建议按“健康 + 具体病害”的方式建类别,因为农户最关心的就是“这是什么病、该打什么药”,只告诉人家“有病”解决不了问题。
2.2 用 labelme 标注转 YOLO 格式:一个脚本解决问题
标注工具我用 labelme,因为它能画多边形,叶片边缘不规则,矩形框会带入大量背景干扰。labelme 输出的是 JSON 文件,YOLOv11 训练需要的是 txt 文件,两者格式差别很大。多作物场景下叶片分析技术的第一步就是做格式转换,下面是我常用来做转换的脚本,按自己的标注目录改一下就能跑。
# labelme_to_yolo.py import json, os, glob from pathlib import Path # 注意:类别顺序必须和后续训练 yaml 里的 names 一一对应 classes = ["healthy", "rice_blast", "corn_leaf_spot", "tomato_late_blight", "cucumber_downy_mildew", "wheat_rust", "grape_black_rot", "apple_scab"] def convert_labelme(json_path, out_dir): with open(json_path, encoding="utf-8") as f: data = json.load(f) img_w, img_h = data["imageWidth"], data["imageHeight"] txt_path = os.path.join(out_dir, Path(json_path).stem + ".txt") with open(txt_path, "w", encoding="utf-8") as out: for shape in data["shapes"]: label = shape["label"] if label not in classes: continue cls_id = classes.index(label) points = shape["points"] # 多边形外接矩形转 YOLO 归一化坐标 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h out.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") if __name__ == "__main__": os.makedirs("labels", exist_ok=True) for f in glob.glob("json/*.json"): convert_labelme(f, "labels") print("converted:", len(glob.glob("json/*.json")))脚本里最关键的是classes的顺序定义。YOLO 格式的第一列是类别索引,如果标注的标签名和classes顺序对不上,后面训练出来的模型会出现类别错乱,而且这种错乱没法从 loss 曲线上看出来。转完格式后随机抽几张图,用yolo detect predict或者可视化脚本把标注框画回去检查一遍再进入训练,这个步骤别省。
另一个容易忽略的问题是坐标归一化。YOLO 格式里中心点、宽高都是 0 到 1 的小数,脚本里除以img_w和img_h做了归一化。如果你的原始图片有 EXIF 旋转信息,得先统一转成无旋转的 JPG 再做标注,否则坐标和实际像素对不上,这是很多标注工具里被隐藏起来的坑。
2.3 样本均衡与划分:按作物分层别让模型偏心
多作物的样本不均衡远比公开数据集严重。同样是 1000 张图,水稻稻瘟病的框可能有 4000 个,而番茄晚疫病只有 300 个。直接训练会出现一个很典型的现象:mAP 看着不错,但按类别看番茄的 AP 只有 20%,大模型在样本多的类别上表现好掩盖了这个问题。我一般用按作物分层的划分脚本,保证每个作物在 train、val、test 里的比例基本一致。
# split_by_crop.py import os, random, shutil from collections import defaultdict random.seed(42) images_dir = "images" labels_dir = "labels" out_dir = "split_dataset" def split_for_crop(crop, imgs, train_ratio=0.8, val_ratio=0.15): n = len(imgs) n_train = int(n * train_ratio) n_val = int(n * val_ratio) for i, img in enumerate(imgs): label = img.replace(".jpg", ".txt") if i < n_train: dest = "train" elif i < n_train + n_val: dest = "val" else: dest = "test" shutil.copy(os.path.join(images_dir, img), os.path.join(out_dir, dest, "images", img)) shutil.copy(os.path.join(labels_dir, label), os.path.join(out_dir, dest, "labels", label)) files = [f for f in os.listdir(images_dir) if f.endswith(".jpg")] by_crop = defaultdict(list) for f in files: crop = f.split("_")[0] by_crop[crop].append(f) for crop, imgs in by_crop.items(): random.shuffle(imgs) split_for_crop(crop, imgs)这个脚本做的事很朴素但特别重要:同一作物的图片随机打散后按 8:1.5:0.5 划分,文件名里不体现作物的项目要先写个映射表。跑完查看每个子目录里各类别的框数量,如果番茄晚疫病在验证集里只有几个框,那宁可把这几个框挪进训练集,也要保证验证集的每个类别有 30 个以上样本,否则评估数字没有参考意义。
3. YOLOv11 环境配置与训练:从 yaml 到 best.pt
3.1 YOLOv11 环境配置:虚拟环境隔离是必须的
“yolov11环境配置”翻车率很高。80% 的问题出在一个环境里同时装了 YOLOv8、YOLOv5 和 labelme,依赖互相覆盖,import 直接报错。我的做法是每个项目单独建一个 Python 虚拟环境。这里有个经验:不要用 conda 的 base 环境,也不要直接pip install ultralytics装到全局。
python -m venv yolo11_env source yolo11_env/bin/activate pip install ultralytics pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 python -c "from ultralytics import YOLO; print(YOLO.__version__)"--index-url指定的是 PyTorch 官网的 CUDA 轮子地址,cu118对应 CUDA 11.8。你的机器 CUDA 版本如果是 12.x,把这里改成cu121或更高。Ultralytics 包会自动带一个匹配版本的 torch,但那种装法经常装成 CPU 版本,训练时 CUDA 不可用,所以先装 torch 再装 ultralytics 的顺序更可靠。装完以后直接yolo predict跑一张图验证环境,比训练到一半才发现显存没跑满要省事得多。
3.2 数据 yaml 与最小训练命令
YOLOv11 训练入口就是一个 yaml 文件加一条命令。这个 yaml 决定了模型认识哪几种树叶、从哪个目录读图,配错了后面全部白干。多作物叶片分析里面,叶片类别和病害类别混在同一份 names 列表里再常见不过。
# datasets/leaf_disease.yaml path: /home/user/datasets/leaf_disease train: train/images val: val/images test: test/images nc: 8 names: 0: healthy 1: rice_blast 2: corn_leaf_spot 3: tomato_late_blight 4: cucumber_downy_mildew 5: wheat_rust 6: grape_black_rot 7: apple_scabpath是绝对路径,建议写绝对路径而不是相对路径,因为很多训练服务器上工作目录和代码目录不一致,train/images是在 path 之下的相对路径。nc必须和 names 数量一致。训练命令用 yolov11s 起步,内存和显存都吃紧的团队先用 s 而不是 x。
yolo detect train \ data=datasets/leaf_disease.yaml \ model=yolo11s.pt \ epochs=200 \ imgsz=1280 \ batch=16 \ patience=30 \ device=0 \ project=runs/leaf_disease \ name=exp01这条命令里我唯一强调的参数是imgsz=1280。很多人拿着默认 640 直接训练,病斑在缩小的图上只有十几个像素,模型根本学不到纹理特征,后面怎么调注意力都没用。GPU 显存不够的话,先把 batch 从 16 降到 8,不要动 imgsz,1280 对农业病害识别来说是底线而不是上限。
训练完成后产物在runs/leaf_disease/exp01/weights/下,best.pt和last.pt。best.pt是按验证集指标保存的最优权重,后续推理全部用它。训练日志里出现nanloss 的话,优先查学习率是不是默认值在数据量太小时炸了,把lr0从默认值降到 0.0005 重跑。
3.3 推理结果保存:save_txt 与 save_crop 的正确用法
热词里常搜“yolov11保存推理结果”和“yolov11预测后保存”,这里一次写清楚。训练完跑批量预测时,save=True只能输出画了框的图片,而真正落到病害清点系统里的是 txt 文件和按类别裁剪出来的病斑图。
from ultralytics import YOLO model = YOLO("runs/leaf_disease/exp01/weights/best.pt") results = model.predict( "field_photos/2024_07_rice/", imgsz=1280, conf=0.25, save=True, save_txt=True, save_conf=True, save_crop=True, ) for res in results: print(res.path, res.boxes.cls.cpu().numpy(), res.boxes.conf.cpu().numpy())sava_txt=True会在每个图片目录下生成同名 txt,里面是类别id 中心x 中心y 宽 高 置信度,save_crop=True会把框内区域按类别存成裁剪图,这个功能在农业场景里价值很大:病害识别结果最后要拿给农艺师看,裁剪图比整图更容易放大查看病斑形态,而且可以用来做二次复核数据集的素材。
注意conf阈值的设定。田间场景类别不平衡时,0.25 会漏掉大量低置信度的真实病斑,农艺师更反感漏报而不是误报。实际项目里我会分两档:给农户的告警用 0.4 以上,给数据分析团队的用 0.15 的结果全量导出,后面人工复核。
4. 叶片病斑小目标优化:精度从 60% 拉到 85% 的三种做法
4.1 病斑为什么是小目标:分辨率决定的
水稻稻瘟病的典型病斑只有米粒大小,在一张 3000×4000 的田间照片里可能只占 80×60 像素,缩到 640×640 之后只剩 17×13 像素。YOLOv11 默认的检测头对这种尺寸非常吃力。小目标优化不是玄学,核心只有三条路:提高输入分辨率、切图推理、加针对小目标的检测头或注意力机制。三条路的效果是叠加的,不是三选一。
这里提一下网络结构层面的做法。“yolov11网络结构”在农业场景里比较大的改动是增加 P2 小目标检测头——把浅层的高分辨率特征图也接入检测分支,让模型能看到更小的病斑细节。Ultralytics 官方没有直接提供 P2 配置,社区里常见的做法是在 yaml 里手动扩展 neck 和 head 部分,把 stride 为 4 的特征图接进来。这条路需要对网络结构比较熟,新手可以先不做,把输入分辨率和切图做起来,效果提升比改结构来得快得多。
注意力机制在叶片病害识别上也被广泛使用,例如 HCANet 这类以通道注意力为核心的结构,思路是让网络更关注病害区域在 RGB 通道上的细微色差。实际项目中,叶片病斑和健康组织的颜色差异往往只集中在某个通道上,通道注意力对这类任务有正向作用,但注意:加注意力模块会让推理速度下降,边缘部署时可能得不偿失。
4.2 切图预测:SAHI 思路的最小实现
把大图切成瓦片再预测,是小目标优化里性价比最高的一步。本质就是训练时用 1280,推理时用 640 的滑动窗口把大图切开放大后再进模型,相当于变相把病斑放大。这也解释了热词里经常搜的“yolov11小目标优化”——大多数场景不需要改模型结构,切图就能解决一大半问题。
# tile_predict.py from ultralytics import YOLO import cv2 import numpy as np model = YOLO("best.pt") tile_size = 640 overlap = 0.2 def non_max_suppress(boxes, scores, iou_thr=0.5): # 简易 NMS,实际可直接用 torchvision.ops.nms from torchvision.ops import nms keep = nms(torch.tensor(boxes), torch.tensor(scores), iou_thr) return keep img = cv2.imread("large_field.jpg") h, w = img.shape[:2] step = int(tile_size * (1 - overlap)) boxes_all, scores_all = [], [] for y in range(0, h, step): for x in range(0, w, step): tile = img[y:y+tile_size, x:x+tile_size] res = model.predict(tile, imgsz=640, conf=0.2, verbose=False)[0] for box, score in zip(res.boxes.xyxy.cpu().numpy(), res.boxes.conf.cpu().numpy()): # 把瓦片坐标还原成原图坐标 x1, y1, x2, y2 = box boxes_all.append([x + x1, y + y1, x + x2, y + y2]) scores_all.append(score) # NMS 合并重叠区域的重复框 boxes_np = np.array(boxes_all) scores_np = np.array(scores_all)切图预测有两个关键参数。tile_size=640对应训练时的输入尺寸,如果你的模型训练时用的就是 1280,这里可以用 640 或 832,既放大原图细节又不至于让推理时间爆炸。overlap=0.2是相邻瓦片的重叠率,病斑正好被切到边缘时,另一个瓦片能兜住,重叠太小会漏检,太大推理时间翻倍。合并阶段 NMS 的 IoU 阈值取 0.5 比较合适,重叠产生的重复框一般 IoU 都在 0.7 以上,0.5 能稳定合并掉。
4.3 训练增强与后处理:三个容易被忽视的细节
第一,关闭 Mosaic 增强。YOLOv11 默认训练开了 Mosaic,它在通用目标检测里能提升鲁棒性,但农业叶片场景反而会掉点:多作物叶片分析里叶片往往占满整张图,Mosaic 把四张图拼在一起后,单个病斑被缩到极小,模型学到的是拼接痕迹而不是病害特征。在训练 yaml 里显式设置mosaic: 0.0,并用close_mosaic: 10让最后 10 个 epoch 彻底关闭,效果会比较明显。
第二,训练用的类别名不要用中文。这不是 YOLO 的问题,而是后续部署到 Jetson 或导出 TensorRT 时,中文类别名在保存推理结果的 txt 和画框的字体渲染上会出乱码。用拼音或者英文维护一份 id 到中文病名映射表,部署层再做展示转换,这是农业项目最省心的做法。
第三,验证时把原始大图直接喂给模型而不是切图。很多人做完切图推理后,拿切图 FPS 和大图指标对比,发现 mAP 反而下降了,误以为是切图的锅。实际上切图验证要用同样的切图流程重新跑一遍验证集再算指标,否则“三种做法叠加提升了多少”根本没法衡量。每次改动都单独记一组指标,别凭感觉判断。
5. 部署与推理环境避坑:在 Jetson Nano 上跑 YOLOv11 的排雷记录
5.1 环境配套与导出流程
“jetson nano部署yolov11”是搜索热度很高的方向,但部署这一步真正动手时会发现,坑全在环境配套上。Jetson 的 JetPack 版本决定 CUDA、cuDNN 和 PyTorch 的可用版本,官方 PyTorch 轮子和 Jetson 的 CUDA 不完全兼容,直接pip install torch装出来的版本百分之百报 CUDA 错误。常见做法是先查 JetPack 版本,然后从对应渠道下载对应版本的 torch 和 torchvision。
# 查看 JetPack 版本 cat /etc/nv_tegra_release # 在设备上用对应版本 torch 跑通一条预测 python -c "import torch; print(torch.cuda.is_available())"在电脑上训练完,导出成 TensorRT 引擎再部署到 Jetson 上,推理速度能快 3 到 5 倍。导出命令本身不复杂,但有个坑:导出的结果是针对你的 GPU 架构优化的,在电脑上导出的 .engine 文件不能直接复制到 Jetson 上用,必须在 Jetson 上重新导出一次。这个过程会跑几分钟,别以为是指纹不对或者文件损坏。
yolo export model=best.pt format=engine device=0在 Jetson 上推理时,imgsz必须与导出时一致。导出时指定了 640,推理时用 1280 会直接报 shape mismatch 或者自动重新编译,速度慢且结果异常。
5.2 三个常见问题:现象、原因、解决
问题一:训练好的模型在 Jetson 上预测结果全是乱框。
现象:检测框大量重叠,类别完全对不上。原因:数据集 yaml 的 names 顺序和导出时不一致,或者推理脚本里把类别 id 当成了类别名直接输出。解决:把数据集 yaml 备份一份放在 weights 目录旁边,部署时强制读取这份 yaml,不要靠记忆手写 names 列表。踩过一次这个坑之后,我的目录结构固定是weights/里同时放best.pt、leaf_disease.yaml和一份labels.txt,部署脚本强制校验三者类别数一致。
问题二:保存推理结果的 txt 里坐标和图片对不上。
现象:画框在原图上看位置正确,但 txt 里的坐标拿去清点系统里画就偏了。原因:save_txt=True输出的坐标是归一化的,而且基于原始输入尺寸,如果你的推理脚本对图片做了 resize 预处理,需要先还原到原图坐标再保存。解决:用results[0].boxes.xyxy拿到的绝对坐标,自己做一次映射,不要直接用 save_txt 的产物,除非你确认推理时没有额外 resize。这个坑在部署到 Jetson 上做视频流推理时特别容易出现,因为视频帧的尺寸和训练集不一致。
问题三:汉显乱码。
现象:画框的标签显示为方块。原因:OpenCV 的 putText 不支持中文,Jetson 上也未必装了中文字体。解决:画框时只画类别 id 或拼音,再在 UI 层做中文映射。别在检测环节里硬画中文,既影响推理速度,又给自己制造字体依赖。
5.3 Jetson 部署中“yolov11预测后保存”的整体流程
Jetson 上的推理结果保存和桌面端不完全一样。桌面端可以直接save=True让 Ultralytics 帮你存图,Jetson 上内存吃紧,一般建议只保留检测框数据,异步写盘。做法是推理循环里只存boxes.xyxy和cls、conf的 numpy 数组,攒满一批再统一回调保存,避免每帧都做一次图像编码。这样跑视频流时内存占用能下降 30% 到 40%,帧率也稳得多。前面提到的save_crop=True在 Jetson 上慎用,裁剪和编码非常费 CPU,实测会让推理耗时翻倍。
6. 模型验证与现场可用性:不看单点 mAP,看三类关键结果
6.1 三个比 mAP 更能说明问题的指标
训练结束的第一时间,不要只盯着训练日志里的 mAP50。多作物叶片分析里,我更关注验证集混淆矩阵、每个类别的单独 AP、以及低置信度样本的检测结果。混淆矩阵能直接看出番茄晚疫病和健康叶片被分错的比例,这一点比整体 mAP 更关键——整体 mAP 高,很可能只是因为健康叶片的样本多。按类别拆 AP 的做法是:用model.val()跑完一次验证后,结果里ap_class_index会把每个类别的 AP 输出到 CSV 里,按类别名单独看。
另一个容易被忽略的是 [email protected]:0.95 这个指标。田间病害识别最终要按病斑面积估损,框的位置精度直接决定面积计算的准确性,mAP50 只要求框和标注框有 50% 的重叠,对面积估算来说太宽松。如果这个指标上不去,大概率是标注框本身就不准,回查标注数据比继续调模型收益更大。
6.2 野外盲测:保存推理结果做回看库
验证的最后一步是盲测。我带 20 张完全没参与训练的新田块照片,跑完推理后把结果全部保存下来,和农艺师的人工判读做对比。注意盲测照片的拍摄设备要和部署现场的设备一致,手机拍的模型不一定能适应无人机视角,这是“多作物叶片分析技术”落地时最常被忽视的问题。
现在我的习惯是,每次盲测的推理结果都单独建一个error_analysis/目录,把漏检和误检的图按原因分类归档,比如“遮挡严重”“光照过暗”“病斑过小”“背景干扰”。一个项目迭代到第三轮时,翻看误检归档比看指标曲线更能找到下一步优化方向——这个代码库的说明书给不了你,只有你自己保存的推理结果能告诉你模型真正在什么地方失效。希望这些在田间和设备上磨出来的做法能帮到你。
本文还有配套的精品资源,点击获取