ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java与YOLOv5的柑橘病虫害检测系统实战解析

基于Java与YOLOv5的柑橘病虫害检测系统实战解析 简介本资源是一款面向农业信息化开发者与高校计算机专业学生的柑橘病虫害智能检测系统完整Java实现聚焦农作物病虫害快速识别这一实际农业植保需求适用于课程设计、毕业设计及轻量级智慧农业工具开发场景。压缩包共30个文件总计124KB涵盖19个核心Java源码实现图像采集、预处理、特征提取与分类识别逻辑、4个XML配置文件管理数据库连接与模块参数、2个properties属性文件存储环境变量与版本信息、1个可执行JAR包支持一键部署运行以及Maven构建脚本pom.xml和跨平台启动脚本mvnw.cmd目录结构遵循标准Spring/Maven工程规范便于二次开发与调试。目前已有256人学习下载提供从代码组织、配置管理到打包部署的全流程实践参考特别适合掌握Java基础并希望拓展CV农业交叉应用能力的学习者。1. 最初的困惑Python的AI项目为什么最后选了Java落地我先说个真实场景。朋友在广西种了三百多亩沃柑每年最头疼的不是行情是病虫害。红蜘蛛、溃疡病、潜叶蛾、炭疽病稍不注意一整片园子就废了。他请的技术员看不过来只能靠老经验抽查效率很低。后来他问我能不能搞一套自动识别系统果园里装摄像头出了问题直接推手机告警。我第一反应和大多数人一样AI图像识别Python PyTorch一条龙搞定。但在实际搭建方案的时候我发现事情没那么简单。果园环境不是实验室摄像头采集的图片要回传识别结果要进业务系统告警要推给微信、短信农场管理员要能看到历史记录、按品种统计发病趋势还可能要对接已有的农事管理系统。这些环节恰恰是Python生态比较薄弱的——不是不能做是要做的工程化工作非常多团队维护成本也高。反过来想Java在服务端领域积累了十多年的成熟框架。Spring Boot对事务处理、权限控制、定时任务、消息推送的支持已经非常完善生态里大量现成的中间件和运维方案。那核心的AI推理部分能不能交给Java其实可以把训练好的模型转成ONNX格式Java用ONNX Runtime做推理性能损失在可接受范围内。这就是这个项目的最终思路Python负责离线训练模型Java负责在线检测、业务闭环、系统集成。Java不是来竞争谁更懂AI而是让AI真正落地到生产环境中去。这套系统最终实现了以下核心功能柑橘叶片病虫害检测支持红蜘蛛、溃疡病、潜叶蛾、炭疽病等常见病虫害类型识别支持图片上传检测、摄像头定时抓帧检测两种工作模式识别结果自动回写业务库可追溯每次检测的置信度和位置信息当检测到指定病虫害且置信度超过阈值时自动触发告警推送基于时间维度的病虫害趋势统计帮助农场做精准施药决策2. 检测模型选型不是越先进的算法就越适合果园模型选型是整个系统的基础这个选择直接影响识别精度和部署成本。我在确定方案之前把主流的检测算法拉出来做了一轮对比列表放在下面模型模型大小CPU推理速度小目标检测能力工程集成难度是否适合本项目YOLOv5s约14MB60-80ms/帧中上低成熟生态推荐YOLOv8s约22MB70-90ms/帧中上中需转ONNX推荐备选SSD-MobileNet约20MB40-60ms/帧一般低一般EfficientDet-D0约15MB100-120ms/帧中中不推荐Faster R-CNN约110MB1-2s/帧强中不推荐太重从表格可以看出来这个项目选择YOLOv5s。PyTorch生态的模型导出ONNX最顺畅Java推理的兼容性也比较好。D0模型虽然精度潜力大但在低配服务器上的推理速度不够理想而果园场景又需要尽量降低服务器成本。Faster R-CNN的精度确实高但单张图片1-2秒的推理速度对实时巡检来说完全没有实用价值。可能有人会问为什么不选最新的YOLOv9、v10这个问题的答案很简单不是追新而是求稳。YOLOv5s经过大量项目验证ONNX导出不会莫名其妙报错Java端兼容性也最成熟。型号越新对部署环境的要求越高对转换工具链的依赖越大万一遇到兼容问题排查成本不可控。模型框架选定之后数据集是最核心的资产。我搜集了PlantVillage公开数据集中的柑橘类图片包括柑橘溃疡病、黑斑病、健康叶片等类型同时又从当地果园实地采集了五千多张田间照片。公开数据集的问题是照片背景干净、光照均匀而真实果园里的叶片有阴影、露水、泥土、虫网等干扰。判断数据集好坏的标准是训练集里的图片和实际部署场景的匹配程度。迁移学习用的是YOLOv5s在COCO数据集上的预训练权重在自建数据集上继续训练。基础训练参数如下# 训练参数配置 epochs: 150 batch_size: 16 img_size: 640 optimizer: SGD momentum: 0.937 weight_decay: 0.0005 lr0: 0.01 lrf: 0.2这里有个关键细节我试过直接使用Adam优化器训练初期收敛很快但到了后期精度反而上不去最后换回SGD加动量训练到150轮才稳定下来。数据增强方面用了Mosaic、随机翻转、HSV色域变换这些策略对增强模型在真实果园环境下的鲁棒性很有帮助。最终模型在自建测试集上的mAP0.5达到89.6%单张640x640图片在CPU上的推理时间大约70ms。3. 系统架构设计从摄像头采集到用户告警的完整数据链路先放一张系统模块的逻辑划分用文字描述方便没有绘图工具的读者脑补整个系统分成四层数据采集层、平台服务层、算法推理层、应用展示层。数据采集层对接两类上游一类是人工通过手机拍照上传另一类是果园布设的枪机摄像头定时抓帧。平台服务层基于Spring Boot搭建就是整个系统的中枢负责接收图片、存储文件、调用推理、落库告警。算法推理层是整合了ONNX Runtime的检测服务。应用展示层面向农场管理员和工作人员。通信方式上手机端通过HTTP接口上传图片和参数摄像头端通过定时任务拉取RTSP流并截帧再通过HTTP Post转给检测接口。前后端之间完全独立前端技术栈不限后端只做数据接口。实测在4G网络下一张压缩到约500KB的图片上传、检测、回传结果整体耗时约1.5秒基本达到业务可用状态。核心数据流程是这样的摄像头定时抓帧 - 图片压缩 - 传输到服务端 - 写入本地缓存目录 - 调用ONNX推理 - 结果写入告警表 - 触发微信/短信通知这里有一个容易忽视的问题摄像头自动抓帧和手机拍照上传图片质量差异非常大。手机端拍照通常是近景病虫害特征明显识别准确率很高摄像头抓帧则需要看安装角度和距离过远的话叶片占比小很容易漏检。我后来在摄像头的选型和安装高度上做了比较严格的规范建议摄像头距离树冠不超过3米俯角约30-45度这样画面中叶片能占有效面积的三分之一以上。再说说数据库设计。系统采用了MySQL建了四张核心表检测记录表、告警记录表、摄像头配置表、病虫害字典表。这里贴一下检测记录表的关键字段设计字段类型说明idbigint主键image_urlvarchar(255)原图存储路径result_jsontext推理结果JSON类别、坐标、置信度device_codevarchar(64)来源设备编码detect_timedatetime检测时间statustinyint状态0-正常1-告警create_timedatetime创建时间告警表和检测表是分开的因为不是每次检测到病虫害都需要告警只有连续多次检测都超过置信度阈值的才算可疑。这个逻辑在后面会展开讲是减少误报的关键一环。4. Java服务的核心实现从文件接收、推理调用到告警落库4.1 图片上传与请求接收Spring Boot的Controller层非常简单接收MultipartFile文件调用预处理的Service层把文件保存到本地目录同时生成访问用的URL。这里我用了异步调用因为ONNX推理是耗时操作同步阻塞会大大降低接口的吞吐量。具体的Controller示例RestController RequestMapping(/api/detect) public class DetectController { Autowired private DetectService detectService; PostMapping(/upload) public ResultVO upload(RequestParam(file) MultipartFile file, RequestParam(value deviceCode, required false) String deviceCode) { // 校验文件类型和大小只允许jpg/png最大5MB if (file.isEmpty() || file.getSize() 5 * 1024 * 1024) { return ResultVO.error(文件为空或超过大小限制); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); if (!Arrays.asList(jpg, jpeg, png).contains(ext)) { return ResultVO.error(不支持的图片格式); } DetectResult result detectService.detect(file, deviceCode); return ResultVO.success(result); } }这里要注意文件类型校验。如果只依赖前端的文件类型验证恶意请求直接传一个非图片文件上来可能导致后续处理直接报错。后端加一道校验同时限制文件大小避免有人一次性传大文件耗尽服务端内存。4.2 图片预处理的关键步骤图片预处理是推理准确率的隐形杀手。原始图片可能非常大手机拍出来动辄3000x4000像素直接塞给模型推理不仅慢精度也不会更好因为模型会把图片压缩到一个固定尺寸。如果压缩方式和训练数据不一致比如训练时用的是等比缩放加灰色填充推理时用了直接拉伸识别效果会很差。我的实现方式是这样的public static Mat preprocess(File imageFile, int targetWidth, int targetHeight) { Mat src Imgcodecs.imread(imageFile.getAbsolutePath()); Mat resized new Mat(); // 等比缩放按较大边适配目标尺寸 double ratio Math.min((double) targetWidth / src.width(), (double) targetHeight / src.height()); int newW (int) Math.round(src.width() * ratio); int newH (int) Math.round(src.height() * ratio); Imgproc.resize(src, resized, new Size(newW, newH)); // 灰色填充到640x640保证长宽比不变 Mat canvas new Mat(targetHeight, targetWidth, src.type(), new Scalar(114, 114, 114)); int xOffset (targetWidth - newW) / 2; int yOffset (targetHeight - newH) / 2; resized.copyTo(canvas.submat(yOffset, yOffset newH, xOffset, xOffset newW)); return canvas; }代码里用了OpenCV的Java接口org.bytedeco.opencv。这里有个很关键的细节训练时怎么预处理推理时就必须一模一样。YOLOv5官方训练时用的是letterbox方式也就是等比缩放加灰色填充推理时也必须这样操作。如果直接把图片resize成正方形病虫害特征会被拉伸或压缩检测框的位置会偏移置信度也会下降。这是很多人在集成的初期怎么调都发现精度不对的根本原因。4.3 ONNX Runtime推理的实现预处理结束之后将图片转成模型需要的输入格式。ONNX模型的输入是一个维度为[1, 3, 640, 640]的张量对应Batch Size、通道数、宽度、高度。转换为Float数组的过程其实很花费CPU执行数千次浮点操作再加上归一化处理。我在这里用了ByteBuffer加Direct Memory的方式减少Java堆内存的分配压力。核心推理代码如下public class YoloV5Detector { private OrtSession session; private final String[] classNames {citrus_canker, red_spider, leaf_miner, anthracnose}; public YoloV5Detector(String modelPath) throws OrtException { OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions opts new OrtSession.SessionOptions(); opts.setIntraOpNumThreads(4); opts.setGraphOptimizationLevel(GraphOptimizationLevel.ORT_ENABLE_ALL); session env.createSession(modelPath, opts); } public ListDetection detect(Mat processedImage) throws OrtException { // 将Mat转换为ONNX Runtime需要的float数组 float[] inputData matToFloatArray(processedImage); OnnxTensor inputTensor OnnxTensor.createTensor( OrtEnvironment.getEnvironment(), inputData, new long[]{1, 3, 640, 640} ); MapString, OnnxTensor inputs Collections.singletonMap(images, inputTensor); MapString, OnnxTensor outputs session.run(inputs, Collections.singleton(output0)); // 解析输出进行NMS后处理 float[] outputData outputs.get(output0).getFloatArray(); return postProcess(outputData); } }这段代码里有两个配置值得展开说。第一行setIntraOpNumThreads(4)不是越大越好。我一开始设置为8发现推理耗时反而增加因为线程切换开销抵消了并行计算收益。4个线程在大多数8核服务器上表现最好。第二行setGraphOptimizationLevel(ORT_ENABLE_ALL)这个选项让ONNX Runtime对模型图进行优化包括算子融合、常量折叠等实测推理耗时可以减少约15%。4.4 告警判定与通知落地告警模块是整个系统的业务闭环重点。如果每次检测到单张图片有病虫害就立即告警果园里飞过一只飞虫、一片枯叶都会触发告警管理员一天会被骚扰到烦死。所以我设计了一个“连续N次检测确认”的逻辑在五分钟时间窗口内同一摄像头来源的图片如果连续3次检测到同一类病虫害且置信度都超过阈值默认0.6才触发告警。这个逻辑在极大减少误报的同时也避免了单个误检导致告警轰炸的情况。告警落地采用两种方式微信模板消息通过企业微信应用推送短信接口作为独立通道。实际体验中企业微信推送的送达率很高且不额外产生短信费用适合作为默认告警渠道。短信作为企业微信不可用时的备选通道。下面是告警判定部分的伪代码逻辑public void evalAlert(DetectionResult latest) { String key latest.getDeviceCode() _ latest.getDiseaseType(); // 从Redis中获取同一个摄像头连续检测结果 ListDetectionResult history redisTemplate.opsForList().range(key, -2, -1); history.add(latest); long sameTypeCount history.stream() .filter(r - r.getDiseaseType().equals(latest.getDiseaseType())) .filter(r - r.getConfidence() 0.6) .count(); if (sameTypeCount 3) { alertService.push(latest); redisTemplate.delete(key); } else { redisTemplate.opsForList().rightPush(key, latest); redisTemplate.expire(key, Duration.ofMinutes(5)); } }Redis在这里的角色是滑动窗口计数器保存连续检测的结果窗口过期时间是5分钟。这个方案很简洁也容易横向扩展不需要引入复杂的规则引擎。5. 模型部署细节YOLOv5导出ONNX整个链路里踩过的坑5.1 导出ONNX的正确姿势模型训练完成后部署环节最容易出错的就是PyTorch模型转ONNX。我用的导出命令如下python export.py --weights best.pt --img 640 --batch 1 --include onnx --simplify导出之后第一步是用Netron打开ONNX文件检查输入节点名称和输出节点名称。YOLOv5的输入名通常是“images”输出名是“output0”。每种版本的yolo代码可能不太一样所以在Java代码里写死节点名字之前先确认一下不然运行时会报Invalid Argument错误。第二步是检查输出维度。YOLOv5的原始输出维度是[1, 25200, 85]25200是三个不同尺度特征图预测框的总数80x80 40x40 20x20 8400 8400 8400不对这里应该是 80x80x3 40x40x3 20x20x3实际上YOLOv5的anchor是3个所以是8400x3 2520085代表cxcywh四个坐标加一个置信度加80个类别概率。如果模型是自定义类别比如我只检测4类那么输出头是[1, 25200, 4149]的关系。这里要注意NMS后处理的坐标偏移计算因为预处理的letterbox会改变原始图像的宽高映射关系坐标需要还原到原图。这里放一个坐标还原的典型代码片段// 把640x640坐标还原为原始图片坐标 float originalW src.width(); float originalH src.height(); float ratio Math.min((float) targetW / originalW, (float) targetH / originalH); int newW Math.round(originalW * ratio); int newH Math.round(originalH * ratio); float xOffset (targetW - newW) / 2.0f; float yOffset (targetH - newH) / 2.0f; float x1 (box[0] - xOffset) / ratio; float y1 (box[1] - yOffset) / ratio; float x2 (box[2] - xOffset) / ratio; float y2 (box[3] - yOffset) / ratio;如果不做这个还原检测框的位置会整体偏移虽然病虫害的类别识别可能还是对的但标出来的位置会偏影响后续人工查看。5.2 Java端依赖与内存管理Java集成ONNX Runtime只需要一个依赖dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.16.3/version /dependency这个依赖默认支持Windows、Linux、MacOS等多个平台无需额外处理Native库。但如果项目运行在Docker容器里要注意镜像的glibc版本建议用基于Ubuntu的基础镜像而不是AlpineAlpine用的musl libc可能导致ONNX Runtime的Native库无法加载。内存是另一个容易踩坑的地方。ONNX Runtime默认会额外申请Direct Memory用于推理计算如果项目中还用了Netty等同样依赖Direct Memory的框架建议启动参数加上-XX:MaxDirectMemorySize1g防止直接内存溢出。我第一版系统上线时就是因为没有设置这个参数运行两天后报一次OutOfMemoryError。这个问题特别隐蔽因为它不是堆内存溢出常规的JVM调优手段根本看不出来。5.3 推理线程池的设计ONNX Runtime的Session可以安全地多线程并发调用但内部并行度需要合理配置。我使用了固定线程池线程数和CPU核心数保持一致Configuration public class DetectThreadPoolConfig { Bean(detectExecutor) public ExecutorService detectExecutor() { int cpuCores Runtime.getRuntime().availableProcessors(); return new ThreadPoolExecutor( cpuCores, cpuCores, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(200), new ThreadPoolExecutor.CallerRunsPolicy() ); } }CallerRunsPolicy的选择有讲究。当任务队列满了以后新提交的任务会由提交线程直接执行相当于一种背压机制不会无限制地堆积任务导致内存爆炸。如果配置了丢弃策略高峰期检测请求会被悄悄丢弃果园现场问题没查出来这才是最致命的。6. 部署环境实测服务器选型、性能指标与Docker化部署这套系统对服务器要求其实不高。单个摄像头日均抓帧约8640张每10秒一帧但大部分帧其实画面变化很小。我加了帧差判断只有画面内容变化超过一个像素阈值才上传检测大约会把有效检测量减少到每天500到1500张。这个设计极大降低了服务器的负载和存储成本。实测的服务器配置是4核8G内存的云主机CPU为Intel Xeon Platinum无GPU。在这个配置下单张图片推理耗时约70到90ms接口整体吞吐量约6到8 QPS。对单人果园管理来说完全够用。如果农场规模很大摄像头数量多建议CPU升到8核或者考虑上一块消费级GPU推理速度能提升10倍以上。Docker化部署我建议分成三个容器应用容器Spring Boot、数据库容器MySQL、缓存容器Redis。模型文件不需要打进镜像通过挂载目录挂载进去这样模型更新时只需要替换文件不需要重新构建镜像。docker-compose的简化配置如下version: 3.8 services: app: image: citrus-detect:latest ports: - 8080:8080 volumes: - ./models:/app/models - ./images:/app/images environment: - JAVA_OPTS-XX:MaxDirectMemorySize1g -Xmx2g depends_on: - mysql - redis mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: citrus_detect volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine volumes: - redis_data:/data volumes: mysql_data: redis_data:这里提示三点第一MySQL使用8.0以上版本字符集必须设置成utf8mb4否则存不了特殊字符第二Redis用Alpine版本就行系统对它没有任何特殊依赖第三不要忽略-XX:MaxDirectMemorySize这是推理框架运行时最容易引发隐性故障的参数。7. 田间实测中的性能与精度细节部署完成后最关心的就是真实环境表现。我记录了连续五天的田间实测这里列一个抽样结果日期检测图片数正确检测漏检误报准确率第一天38635333591.4%第二天42138635691.7%第三天39836236491.0%第四天45241735592.3%第五天40937831392.4%从数据来看准确率基本稳定在91%左右漏检的比例比误报略高。漏检大多出现在光线过暗、叶片遮挡严重或者距离过远的图片上。误报的典型场景则是嫩芽、露珠反光等和病虫害颜色相近的特征集中在溃疡病类别上。这个结果其实已经具备初步的业务可用性。如果追求更高准确率方向有几个一是扩充训练数据专门采集早中晚不同光线的照片构建训练集尤其在逆光场景下补充数据可以显著改善暗光漏检问题二是用视频帧序列提高置信度连续多帧检测到同类病斑才认定是真实病斑这个思路在之前的告警判定里已有雏形可以扩展到识别判定层三是结合温湿度传感器数据辅助判断溃疡病的高发条件与温度和湿度显著相关作为先验条件能有效过滤一部分误报。8. 关于系统演进我目前的一些思考与实践系统的第一阶段目标已经达成但后期演进我还有几个具体方向。第一个是主动预警模型目前系统只能做检测不能预测。我计划把历史检测结果和气象站数据温度、湿度、降雨量打通做一个简单的发病风险指数模型。比如溃疡病在气温25到30度、相对湿度80%以上时传播风险高当连续三天满足这个条件系统能提前推送“高风险预警”提醒管理员提前预防。第二个方向是移动端适配。目前的接口对前端完全是解耦的微信小程序和安卓App已经可以无缝对接。小程序可以实现现场拍照检测、查看历史记录、接收告警消息。这个由农场工人来操作门槛很低。第三个方向是模型持续迭代。因为模型是挂载目录加载的我设计了一个模型热更新接口上传新的ONNX模型文件后服务端动态创建新的OrtSession替换旧Session不需要重启应用。在扩展接口时要注意Session的关闭顺序必须先创建新Session再关闭旧Session否则会出现约100毫秒的推理空窗期并发请求会直接报错。以上就是这套Java柑橘病虫害智能检测系统从技术选型到部署实测的全部核心内容。如果正在做类似农林业AI落地项目最想提醒的还是那句话真正难的不是训练模型而是把模型装进一个稳定、高效、可维护的工程系统里。Java在这个环节的表现远比很多人想象中可靠得多。本文还有配套的精品资源点击获取
返回列表