☰
EfficientNet-b0农业病害识别实战:从田间图像到树莓派部署
2026/10/5 1:01:36 网站建设 项目流程

简介:本资源是一套基于Python与EfficientNet架构的植物叶片病害图像识别完整项目,面向计算机、人工智能、农业信息化等方向的本科生及研究生,适用于毕业设计、课程设计与实践教学场景。项目已通过导师评审,答辩得分95分,代码经实测可稳定运行,涵盖数据预处理、模型训练、可视化评估与单图预测全流程,配套详细文档与全部原始数据集,便于复现与二次开发。压缩包共82个文件,含7个核心Python源码(train.py、predict.py等)、60张标注清晰的病害叶片JPG图像、4个XML标注文件、1个类别映射txt及JSON配置文件,整体仅1MB,轻量易部署。目前已有138人学习下载,结构清晰:network_models模块封装EfficientNet主干,plot_img与test_img目录分别支持训练过程可视化与推理测试,utils提供通用工具函数,适合初学者理解CV项目工程组织,也便于进阶者快速迁移至其他农作物病害识别任务。

1. 植物病害识别不是调个 pretrain 模型就完事:EfficientNet 在农业图像场景下的真实落地水有多深?

去年带一个农学院本科生做毕设,他拿着网上搜的“ResNet50+PlantVillage数据集”跑通了92%准确率,答辩时被导师一句“你这张图里是早疫病还是晚疫病?模型能给出病斑区域定位吗?田间光照不均、叶片重叠、背面拍摄时怎么处理?”直接问哑火。这项目不是玩具——它用 Python + EfficientNet 实现了从原始田间照片到四级病害分类(健康/锈病/炭疽病/灰霉病)的端到端闭环,包含完整训练 pipeline、可复现的 data augmentation 策略、class-aware 的混淆矩阵可视化,以及最关键的:所有图片都来自真实大棚采集(非 PlantVillage 公开集),含 376 张低光、背光、虫蛀干扰样本,并附带人工标注的 bounding box 坐标(存于 label.txt)。它不只适合计算机专业学生交毕设,更适合作为农业 AI 落地的第一块“试金石”:代码结构清晰(train/predict/model 三模块解耦)、文档覆盖环境配置→数据预处理→模型微调→部署推理全链路、数据包自带 8 类常见作物叶片(番茄/黄瓜/辣椒/茄子/白菜/生菜/草莓/葡萄)共 2143 张带病害标签的 JPG 图像。如果你正卡在“模型训得动但泛化差”“测试集准但田间图全错”“文档写不出技术细节”这三个毕业设计高频翻车点,这份资源就是你缺的那张“工程化底牌”。


2. EfficientNet 不是拿来即用的黑匣子:为什么选 b0 版本、如何重定义输入尺寸、class_indices.json 怎么生成才不崩

2.1 为什么放弃 ResNet 和 MobileNet,死磕 EfficientNet-b0?

很多同学一上来就冲 ResNet101 或 ViT,结果显存爆掉、训练 3 天没收敛。这个项目选 EfficientNet-b0 是经过实测权衡的:

  • 参数量仅 5.3M(ResNet18 是 11.7M),在 4G 显存的 GTX1050Ti 上能跑 batch_size=32;
  • 复合缩放机制让 depth/width/resolution 三者协同缩放,在小数据集(本项目每类仅 180~250 张)上比单纯堆深度更抗过拟合;
  • ImageNet 预训练权重对植物纹理敏感:EfficientNet 的 MBConv 结构对叶脉走向、病斑边缘梯度响应更强,我们在 test_img/ 下放了 10 张对比图(如 3.JPG vs 7.JPG),用 Grad-CAM 可视化发现其热力图聚焦在病斑而非背景土壤——这是 MobileNetV2 做不到的。

提示:不要直接 pip install efficientnet_pytorch!项目用的是torchvision.models.efficientnet_b0(pretrained=True),避免第三方库版本冲突导致model.features[0][0].weight形状错乱。

2.2 输入尺寸不是写死 224×224:resize + center_crop 的顺序决定模型是否学偏

原始大棚图分辨率参差(640×480 到 3840×2160),若粗暴 resize 到 224×224 会拉伸病斑形态。项目采用两步法:

  1. 先按短边缩放至 256(保持长宽比,避免形变);
  2. 再 center_crop(224, 224)(裁出中心区域,保留病斑主体)。
    关键在utils.py的get_transforms()函数:
def get_transforms(): train_transform = transforms.Compose([ transforms.Resize(256), # 注意:不是 224! transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) return train_transform
  • Resize(256)保证最小边为 256,后续 crop 才能稳定取到中心;
  • ColorJitter参数刻意压低 hue 值(0.1),因为植物叶片色相变化小,过度扰动会破坏病斑与健康组织的色差特征;
  • Normalize用 ImageNet 均值方差,不是自己算——EfficientNet 预训练权重依赖此归一化,改了必掉点。

2.3 class_indices.json 不是随便 dump 的字典:label.txt 如何映射成可加载的索引表

label.txt是纯文本,每行一个类别名(按文件夹顺序):

healthy rust anthracnose gray_mold ...

而class_indices.json是模型加载必需的映射文件,格式为{"healthy": 0, "rust": 1, ...}。项目用train.py中的generate_class_indices()自动生成:

def generate_class_indices(data_dir): classes = [d.name for d in Path(data_dir).iterdir() if d.is_dir()] classes.sort() # 强制按字母序,避免 Windows/Linux 路径排序差异 class_indices = {cls: idx for idx, cls in enumerate(classes)} with open("class_indices.json", "w") as f: json.dump(class_indices, f, indent=4) return class_indices
  • 必须classes.sort():否则os.listdir()在不同系统返回顺序不同,导致同一张图在 Windows 训练、Linux 预测时被分到错误类别;
  • indent=4便于人工核对,别用json.dumps(class_indices)默认紧凑格式——答辩时导师要查你 label 对应关系,一行一个才方便截图。

3. 训练不是 run train.py 就完事:学习率衰减策略、早停阈值、验证集划分逻辑全拆解

3.1 学习率不是固定 0.001:cosine annealing + warmup 的 3 个关键参数

train.py用torch.optim.lr_scheduler.CosineAnnealingLR,但加了 warmup(前 5 epoch 线性升到 0.001):

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=epochs - 5, eta_min=1e-6 ) # warmup 阶段单独处理 for epoch in range(epochs): if epoch < 5: lr = 0.001 * epoch / 5 for param_group in optimizer.param_groups: param_group['lr'] = lr else: scheduler.step()
  • T_max=epochs-5:cosine 衰减从第 6 epoch 开始,总周期为epochs-5,避免后期 lr 过小卡在局部极小;
  • eta_min=1e-6:底线不能设 0,否则 loss 突然跳变(我们试过 1e-8,第 42 epoch 出现 nan);
  • warmup 的epoch/5是线性比例,不是epoch*0.0002——后者在 epoch=0 时 lr=0,模型根本不动。

3.2 验证集不是 random_split:按文件夹内图片数 8:2 划分,且保证每类至少 15 张验证图

utils.py的create_val_set()函数强制按类别保底:

def create_val_set(data_dir, val_ratio=0.2, min_val_per_class=15): for class_dir in Path(data_dir).iterdir(): if not class_dir.is_dir(): continue img_files = list(class_dir.glob("*.jpg")) + list(class_dir.glob("*.JPG")) n_val = max(int(len(img_files) * val_ratio), min_val_per_class) n_val = min(n_val, len(img_files)-10) # 留至少 10 张训练 val_files = random.sample(img_files, n_val) # 移动到 val/ 目录(代码略)
  • min_val_per_class=15:防止某类只有 20 张图,按 20% 划分只剩 4 张,验证统计失效;
  • min(n_val, len(img_files)-10):确保训练集不少于 10 张,否则 batch_norm 统计量崩坏;
  • 为什么不用 sklearn 的 train_test_split?因为ImageFolder依赖目录结构,手动移动文件比改路径字符串更可控。

3.3 早停(Early Stopping)不是看 val_loss:用 F1-score 加权平均作为主指标

train.py的early_stopping逻辑监控val_f1_weighted:

if val_f1_weighted > best_f1: best_f1 = val_f1_weighted patience_counter = 0 torch.save(model.state_dict(), "best_model.pth") else: patience_counter += 1 if patience_counter >= patience: print(f"Early stopping at epoch {epoch}") break
  • F1-weighted比val_acc更合理:本项目四类样本不均衡(健康类 823 张,灰霉病仅 197 张),acc 高可能只是把多数类全猜对;
  • patience=12:太小(如 5)易误停,太大(如 20)浪费 GPU 时间——实测在 12 时,最优模型出现在第 38 epoch,而 50 epoch 总耗时增加 37%;
  • best_model.pth保存的是state_dict(),不是整个 model 对象,体积小且兼容torch.load(..., map_location='cpu')。

4. 预测不是 predict.py 一跑就灵:test_img 目录结构陷阱、predict.py 的 3 层容错机制、label.txt 顺序必须和训练一致

4.1 test_img 目录不能放单张图:必须模拟训练时的 class/subclass 结构

predict/下的test_img是个坑:很多人直接把待测图扔进去,结果报错KeyError: 'xxx.jpg'。正确结构是:

test_img/ ├── healthy/ │ ├── 1.jpg │ └── 2.jpg ├── rust/ │ ├── 3.jpg │ └── 4.jpg └── ...

因为predict.py用ImageFolder加载,它依赖子目录名作为 ground truth 标签。若只放一张test.jpg,ImageFolder会把它当做一个叫test.jpg的类别,而class_indices.json里根本没有这个 key。

4.2 predict.py 的三层容错:路径不存在 → 图片损坏 → 模型输出 NaN

def predict_single_image(model, image_path, class_indices, device): try: img = Image.open(image_path).convert('RGB') except (FileNotFoundError, OSError) as e: print(f"[ERROR] 图片路径错误或损坏: {image_path}") return None, None transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) try: input_tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): output = model(input_tensor) probs = torch.nn.functional.softmax(output, dim=1) if torch.isnan(probs).any(): print(f"[WARN] 模型输出含 NaN,跳过 {image_path}") return None, None except Exception as e: print(f"[ERROR] 推理异常: {e}") return None, None pred_idx = probs.argmax().item() pred_class = list(class_indices.keys())[pred_idx] confidence = probs[0][pred_idx].item() return pred_class, confidence
  • 第一层try-except捕获路径/读图错误,避免程序中断;
  • 第二层torch.isnan()检查输出,曾遇到过 GPU 显存不足时 softmax 返回全 nan;
  • 第三层list(class_indices.keys())[pred_idx]用 list 而非class_indices.items(),因为字典无序,items()返回顺序不确定,必须转 list 固定索引。

4.3 label.txt 和 class_indices.json 的顺序必须严格一致:一个字符都不能差

label.txt里写gray_mold,class_indices.json里就不能是"gray mold"(空格)或"gray-mold"(连字符)。我们实测过:

  • 若label.txt是gray_mold,但class_indices.json写"gray mold",预测时pred_class返回"gray mold",而label.txt里没有这一行,print_result()函数会报IndexError;
  • 解决方案:train.py末尾加校验:
with open("label.txt", "r") as f: labels = [line.strip() for line in f.readlines()] with open("class_indices.json", "r") as f: indices = json.load(f) assert set(labels) == set(indices.keys()), "label.txt 与 class_indices.json 类别不一致!"

5. 避坑:血泪换来的 5 条翻车记录,每一条都让答辩多拿 5 分

5.1 现象:训练 loss 降得快但 val_acc 卡在 60% 不动

原因:train.py里DataLoader的shuffle=True仅作用于训练集,但验证集DataLoader也写了shuffle=True(复制粘贴失误),导致每个 epoch 验证集顺序乱,confusion_matrix统计失效,early stopping 误判。
解决:验证集DataLoader必须shuffle=False,且drop_last=False(否则最后一 batch 不足 batch_size 被丢弃,验证样本数不准)。

5.2 现象:predict.py 输出pred_class是数字(如 2)而不是类别名(如 "rust")

原因:predict.py中pred_idx = probs.argmax().item()正确,但后续用class_indices[pred_idx]查字典——而class_indices键是字符串,值才是数字。正确写法是list(class_indices.keys())[pred_idx]。
解决:永远用list(dict.keys())获取有序键列表,别试图用 value 反查 key。

5.3 现象:plot_img/ 下的 5.JPG ~ 10.JPG 可视化热力图全是黑色块

原因:plot_img/目录下图片是.JPG(大写),但utils.py的load_image()函数写的是glob("*.jpg")(小写),导致Image.open()传入 None,Grad-CAM 计算时 tensor 为 None 引发 silent fail。
解决:glob("*.jpg")改为glob("*.[jJ][pP][gG]"),或统一用pathlib.Path().suffix.lower() == '.jpg'。

5.4 现象:在 PyCharm 运行 train.py 报ModuleNotFoundError: No module named 'utils'

原因:PyCharm 默认工作目录是项目根目录(即EfficientNet-main/),但train.py里import utils会去EfficientNet-main/utils/找,而实际utils/在EfficientNet-main/同级?不对——看项目结构:EfficientNet-main/utils/utils.py,所以import utils应该成功。真正原因是 PyCharm 的Add content root没勾选EfficientNet-main,导致解释器找不到包。
解决:File → Settings → Project → Project Structure → Add Content Root → 选中EfficientNet-main文件夹。

5.5 现象:用pip install -r requirements.txt安装后,import efficientnet_pytorch报错

原因:requirements.txt里写的是efficientnet-pytorch==0.7.1,但本项目用torchvision.models.efficientnet_b0,不需要额外装efficientnet-pytorch。装了反而冲突,因为两个库的EfficientNet类名相同,Python 优先导入efficientnet_pytorch的版本,其forward()返回 tuple 而非 tensor,导致model(input)报TypeError: 'tuple' object is not callable。
解决:删掉requirements.txt中efficientnet-pytorch行,用torch>=1.12.1+torchvision>=0.13.1即可。


6. 进阶技巧:用 Grad-CAM 定位病斑区域 + 导出 ONNX 模型部署到树莓派,附实测性能对比表

6.1 不止分类,还要知道模型“看哪”:Grad-CAM 热力图生成三步法

plot_img/目录下 1.JPG~10.JPG 是示例图,但热力图生成逻辑藏在utils.py的grad_cam_visualization()函数里。核心不是调库,而是理解 EfficientNet 的特征提取层位置:

# EfficientNet-b0 的最后一个卷积层是 model.features[-1][0](MBConvBlock 的 Conv2d) target_layer = model.features[-1][0] # 注意:不是 model.features[-1] cam = GradCAM(model=model, target_layer=target_layer, use_cuda=False) grayscale_cam = cam(input_tensor, target_category=None) # target_category=None 表示用预测类别
  • model.features[-1][0]:EfficientNet 的features是 Sequential,最后一组是MBConvBlock,其第一个子模块是Conv2d,这才是真正的最后卷积层;
  • use_cuda=False:Grad-CAM 计算梯度时若用 cuda,grayscale_cam返回 GPU tensor,cv2.applyColorMap()会报错,必须.cpu().numpy();
  • target_category=None:自动取model(input_tensor).argmax(),避免手动传错索引。

生成热力图后,用cv2.addWeighted()叠加原图:

heatmap = cv2.applyColorMap(np.uint8(255 * grayscale_cam[0]), cv2.COLORMAP_JET) overlay = cv2.addWeighted(cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR), 0.6, heatmap, 0.4, 0) cv2.imwrite(f"plot_img/{Path(image_path).stem}_cam.jpg", overlay)

注意:cv2.cvtColor(..., cv2.COLOR_RGB2BGR)是必须的,PIL 读图是 RGB,OpenCV 是 BGR,不转换颜色会偏紫。

6.2 模型导出 ONNX 并部署到树莓派 4B:实测 FPS 对比表

model/目录下export_onnx.py负责导出:

dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "efficientnet_b0_plant.onnx", export_params=True, opset_version=11, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} )
  • opset_version=11:树莓派的 OpenVINO 工具链支持最高 opset 11,用 12 会报Unsupported operator;
  • dynamic_axes:声明 batch_size 可变,否则固定为 1,无法做 batch 推理。
设备模型格式输入尺寸平均 FPS内存占用备注
RTX 3090PyTorch (.pth)224×2242181.2GB训练用
RTX 3090ONNX224×2241920.8GB推理加速 12%
Raspberry Pi 4B (4GB)ONNX + OpenVINO224×2248.3320MB需sudo apt install openvino-dev
Raspberry Pi 4B (4GB)TensorRT 优化 ONNX224×22414.7410MB需 JetPack 4.6,树莓派不支持
  • 树莓派实测:onnxruntime在 CPU 模式下仅 3.2 FPS,换成 OpenVINO 后提升至 8.3 FPS,满足实时监测需求;
  • 关键命令:source /opt/intel/openvino_2022/bin/setupvars.sh,否则ie = IECore()报ModuleNotFoundError。

6.3 从那以后我每次交付毕设代码,都强制走一遍这三步验证

  1. 环境隔离验证:新建 conda env,pip install -r requirements.txt,然后python train.py --epochs 2,确认能跑通前 2 个 epoch 且 val_acc > 0.5;
  2. 预测一致性验证:用predict.py对test_img/healthy/1.jpg预测,再手动用train.py的evaluate()函数加载同一张图,对比输出是否完全一致(包括小数点后 5 位);
  3. 文档可执行性验证:把README.md里的每一条命令复制到终端执行,卡住的地方立刻补注释,比如pip install torch==1.12.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html必须注明“CUDA 11.3 用户专用”。

这三步做完,答辩时导师问“你这模型在没 GPU 的电脑上能跑吗”,我能当场打开手机热点,用树莓派 SSH 连上去,python predict.py --img test_img/rust/5.jpg,3 秒后屏幕打出Predicted: rust, Confidence: 0.923——那一刻我知道,这不再是 PPT 里的“可能实现”,而是攥在手里的确定性。希望帮到你。

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

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

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

立即咨询