
简介这是一套基于Python构建的无人机病虫害智能识别与精准施药系统资源包面向计算机、农业工程等专业的学生及开发者适用于毕业设计、课程设计或实际项目二次开发。资源完整覆盖图像分类与识别、模型训练及验证流程包含CrossViT等模型实现、数据集处理、训练与验证脚本并配套项目PDF文档可帮助理解从数据组织到模型部署的完整链路。压缩包共25个文件以Python源码.py为主辅以数据集处理与训练日志.txt、编译缓存.pyc及许可证说明整体大小4.74MB目录按模型、数据、工具脚本等模块划分便于快速定位。源码已经过严格测试可放心参考并在此基础上扩展功能例如更换骨干网络、调整训练参数或接入实时识别模块。目前已有61人学习适合希望快速搭建智能农业识别系统或借鉴完整项目框架的读者。1. 无人机病虫害识别项目从一张带病斑的叶片到精准施药方案农田巡检的身影还在人工下地一张张翻叶子的时候这套系统已经能通过无人机拍回的图像直接判断作物得了什么病、严重程度如何并给出对应的施药建议。基于Python实现的无人机病虫害智能识别与精准施药系统本质上是一个把图像分类模型和植保决策规则串起来的完整工程前端是无人机采集的图像中间是CrossViT视觉Transformer在跑推理后端把识别结果映射成药方。对做毕业设计或课程设计的同学来说它最大的价值不是一个孤立的训练脚本而是一套包含数据采样、模型训练、验证、推理、项目文档的完整链路源码经过严格测试照着跑能复现结果在这个基础上扩展成自己的课题也顺手。2. 技术栈与目录结构从CrossViT到分布式训练的完整工具箱2.1 技术选型为什么是CrossViT而不是ResNet这个项目解决的是农业场景的图像分类问题但病虫害识别有几个和通用分类不太一样的地方病斑往往只占叶片的一小块区域类别之间差异细微而且无人机在不同高度拍到的尺度变化很大。如果只用ResNet这类纯卷积网络局部纹理能抓到但全局上下文关系偏弱如果只用标准ViT又容易在小数据集上过拟合。CrossViT的思路是双分支一个分支用较小的patch提取局部细节另一个分支用较大的patch捕获全局结构最后把两个分支的token融合起来。这样对“叶片上某一块病斑 整株作物的生长状态”这种组合特征特别合适。从工程角度看项目在models目录里同时放了crossvit.py和t2t子模块说明实现上不是从timm里现成拉一个模型而是自己搭了CrossViT的变体。t2tTokens-to-Token模块的作用是把图像先切成重叠的token序列再逐步扩大感受野减少token数量。相比直接线性切patcht2t能保留更多局部结构对病斑边缘这种弱特征更友好。整套模型用PyTorch实现训练和推理都走标准的nn.Module接口二次开发时不管是换backbone还是接自己的分类头都很方便。我一般在看这类项目时会先确认一件事模型是用ImageNet预训练权重初始化还是从零训练。这个项目源码里crossvit.py和t2t的写法保留了加载预训练权重的入口实际跑的时候推荐加载timm提供的预训练模型否则在小规模病害数据集上收敛会很慢。后面讲训练部分会细说。2.2 目录结构逐层拆解解压源码包后根目录下文件不多但分工很清晰。我按从入口到内部的顺序整理了一份结构. ├── main.py # 训练入口负责解析参数、初始化模型、调用engine ├── main_vaild.py # 验证入口通常读取训练好的checkpoint做精度评估 ├── valid.py # 单独验证脚本可以指定单张图或整个验证集 ├── test_leaf.py # 单张叶片图像的快速推理脚本返回类别和置信度 ├── classify.py # 分类推理脚本一般用于批量图像预测 ├── engine.py # 训练/验证引擎封装一个完整的epoch ├── datasets.py # 数据集定义处理目录结构、标签映射、图像读取 ├── samplers.py # 采样器常用类别均衡采样处理不平衡样本 ├── trans.py # 数据增强与预处理训练/验证/推理各一套 ├── utils.py # 工具函数包括指标计算、日志、模型保存等 ├── models/ │ ├── __init__.py # 模型注册入口统一导出crossvit等结构 │ ├── crossvit.py # CrossViT模型定义双分支Transformer │ └── t2t/ # Tokens-to-Token模块用于patch序列化 ├── run_with_submitit.py # 基于submitit的分布式训练启动脚本 ├── requirements.txt # Python依赖清单 ├── log.txt # 训练日志示例跑通后的输出参考 └── 项目文档.pdf # 系统设计说明、使用指南和展示PPT内容这里要注意一个细节根目录下的main_vaild.py拼写是vaild而不是valid这是原项目里的小习惯不是错误跑的时候别改文件名不然引用会断。另外log.txt是项目自带的一次训练日志安装好环境后可以打开对一下看正常输出应该长什么样。项目文档PDF里包含了演示PPT的内容初跑项目之前建议先翻一遍里面往往有作者记录的实验环境版本和生成log.txt时用的具体命令这是排查环境问题最快的线索。models/t2t目录里放的是一组Token序列化相关的模块包括tokenizer和transformer block。crossvit.py会引用t2t里的组件来构造两个分支然后通过交叉注意力把两个分支的特征融合。理解这个结构对后面的二次开发很重要如果你想替换成更轻量的模型只要改models下的实现并保证engine.py调用模型时的接口不变就行。注意根目录下还有LICENSE和__pycache__这些文件__pycache__是本地运行后生成的缓存可以忽略不用提交到自己的仓库。2.3 依赖清单requirements.txt里有什么requirements.txt是项目能跑起来的最低保障。虽然原始文件里没有逐个列出版本号但按这类项目的常见做法核心依赖基本是这几类torch和torchvision提供张量运算、数据加载接口和视觉预训练模型。timmImageNet预训练权重来源也是很多Transformer模型的参考实现。opencv-python和Pillow图像读写与预处理。numpy数值计算。submititrun_with_submitit.py需要它做分布式训练调度本地单卡跑可以不装。我没法保证requirements.txt里每个包版本都兼容实际上在本地复现时最容易翻车的就是版本错位。建议按requirements.txt装完后先跑一个最简单的数据加载测试再启动训练。一个常见的做法是手动指定PyTorch的大版本比如torch 2.x配合对应的torchvision然后让timm使用较新的1.x版本。如果timm版本太旧可能不支持CrossViT的某些层如果太新又可能因为一些接口改名导致模型定义报错。遇到这类问题优先看官方变更日志而不是强行改代码。2.4 参数与配置的管理习惯main.py这类训练脚本通常会接收大量命令行参数这是一个健壮的工程习惯。常见的参数包括data_dir、model、epochs、batch_size、lr、num_classes、img_size、checkpoint_dir等。你在运行任何脚本时先用--help扫一遍参数名是避免踩坑的最好方式python main.py --help这个命令会列出所有可接受的参数名比直接翻代码快很多。项目里main_vaild.py和valid.py也支持类似参数但参数名不一定完全一致因为valid.py可能被设计成只接受checkpoint和image两个参数。理解这种差异对后面的避坑很重要。2.5 配置漂移train、valid、infer三套流程如何共用一份配置main.py、main_vaild.py和test_leaf.py通常各自解析参数这意味着同一个模型有可能在三处被配置成不同尺寸或不同类别数。常见做法是维护一个config对象把data_dir、num_classes、model_name等字段集中管理训练时把config序列化到checkpoint里验证和推理时直接反序列化出来避免手动重复传参数。这个设计细节很容易被忽略但实际踩坑概率极高。你会发现训练时报告准确率90%用main_vaild.py一跑变成30%原因往往不是模型坏了而是验证脚本里img_size或num_classes和训练时对不上。项目源码里checkpoint文件的保存逻辑值得仔细看如果它已经内置了config存储那你在二次开发时也沿用这个习惯如果没有建议自己加上能省很多排查时间。3. 核心模块拆解从叶片图像到施药决策的完整链路3.1 数据流设计datasets.py 与 samplers.py 如何组织样本病虫害数据集最常规的组织方式是按类别分文件夹每个文件夹名字就是病害标签比如“leaf_rust_1”表示叶锈病等级1。datasets.py需要做的就是把根目录下的子文件夹扫描一遍生成image路径到标签索引的映射然后按比例切出训练集和验证集。我比较关注的是它是否使用了类别的层级信息。很多病虫害数据集光靠文件夹名没法区分“病害种类”和“严重程度”两个维度比如“rust_1”和“rust_2”其实是同一病害的不同等级。如果直接当成互不相关的类别来训练模型容易把严重程度当成主要区分特征反而忽略病害种类。这个项目在samplers.py里做了采样器设计说明它对这种样本不均衡问题有考虑。# samplers.py 中常见的一种做法按标签频次反向加权 import torch import torch.utils.data as data class BalancedSampler(data.Sampler): def __init__(self, labels, num_samplesNone): self.labels torch.tensor(labels) self.num_samples num_samples or len(self.labels) class_counts torch.bincount(self.labels) self.weights 1.0 / class_counts[self.labels].float() def __iter__(self): # 每次迭代按权重采样权重越小的类别被抽中的概率越高 indices torch.multinomial(self.weights, self.num_samples, replacementTrue) return iter(indices.tolist()) def __len__(self): return self.num_samples这段代码的核心是torch.multinomial它根据每个样本权重的概率分布做有放回采样。类别数量少的样本权重更大被抽中的概率更高从而缓解了类别不平衡。注意num_samples默认等于数据集长度但实际训练时可以设置成期望的迭代步数乘以batch size让每个epoch的样本数不完全等于原始数据集大小。实际使用中建议配合数据增强一起用。因为过采样容易让模型对少数类反复看到同一张图增加泛化风险。trans.py里如果已经写了随机翻转、裁剪、颜色抖动那训练时把这个增强打开少数类每张图都会有不同的增强版本效果会稳很多。3.2 模型构建crossvit.py 与 t2t 的双分支结构CrossViT的核心是两个分支。小patch分支比如patch_size8负责局部细节大patch分支比如patch_size16负责全局结构两个分支各自过几层Transformer后通过交叉注意力交换信息。下面是一个简化的构造示意# models/crossvit.py 中模型构造的简化示意 import torch import torch.nn as nn from .t2t import T2TModule class CrossViT(nn.Module): def __init__(self, img_size224, num_classes10): super().__init__() self.branch_small T2TModule(img_sizeimg_size, patch_size8, embed_dim384) self.branch_large T2TModule(img_sizeimg_size, patch_size16, embed_dim384) self.fusion nn.MultiheadAttention(embed_dim384, num_heads8) self.head nn.Linear(384, num_classes) def forward(self, x): # 两个分支分别提取token序列 tokens_s self.branch_small(x) # 保留局部细节 tokens_l self.branch_large(x) # 捕获全局结构 # 将两个分支的token拼接后做交叉注意力融合 tokens torch.cat([tokens_s, tokens_l], dim1) context, _ self.fusion(tokens, tokens, tokens) # 取全局池化结果做分类 out self.head(context.mean(dim1)) return out这里T2TModule是我按项目里t2t目录的名字写的占位实际实现里它会把图像切成重叠的token并通过两轮Transformer层逐步减少token数量。值得注意的融合方式是torch.cat之后做一次MultiheadAttention这样两个分支的信息不再是被简单拼接后直接分类而是先进行了一次全局交互。实际项目中可能还会在每个分支末端加一个CLS token然后再融合原理类似。参数说明img_size决定了t2t模块的输入尺寸常见的是224patch_size小分支用8、大分支用16时两个分支的序列长度不同但embed_dim保持一致才能做后续融合num_classes要和你数据集的类别数一致。改造时如果想换成ResNet之类的主体只需要保留这个类的外部接口内部把两个分支换成两个不同感受野的CNN骨干然后同样做特征融合即可。3.3 训练引擎engine.py 里的一个epoch发生了什么engine.py承担的职责是标准的训练驱动从dataloader取batch、前向、计算损失、反向更新梯度以及在epoch结束或固定间隔时做验证。这个项目的engine.py还承担了保存最优模型的逻辑一般会按验证集准确率或F1分数来决定是否覆盖当前最优checkpoint。# engine.py 中训练一个epoch的核心流程 def train_one_epoch(model, dataloader, criterion, optimizer, device, log_interval50): model.train() running_loss 0.0 for step, (images, labels) in enumerate(dataloader): images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() if step % log_interval 0 and step 0: avg_loss running_loss / (step 1) print(fstep {step}, loss {avg_loss:.4f})这段流程看起来很简单但实际项目里需要在训练过程中同步维护一个EMA指数移动平均版本的模型参数或者定期做一次验证来记录最优acc。如果只打印loss很难判断模型是否过拟合。我一般会在流程里加一个early stopping计数器连续几个epoch验证指标不升就提前终止。损失函数和优化器通常在main.py里设置。分类任务默认用nn.CrossEntropyLoss优化器选择AdamW或Adam学习率初期可以设1e-4到3e-4。如果模型用了预训练权重通常会把backbone部分的学习率调低一点比如让Transformer主干用1e-4分类头用3e-4这样微调更稳定。3.4 推理链路classify.py 与 test_leaf.py 如何落地施药决策推理链路是这套系统区别于普通图像分类项目的地方。classify.py加载训练好的权重后不只是输出一个类别编号还会把类别编号映射回病害名称和施药建议。test_leaf.py更像一个单张图片的演示脚本适合本地跑一张叶片看效果。# test_leaf.py 简化示意加载模型输出病害类别和置信度 import torch from models.crossvit import CrossViT model CrossViT(num_classesCLASS_NUM) model.load_state_dict(torch.load(best_checkpoint.pth, map_locationcpu)) model.eval() from PIL import Image from trans import get_inference_transform img Image.open(test_leaf.jpg).convert(RGB) transform get_inference_transform() tensor transform(img).unsqueeze(0) # 增加batch维度 with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) cls_id probs.argmax(dim1).item() conf probs[0, cls_id].item() print(f预测类别ID: {cls_id}, 置信度: {conf:.3f})上面这段代码里的get_inference_transform来自trans.py通常包含resize、归一化等与训练时一致的预处理步骤。注意推理时一定要关闭model.training状态并且用torch.no_grad()包住前向否则会占用不必要的显存。精准施药决策的核心在类别ID到施药建议的映射表。比如类别0对应“稻瘟病-轻”施药建议可能是“亩用三环唑可湿性粉剂20克无人机飞行高度2米喷幅3米”类别1对应“稻瘟病-重”建议中会提高药量和喷洒时长。这个映射表通常写在classify.py中以字典或外部JSON文件维护。改造时最需要小心的是类别ID顺序必须与训练时一致否则会出现识别正确但施药建议错配的尴尬情况。4. 训练与运行指南从零跑通一个病虫害识别模型4.1 环境准备与数据组织先准备Python环境。推荐用conda创建一个独立环境避免把系统Python弄乱。命令行里按顺序执行conda create -n plantguard python3.10 -y conda activate plantguard cd 项目根目录 pip install -r requirements.txt如果requirements.txt里没有包含全部依赖或者版本冲突手动补装以下常用包pip install torch torchvision timm opencv-python pillow numpy装完依赖后先用一段小代码验证数据加载是否正常。看一下datasets.py里默认读取的数据目录结构通常是这样data/ ├── train/ │ ├── rice_blast/ │ ├── rice_bacterial_blight/ │ └── ... └── val/ ├── rice_blast/ └── ...每个子文件夹名就是类别标签。如果你的数据是这种格式直接把根目录路径传给训练脚本即可。如果原始数据是CSV标注格式需要先写一个小脚本把图像按标签移动到对应文件夹因为项目的datasets.py大概率是按目录结构读取的。4.2 训练main.py 的关键参数与启动方式main.py是训练的总入口。它内部会经历解析命令行参数、建立数据集和dataloader、初始化模型并加载预训练权重、构造优化器和学习率调度、然后调用engine.py里的训练循环。命令行启动方式类似python main.py --data-dir ./data --model crossvit --epochs 100 --batch-size 32 --lr 3e-4 --num-classes 10各参数含义--data-dir数据根目录必须是train和val子文件夹的上一级。--model指定模型结构当前项目可用的值一般是crossvit。--epochs总训练轮数小数据集100轮足够但要看验证曲线。--batch-size单卡batch大小。显存不足时从32降到16或8同时可以相应调小学习率。--lr初始学习率。微调场景下3e-4是一个比较稳的起点如果loss震荡降到1e-4试试。--num-classes类别总数必须和你的数据文件夹数量一致。训练过程中控制台会每隔一定步数打印loss每个epoch结束后打印验证准确率。项目里自带的log.txt可以作为对照看你的输出格式是否和它对得上。如果显存不够一个很实用的替代方案是降低输入分辨率。CrossViT在224分辨率下的计算量较大如果改成192或160显存占用会下降不少准确率一般只掉1到2个点。修改时找到main.py里设置图像尺寸的地方把--img-size改成192即可。另外当batch-size减小后建议同步将学习率按比例缩小比如batch从32降到16lr从3e-4降到2e-4否则梯度噪声变大模型容易不稳定。4.3 验证与推理main_vaild.py、valid.py、test_leaf.py 怎么用训练完成后先用main_vaild.py在验证集上做一次完整评估python main_vaild.py --data-dir ./data --model crossvit --checkpoint ./checkpoints/best.pth这个脚本会遍历整个验证集计算平均准确率、每类准确率可能还会输出混淆矩阵。main_vaild.py和valid.py的区别我理解是前者是主流程的一部分会读取main.py训练时保存的配置后者更适合单独快速验证一个指定权重比如你刚换了一个数据增强方案想对比效果。如果只想看单张图用test_leaf.pypython test_leaf.py --image ./test_images/leaf_01.jpg --checkpoint ./checkpoints/best.pth输出会打印预测的类别ID、类别名和置信度。这个脚本适合在部署现场快速验证模型文件是否损坏、预处理是否正确。注意test_leaf.py内部可能没有做类别名映射打印的可能是整数标签需要对照数据集里文件夹的顺序去理解。如果打印出来的置信度普遍很低比如所有类别都在0.5左右说明模型没有正常收敛先回训练阶段检查loss曲线。4.4 分布式训练run_with_submitit.py 何时使用项目里带了run_with_submitit.py这是一个基于submitit库的分布式启动脚本。如果你的训练数据量很大比如几十万张无人机图像单卡要跑几十个小时可以考虑用它在多卡服务器上跑。它的作用是把训练命令提交到多卡环境自动分配节点和GPU。python run_with_submitit.py --data-dir ./data --epochs 100 --num-gpus 4如果是本地单机单卡不要用这个脚本直接跑main.py即可。分布式训练涉及环境变量、单卡batch size与全局batch size的换算调试成本高。对大多数毕业设计来说单卡训练够了。如果你想试试多卡务必记住数据加载部分的num_workers不要设置过高否则会在数据读取阶段出现CPU瓶颈导致GPU利用率上不去。4.5 训练日志与指标解读训练结束后不要只看最后一个epoch的accuracy。建议从中期开始记录验证集的每类准确率和平均准确率。这个项目自带log.txt就是很好的模板。如果你发现训练loss持续下降但验证准确率反而下降这是典型的过拟合信号。解决办法是增大数据增强强度、增加dropout比例或者把epoch数减小。另一个常见现象是loss下降很慢然后突然跳到正常范围。这通常发生在交叉熵损失中模型初期把所有样本都预测成均匀分布。不用慌多跑几个epoch就会收敛。但如果过了10个epoch还没有变化就要检查学习率和数据预处理了。5. 避坑指南我在这类项目里踩过的五个坑5.1 坑一复现时遇到“模块找不到”根目录要设成PYTHONPATH现象运行python main.py时报错ModuleNotFoundError: No module named models.crossvit。原因项目里大量使用了from models.crossvit import ...这种绝对导入但Python默认不会把当前命令行所在目录加入模块搜索路径尤其是当你从别的路径调用脚本时很自然就找不到。解决进入项目根目录再运行或者在运行前执行export PYTHONPATH.。如果是从PyCharm这类IDE里跑要先把根目录标记为Sources Root。验证方法很简单进入根目录后执行python -c import models.crossvit不报错就说明路径没问题。我一般会在自己的项目里直接写清楚这两步但这份源码包里没有统一说明所以第一次跑通常在这里卡住。5.2 坑二训练loss不降大概率是数据预处理不一致现象loss一直徘徊在初始值附近比如2.3左右准确率也不升。原因trans.py里的归一化使用了ImageNet的mean/std比如mean0.485。如果你的图像不是标准的RGB三通道或者尺寸没有正确resize到模型输入尺寸模型看到的实际上是乱码自然学不动。解决检查trans.py中训练和推理的transform是否一致。特别是推理时如果用了不同的resize方式训练时积累的分布就会被破坏。另外用PIL读取后要convert(RGB)灰度图或RGBA图都要先转成RGB。我在调试时会单独写一段代码把transform后张量输出成图片用肉眼看一眼有没有被裁成奇怪形状这比盯数值高效得多。5.3 坑三类别不平衡导致模型变成“健康叶识别器”现象验证集整体准确率90%但查看每一类的准确率病害严重的类别只有30%。原因病害类别天然是少数类如果采样器没有生效模型为了降低整体损失会倾向于把所有样本都预测成占多数的健康类。解决在main.py里确认是否真的启用了samplers.py中的BalancedSampler且传给DataLoader时设置了sampler而不是shuffleTrue。注意两者不能同时用否则DataLoader会报错。如果效果还不够可以在损失函数上给少数类加权重比如CrossEntropyLoss(weightclass_weights)。验证是否生效的方法是看每个epoch的loss是否比不使用时更高如果更高说明少数类的贡献确实被放大了。5.4 坑四跑分布式脚本卡死submitit需要特殊的启动环境现象run_with_submitit.py在本地跑了半天进程一直在等没有启动训练。原因submitit设计的场景是slurm这类集群调度器在本地没有slurm时它会尝试启动本地队列但经常因为端口、权限等问题卡住。解决本地复现一律不要用这个脚本。如果你确实要测试分布式逻辑先装standalone模式并设置localTrue之类的参数。否则检查python -c import submitit; print(submitit.__version__)确保安装正确。老实说对课程设计而言分布式训练是加分项但不是必需项单卡跑通已经足够展示工程能力。5.5 坑五模型checkpoint文件损坏加载时shape不匹配现象加载.pth文件时报错RuntimeError: Error(s) in loading state_dict。原因这个项目里保存的state_dict里包含完整的模型参数名如果你中途改过num_classes或者换了一个不同结构模型之前的权重自然对不上。解决加载前先打印torch.load(checkpoint).keys()和当前模型参数逐一比对。我一般会在保存时额外存一个args.json记录训练配置加载时直接从中读取num_classes和img_size避免手滑改错。如果只是分类头不匹配可以尝试加载除head以外的权重然后随机初始化新的head。这个办法在迁移学习场景里非常常用。6. 二次开发把识别模型接进自己的施药控制流程这套系统最容易改造的地方在classify.py到施药决策的映射部分。如果你想把它真正对接无人机飞控我建议先不要动训练代码而是把推理链路抽成一个独立模块输出结构化结果。具体做法是把classify.py里的预测函数包装成predict(image_path)返回一个字典包含class_id、class_name、confidence和suggested_actions。这样不管后续是接入HTTP服务还是写入CSV都不用再改模型代码。比如可以写成def predict(image_path): # 省略模型加载和预处理 result { class_id: cls_id, class_name: label_map[cls_id], confidence: conf, dosage: dosage_map[cls_id][ml_per_mu], height: dosage_map[cls_id][flight_height_m], } return result后面接Web服务时只需要在这个函数外面套一层FastAPI或Flask路由。class_id是定位到的病害类型confidence可以作为施药执行的可信度门槛比如置信度低于0.7就触发人工复核。这个思路比把整个训练脚本部署上线要干净得多因为训练脚本里大量参数对推理环境是无意义的。如果你想迁移到新的作物数据集核心步骤是按照第4.1节的数据目录格式整理新数据然后修改main.py里的--num-classes并考虑是否复用这个项目的预训练权重。由于病害图像的视觉特征有一定的共通性比如病斑边缘纹理、褪绿区域形状用原来作物上训练出的权重初始化新模型的浅层收敛速度会比从零开始快很多。具体做法是在main.py里找到预训练权重的加载逻辑把它改成pretrainedFalse或手动移除最后一层分类头的参数。我踩过的教训是改造前一定要先看log.txt里的训练日志格式确认项目跑通时的指标水平。否则你改了一轮之后连“变好还是变差”都判断不清。从那以后我每次拿到一个视觉分类项目都强制先跑通原始训练脚本、保存一份baseline结果再做任何结构上的改动。希望帮到你。本文还有配套的精品资源点击获取