基于深度学习的农作物病虫害识别APP:从模型训练到移动端部署全链路实战 简介这份资源是面向农业信息化开发者、深度学习入门者与智慧农业方向学生的完整安卓工程包聚焦农作物病虫害图像识别这一实际场景帮助读者理解从数据到移动端推理的落地路径。压缩包共453个文件、约119.8MB包含93个Java源文件与3个Gradle构建脚本构成的应用主体76个CSV与118个XML分别承载水稻、小麦、番茄、棉花、大豆、油菜等作物样本数据及界面配置另有95张PNG、6张JPG图片素材、38个SO动态库、12个JAR依赖和1个PT模型权重文件结构完整可直接导入Android Studio运行调试。目前已有77人学习下载。读者可从中获得一套可复用的病虫害识别APP源码、多作物标注数据集、模型推理集成方式与移动端工程目录组织参考适合作为课程设计、毕业设计或农业AI应用二次开发的基础模板。1. 农作物病虫害识别 APP从模型到手机端这份源码包能跑通吗去年帮一个做农业植保的朋友看需求他手底下十几个农技员每天在田里拍叶子晚上回来用微信传图再由两个植保专业的学生一张张翻着认。玉米大斑病和小斑病混在一起的时候学生也得翻书对。他问我能不能做个 APP拍张照就出结果。我当时的第一反应是能做但坑不在模型在从训练到部署的那条链路。这份「基于深度学习的农作物病虫害识别 APP.zip」正好就是冲着这个场景去的——它把图像分类模型和移动端 APP 打包在一起省掉了自己从零搭骨架的功夫。这个资源适合两类人一类是手里有病虫害数据集、想快速验证移动端识别可行性的开发者另一类是课程设计或毕设需要完整 APP 演示的在校生。它解决的核心问题是「模型训练完之后怎么塞进手机里跑起来」而不是教你卷积核怎么算。如果你已经跑过 MNIST 或 CIFAR-10 的分类任务知道 DataLoader 和 CrossEntropyLoss 怎么用那这份源码包能让你少走至少两周的部署弯路。但如果你连 Python 环境都没配过建议先把深度学习入门的环境配置过一遍再来看否则光一个依赖版本冲突就能卡住三天。2. 拆开压缩包模型选型、数据集结构与 APP 骨架2.1 为什么是 CNN 而不是 Transformer农作物病虫害识别这个任务有几个硬约束叶片图像背景相对固定、病斑纹理是主要判别特征、移动端算力有限。这三个条件叠在一起CNN 系列模型仍然是性价比最高的选择。源码包里大概率用的是 ResNet 或 MobileNet 的变体前者精度稳后者参数量小、推理快。我一般会先看模型文件里有没有torchvision.models的调用如果有mobilenet_v2(pretrainedTrue)这类写法说明作者考虑了端侧部署。Transformer 在图像分类上不是不能用ViT 系列在足够大的数据集上确实能打但它的参数量和推理延迟对手机端不友好。一个 ViT-Base 大概 86M 参数量化之后也还有 20 多 M而 MobileNetV2 只有 3.4M。病虫害识别不是 ImageNet 那种千类任务通常几十个类别MobileNet 的容量够用。源码包如果选了 ResNet50精度会高一点但导出 ONNX 再转 TFLite 的时候模型体积和推理速度都要重新评估。2.2 数据集目录应该长什么样不管源码包里有没有附带数据集你拿到手之后第一件事是确认目录结构。PyTorch 的ImageFolder要求按类别分文件夹常见做法是这样dataset/ ├── train/ │ ├── 玉米大斑病/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── 玉米小斑病/ │ └── 健康叶片/ ├── val/ │ ├── 玉米大斑病/ │ └── ... └── test/ └── ...如果你的数据是一张 CSV 标注文件加一个 images 文件夹那就得自己写 Dataset 类。我一般会先跑一个统计脚本看每个类别的样本数是否均衡。病虫害数据最容易出现的问题是某些病害样本极少比如玉米锈病可能只有几十张而健康叶片有上千张。这种不均衡直接训练会导致模型偏向多数类后面在 APP 上表现为「拍什么都说是健康」。import os from collections import Counter data_dir dataset/train counts Counter() for cls in os.listdir(data_dir): cls_path os.path.join(data_dir, cls) if os.path.isdir(cls_path): counts[cls] len(os.listdir(cls_path)) for cls, n in counts.most_common(): print(f{cls}: {n}) print(f最多/最少 {max(counts.values()) / min(counts.values()):.1f})这段脚本的作用是快速摸清数据分布。Counter统计每个类别的文件数最后一行算不平衡比。如果比值超过 5就得考虑用 WeightedRandomSampler 或者对少数类做数据增强。参数方面data_dir指向训练集根目录脚本假设每个子目录是一个类别且里面全是图片文件。如果目录里混了标注文件len(os.listdir())会把非图片也算进去需要加后缀过滤。2.3 APP 端的技术栈判断解压之后先看 APP 目录里有没有build.gradle或pubspec.yaml。如果是 Android 原生大概率是 Java/Kotlin 加 TFLite 或 ONNX Runtime如果是 Flutter那就是 Dart 调原生推理库如果是 React Native 或 uni-app推理部分通常走原生模块桥接。源码包标题写的是「APP.zip」没有指定平台我一般会优先找android/和ios/两个目录是否存在。移动端推理的常见方案有三种TFLite、ONNX Runtime Mobile、NCNN。TFLite 生态最成熟Android 上直接用 Interpreter 加载.tflite文件ONNX Runtime 跨平台好但包体积大一些NCNN 是腾讯开源的对 ARM 优化好但没有官方 Python 训练侧支持需要自己转模型。源码包如果用的是 TFLite那模型转换链路就是 PyTorch → ONNX → TensorFlow → TFLite中间任何一步版本不匹配都会翻车。3. 从权重到手机模型转换与 APP 推理链路实操3.1 PyTorch 导出 ONNX 的关键参数训练完的.pth文件不能直接给手机用中间得转一道。第一步是导出 ONNXimport torch import torchvision.models as models # 假设源码包用的是 mobilenet_v2类别数按实际改 num_classes 38 model models.mobilenet_v2(pretrainedFalse) model.classifier[1] torch.nn.Linear(model.last_channel, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 构造一个符合输入尺寸的假数据 dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, plant_disease.onnx, export_paramsTrue, opset_version11, do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )这里有几个参数值得说。opset_version11是比较稳的选择太低不支持某些算子太高 TFLite 转换器可能不认。do_constant_foldingTrue会把 BN 层和 Conv 层合并减小模型体积。dynamic_axes把 batch 维度设为动态这样手机端可以一次推理一张图也可以批量推理。model.eval()必须加否则 Dropout 和 BatchNorm 的行为不对导出的模型推理结果会飘。导出之后用onnxruntime验证一下import onnxruntime as ort import numpy as np sess ort.InferenceSession(plant_disease.onnx) input_name sess.get_inputs()[0].name fake np.random.randn(1, 3, 224, 224).astype(np.float32) out sess.run(None, {input_name: fake}) print(out[0].shape) # 应该是 (1, num_classes)如果这一步报错大概率是 PyTorch 和 ONNX 的版本不匹配。我一般会锁定torch1.13.1和onnx1.13.0这个组合比较稳。3.2 ONNX 转 TFLite 的踩坑路径ONNX 到 TFLite 没有官方直通车常见做法是 ONNX → TensorFlow SavedModel → TFLite。需要装onnx-tf和tensorflowpip install onnx-tf tensorflow2.12.0 onnx-tf convert -i plant_disease.onnx -o saved_model/转换完之后用 TensorFlow 的 TFLiteConverterimport tensorflow as tf converter tf.lite.TFLiteConverter.from_saved_model(saved_model) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_types [tf.float16] tflite_model converter.convert() with open(plant_disease.tflite, wb) as f: f.write(tflite_model)Optimize.DEFAULT会做量化float16把权重从 32 位降到 16 位模型体积大概减半精度损失通常在 1% 以内。如果手机端支持 int8可以用converter.representative_dataset做全整型量化体积再减一半但需要提供一批校准数据。病虫害识别对精度敏感我一般先用 float16 跑通确认 APP 端推理正常再考虑 int8。3.3 Android 端加载 TFLite 的最小代码APP 端如果是 Java/Kotlin核心就是加载模型和预处理// 加载模型 Interpreter interpreter new Interpreter(loadModelFile(context, plant_disease.tflite)); // 预处理Bitmap - ByteBuffer ByteBuffer input ByteBuffer.allocateDirect(1 * 224 * 224 * 3 * 4); input.order(ByteOrder.nativeOrder()); Bitmap resized Bitmap.createScaledBitmap(bitmap, 224, 224, true); for (int y 0; y 224; y) { for (int x 0; x 224; x) { int pixel resized.getPixel(x, y); input.putFloat(((pixel 16) 0xFF) / 255.0f); input.putFloat(((pixel 8) 0xFF) / 255.0f); input.putFloat((pixel 0xFF) / 255.0f); } } // 推理 float[][] output new float[1][numClasses]; interpreter.run(input, output);预处理这块最容易出问题。训练时用的归一化方式必须和 APP 端一致如果训练用了Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])APP 端只除了 255那推理结果基本是随机的。我一般会在 Python 端先跑一张测试图把输出 logits 存下来然后在 APP 端用同一张图跑对比数值是否一致。不一致就查预处理。4. 避坑与排查模型转换和 APP 联调中最容易翻车的五件事4.1 现象ONNX 导出成功但 TFLite 转换报算子不支持原因通常是模型里用了 TFLite 不支持的算子比如某些自定义的激活函数或池化方式。MobileNetV2 里的ReLU6和DepthwiseConv2d一般没问题但如果你改了模型结构加了Swish或GELU转换就会挂。解决办法是先把模型换回标准结构或者用tf.lite.TFLiteConverter的supported_ops参数指定TFLITE_BUILTINS加SELECT_TF_OPS。后者会引入 TensorFlow 的算子库包体积会大一些但兼容性好。我一般优先改模型结构实在不行才加 SELECT_TF_OPS。4.2 现象APP 端推理结果全是同一个类别这个问题的原因比较多按概率排序预处理不一致、模型输入尺寸不对、输出层没做 softmax、类别标签映射错位。先查预处理确认 APP 端的归一化参数和训练时完全一致。再查输入尺寸训练时是 224×224APP 端如果传了 256×256卷积层的特征图尺寸会变全连接层的输入对不上但 TFLite 不一定报错可能直接输出垃圾。类别标签映射错位也很常见。训练时ImageFolder按文件夹名排序生成类别索引APP 端的标签列表如果手写顺序不对就会出现「模型认对了但显示错了」。我一般会把训练时的class_to_idx存成 JSONAPP 端直接读这个文件。4.3 现象模型文件超过 50MBAPP 安装包太大float32 的 MobileNetV2 大概 14MBResNet50 大概 98MB。如果源码包用的是 ResNet50 且没做量化APK 体积会很难看。解决办法就是前面说的 float16 或 int8 量化。float16 能把 ResNet50 压到 50MB 左右int8 能压到 25MB。但 int8 需要校准数据而且精度损失需要实测。另一个思路是换模型。如果精度要求不是特别高MobileNetV3 或 EfficientNet-Lite 是更好的选择。EfficientNet-Lite 是专门为端侧设计的TFLite 官方支持好量化之后精度损失小。4.4 现象手机拍照识别率远低于测试集准确率这是最典型的「实验室模型到真实场景」的落差。原因通常是训练数据太干净而手机拍的照片有光照变化、背景杂乱、角度倾斜。病虫害识别尤其明显田间拍照经常有阴影、反光、叶片重叠。解决办法是在训练时加数据增强RandomResizedCrop、ColorJitter、RandomRotation都加上。如果源码包里没有增强代码可以自己补from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.RandomRotation(30), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])scale(0.6, 1.0)让模型看到不同大小的病斑ColorJitter模拟光照变化RandomRotation处理拍摄角度。增强之后训练集准确率会降但测试集和真实场景的准确率会升。4.5 现象APP 在部分机型上闪退TFLite 的Interpreter在不同 Android 版本和芯片上有兼容性差异。常见原因是 NNAPI 委托不支持某些算子或者 GPU 委托在特定驱动上崩溃。解决办法是在初始化时禁用硬件加速强制用 CPUInterpreter.Options options new Interpreter.Options(); options.setUseNNAPI(false); options.setNumThreads(4); Interpreter interpreter new Interpreter(modelFile, options);setNumThreads(4)对推理速度有提升但线程数不是越多越好超过 CPU 核心数反而会争抢资源。我一般设成 2 到 4 之间根据机型实测调。5. 进阶技巧用混淆矩阵定位模型到底「偏」在哪模型在测试集上报 92% 准确率不代表它在 APP 上就好用。病虫害识别最怕的是「把 A 病认成 B 病」因为这两种病的防治方案可能完全不同。光看准确率看不出这个问题得看混淆矩阵。import numpy as np from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt # y_true 和 y_pred 是测试集上的真实标签和预测标签 cm confusion_matrix(y_true, y_pred) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)跑完这个脚本重点看非对角线上的大数字。如果「玉米大斑病」被大量预测成「玉米小斑病」说明这两个类别的特征区分度不够。解决办法有两个一是补充这两个类别的训练数据尤其是边界样本二是用 focal loss 替代交叉熵让模型更关注难分类的样本。import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2): super().__init__() self.alpha alpha self.gamma gamma def forward(self, inputs, targets): ce_loss F.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-ce_loss) focal_loss self.alpha * (1 - pt) ** self.gamma * ce_loss return focal_loss.mean()gamma2是原论文的推荐值alpha1表示不额外加权类别。如果数据不均衡严重可以把alpha设成类别频率的倒数。focal loss 在病虫害识别这种「难样本集中在少数类别」的场景下通常能带来 2 到 5 个百分点的提升。还有一个容易被忽略的点APP 端的输入图像质量。手机拍照的 JPEG 压缩、自动白平衡、HDR 都会改变像素分布。我一般会在 APP 端加一个「拍照后裁剪」的交互让用户把叶片放在画面中央减少背景干扰。这个改动不需要动模型但对实际识别率提升很明显。从那以后我每次拿到端侧部署的模型都会先在 Python 端跑一遍混淆矩阵再在 APP 端用同一批测试图跑一遍对比两边的输出差异。差异超过 3% 就说明预处理或推理链路有问题必须查到底。希望帮到你。本文还有配套的精品资源点击获取