☰
无人机病虫害智能识别与精准施药系统:基于PyTorch与CrossViT的实现
2026/10/19 2:54:47 网站建设 项目流程

简介:基于Python实现的无人机病虫害智能识别与精准施药系统,是一套面向农业植保场景的完整毕业设计/课程设计项目资源,适合计算机视觉、深度学习方向的本科生或开发者参考。系统围绕作物病虫害识别与精准施药两条主线,配套源码、训练与验证脚本、模型定义(crossvit、t2t)、数据集处理、日志及项目说明文档,结构完整,可直接在原有基础上延展二次开发。

资源包共25个文件,压缩后约4.74MB,其中15个py文件承载主程序、训练引擎、模型与采样逻辑,6个pyc为编译产物,另含requirements依赖列表、log.txt运行日志、LICENSE及1份PDF项目文档(智农先锋-无人机病虫害智能识别与精准施药系统),便于对照原理、复现环境。项目源码已经过严格测试,测试脚本test_leaf.py、valid.py等可帮助快速验证效果。目前已有61人学习下载,适合毕业设计开题、课程项目演示或无人机植保应用探索。

1. 从飞控到药箱:无人机病虫害智能识别与精准施药系统的整体破题

做无人机病虫害智能识别与精准施药系统,最容易被卡住的不是飞控代码,而是“无人机飞上去拍回一堆叶片照片,怎么把它们变成一句靠谱的施药指令”。这套基于 Python 与 PyTorch 的方案,把整条链路拆成数据集构建、模型训练、推理决策三段:模型侧采用带 Token-to-Token 结构的 CrossViT,双分支注意力同时兼顾全局纹理与局部病斑;后端再由 classify.py 把概率向量映射成打药剂量与动作。它适合两类人——一类是做毕设或课设的学生,需要一份能跑通、能讲清原理、能上演示的完整工程;另一类是做农机智能化 Demo 的开发者,想在巡检图片上快速验证“识别—决策—施药”闭环是否成立。

2. 总体架构与数据链路:感知层、识别层与决策层的职责切分

从项目的文件布局能看出它的主线是清晰的:datasets.py 负责读数据,trans.py 做图像变换,main.py 启动训练,valid.py 做交叉验证,classify.py 做最终分类与施药决策。这套结构与真实的农业植保作业流程是对应的,我可以把它理解成四个分层:感知层是无人机与图传系统,数据层负责图像采集和标注,识别层是模型推理,决策层输出施药指令。分层最大的好处是每一层可以独立替换——毕设阶段用本地图片测试,后续接入实时图传时只需替换感知层的数据来源,识别与决策代码完全不动。

2.1 四层架构:无人机采集、叶片图像、模型推理与施药决策

感知层的核心是无人机挂载的可见光或多光谱相机,采集到的图像会先经过裁剪与筛选,剔除背景占比过高的帧。数据层是这套系统里最容易被低估的部分,datasets.py 的核心工作就是把“目录名即类别”的原始数据读进来,转换成模型可迭代的 Dataset。识别层拿到的是归一化后的张量,输出的是每个类别的概率分布。决策层则根据概率和预设阈值决定施药动作——这四层里,我认为识别层与决策层必须分开,因为识别模型只回答“叶片有没有病、是什么病”,而施药决策还需要考虑置信度、作业速度和药剂成本,放在一起会让模型训练和业务调参互相干扰。

在实际工程里,我会把决策层的阈值单独放到配置项里,而不是写死在代码中。比如叶斑病的置信度高于 0.85 才触发施药指令,低于 0.6 则标记为待复检。这样做的好处是,当你换一块田、换一种作物时,不需要重新训练模型,只需要调整阈值配置就能改变作业策略。这个习惯在答辩时也很加分,能直接体现你对工程边界的理解。

2.2 数据集组织与 datasets.py 的加载逻辑

这套项目的数据集目录组织方式,沿用的是 PyTorch 里最常见也最稳妥的 ImageFolder 结构。训练集、验证集、测试集各自独立目录,目录名就是类别名。一个典型的结构如下:

data/ train/ healthy/leaf_001.jpg blight/leaf_002.jpg rust/leaf_003.jpg valid/ healthy/leaf_101.jpg blight/leaf_102.jpg test/ healthy/leaf_201.jpg rust/leaf_203.jpg

datasets.py 在加载时通常做的第一件事是统计类别数并生成标签映射表,这一步能避免很多后续的错位问题。我一般会先打印出每个类别的样本数量,确认没有空目录或标签顺序错乱,再进入训练流程。需要注意的一个关键点是:无人机在一趟航线上可能会拍到同一株作物的多张叶片,如果按“全局随机切分”来划分训练集与验证集,同一株作物的不同叶片会同时出现在两边,造成数据泄漏,验证指标虚高。正确做法是按植株 ID 或航拍点位 ID 进行分组切分,这一点项目里如果没做到,扩展时一定要补上。

2.3 叶片图像的预处理与数据增强:trans.py 的常用配置

trans.py 里的增强策略,直接决定模型在大田环境下的泛化表现。无人机拍摄的图片会面临光照变化、叶片角度偏移、飞行高度不同导致的尺度差异,所以增强不能只做简单的翻转和裁剪。通常的做法是采用以下几种组合变换:

import torchvision.transforms as T train_transform = T.Compose([ T.RandomResizedCrop(size=224, scale=(0.6, 1.0)), T.RandomHorizontalFlip(p=0.5), T.RandomVerticalFlip(p=0.3), T.RandomPerspective(distortion_scale=0.2, p=0.3), T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

RandomResizedCrop 里的 scale 参数控制在 0.6 到 1.0,对应无人机在不同高度拍摄时叶片占画面比例的变化;RandomPerspective 模拟飞行角度倾斜带来的透视变形;ColorJitter 负责应对早晚阳光色温的差异。需要特别注意的是,ColorJitter 的幅度不能太高,否则会把叶片本身的颜色特征破坏掉,而叶斑病的判断非常依赖颜色信息。这套变换输出尺寸统一为 224,如果后续要换更大的输入分辨率,务必同步调整模型的位置编码或 patch 尺寸,否则会报维度错误。

2.4 类别映射与施药决策的 JSON 约定

为了让识别结果能真正驱动施药设备,项目中需要一个明确的类别到动作的映射约定。我建议把所有类别、阈值、施药动作集中放到一个配置字典里,例如:

crop_actions = { "healthy": {"threshold": 0.60, "action": "no_spray"}, "blight": {"threshold": 0.85, "action": "variable_rate", "dosage": 1.0}, "rust": {"threshold": 0.85, "action": "variable_rate", "dosage": 1.2} }

这样一个配置的好处是,识别模型输出类别概率后,classify.py 只需要读取该字典就能生成结构化的施药指令。最终下发的 JSON 里包含叶片编号、病害类别、置信度和建议施药动作,既方便日志追溯,也方便与植保无人机的地面站软件对接。在设计答辩时,这往往是评审老师最感兴趣的模块之一,因为它把“图像分类”这个通用任务,真正落到了“精准施药”这个业务场景上。

3. 识别模型实现:T2T-CrossViT 的结构拆解与单叶推理代码解读

模型是整个系统的核心“黑匣子”,但毕设阶段最忌讳的就是把模型当成黑匣子直接跑。我在展开代码之前,习惯先把“为什么选这个结构”讲清楚。项目模型目录下同时存在 t2t 和 crossvit 相关文件,说明采用的是一类带 Token-to-Token 机制的 CrossViT 变体。选它的理由有两个:一是 CrossViT 用双分支结构同时保留不同粒度的视觉信息,符合叶片病斑“既需要看局部细节、又需要看整体分布”的特点;二是 T2T 机制能把相邻像素渐进地聚合成 token,比标准 ViT 直接切 patch 的方式更擅长处理小尺寸病斑。

3.1 模型选型逻辑:为什么弃用 ResNet 而选 CrossViT

传统 CNN 模型(如 ResNet)在叶片病害分类上当然能跑,但它的问题在于感受野是逐步扩大的,浅层特征只能看到局部,深层特征又容易丢失病斑的精确边界。标准 ViT 虽然一次就能看到全局,但固定大小的 patch 切分方式会把病斑切成碎片。T2T-CrossViT 走了中间路线:T2T 模块先把图像从 224 分辨率逐步 token 化,类似“先 8x8 看大结构,再 4x4 看细节”的过程;CrossViT 的双分支随后用不同 patch 尺寸的 attention 分支提取特征,并用交叉注意力机制把两个分支的信息融合。这套设计在处理叶片这种“纹理和颜色都重要”的目标时,实际效果通常优于单分支 ViT,尤其在病斑小且分散的图片上,优势更明显。

从工程量角度看,这个选择还有一个实际好处:CrossViT 在视觉 Transformer 生态里属于成熟结构,能够加载公开的预训练权重。用预训练权重做迁移学习,对样本量有限的作物病害数据集来说,通常比从零训练少用一半以上的数据就能收敛到可用水平,这一点在毕设周期内非常重要。

3.2 模型实例化与权重加载:保持结构一致性的关键

实例化模型时最容易翻车的地方是分类头维度与预训练权重不一致。官方预训练权重通常是在 ImageNet 的 1000 类上训练的,而本项目只需要识别几个作物类别。常见的做法是把原分类头替换为新的全连接层,然后加载除分类头之外的权重。

import torch import torch.nn as nn class CrossViTClassifier(nn.Module): def __init__(self, base_model, num_classes=5, dropout=0.1): super().__init__() self.backbone = base_model in_features = self.backbone.head.in_features self.backbone.head = nn.Sequential( nn.Dropout(dropout), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x) # 加载预训练权重时,忽略 head 参数形状不匹配的部分 def load_pretrained(model, weights_path): state_dict = torch.load(weights_path, map_location="cpu") new_state_dict = { k: v for k, v in state_dict.items() if not k.startswith("head.") } model.load_state_dict(new_state_dict, strict=False) return model

这段代码里有两个关键点。一是 strict=False 必须配合“过滤 head 前缀”使用,否则会漏掉 backbone 里的缺失键;二是 dropout 不要设太大,叶片分类任务通常 0.1 即可,过大会导致训练收敛变慢。模型文件名里的 t2t 与 crossvit 分别对应 token 化模块和双分支编码器,在扩展开源代码时,你要确保加载的权重文件与模型结构严格匹配,否则会出现“名称对不上”的报错。

提示:如果遇到权重键名不匹配,先用 torch.load 打印 state_dict 的 keys,对比模型参数名,找出差异后再决定是改模型结构还是改加载逻辑,不要盲目重下权重。

3.3 classify.py 的单叶推理与 top-k 输出

classify.py 在系统里的定位是“离线单张图片识别入口”,它做的事情是:读图、预处理、推理、输出概率向量。这个脚本的代码量不大,但它串联了数据层和决策层,是整个链路能否跑通的关键验证点。

import torch from PIL import Image import torchvision.transforms as T def predict_one_image(model, img_path, class_names, device): transform = T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) img = Image.open(img_path).convert("RGB") x = transform(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits = model(x) prob = torch.softmax(logits, dim=-1)[0] topk = torch.topk(prob, k=2) results = [ {"class": class_names[idx], "probability": round(p.item(), 4)} for p, idx in zip(topk.values, topk.indices) ] return results

这里用 Resize(256) 再接 CenterCrop(224) 而不是直接 Resize(224),是为了保留叶片中心区域更多的有效信息。推理阶段不要开 Dropout 和 BatchNorm 的训练状态,model.eval() 是必须的操作,否则同一张图每次预测结果都会抖动。test_leaf.py 与 classify.py 的区别通常是粒度更小,只针对单张叶片跑通并打印类别与置信度,适合现场演示时快速展示效果。

3.4 从概率到施药指令:决策层的动作映射

单叶推理拿到的是类别概率,但施药系统最终需要的是一个动作指令。我会在 classify.py 之后接一节决策逻辑,把概率与阈值做比较,生成最终动作。例如输出:

{ "leaf_id": "A-12", "disease": "rust", "confidence": 0.93, "action": "variable_rate", "dosage_ratio": 1.2, "spray_area": "zone_3" }

这个 JSON 是决策层的交付物。之所以在方案里强调它,是因为评审老师经常会问“识别出来之后呢”——如果你只能回答“输出类别”,方案就显得只做了一半;而有了这个 JSON 结构,就把识别模型与施药执行器之间的数据接口完整地呈现出来了。实际对接时,地面站程序读取这个 JSON,根据 zone 编号规划飞行路径,再根据 dosage_ratio 调节水泵流量,整条链路就闭合了。即便毕设阶段没有真实无人机硬件,用模拟器接收这个 JSON 也足以演示完整流程。

4. 训练与验证工程化:main.py、valid.py 与 test_leaf.py 的完整闭环

很多新手拿到源码后习惯直接跑 main.py,跑通了就算完事,这恰恰是最容易翻车的做法。我建议按“单折训练 → 交叉验证 → 最终测试 → 单图演示”四步推进。项目里 main.py 负责训练,valid.py 负责交叉验证,main_valid.py 负责汇总多次验证结果,test_leaf.py 负责单图端到端检测。这个闭环的设计思路是:训练时看 loss 和准确率是不是正常下降,验证时看模型在不同数据划分下是否稳定,最终测试时只看一次结果,避免反复调 test 集造成偏差。

4.1 main.py 的启动参数与超参配置

main.py 通常以命令行参数或配置文件方式接收超参。典型启动命令如下:

python main.py \ --data_root ./data \ --model_type t2t_crossvit \ --img_size 224 \ --batch_size 32 \ --lr 3e-4 \ --weight_decay 1e-4 \ --epochs 60 \ --warmup_epochs 5 \ --num_classes 5 \ --output_dir ./checkpoints

这些参数里,学习率 3e-4 是 ViT 系列模型在 AdamW 优化器下的常见起点,如果数据集小于几千张,建议再降到 1e-4 附近,避免过拟合。warmup_epochs 的作用是让学习率从极小值逐步爬升,因为 ViT 的注意力模块对早期大学习率非常敏感,直接冲到 3e-4 很容易出现 loss 前几轮不降甚至上升的情况。batch_size 的选择要看显存,224 分辨率、单卡 16GB 显存建议 32 左右,如果显存不够可以把 batch_size 降到 16 同时把学习率按比例调低。

4.2 engine.py 的训练与验证循环

engine.py 封装了核心的训练与验证循环,我见过不少学生自己写的训练代码,问题集中在三处:忘了切换 model.train() 和 model.eval()、验证时忘了包 torch.no_grad()、学习率调度器更新时机错乱。一个标准的 epoch 循环大致是这样的:

def run_one_epoch(model, loader, optimizer, criterion, device, is_train=True): if is_train: model.train() else: model.eval() total_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images = images.to(device) labels = labels.to(device) with torch.set_grad_enabled(is_train): outputs = model(images) loss = criterion(outputs, labels) if is_train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) preds = outputs.argmax(dim=-1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total

这个循环里最关键的是 torch.set_grad_enabled(is_train),它能确保验证阶段不计算梯度,大幅降低显存占用并防止 BatchNorm 统计量被意外更新。如果项目里还用了混合精度训练,别忘了在反向传播前调用 scaler.scale(loss),否则梯度会下溢到接近零,表现就是训练 loss 像“心电图一样上下跳”。

4.3 三级验证体系:valid.py、main_valid.py 与 test_leaf.py 的分工

valid.py 做的通常是 K 折交叉验证,把训练数据分成 K 份,轮流取一份做验证、其余做训练,最终输出每一折的准确率和平均准确率。它解决的是“单次划分运气不好”的问题——有时候随机种子不同,验证集效果能差出几个点。main_valid.py 则更进一步,它会在多折训练完成后,汇总所有模型对测试集的预测结果,再取平均或投票作为最终预测,这种集成方式在样本量不足的作物病害场景中,通常能比单模型提升 1% 到 3% 的准确率。

test_leaf.py 是演示闭环的关键脚本。答辩现场最稳妥的演示方式是预先准备几张没参与过训练和验证的叶片图片,现场跑 test_leaf.py 打印出类别和置信度,再把结果与真实标签对照。要特别注意:不要在现场边训练边演示,万一训练中断或显存不足,整个答辩节奏会被打乱。所有模型权重应该在答辩前固化好,现场只做推理。

4.4 从 log.txt 里读懂训练状态

项目根目录下的 log.txt 记录了训练过程的关键指标,一般包括每个 epoch 的 loss、准确率、学习率。我习惯重点看三个信号:一是前 5 个 epoch 的 loss 是否在 warmup 后稳定下降,如果前 5 轮不降反升,先检查学习率是否过大;二是验证准确率是否在某个 epoch 后开始震荡甚至下降,如果是,说明模型开始过拟合,此时应该触发早停或提高 dropout;三是训练集准确率与验证集准确率的差距,长期超过 5 个百分点,说明数据增强不够或者训练样本太少。

日志里还应该记录每个 epoch 保存的 checkpoint 路径,方便在训练中断后恢复。通常的做法是保存“最近的权重”和“验证准确率最高的权重”两份,恢复的时候优先用准确率最高那份,而不是最后一份。

5. 常见问题排查与避坑:数据泄漏、类别不均衡与训练玄学

这套系统从头到尾跑一遍,至少会经历训练、验证、测试、演示四个阶段,每个阶段都有容易翻车的地方。我根据自己的经验,把最容易出问题的五个点整理如下,每条都按“现象 → 原因 → 解决”的顺序给出行之有效的处理办法。

5.1 数据泄漏:验证集准确率 98%,测试集却一塌糊涂

现象:valid.py 输出的交叉验证准确率非常高,甚至接近 99%,但用 test_leaf.py 跑一张全新图片时,识别结果明显不对。

原因:最常见的是切分数据时没有按“植株个体”划分,而是直接全局随机切片。同一个植株的不同叶片,一张进了训练集、一张进了验证集,模型实际记住了植株本身的背景和光照特征,而不是病害特征。另一种隐蔽来源是,同一航拍点位的连续帧高度相似,也被随机拆开了。

解决:切分数据前,先按图像文件名里的植株 ID 或拍摄点位 ID 分组,保证同一 ID 的所有图片只出现在一个集合中。如果原图没有 ID,可以按采集时间戳模糊分组。这个修改通常能把测试集准确率拉回合理范围。

5.2 训练 loss 不下降:前 10 个 epoch 就卡住

现象:loss 在前几个 epoch 内不降反升,或者一直维持在一个固定值附近震荡,像极了一条“心电图”。

原因:学习率过大是最常见原因,其次是 BatchNorm 与 ViT 结构不兼容。很多从 CNN 迁移过来的同学习惯用 1e-3 起步,但 Transformer 系列模型对学习率更敏感。还有一个隐蔽原因是数据预处理里忘了做 Normalize,导致输入范围变成 0 到 255,梯度方向完全错乱。

解决:先用单 batch 过拟合测试——把数据集缩到 32 张图片,训练 20 个 epoch,如果 loss 能降到很低,说明代码链路没问题,再恢复全量训练并调整学习率为 1e-4 到 3e-4。如果过拟合测试也失败,检查 Normalize 和模型输入通道数。

5.3 类别不均衡:健康叶片太多,病叶识别不出来

现象:训练结束后,健康叶片的召回率很高,但几种病害类别的召回率低,即使个别病害样本数量明显偏少。

原因:无人机在大田采集时,健康叶片占比天然高于发病叶片,模型学到的是“多数类优先”的捷径,少数类被抑制。

解决:项目里既然有 samplers.py,就要利用起来。最直接的方法是给每个类别按样本数的反比设置采样权重,在 DataLoader 里传入 WeightedRandomSampler。同时配合类别权重交叉熵,在 loss 里加大少数类的惩罚权重。这两招通常能把少数类召回率拉回 10 个百分点以上。

5.4 无人机视角下的叶片模糊:单叶识别结果时好时坏

现象:同一张叶片,原图识别正确,但缩放或旋转后识别错误;飞行高度变化后,识别结果波动大。

原因:无人机拍摄的叶片在画面中占比小、存在运动模糊和透视畸变,训练集里缺少这类退化图像。

解决:推理阶段引入测试时增强(TTA),对同一张图做水平翻转、多尺度缩放后分别推理,再对概率取平均。一般来说,3 次推理取平均就能显著提升稳定性。另一个做法是训练时增加 MixUp 和随机模糊增强,让模型适应低质量输入。

5.5 显存溢出与训练中断:跑了一晚上,醒来发现白跑了

现象:训练到第 20 个 epoch 时报 CUDA out of memory,或者断电导致进程终止,checkpoint 没保存。

原因:中间层激活值累积过多,尤其是输入分辨率大、batch_size 偏高;另外很多新手只在每个 epoch 结束才保存一次权重,中断后丢失大量训练进度。

解决:开启梯度累积,用更小的 batch_size 累积多步再更新参数,显存占用能降一半;保存权重时同时保存 optimizer 和 scheduler 的状态,恢复时用 torch.load 和 model.load_state_dict 直接接续训练。这个“后悔药”习惯,能在答辩前的密集调参期救回大量时间。

6. 验收标准、现场演示与答辩要点:把工程讲成一套完整方案

6.1 离线验收指标与测试方法

我会用一张验收表来框定结果是否达标,避免答辩时被问住:

指标建议目标测试方法
分类准确率95% 以上test 集全量推理计算
单张推理耗时GPU 小于 50msclassify.py 内计时
少数类召回率90% 以上valid.py 输出分类报告
施药决策正确率与人工判断一致随机抽样 50 张逐张核对 JSON

准确率不是唯一的验收标准,能把“识别—决策”链路完整走通,比单纯刷高一个百分点的准确率更能体现工程能力。

6.2 答辩演示的三步走

答辩现场我建议按“数据准备 → 模型训练结果 → 单图推理闭环”三步走。先用目录结构讲清楚数据从哪来、分成几类;再展示 log.txt 里的训练曲线和 valid.py 输出的混淆矩阵;最后现场运行 test_leaf.py,输入一张叶片图片,输出包含类别、置信度和施药动作的完整 JSON。演示结束时,评审老师通常会问“这个模型在真实无人机上怎么部署”,你可以回答:权重固化后转成推理脚本,地面站读取 JSON 决策下发施药指令,这就是整个系统能交付的依据。

6.3 答辩前的自检清单

最后一份自检清单,按顺序过一遍:数据是否按植株 ID 切分、有没有做过单 batch 过拟合验证、少数类是否用了采样器、checkpoint 是否保存了优化器状态、演示图片是否确认没进过训练集。这五条只要有一条没过,答辩就可能被追问到细节。

我自己做这一类项目时吃过亏,当时把 test 集和 valid 集混着调,结果演示现场翻了车。从那以后,我每次做图像识别项目,都会强制先走一遍数据泄漏检查再谈模型调参。这条习惯远比多跑一组实验值钱。希望帮到你。

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

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

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

立即咨询