☰
YOLOv11农业无人机病虫害检测实战:数据、训练与部署全解析
2026/10/6 12:04:10 网站建设 项目流程

简介:《农业无人机巡检实战:YOLOv11实现作物病虫害实时识别与定位》是一份面向农业科技从业者、无人机应用开发者和计算机视觉学习者的实战型技术文档。文档围绕YOLOv11单阶段目标检测算法,系统讲解作物病虫害数据采集、标注、清洗、增强与数据集划分,再到模型训练、优化、评估的全部流程。内容覆盖无人机选型与搭载设备适配、数据传输与通信、模型轻量化及边缘端部署,并结合具体农场案例展示病虫害实时识别与定位的实施过程与效益,适合作为农业智能化项目的技术参考。资源包共1个PDF文件,大小1.98MB,全篇35页,支持目录章节跳转,可快速定位感兴趣的内容。已有79人学习下载,适合需要将目标检测技术落地于农业巡检场景的读者。

1. 从一张航片到一份“病虫害地图”:YOLOv11到底解决了什么

做农业无人机巡检的同行应该都有过这种体验:飞一圈下来几百上千张航片,晚上导进电脑里一张张看,圈出病斑、数出虫害区域,手脚快的人一小时也处理不完一个架次的数据。这个场景最痛的不是飞,而是“看”。而 YOLOv11 这类目标检测模型,解决的就是把“人眼盯屏幕”变成“模型自动框出病斑和虫害区域”——识别出是什么病害、给出置信度、标出位置,顺带统计每块田的发病面积占比。这篇笔记我就围绕“农业无人机巡检 + YOLOv11 实时识别与定位”这条主线,把数据准备、模型训练、部署推理和常见翻车点一次讲透。

先给结论:YOLOv11 在农业场景里最大的价值不是精度分数,而是“推理速度快 + 小目标检测上限高”。无人机航拍不同于手机拍叶子,目标在整幅图里往往只占几十个像素,模型能不能抓住这些小目标,直接决定了这套方案能不能落地。本文适合手里已经有无人机和基础 Python 环境、想自己从零跑通一版检测模型的从业者,也适合只想评估“这技术值不值得投入”的团队负责人。

2. 无人机图像数据准备:从视频抽帧到 YOLO 格式的完整链路

2.1 为什么不能直接拿原始航片去训练

很多新手拿到无人机拍的 4K 视频或 JPG 航片,直接就开始标注,然后丢进 YOLO 训练——十有八九翻车。原因有两个维度。第一,无人机默认拍的是广角地面影像,视场大但单株作物占比小,直接把整张图缩放到 640x640 去训练,一个病斑可能只有 2-3 个像素,模型根本学不到纹理特征。第二,航片存在重叠率,同一株病株会出现在连续好几张照片里,不经去重直接划分训练集和验证集,会造成严重的数据泄漏,验证分数虚高到没参考意义。

正确的做法是先做“切片预处理”。把原始大图切成 640x640 或 1024x1024 的小块,每块之间保留一定重叠率,然后只保留含有目标的切片参与标注和训练。常见做法是先用 OpenCV 写一个滑窗切片脚本,把一亩地的航片切成几百张子图,再做一次人工筛选,去掉完全没有作物的空白块。

2.2 视频抽帧 + 滑窗切片脚本示例与参数解释

下面这段脚本是我常用的抽帧加切片方案,输入是无人机视频或照片文件夹,输出是可直接进入标注工具的切片图集:

import cv2 import os from glob import glob def extract_frames(video_path, output_dir, interval=30): """按间隔帧抽图,避免连续帧重复度过高""" cap = cv2.VideoCapture(video_path) os.makedirs(output_dir, exist_ok=True) frame_count = 0 saved_count = 0 while True: ret, frame = cap.read() if not ret: break if frame_count % interval == 0: cv2.imwrite(os.path.join(output_dir, f"frame_{saved_count:05d}.jpg"), frame) saved_count += 1 frame_count += 1 cap.release() print(f"抽取 {saved_count} 帧,原始视频共 {frame_count} 帧") def sliding_window_crop(image_path, output_dir, crop_size=640, overlap=0.2): """滑窗切片,overlap 控制相邻切片的重叠度""" img = cv2.imread(image_path) h, w = img.shape[:2] step = int(crop_size * (1 - overlap)) os.makedirs(output_dir, exist_ok=True) base_name = os.path.splitext(os.path.basename(image_path))[0] idx = 0 for y in range(0, h - crop_size + 1, step): for x in range(0, w - crop_size + 1, step): crop = img[y:y + crop_size, x:x + crop_size] cv2.imwrite(os.path.join(output_dir, f"{base_name}_{idx:03d}.jpg"), crop) idx += 1 print(f"{base_name} 共生成 {idx} 个切片")

逻辑说明:extract_frames里的interval=30表示每 30 帧抽一帧,对应 30fps 视频就是每秒取一张,兼顾信息密度和后续去重成本。sliding_window_crop里的overlap=0.2是给相邻切片留 20% 的重叠区,防止病斑恰好被切边界切断——这是训练数据增强之外最朴素的“保召回”手段。如果目标是水稻稻瘟病这类病斑极小的场景,切片尺寸建议缩到 512,让病斑在切片里的相对占比更大,模型更容易学到判别特征。

2.3 标注工具选型与格式转换:LabelImg 到 YOLO txt 的四步流程

切片准备好以后就是标注。常见做法是先用 LabelImg 或 X-AnyLabeling 做手工框选,导出 Pascal VOC 格式的 XML,再转成 YOLO 需要的 txt 格式。为什么不直接用 LabelImg 存 YOLO 格式?因为团队协作或多轮复标时,XML 的兼容性更好,而且很多公开的农业病虫害数据集都是 VOC 格式。

转换的四个步骤是:解析 XML 中的 object 框坐标 → 将左上角坐标加宽高换算成中心点坐标 → 分别除以图片宽高做归一化 → 按“类别ID x_center y_center width height”写入 txt。下面给一段单文件转换的参考逻辑:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, txt_path, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: continue class_id = class_map[name] box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) x_center = (x1 + x2) / 2 / img_w y_center = (y1 + y2) / 2 / img_h width = (x2 - x1) / img_w height = (y2 - y1) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines))

参数说明:class_map是类别名字到 ID 的映射字典,比如{"稻瘟病": 0, "二化螟": 1}。注意归一化必须用该图片的实际宽高,不能统一用 640,否则框位会整体偏移。这个脚本只处理了单张,实际使用时建议写批量循环,同时在转换后抽几张图做可视化校验——把框画回原图确认没有坐标错位。这一步偷懒,后面训练出来的模型框全是偏的,找错能找一整天。

3. 训练自己的 YOLOv11 检测模型:环境配置、模型选型与参数调优

3.1 Ultralytics 环境搭配:适合 0 基础纯小白的安装步骤

YOLOv11 目前最省事的落地路径是直接走 Ultralytics 的 Python 包,它把训练、验证、导出、推理统一封装了。很多博客里说的“yolov11(ultralytics)环境配置,适合0基础纯小白”,本质就是三件事:装 Python 3.8+、用 pip 装 ultralytics、跑通官方预训练权重。

# 建议使用 conda 创建独立环境,避免污染系统 Python conda create -n yolo11 python=3.10 -y conda activate yolo11 pip install ultralytics # 验证安装是否成功 yolo predict model=yolo11n.pt source=https://ultralytics.com/images/bus.jpg

如果这一步能正常打印检测结果,说明环境没问题。GPU 用户还需要额外装 CUDA 版 PyTorch,顺序是先装 torch 再装 ultralytics,避免依赖冲突。常见错误是装完 ultralytics 之后发现 torch 是 CPU 版,训练慢到怀疑人生,检查方式是python -c "import torch; print(torch.cuda.is_available())"。

3.2 迁移学习:用 yolo11n.pt 还是从头训练

做农业病虫害识别不建议从头训练。公开的作物病虫害数据集规模通常在几千到几万张,比起 ImageNet 级别的预训练数据差了两个数量级,从头训练不仅收敛慢,而且极易过拟合。常见做法是加载 COCO 预训练权重做迁移学习,只替换最后分类头,冻结前几层或全量微调看数据规模而定。

数据量在 3000 张以下时,我一般会冻结 backbone 的前 10 层,只训练 head 部分;数据量超过 8000 张,直接全量微调,效果更好。冻结层数的操作在 Ultralytics 里没有直接开关,需要手动遍历模型参数设置requires_grad=False,对纯小白来说略复杂,所以更推荐一个折中方案:先全量微调 50 个 epoch,如果验证集出现明显过拟合(训练 loss 降但验证 loss 升),再回头冻结层重训。

3.3 三个必调参数:imgsz、epochs、batch 的取值逻辑

YOLOv11 训练脚本里最关键的参数不是 learning rate,而是这三个:

yolo train data=custom.yaml model=yolo11n.pt epochs=200 imgsz=640 batch=16 device=0

imgsz直接决定小目标检测上限。叶片病斑在 640x640 图上可能只有 10x10 像素,你把它压到 320 训练,特征直接消失。一般建议是 640 起步,显存允许的话用 1024。epochs不是越大越好,农业数据背景相对单一,200 epoch 足够,配合早停反而比 500 epoch 更稳。batch主要受显存限制,如果训练时显存溢出,优先减小 batch 而不是 imgsz——因为减小 imgsz 的副作用比减小 batch 更难以察觉。

data=custom.yaml是数据集配置文件,核心字段包括path(数据集根目录)、train和val(相对路径)、names(类别名列表)。一个典型的配置长这样:

path: /data/agri_drone train: images/train val: images/val nc: 2 names: ['rice_blast', 'stem_borer']

3.4 小目标优化:为什么要单独给航拍场景调 anchor

YOLOv11 已经内置了自适应 anchor 计算,训练时会根据你的数据集自动重新聚类 anchor 尺寸。但无人机视角下的目标尺寸分布极端——大量目标小于 32x32 像素——默认 anchor 仍然偏向常规物体尺度。常见做法是在训练前先统计标注框的尺寸分布,确认小目标占比,再决定是否启用额外的数据增强策略。

Ultralytics 里和这个小目标问题直接相关的参数是scale和flipud。scale=0.5表示随机缩放范围是 0.5 到 1.5,对小目标场景建议改成scale=0.3,减小下采样概率。flipud=0.5开启上下翻转,对无人机俯拍图适用,但如果你拍的是斜视角度航片,上下翻转会让叶子纹理方向混乱,建议关掉。直观来说,无人机图是“俯视大地”,不做翻转就已经很丰富了,叠太多增强反而容易让模型学到错误的纹理方向。

3.5 训练结果评估:mAP50 和 mAP50-95 到底该看哪个

训练结束以后 Ultralytics 会在runs/detect/train目录下输出一堆指标曲线,新手容易只盯着 mAP50 看。这两个指标的含义有实质区别:mAP50 只要求在 IoU 阈值 0.5 下框和真值重叠过半就算命中,对定位精度要求低;mAP50-95 是把 0.5 到 0.95 的 IoU 阈值都算一遍取平均,对框的贴合度要求苛刻得多。

农业巡检场景建议两个都看,但侧重点不同。如果你做的是大范围虫害普查,只需要知道“哪块田有虫”,mAP50 够用;如果你要做精准施药,需要框出病斑边缘来控制喷头开关,那 mAP50-95 必须达到 0.5 以上,否则定位误差会直接导致漏喷或误喷。调参时如果 mAP50 涨但 mAP50-95 不涨,说明模型“找到了目标但框不准”,优先去调回归损失权重或换更高输入分辨率。

4. 部署到无人机边缘端:实时识别与定位的两种落地路径

4.1 机载端实时推理的算力约束

无人机机载电脑的算力远不如地面工作站,YOLOv11 的 n 版本在 Jetson Orin Nano 上跑 640x640 输入大约能到 30-50 FPS,s 版本大概 20-30 FPS。这里有个容易被忽略的点:实时识别的瓶颈往往不是模型推理速度,而是相机数据的读取和解码。USB 摄像头或 HDMI 采集卡在 Jetson 上的延迟可能达到 100-200ms,你模型再快也补不回来。

机载端实时推理的典型方案是:用 CSI 接口相机或支持直接内存映射的网络相机,图像先缩小到 960x540 再做模型推理,检测结果直接叠加在视频流上,同时把目标 GPS 坐标通过 RTK 定位模块写入 CSV。要做实时识别与定位,“定位”靠的其实是无人机自身的 GPS/RTK 数据,模型只负责告诉你“这个框里的病害类型”,然后把框中心像素坐标换算成相机光心偏移角,结合无人机姿态角算出地理坐标。这套坐标换算的精度受飞行高度和云台角度影响很大,飞行高度 30 米时,一个像素的误差可能就是几厘米。

4.2 地面端离线批处理方案:飞完再算更稳

另一种更稳妥的做法是无人机只管采集,降落后把航片拷贝到地面工作站批量推理。这种做法在农业植保队里非常常见——因为植保无人机的载重和供电都优先保证喷洒系统,机载电脑往往只能跑个 n 模型,检测精度上不去。地面端用 s 甚至 m 模型,配一张 RTX 4070 显卡,一张 20MB 的航片推理时间在 100ms 以内,加上切片拼接的后处理,整个流程比机载端更快更准。

地面端的另一个优势是可以做大图拼接和重复检测去除。机载端因为每帧都独立推理,同一株病株会连续出现在多帧画面里,重复计数。地面端可以用 SIFT 特征做图像配准,或者用简单粗暴的“检测框中心距离小于阈值即合并”策略去重。我做项目时更倾向后者,省时间而且对精度影响可控。

def merge_duplicate_boxes(boxes, distance_thresh=30): """合并中心距离过近的检测框,去重重复识别""" merged = [] used = [False] * len(boxes) for i, box in enumerate(boxes): if used[i]: continue group = [box] used[i] = True for j, other in enumerate(boxes): if used[j]: continue center_i = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2) center_j = ((other[0] + other[2]) / 2, (other[1] + other[3]) / 2) dist = ((center_i[0] - center_j[0]) ** 2 + (center_i[1] - center_j[1]) ** 2) ** 0.5 if dist < distance_thresh: group.append(other) used[j] = True merged.append(group) return merged

参数说明:distance_thresh=30表示两个框的中心点距离在 30 像素以内就视为重复检测。这个值要根据飞行高度和航片分辨率调整,高度越低、分辨率越高,同一个目标在相邻帧中的位移越大,阈值要相应放大。group 里的多个框取置信度最高那个作为最终框,避免平均框导致定位偏移。

4.3 模型导出与推理脚本:从 .pt 到 .onnx 再到 TensorRT

机载端和地面端的部署形态不同,但导出流程是一致的。Ultralytics 提供了一行命令导出多种格式:

yolo export model=best.pt format=onnx imgsz=640 opset=12 yolo export model=best.pt format=engine device=0

format=engine是导出 TensorRT 引擎,只对 NVIDIA 设备生效,导出时指定的imgsz会被固定,运行时不能动态改分辨率。这一点要注意,很多新手导出 engine 后换了个分辨率预测,直接报错找半天原因。opset=12是为了兼容 JetPack 里的老版本 TensorRT,如果你用的是最新 JetPack 6.x,可以适当调高到 13 或 14。

导出为 engine 后推理脚本也需要同步调整,TensorRT 引擎的输入输出是固定 shape,不能再像 .pt 那样随意接收任意尺寸。建议在代码里写死imgsz并在预处理阶段做 letterbox 填充,而不是直接 resize——letterbox 能保持目标不被拉伸变形,对检测精度影响最小。

5. 农业巡检测模型的避坑手册:5 个高频翻车场景与对应解法

5.1 现象:模型把田埂误检成作物病害

这是最常见的问题,特征是在验证集上 mAP 很高,但拿到新地块一测全是误报。原因是你训练集里的负样本太少——无人机航拍图里除了作物,还有田埂、杂草、泥土、水渠,这些背景如果没标注为“背景”也没有在训练时被当作负样本,模型就会把纹理相似的区域一律当成目标。

解决方法是先人工快速看一遍训练集切片的背景多样性,如果发现绝大多数切片都是纯作物,就去采集一些不同地块、不同光照、不同土质的负样本加入训练集。另一个简便做法是在数据增强里增加mosaic=1.0和copy_paste=0.3,让模型见的背景组合更丰富。

5.2 现象:病斑太小导致漏检,mAP50 很高但 mAP50-95 极低

特征是小目标漏检率超过 50%,尤其在大面积航片上。原因是切片后病斑即使放大了,仍然只占图幅的很小比例,模型的特征金字塔对小目标的响应天然弱于大目标。

推荐解法是切换 YOLOv11 的变体。YOLOv11 有个专门优化小目标的思路是引入更细粒度的检测头或更高的特征分辨率,对应到 Ultralytics 的层面,最简单的做法是提升输入分辨率到 1024,同时开启augment=True里的multi_scale参数,让模型在训练时见到多种尺度的目标。进阶做法是修改模型结构,在 backbone 后面额外接一个针对小目标的检测层,这个需要改源码,不是开箱即用,建议先试分辨率方案。

5.3 现象:训练 loss 正常下降,但每个 batch 速度慢到无法忍受

这一般不是代码问题,是数据加载瓶颈。无人机航拍切片都是几千像素的大图,虽然训练时随机裁剪到 640,但解码整张大图比解码小图慢很多,CPU 数据加载跟不上 GPU 消费速度。Ultralytics 里workers参数默认值偏保守,建议根据 CPU 核数调整:

yolo train data=custom.yaml model=yolo11n.pt workers=8

workers=8一般能缓解,但如果你的 CPU 核心数少,或硬盘是机械盘,瓶颈会转移到磁盘 IO,这个参数反而过犹不及。还有一个隐蔽的坑:航片如果是超大分辨率,预处理时做 letterbox 会生成临时数组,内存占用高,设置cache=disk可以把预处理后的图提前缓存成 LMDB 格式,大幅减少重复计算。

5.4 现象:训练集和验证集都是同一块田拍的,性能虚高

验证 mAP 达到 0.9,换一个地块直接崩,原因就是数据划分不科学。无人机航拍照片有空间相关性,同一块田的病斑分布高度相似,随机划分会把这些强相关图片同时放进训练集和验证集,模型等于“记住了这块田”,而不是学到了“病害的特征”。

正确做法是按地块或飞行架次划分数据集,而不是按单张图划分。如果一个数据集包含 10 块不同的田,就按田块划分成 8 块训练、1 块验证、1 块测试。这样验证集的分数才真实反映跨地块的泛化能力。

5.5 现象:推理时返回结果为空,但训练时 mAP 正常

通常是推理预处理和训练预处理不一致导致。训练时 Ultralytics 会自动做 letterbox 和归一化,但如果你自己写了推理脚本,很容易忘记做归一化(像素除以 255),或者用了直接 resize 而不是 letterbox,导致目标变形、模型输出为空。另外检查一下类别 ID 映射,训练时的names顺序和推理脚本里的class_map是否一致,差一位就全错。

6. 进阶:把 YOLOv11 的检测结果做成可发布的“病虫害分布图”

模型只回答“这是什么、在哪”还远远不够,用户要的是“这块田打不打药、打多少”。我一般会在检测后接一个可视化和统计脚本,输出三样东西:带框标注的巡检图、按网格统计的发病密度图、以及一份可以直接交给植保机作业的处方图(CSV 格式)。

做法不复杂:把航片地图按经纬度网格切块,用检测框中心点做空间聚合统计,落到每个网格里计算病害面积占比。网格大小按作业幅宽来定,比如植保机喷嘴幅宽 5 米,网格就设 5 米,这样处方图可以直接对应到飞机的喷药开关。下面这段逻辑是网格统计的核心:

import pandas as pd import numpy as np def disease_density_map(detections, grid_size_m=5, flight_altitude_m=30, gsd_m_per_px=0.02): """按网格统计病害密度,输出处方图坐标""" df = pd.DataFrame(detections, columns=["x_px", "y_px", "w_px", "h_px", "conf", "class_id"]) df["x_m"] = df["x_px"] * gsd_m_per_px df["y_m"] = df["y_px"] * gsd_m_per_px df["grid_x"] = np.floor(df["x_m"] / grid_size_m).astype(int) df["grid_y"] = np.floor(df["y_m"] / grid_size_m).astype(int) grid_stats = df.groupby(["grid_x", "grid_y"]).agg( disease_area_pct=("w_px", lambda x: (x * gsd_m_per_px).sum() / (grid_size_m ** 2) * 100), det_count=("conf", "count") ).reset_index() return grid_stats

逻辑说明:gsd_m_per_px是地面采样距离,即每个像素对应的实际地面尺寸,这个值由飞行高度和相机焦距决定。不同高度飞出来的数据,这个值完全不同,最好从飞行日志里读取或用一个固定的高度换算——但要记得最后在报告里注明,否则处方图会直接错位。grid_size_m=5跟植保机作业幅宽对齐,统计的是每个 5x5 米网格里的病害占面积比例。

这套方案做到这个程度,已经不是“跑通了一个模型”,而是形成了一套可交付给农户或植保队的决策支持产品。我的经验是:模型精度达到 0.8 的 mAP50 就可以试点,不用死磕到 0.9 再上线,因为实际使用中农户最关心的是“哪个网格有、大致有多大面积”,而不是精确到每一株。等试点反馈回来再针对特定地块做增量训练,投人投时间都更划算。希望这篇笔记能帮你少踩几个我当年踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询