
简介基于AI的农作物病虫害预警系统是一套面向农业信息化场景的完整Java Web源码主要服务农民、农业技术员以及有意二次开发的Java开发者。系统通过拍照识别病虫害、植物与动物配合查询、防治方案、全国病虫害形势图与农历表实现从识别到预警的一体化助农闭环。压缩包共1647个文件大小约232.15MB涵盖PNG图片资源、JavaScript/SCSS/CSS前端交互文件、Java源码及class/jar后端逻辑、JSP动态页面、SQL数据库脚本与项目说明文档等结构清晰便于前后端对照学习。目前已有1804人学习/浏览。借助源码中的核心控制器与服务实现类可深入理解AI图片识别接口如何与业务逻辑结合也能掌握病虫害库构建、预警信息生成及全国形势图展示等关键模块的落地方式。对于希望搭建农业AI应用或从事智慧农业项目的开发者这份源码提供了可运行的项目骨架、数据脚本和配置说明能显著降低从零起步的难度。1. 农作物病虫害预警系统这份Java源码到底解决了什么问题先说结论这套基于Java的AI农作物病虫害预警系统解决的是「靠人眼看田、等虫害爆发了才打药」这个老问题。它的核心流程不复杂——前端或采集端把农作物叶片图像传上来后端用训练好的深度学习模型做病害识别再结合温湿度、降雨量等环境数据算出爆发风险等级最后把预警结果推送给农户或植保站。整套逻辑跑在Java生态里Spring Boot做服务框架模型推理走ONNX Runtime或DJL数据落在MySQL和Redis里。适合谁看两类人。一类是Java后端工程师想接农业AI项目但不知道模型怎么和Spring Boot工程整合另一类是带学生做课程设计的高校老师或自己接私活的开发者手头有一份zip源码但跑不起来、不会改。这篇文章不聊泛泛的AI概念直接把这套系统的数据流、模型选型、Java推理、启动配置、踩坑记录和阈值调优讲透。你有源码包也好没有源码包照着从零搭也好下面这些内容都能直接落地。2. 系统怎么搭从图像采集到预警推送的完整数据流这套系统不是单点功能而是「采集端 → 服务端 → 模型推理 → 预警推送」一条完整链路。很多人在源码里找不到头绪就是因为没先把这条链路捋清楚。2.1 数据从哪来摄像头/无人机/农户上传三类来源的统一接入常见的接入方式有三种源码里一般会为这三种预留接口。第一种是固定摄像头定时抓拍。田里架着球机或枪机每隔半小时或一小时拍一张作物叶片照片。实现上就是定时任务去拉流调用摄像头SDK截帧然后HTTP上传到后端的/api/image/upload接口。第二种是无人机巡田飞完一圈导出一批高分辨率图像通过批量上传接口导入。第三种是农户用手机拍病叶微信小程序或App里上传这种图片质量最差但覆盖面最广。PostMapping(/api/image/upload) public ResultString uploadImage(RequestParam(file) MultipartFile file, RequestParam(sourceType) Integer sourceType, RequestParam(value fieldId, required false) Long fieldId) { // 校验文件格式jpg/png/webp大小限制10MB String originalFilename file.getOriginalFilename(); if (!isAllowedImage(originalFilename)) { return Result.error(仅支持jpg/png/webp格式); } // 按日期分目录存储原始图如 /data/crop-images/2025/01/15/ String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String filePath ossService.store(file, datePath); // 写入待识别队列Redis list结构LPUSH BRPOP实现异步解耦 ImageRecord record new ImageRecord(); record.setImageUrl(filePath); record.setSourceType(sourceType); record.setStatus(0); // 0-待识别 1-识别中 2-已完成 3-识别失败 imageRecordMapper.insert(record); redisTemplate.opsForList().leftPush(REDIS_IMAGE_QUEUE, String.valueOf(record.getId())); return Result.success(上传成功); }这段代码里值得注意两个设计。一是用Redis list做任务队列LPUSH入队、BRPOP阻塞出队这是最轻量的异步方案不需要上RabbitMQ或Kafka——对于每天几千张图片的规模绰绰有余。二是sourceType字段必须保留不同来源的图片清晰度和拍摄角度差异极大后面模型推理时要做不同的预处理比如农户手机拍的图往往过曝或欠曝需要颜色校正。2.2 预警链路怎么串消息队列、任务调度与状态机设计图像落库入队之后识别服务和预警判定服务协同工作。识别队列的消费者拿到图片ID调用模型推理接口得到病害类型和置信度然后把结果写回image_record表。预警服务再定时扫表结合气象数据做综合判定。Component public class ImageRecognitionConsumer { // 阻塞式消费识别队列线程池控制并发数 Scheduled(fixedDelay 500) public void consume() { for (int i 0; i consumeBatchSize; i) { // 每轮最多处理10张 String idStr redisTemplate.opsForList().rightPop(REDIS_IMAGE_QUEUE, Duration.ofMillis(200)); if (idStr null) break; Long id Long.valueOf(idStr); try { // 只处理待识别状态防止重复消费 ImageRecord record imageRecordMapper.selectByIdForUpdate(id); if (record null || record.getStatus() ! 0) continue; record.setStatus(1); imageRecordMapper.updateById(record); // 调用推理引擎DnnPredictor封装了ONNX Runtime PredictResult predict dnnPredictor.predictByImageUrl(record.getImageUrl()); record.setDiseaseCode(predict.getDiseaseCode()); record.setConfidence(predict.getConfidence()); record.setStatus(2); imageRecordMapper.updateById(record); // 触发预警判定 warningService.evaluate(record); } catch (Exception e) { log.error(图片识别失败 id{}, id, e); ImageRecord record new ImageRecord(); record.setId(id); record.setStatus(3); // 失败状态后续人工介入 imageRecordMapper.updateById(record); } } } }状态机设计是这套系统的地基。0-待识别 1-识别中 2-已完成 3-识别失败四个状态必须在数据库里硬编码并且用selectByIdForUpdate加行锁防止多个消费者线程同时取到同一张图片。很多人跑源码翻车就是因为把状态判断放在了Java内存里而不是数据库里服务一重启状态就丢了。预警判定不是简单看置信度。一个合理的方案是识别出来的病害码匹配一个预警规则表规则表里有「该病害的传播条件」比如稻瘟病在温度22-28℃、湿度大于90%时爆发风险高。预警服务把识别结果和气象数据组合打分分数超过阈值才推送。2.3 数据库与文件存储病虫害样本和模型文件的存放方案数据库一般三张核心表image_record存图片元数据和识别结果disease_info存病虫害字典warning_record存预警记录。模型文件不放数据库放在文件系统里。CREATE TABLE image_record ( id bigint(20) NOT NULL AUTO_INCREMENT, image_url varchar(255) NOT NULL COMMENT 图片存储路径, source_type tinyint(4) DEFAULT 1 COMMENT 1-摄像头 2-无人机 3-农户上传, field_id bigint(20) DEFAULT NULL COMMENT 地块ID关联农田信息, disease_code varchar(32) DEFAULT NULL COMMENT 识别出的病害编码, confidence decimal(5,4) DEFAULT NULL COMMENT 置信度0-1, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-待识别 1-识别中 2-已完成 3-识别失败, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status_queue (status, id), KEY idx_field_time (field_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图片识别记录表;索引设计是两个高频查询的关键idx_status_queue是消费端扫待识别任务用的idx_field_time是查某个地块历史发病趋势用的。utf8mb4必须用病害名称里要存汉字有些生僻字用utf8会存不进去。模型文件放在/models/目录下ONNX格式的模型一般几十MB到两百MB。Spring Boot配一个ModelPathConfig读取配置项即可不要打成jar包内部资源去读取否则模型更新一次就得重新打包部署。3. 模型训练与Java推理选型、部署与精度调优有了数据链路接下来是整个系统的技术核心——AI模型怎么选、怎么训练、怎么在Java里跑起来。这章的选型思路直接影响你改源码时的工作量。3.1 模型选型YOLOv8还是ResNet按你的硬件条件决定农作物病虫害识别本质上是一个图像分类或者目标检测问题。分类是「这张叶子得了什么病」检测是「这张图里哪几个区域有病斑、各是什么病」。国内公开的农作物病虫害数据集比如AI Challenger的农作物病害数据集大部分标注的是整张图级别也就是分类标签。如果你拿到的源码用的是分类模型大概率是基于ResNet或EfficientNet的预训练模型做微调。YOLOv8适合目标检测场景能定位病斑位置但训练数据要求更高——你得有边界框标注。对大多数农田场景病斑长什么样、长在哪其实农技员更关心但采集和标注成本不是一般团队能承受的。我见过不少项目硬上目标检测标注了几千张图效果反而不如分类模型稳定。选型建议是训练数据如果只有整图标签老老实实用分类模型如果数据自带框标注用YOLOv8收益更明显。3.2 训练数据从哪里来开源数据集与自采样本的配比最常用的方式是「开源预训练权重 自采数据微调」。ImageNet上预训练的ResNet50权重在PyTorch里一行代码就能加载然后冻结前几层只微调最后几层全连接层。训练数据配比上我建议开源数据集占70%自采数据占30%——纯用开源数据模型在本地田块上表现往往大打折扣因为不同地区的光照、土壤背景、拍摄设备差异很大纯用自采数据样本量不够容易过拟合。import torch import torchvision.models as models from torchvision import transforms from torch.utils.data import DataLoader, Dataset # 使用ImageNet预训练的ResNet50作为特征提取器替换最后的全连接层 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) num_classes 10 # 稻瘟病、稻曲病、玉米大斑病等10个类别 model.fc torch.nn.Linear(model.fc.in_features, num_classes) # 训练数据增强随机裁剪、翻转、颜色抖动模拟田间光照变化 transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 损失函数类别不均衡用加权交叉熵 class_counts [1200, 850, 300, 2000, 600, 150, 900, 400, 500, 250] weights torch.tensor([1.0 / c for c in class_counts], dtypetorch.float32) criterion torch.nn.CrossEntropyLoss(weightweights) # 优化器SGD Momentum学习率0.001冻结前40层 optimizer torch.optim.SGD([ {params: model.conv1.parameters(), lr: 0.0}, {params: model.layer1.parameters(), lr: 0.0}, {params: model.layer2.parameters(), lr: 0.0}, {params: model.layer3.parameters(), lr: 0.0001}, {params: model.layer4.parameters(), lr: 0.001}, {params: model.fc.parameters(), lr: 0.001} ], momentum0.9, weight_decay1e-4)这份代码有几个参数值得反复调。RandomResizedCrop(224, scale(0.7, 1.0))的scale下限可以改到0.5让模型看到更大范围的叶片截取ColorJitter的brightness参数对农户手机拍的暗光图片特别有用。类别不均衡是病虫害数据集最坑的地方——稻瘟病样本可能几千张某种罕见病害只有一百多张不做加权的话模型对罕见病害几乎不识别。学习率这里用了分层策略预训练层不更新或低学习率新加的全连接层用稍高的学习率能有效避免灾难性遗忘。训练完成后导成ONNX格式torch.onnx.export(model, dummy_input, crop_disease.onnx, opset_version11)。为什么用ONNX而不是直接用PyTorch因为Java生态里跑PyTorch原生的方式太重而ONNX Runtime在Java侧有完善的API无需额外起Python服务部署简单太多。3.3 Java侧模型推理用ONNX Runtime还是DJLJava侧做深度学习推理主流就两条路ONNX Runtime Java API和DJLDeep Java Library。ONNX Runtime是微软出品把PyTorch导出的模型直接跑依赖简单一个jar包搞定DJL是亚马逊开源抽象层次更高支持多种引擎后端。我平时更推荐ONNX Runtime因为它对ONNX格式的支持最完整遇到算子不兼容的问题少。public class DnnPredictor { private OrtEnvironment env; private OrtSession session; public void init(String modelPath) throws OrtException { env OrtEnvironment.getEnvironment(); OrtSessionOptions options new OrtSessionOptions(); // 启用CPU优化支持AVX指令集 options.addCPU(true); // 如果模型需要GPU这里改为CUDA选项注意对应CUDA版本 // options.addCUDA(0); session env.createSession(modelPath, options); } public PredictResult predictByImageUrl(String imageUrl) throws Exception { // 读取图片并缩放至模型输入尺寸 BufferedImage img ImageIO.read(new URL(imageUrl)); Image scaled img.getScaledInstance(224, 224, Image.SCALE_SMOOTH); // 转换为RGB float数组归一化到0-1之间 float[] input imageToFloatArray(scaled); // ONNX Runtime的输入Tensor必须是NCHW格式 [1, 3, 224, 224] long[] shape {1, 3, 224, 224}; OnnxTensor tensor OnnxTensor.createTensor(env, input, shape); MapString, OnnxTensor inputs new HashMap(); inputs.put(input, tensor); // model.onnx的输入节点名必须是input OrtSession.Result result session.run(inputs); float[] output result.get(0).getFloatValue(); // 形状是[1, 10]10个类别的概率 // 取最大概率下标作为预测类别 int maxIdx 0; float maxVal output[0]; for (int i 1; i output.length; i) { if (output[i] maxVal) { maxVal output[i]; maxIdx i; } } return new PredictResult(maxIdx, maxVal); } }这段代码有两个常见的性能瓶颈。一是ImageIO.read(new URL(imageUrl))每次都走网络IO拉图片高并发下要做本地缓存二是imageToFloatArray的像素值归一化代码很多人忘了把RGB除以255导致推理结果完全不对。模型推理的输入输出节点名必须以model.onnx为准用Python脚本或Netron工具先查一下节点名不要想当然填input。4. 把源码跑起来Maven工程目录、配置与最小启动命令拿到zip包第一件事不是双击解压导入IDE而是按下面这套顺序做启动前检查。很多源码运行不了问题出在环境而不是代码上。4.1 拿到zip后先做三件事检查JDK版本、确认Maven依赖、看application.yml解压之后先看pom.xml里的java.version属性。如果写的java.version1.8/java.version就装JDK 8如果写的17就装JDK 17。Spring Boot 2.x配JDK 8Spring Boot 3.x强制JDK 17。不要用高版本JDK跑低版本Spring Boot的源码ClassNotFound和NoSuchMethod的报错会让你怀疑人生。然后确认环境变量JAVA_HOME指向了正确的JDK路径执行java -version验证。Maven依赖下载慢是另一个常态问题尤其在有防火墙的环境里。国内镜像配置在~/.m2/settings.xml里加上阿里云镜像即可mirror idaliyunmaven/id mirrorOfcentral/mirrorOf nameAliyun Maven Repository/name urlhttps://maven.aliyun.com/repository/public/url /mirror依赖拉取成功后打开src/main/resources/application.yml检查数据源配置。数据库名、账号密码、Redis地址都要改成本地的下面是最常见的配置项spring: datasource: url: jdbc:mysql://localhost:3306/crop_disease?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: # 本机Redis没密码就留空 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis-plus: configuration: map-underscore-to-camel-case: true ai: model: path: /models/crop_disease.onnx # 模型文件绝对路径 predict: threshold: 0.6 # 置信度低于0.6视为未识别出病害serverTimezoneAsia/Shanghai不加的话MySQL 8.0会报时区错误。Redis有密码就填密码错了连接池初始化不会报错但第一次写入队列时阻塞超时排查起来很隐蔽。4.2 最小启动流程从数据库初始化到模型加载启动之前先把SQL脚本导进MySQL。源码里一般有sql/init.sql或db/crop_disease.sql用命令行导入mysql -uroot -p123456 -h127.0.0.1 crop_disease.sql导入后验证三张核心表是否存在SHOW TABLES;。如果脚本缺表手动执行CREATE DATABASE crop_disease DEFAULT CHARACTER SET utf8mb4;再切库导入。启动Spring Boot工程执行mvn spring-boot:run或打包后java -jar正常会看到Tomcat started的日志。模型加载阶段特别需要注意日志里有没有init model success。如果加载失败检查模型文件路径是否有读权限Windows上路径分隔符要用双反斜杠。工程启动成功后先用Swagger或Postman调一个上传接口传一张测试图走一遍0-1-2的状态流转确认能从Redis队列消费、能调通推理引擎。4.3 几个必调参数检测阈值、推送频率、图片压缩质量ai.predict.threshold是最核心的参数默认0.6。调低到0.5会让更多弱病害被识别出来代价是误报增多调高到0.7会漏掉早期病斑。农技站更怕漏报农户更烦误报怎么调取决于部署方的忍耐度。没有绝对正确值前期上线跑两周统计识别结果的置信度分布再决定。Scheduled(fixedDelay 500)是消费线程的轮询间隔。500毫秒跑一次每次最多处理10张图吞吐量就是每秒20张对绝大多数场景够用。如果你用的是高性能服务器和GPU推理卡可以把这个间隔缩到200毫秒批量处理数提高到32。图片压缩质量控制在ImageRecord落库之前设置长边压到1024像素、JPEG质量0.8一张照片从2MB压到200KB识别速度翻倍准确率几乎不受影响。推送频率由预警规则决定。常见做法是同一地块同一病害在24小时内只推送一次否则图片识别任务源源不断农户手机被打爆。在warning_record表里加一个last_push_time字段判断超过24小时才创建新的推送记录。5. 避坑指南从解压zip到上线预警的常见问题与排查这章是血泪经验汇总。源码能跑起来只是第一步要稳定上线看这些坑。5.1 zip解压后中文文件名和路径乱码现象解压源码zip后README-病虫害说明.md变成乱码部分目录无法编译。原因Windows默认GBK编码zip包在打包时用了UTF-8解压软件没识别出来或者反过来。解决用7-Zip打开zip选「工具 → 选项 → 名称编码」强制指定UTF-8。在Linux服务器上解压时用unzip -O gbk crop_disease.zip按GBK解码或者unzip -O utf-8按UTF-8解码。Java源码里的注释乱码不影响编译但properties文件乱码会导致Spring Boot启动时读取配置异常。统一在IDE里将文件编码设为UTF-8再编译。5.2 模型文件加载慢或OOM现象启动耗时超过1分钟日志出现OutOfMemoryError: Java heap space。原因Spring Boot默认堆内存只有物理机的四分之一检测模型加载需要数百MB内存推理时要创建中间Tensor。解决在启动脚本里显式设置堆内存java -Xms512m -Xmx2048m -jar crop-disease.jar-Xms512m设初始堆-Xmx2048m设最大堆。如果还OOM检查模型文件是不是FP32格式换用FP16或INT8量化的ONNX模型体积和内存占用直接减半到四分之一。5.3 本地跑通但服务器上图片检测结果差异大现象本地识别稻瘟病置信度0.9服务器上同一张图只有0.6甚至识别错误。原因服务器的CPU指令集和本地可能不同。ONNX Runtime在编译时针对AVX512做过优化老服务器不支持就回退到SSE推理结果浮点精度会有细微差异放大到置信度上就是几个百分点。更隐蔽的原因是图片解码库差异服务器上缺ImageIO对WebP格式的支持读图失败走了异常分支。解决确保服务器CPU支持AVX指令集。在服务启动日志里查ort的编译选项如果提示The CPU does not support AVX instructions换成非优化版jar或改用Docker镜像部署。图片格式在入口统一转成JPEG不要依赖运行时解码器能否识别WebP这也是最快的止损办法。5.4 预警推送重复或丢失现象同一病害同一天推了3次或者发生过疫情却没推送。原因重复推送多半出在Redis消费者逻辑上——rightPop拿到图片后处理成功但还没来得及改数据库状态服务重启了重启后图片还在队列里被再次消费。丢失是反过来的问题BRPOP超时后图片被消费了但识别进程异常退出状态永远停在1。解决核心方法是让状态变更和队列移除原子化。用Redis的LREM显式移除已处理任务同时加一个processed_set去重集合消费前先SISMEMBER判断图片ID是否已处理。更稳妥的方案是引入一个简单的定时补偿任务每小时扫一遍状态为1超过10分钟的记录重置回0重新消费。5.5 JDK版本和Maven依赖冲突导致编译失败现象导入IDE后大量红线编译报Cannot resolve symbol jakarta或程序包javax.annotation不存在。原因Spring Boot 3.x把javax迁移到jakarta包源码如果是Boot 3而你的本地方——嗯没控制住手。继续调整措辞源码如果是Boot 3而你本地只有JDK 8编译必然失败反之源码是Boot 2而你用了JDK 17javax.annotation相关类在JDK 11以后被移出默认模块需要手动引入依赖。解决严格按pom.xml里spring-boot-parent版本选JDK。Spring Boot 2.x用JDK 8Spring Boot 3.x用JDK 17。不确定的时候打开pom.xml第一行看parent节点里的版本号回退JDK版本比改源码里所有import语句快得多。依赖冲突的排查命令是mvn dependency:tree -Dverbose看具体哪个包带入了旧版本的javax.servlet-api在pom.xml里用exclusion排除。6. 让预警更准阈值调优、A/B对比与误报回收机制系统稳定运行之后真正的挑战在于把预警准确率从「能用」提到「好用」。这里分享三个具体做法。阈值调优不要凭感觉。后端加一个接口把每张图的识别置信度、真实标签农技员后续复核填写、预警推送状态全部记录到日志表。跑两周后导出来画一条置信度分布曲线——你会发现在0.5到0.7之间有大量真实病害和大量误报纠缠在一起这时候用F1分数而不是准确率来选最优阈值。F1是精确率和召回率的调和平均对不平衡的病虫害数据远比其他指标可靠。import pandas as pd from sklearn.metrics import f1_score # 读取两周的识别和复核记录 df pd.read_csv(recognition_review_log.csv) for threshold in [round(i * 0.05, 2) for i in range(10, 15)]: # 置信度大于阈值视为正报否则视为漏报 y_pred (df[confidence] threshold).astype(int) f1 f1_score(df[is_real_disease], y_pred) print(fthreshold{threshold:.2f}, F1{f1:.4f})这个脚本的输出能直观地告诉你阈值该往哪个方向调。如果0.65的F1最高就用0.65如果0.55和0.65的F1差距在0.02以内选0.65因为更高的阈值意味着更少的农户打扰。A/B对比更实在的做法是把同一个地块分成两片一片走新阈值一片老阈值跑一个月看两边的实际打药次数和病害蔓延面积。农业系统的评估周期不能用小时要用生长季——一个周期下来数据自然说话。最后说误报回收机制。预警推送出去之后农户在客户端可以点「这不是病害」这个反馈要接回系统的训练数据闭环。每周把这个负反馈数据打标后追加到训练集里做一轮增量微调模型会越来越适应本地的光照和土壤背景误报率会持续下降。这一环是很多源码工程缺失的也恰恰是从demo到产品的分水岭。我自己做这套系统的习惯是版本上线前永远先跑一周影子模式——模型照常出结果推送全部拦截只记日志用历史数据验证规则的命中率再放开真实推送。这个习惯救过我很多次有时候模型和规则单独看都合理组合在一起就是另一回事了。希望你拿到这份源码时也能把这个步骤当成一个必须执行的流程。本文还有配套的精品资源点击获取