第四章 AI垃圾监测识别系统【模型导出+模型使用+系统展现】

序号名称
1第一章 项目背景与实现思路 【农村人居环境整治】
2第二章 Anaconda+Pycharm+Yolov8开发环境搭建与检测验证
3第三章 AI垃圾监测识别系统【系统实施方案+数据集制作+模型训练】
4第四章 AI垃圾监测识别系统【模型导出+模型使用+系统展现
5第五章 持续更新
`

第四章 AI垃圾监测识别系统【模型导出+模型使用+系统展现】

本章节聚焦于探讨训练好的模型如何与项目实际相结合。当我们听到 “人居环境整治平台” 这个名字时,第一反应会觉得它是一个 Web 平台。那么,它到底怎样融入 Web 系统,发挥作用呢?接下来我们带着这些问题深入探究。

本章源码夸克网盘下载地址

系统功能介绍

之前在整理实现思路时提到的垃圾监测:通过在村庄公共区域、垃圾收集点等地安装 AI 摄像头(无人机巡逻),利用图像识别技术对垃圾堆积、垃圾桶溢满等情况进行实时监测和自动识别。例如,XX镇安装的摄像头可识别道路垃圾、垃圾桶溢满等现象,当出现问题时,系统会自动生成事件并通知相关责任人及时处理,提高垃圾清理的效率.

当前我们已经具备垃圾堆积、垃圾桶溢满识别的模型。在这个基础上,我们的任务是定时对摄像头(无人机图像)回传的图像进行识别,将识别结果通知相关责任人,同时展示在系统页面上。之所以选择定时而非实时,一方面是出于对资源节约和成本控制的考虑。实时处理需要消耗大量的计算资源以及数据传输流量,这无疑会增加成本。另一方面,从系统功能需求角度来看,垃圾桶并非时刻都处于满溢状态,垃圾也不是瞬间堆积如山,工作人员无法做到实时响应并立即处理。所以定时监测足以满足实际需求,既能保证系统运行的高效性,又能确保资源合理利用。

简单架构图
在这里插入图片描述
1,飞行控制单元以及无人机我们直接采购现成的产品(如大疆司空系列产品)。这样做能确保我们获得质量可靠、性能稳定的产品,减少自行研发所带来的时间和成本投入。这些产品具备成熟的技术和完善的功能,能够满足我们在农村人居环境整治项目中的各种需求,无论是在复杂的地理环境下进行垃圾监测、数据采集,还是完成其他相关任务,都能提供有力保障。
2,对于已有监控系统的情况,我们可以直接拉取监控平台的视频流获取所需信息。而如果没有监控系统,我认为采用无线 RTU 采集图像方式是个不错的选择。垃圾桶垃圾点其实不需要实时监控,只要能满足识别空满情况即可。这样一来,我们能够以最少的费用实现目标,避免不必要的成本投入。
3,采集回来的图片或视频经 AI 分析后,生成告警视频或信息,通过媒体流服务器以及消息服务器推送至手机端和 PC 端。这样一来,相关人员能够及时接收到告警信息,方便快捷地进行处理,实现对农村垃圾监测情况的实时掌控。

系统实现

根据场景我们使用 ONNX Runtime Server与Java客户端调用方式实现该系统。

  1. ONNX Runtime Server
    ONNX(Open Neural Network Exchange)是一种开放格式,允许模型在不同的深度学习框架间自由转换。通过ONNX Runtime Server,您可以部署经ONNX转换的模型,该服务器支持多种硬件后端(CPU、GPU及特定AI芯片加速器),从而实现实时高效的模型推理。ONNX Runtime Server还拥有跨平台支持、多语言客户端、性能优化以及广泛模型兼容性的优点,使得Java Web应用仅需通过标准HTTP接口就能调用模型,而无需关注底层深度学习框架的具体实现。
  2. Java客户端调用
    Java客户端可以简单快捷地通过RESTful API或gRPC方式调用远程的ONNX Runtime Server服务。这种设计使Java应用开发者能够集中精力于业务逻辑,无需深入理解模型内部工作原理。客户端只需遵循规定的接口规范发送请求,接收并解析服务器返回的预测结果。

模型导出

Ultralytics YOLOv8 的导出模式提供了多种选项,用于将训练好的模型导出到不同的格式,可以在不同平台设备上部署

export format 参数官方说明

以下是 YOLOv8 支持的导出格式。可以通过 format 参数将模型导出为任何格式,例如 format=‘onnx’ 或 format=‘engine’。导出的模型可以直接用于预测或验证,例如使用 yolo predict model=yolov8n.onnx。
在这里插入图片描述
为了适用于跨平台、跨框架模型部署和推理我们选择onnx 格式,导出方式如下:


 pip  install onnxslim #安装OnnxSlim ,OnnxSlim 是一个专为优化大型 ONNX 模型而设计的工具包
 yolo export model=runs/detect/train/weights/best.pt  format=onnx #导出模型

在这里插入图片描述

springboot工程中调用onnx模型

近期,我着手搭建了一个基于 Spring Boot 的多模块工程,旨在充分发挥各模块的独特优势,为不同类型的图像检测需求提供精准且高效的解决方案。在这个精心构建的工程体系里,每一个独立的模块都被赋予了明确且专一的任务 —— 专注于某一特定领域或类型的图像检测功能的实现。通过这种模块化的精细分工,一方面能够让开发过程更加条理清晰、易于维护,当需要对某一类图像检测算法进行优化或扩展时,直接深入对应的模块即可,避免牵一发而动全身;另一方面,不同模块可以依据所处理图像的特性,灵活选用最适配的技术栈与架构设计,从而保障每个图像检测功能都能达到最佳的性能表现。无论是面对复杂场景下的目标识别、细微瑕疵的精准定位,还是特定风格图像的分类筛选,各个模块各司其职,协同发力,力求全方位满足多样化的图像检测应用场景。

工程目前由以下几个模块组成(后继根据需求继续新增新模)
hdsoft-base-core 公共基础模块
hdsoft-identify-environment 环境监测类模块 (垃圾识别,垃圾分类,垃圾桶溢满等)
hdsoft-identify-plate 车辆号牌监测
hdsoft-yolo-start 启动服务模块
在这里插入图片描述

本节主要讲 hdsoft-identify-environment 环境监测类模块的实现,模块目录结构如下图:
在这里插入图片描述

具体实现步骤:
1,创建springboot项目 创建 hdsoft-identify-environment 模块。
2,导出的onnx模型拷贝至resources/model 目录下,为了好辨认命名为litter_detect.onnx(命名没有特点要求)
3,编写EnvironmentDetectionUtil.java

package org.hdsoft.utils;

import java.util.*;


public class EnvironmentDetectionUtil {

    /**
     * 将二维浮点数数组 data 中的检测框信息从 xywh 格式转换为 xyxy 格式,并根据置信度阈值 confThreshold 进行过滤
     *
     * @param data 包含检测框信息的二维浮点数数组,其中每一行表示一个检测框的信息,前四列分别为 x, y, w, h,后面的列表示不同类别的置信度
     * @return 一个列表,其中每个元素都是一个包含四个浮点数的列表,表示一个检测框的 xyxy 坐标
     */
    public static List<ArrayList<Float>> xywh2xyxy(float[][] data, float confThreshold) {
        // 将矩阵转置
        // 先将xywh部分转置
        float rawData[][] = new float[data[0].length][6];
        System.out.println(data.length - 1);
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < data[0].length; j++) {
                rawData[j][i] = data[i][j];
            }
        }
        // 保存每个检查框置信值最高的类型置信值和该类型下标
        for (int i = 0; i < data[0].length; i++) {
            for (int j = 4; j < data.length; j++) {
                if (rawData[i][4] < data[j][i]) {
                    rawData[i][4] = data[j][i];           //置信值
                    rawData[i][5] = j - 4;                  //类型编号
                }
            }
        }
        List<ArrayList<Float>> boxes = new LinkedList<ArrayList<Float>>();
        ArrayList<Float> box = null;
        // 置信值过滤,xywh转xyxy
        for (float[] d : rawData) {
            // 置信值过滤
            if (d[4] > confThreshold) {
                // xywh(xy为中心点,w宽,h高)转x1、y1、x2、y2(检测框左上角和右下角点坐标)
                d[0] = d[0] - d[2] / 2;
                d[1] = d[1] - d[3] / 2;
                d[2] = d[0] + d[2];
                d[3] = d[1] + d[3];
                // 根据所有检测框box置信值大小的进行插入法排序,保存boxes里
                box = new ArrayList<Float>();
                for (float num : d) {
                    box.add(num);
                }
                if (boxes.size() == 0) {
                    boxes.add(box);
                } else {
                    int i;
                    for (i = 0; i < boxes.size(); i++) {
                        if (box.get(4) > boxes.get(i).get(4)) {
                            boxes.add(i, box);
                            break;
                        }
                    }
                    // 插入到最后
                    if (i == boxes.size()) {
                        boxes.add(box);
                    }
                }
            }
        }
        return boxes;
    }


    /**
     * 对输入的检测框列表应用非极大值抑制算法,以消除重叠的检测框并保留最具代表性的检测框
     *
     * @param boxes         包含所有检测框的列表,每个检测框由一个浮点数列表表示,包含检测框的坐标和置信度
     * @param nmsThreshold  非极大值抑制算法的阈值,用于确定哪些检测框应该被抑制
     * @return              经过非极大值抑制处理后的检测框列表
     */
    public static List<ArrayList<Float>> nonMaxSuppression(List<ArrayList<Float>> boxes, float nmsThreshold) {
        // 创建一个整数数组来标记每个检测框是否应该被保留
        int[] indexs = new int[boxes.size()];
        // 初始化所有检测框的标记为1,表示所有检测框都应该被保留
        Arrays.fill(indexs, 1);

        // 遍历每个检测框
        for (int cur = 0; cur < boxes.size(); cur++) {
            // 如果当前检测框已经被标记为删除,则跳过
            if (indexs[cur] == 0) {
                continue;
            }
            // 获取当前检测框,它代表该类置信值最大的框
            ArrayList<Float> curMaxConf = boxes.get(cur);

            // 遍历当前检测框之后的所有检测框
            for (int i = cur + 1; i < boxes.size(); i++) {
                // 如果当前检测框已经被标记为删除,则跳过
                if (indexs[i] == 0) {
                    continue;
                }
                // 获取当前检测框的类别索引
                float classIndex = boxes.get(i).get(5);

                // 如果两个检测框检测到同一类数据,则通过IoU来判断是否检测到同一目标
                if (classIndex == curMaxConf.get(5)) {
                    // 获取当前检测框和候选检测框的坐标
                    float x1 = curMaxConf.get(0);
                    float y1 = curMaxConf.get(1);
                    float x2 = curMaxConf.get(2);
                    float y2 = curMaxConf.get(3);
                    float x3 = boxes.get(i).get(0);
                    float y3 = boxes.get(i).get(1);
                    float x4 = boxes.get(i).get(2);
                    float y4 = boxes.get(i).get(3);

                    // 检查两个检测框是否不相交,如果不相交则跳过
                    if (x1 > x4 || x2 < x3 || y1 > y4 || y2 < y3) {
                        continue;
                    }

                    // 计算两个矩形的交集面积
                    float intersectionWidth = Math.max(x1, x3) - Math.min(x2, x4);
                    float intersectionHeight = Math.max(y1, y3) - Math.min(y2, y4);
                    float intersectionArea = Math.max(0, intersectionWidth * intersectionHeight);

                    // 计算两个矩形的并集面积
                    float unionArea = (x2 - x1) * (y2 - y1) + (x4 - x3) * (y4 - y3) - intersectionArea;

                    // 计算IoU
                    float iou = intersectionArea / unionArea;

                    // 如果IoU超过阈值,则标记当前检测框为删除
                    indexs[i] = iou > nmsThreshold ? 0 : 1;
                }
            }
        }

        // 创建一个新的列表来存储经过非极大值抑制处理后的检测框
        List<ArrayList<Float>> resBoxes = new LinkedList<ArrayList<Float>>();
        // 遍历所有检测框的标记,如果标记为1,则将对应的检测框添加到结果列表中
        for (int index = 0; index < indexs.length; index++) {
            if (indexs[index] == 1) {
                resBoxes.add(boxes.get(index));
            }
        }

        // 返回经过非极大值抑制处理后的检测框列表
        return resBoxes;
    }
}

4编写 EnvironmentDetectionController.java

package org.hdsoft.controller;

import ai.onnxruntime.*;
import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.hdsoft.config.ODConfig;
import org.hdsoft.utils.EnvironmentDetectionUtil;
import org.hdsoft.utils.ImageUtils;
import org.hdsoft.utils.Letterbox;
import org.hdsoft.vo.Result;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.FloatBuffer;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.*;
import java.util.List;

@RestController
@Tag(name = "环境检测")
@RequestMapping("/environment-detect")
public class EnvironmentDetectionController {

    static {
        // 加载opencv动态库,
        nu.pattern.OpenCV.loadLocally();
    }

    // 垃圾检测模型
    final static String model_path = "./hdsoft-identify-environment/src/main/resources/model/litter_detect.onnx";
    final static float confThreshold = 0.25F;
    final static float nmsThreshold = 0.5F;


    @Operation(summary = "垃圾识别", description = "垃圾识别")
    @ApiImplicitParam(name = "file", value = "图片上传", required = true, dataType = "MultipartFile", allowMultiple = true, paramType = "query")
    @PostMapping("/identify-litter")
    public Result<?> plateDetection(@RequestParam("file") MultipartFile[] file) {
        JSONObject jsonObject = new JSONObject();
        try {
            // 加载ONNX模型
            OrtEnvironment environment = OrtEnvironment.getEnvironment();
            OrtSession.SessionOptions sessionOptions = new OrtSession.SessionOptions();
            OrtSession session = environment.createSession(model_path, sessionOptions);

            //转BufferedImage对象
            BufferedImage inputImage = ImageIO.read(file[0].getInputStream());
            Mat img = ImageUtils.convertMat(inputImage);
            Mat image = img.clone();
            Imgproc.cvtColor(image, image, Imgproc.COLOR_BGR2RGB);


            // 在这里先定义下框的粗细、字的大小、字的类型、字的颜色(按比例设置大小粗细比较好一些)
            int minDwDh = Math.min(img.width(), img.height());
            int thickness = minDwDh / ODConfig.lineThicknessRatio;
            long start_time = System.currentTimeMillis();
            // 更改 image 尺寸
            Letterbox letterbox = new Letterbox();
            image = letterbox.letterbox(image);

            double ratio = letterbox.getRatio();
            double dw = letterbox.getDw();
            double dh = letterbox.getDh();
            int rows = letterbox.getHeight();
            int cols = letterbox.getWidth();
            int channels = image.channels();

            image.convertTo(image, CvType.CV_32FC1, 1. / 255);
            float[] whc = new float[3 * 640 * 640];
            image.get(0, 0, whc);
            float[] chw = ImageUtils.whc2cwh(whc);

            // 创建OnnxTensor对象
            long[] shape = {1L, (long) channels, (long) rows, (long) cols};
            OnnxTensor tensor = OnnxTensor.createTensor(environment, FloatBuffer.wrap(chw), shape);
            HashMap<String, OnnxTensor> stringOnnxTensorHashMap = new HashMap<>();
            stringOnnxTensorHashMap.put(session.getInputInfo().keySet().iterator().next(), tensor);

            // 运行推理
            OrtSession.Result result = session.run(Collections.singletonMap("images", tensor));
//            System.out.println("res Data: " + result.get(0));

            OnnxTensor res = (OnnxTensor) result.get(0);
            float[][][] dataRes = (float[][][]) res.getValue();
            float[][] data = dataRes[0];

            List<ArrayList<Float>> boxes = EnvironmentDetectionUtil.xywh2xyxy(data, confThreshold);
            boxes = EnvironmentDetectionUtil.nonMaxSuppression(boxes, nmsThreshold);


            OnnxModelMetadata metadata = session.getMetadata();
            Map<String, NodeInfo> infoMap = session.getInputInfo();
            TensorInfo nodeInfo = (TensorInfo) infoMap.get("images").getInfo();
            String nameClass = metadata.getCustomMetadata().get("names");
            JSONObject names = JSONObject.parseObject(nameClass.replace("\"", "\"\""));

            float netHeight = nodeInfo.getShape()[2];//640 模型高
            float netWidth = nodeInfo.getShape()[3];//640 模型宽
            float srcw = inputImage.getWidth();
            float srch = inputImage.getHeight();
            float scaleW = srcw / netWidth;
            float scaleH = srch / netHeight;

            System.out.println("boxes.size : " + boxes.size());

            BufferedImage bufferedImage = null;
            for (ArrayList<Float> box1 : boxes) {
                Point topLeft = new Point(box1.get(0) * scaleW, box1.set(1, box1.get(1) * scaleH));
                Point bottomRight = new Point(box1.get(2) * scaleW, box1.get(3) * scaleH);
                Imgproc.rectangle(img, topLeft, bottomRight, new Scalar(0, 255, 0), thickness);

                String conf = new DecimalFormat("#.###").format(box1.get(4));
                Imgproc.putText(img, names.get(0).toString() + " " + conf, new Point(box1.get(0) * scaleW, box1.get(1) * scaleH - 5), 0, 0.5, new Scalar(255, 0, 0), thickness);
                bufferedImage = ImageUtils.matToBufferedImage(img);

                // 框上写文字
                //Graphics2D g2d = bufferedImage.createGraphics();
                //g2d.setFont(new Font("微软雅黑", Font.PLAIN, 20));
                //g2d.setColor(Color.RED);
                // String conf = new DecimalFormat("#.###").format(box1.get(4));
                // Imgproc.putText(img,names.get(0).toString() + " " + conf,new Point(box1.get(0) * scaleW,box1.get(1) * scaleW),0,0.5,new Scalar(255,0,0),1);
                // g2d.drawString("垃圾 " + conf, (int) (box1.get(0) * scaleW), (int) (box1.get(1) * scaleW));
                //g2d.dispose();
            }
            String string = ImageUtils.BufferedImageToBase64(bufferedImage);
            jsonObject.put("image", string);
            jsonObject.put("size", boxes.size());
            jsonObject.put("name", names.get(0).toString());
            System.out.printf("time:%d ms.", (System.currentTimeMillis() - start_time));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Result.ok(jsonObject);
    }
}

5.启动项目(注意:hdsoft-yolo-start 模块pom中引入模块)启动成功如下图:
在这里插入图片描述
6,验证接口
浏览器输入上面的地址
在这里插入图片描述
使用接口上传一张图片查看检测:
在这里插入图片描述
验证一下返回的base6图片:找一个在线在线Base64转图片的工具填入返回图片查看,结果如下图:
在这里插入图片描述
截至目前,我们已然成功打通了从模型训练、模型导出,再到模型实际投入使用的全流程链路,完整且深入地掌握了这一系列关键步骤。不仅如此,在技术探索的征程中,我们还成功解锁了一项极具实用价值的技能 —— 学会了巧妙运用 ONNX 模型来打造一个基于 Spring Boot 的高效 Web 应用接口。

接下来,我们将全方位模拟一个智能化、流程化的作业系统:该系统能够定时启动任务,精准捕捉无人机高空俯瞰所拍摄的图像,随即开启一场对垃圾识别的 “智能大作战”。​
在定时检测环节,仿佛给整个系统安装了一个精准的 “生物钟”,按照预设好的时间间隔,有条不紊地触发无人机拍照指令。无人机宛如一只翱翔天际的 “智能之眼”,穿梭于城市上空、郊外旷野或是特定作业区域,利用高清摄像头将下方的景象实时定格为一张张高清图像,并迅速传输回地面控制中心。​
紧接着,图像识别模块大显身手,恰似一位拥有火眼金睛的 “智能卫士”,运用先进的深度学习算法,对接收来的每一张图像进行逐帧扫描与分析。它能在纷繁复杂的画面中敏锐地锁定各类垃圾目标,无论是散落于街角的塑料袋、堆积在河边的废弃杂物,还是隐匿于草丛中的破旧瓶子,都无所遁形。​
一旦垃圾被精准识别,数据生成与保存模块便迅速介入,如同一位严谨细致的 “记录员”,将识别出的垃圾种类、数量、分布位置等关键信息,以结构化的数据形式一一记录下来,并安全、高效地存储至专门的数据库中。这些数据如同蕴藏着无限价值的 “信息宝藏”,为后续的环境分析、环卫作业规划等提供了坚实的数据支撑。​
最后,展示模块粉墨登场,它就像一位出色的 “展示大师”,负责将存储在数据库中的垃圾数据进行可视化处理,以直观、易懂的图表、地图或是报表形式呈现在用户页面上。让使用者能够一目了然地了解到区域内垃圾的整体状况,进而为决策制定、资源调配提供便利,助力打造一个更加清洁、美丽的生活环境。

7,无人机选购与设置
在无人机的选型与设置环节,出于高效便捷的考量,我们强烈推荐直接采购配备智能机场的无人机产品,以大疆的司空 2 为例,它凭借卓越的性能表现与智能化的操控特性,能为整个作业流程带来极大便利。使用这类无人机,用户可以通过简单的操作,依据实际需求轻松设定飞行时间,精准规划飞行轨迹,细致调整拍照角度,从而确保无人机在执行任务时能够全方位、精准化地采集所需图像,极大地节省人力与时间成本。
当然,如果现阶段预算相对紧张,无法进行设备采购,自行实现无人机的相关设置也是完全可行的。虽然这一途径可能需要投入更多的精力与技术考量,但通过合理利用开源飞控软件、搭配自制的地面控制站等手段,同样能够达成设定飞行时间、规划飞行轨迹以及调整拍照角度等目标,保障后续任务的顺利开展。界面效果如图:
在这里插入图片描述

8,无人图像EXIF信息读取(大疆照片的EXIF/XMP信息包括拍摄日期,图片中心经纬度,焦距,高度,航向角,横滚角,俯仰角等等)可以使用图虫EXIF查看器查看图片信息 图虫EXIF查看器地址
编写ExifInfoUtil.java工具类提取EXIF信息

<!--获取相机exif信息-->
<dependency>
    <groupId>com.drewnoakes</groupId>
    <artifactId>metadata-extractor</artifactId>
    <version>2.16.0</version>
</dependency>

package org.hdsoft.utils;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ExifInfoUtil {
    /**
     * 获取图片文件的Exif信息
     *
     * @param file
     * @return
     * @throws ImageProcessingException
     * @throws IOException
     */
    public static Map<String, String> readPicExifInfo(File file) throws ImageProcessingException, IOException {
        Map<String, String> map = new HashMap<>();
        Metadata metadata = ImageMetadataReader.readMetadata(file);
        for (Directory directory : metadata.getDirectories()) {
            for (Tag tag : directory.getTags()) {
                // 输出所有属性
                System.out.format("[%s] - %s = %s\n", directory.getName(), tag.getTagName(), tag.getDescription());
                map.put(tag.getTagName(), tag.getDescription());
            }
            if (directory.hasErrors()) {
                for (String error : directory.getErrors()) {
                    System.err.format("ERROR: %s", error);
                }
            }
        }
        return map;
    }

    /**
     * 打印照片Exif信息
     *
     * @param map
     */
    private static void printPicExifInfo(Map<String, String> map) {
        String[] strings = new String[]{"Compression", "Image Width", "Image Height", "Make", "Model", "Software", "GPS Version ID", "GPS Latitude", "GPS Longitude", "GPS Altitude", "GPS Time-Stamp", "GPS Date Stamp", "ISO Speed Ratings", "Exposure Time", "Exposure Mode", "F-Number", "Focal Length 35", "Color Space", "File Source", "Scene Type"};
        String[] names = new String[]{"压缩格式", "图像宽度", "图像高度", "拍摄手机", "型号", "手机系统版本号", "gps版本", "经度", "纬度", "高度", "UTC时间戳", "gps日期", "iso速率", "曝光时间", "曝光模式", "光圈值", "焦距", "图像色彩空间", "文件源", "场景类型"};
        for (int i = 0; i < strings.length; i++) {
            if (map.containsKey(strings[i])) {
                if ("GPS Latitude".equals(strings[i]) || "GPS Longitude".endsWith(strings[i])) {
                    System.out.println(names[i] + "  " + strings[i] + " : " + map.get(strings[i]) + ", °转dec: " + latLng2Decimal(map.get(strings[i])));
                } else {
                    System.out.println(names[i] + "  " + strings[i] + " : " + map.get(strings[i]));
                }
            }
        }
        
    }

   
    public static void main(String[] args) {
        String imgDir = "E:\\TinyCD\\lj\\litter";
        File[] files = new File(imgDir).listFiles();
        for (File file : files) {
            if (!file.getName().endsWith(".jpg")) {
                continue;
            }
            System.out.println("----------------------------------------\n" + file.getName());
            // 获取照片信息
            Map exifMap = null;
            try {
                exifMap = readPicExifInfo(file);
            } catch (ImageProcessingException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            // 打印照片信息
            printPicExifInfo(exifMap);
        }
    }
}

运行结果如下图:
在这里插入图片描述

9,定时检测图像,推送至前端
在我们的项目中,采用 emqx 搭配 MQTT 的物联网方案,为消息推送提供了极为有效的支持。这一组合凭借其卓越的性能和稳定性,能够高效应对各类复杂的消息推送需求,极大地提升了数据传输的效率和可靠性。鉴于此方案在消息推送领域展现出的巨大价值,我们计划在后续工作中深入研究,撰写一系列针对类似应用场景的详细解决方案,旨在为相关行业提供更具参考性和实用性的技术指导。
第一步需完成 emqx 的搭建工作,关于搭建的具体操作流程和步骤,各位可自行探索并完成 。
在这里插入图片描述
第二步编写一个消息推送模块 ,实现mqtt消息推送功能
在这里插入图片描述
引用依赖包

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
            <version>6.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
            <version>6.3.2</version>
        </dependency>

关键代码
MqttAcceptClient.java

package org.hdsoft.mqtt;

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Description : MQTT接受服务的客户端
 */
@Component
public class MqttAcceptClient {

    private static final Logger logger = LoggerFactory.getLogger(MqttAcceptClient.class);

    @Autowired
    private MqttAcceptCallback  mqttAcceptCallback;

    @Autowired
    private MqttProperties      mqttProperties;

    public static MqttClient    client;

    private static MqttClient getClient() {
        return client;
    }

    private static void setClient(MqttClient client) {
        MqttAcceptClient.client = client;
    }

    /**
     * 客户端连接
     */
    public void connect() {
        MqttClient client;
        try {
            client = new MqttClient(mqttProperties.getHostUrl(), mqttProperties.getClientId(),
                new MemoryPersistence());
            MqttConnectOptions options = new MqttConnectOptions();
            options.setUserName(mqttProperties.getUsername());
            options.setPassword(mqttProperties.getPassword().toCharArray());
            options.setConnectionTimeout(mqttProperties.getTimeout());
            options.setKeepAliveInterval(mqttProperties.getKeepAlive());
            options.setAutomaticReconnect(mqttProperties.getReconnect());
            options.setCleanSession(mqttProperties.getCleanSession());
            MqttAcceptClient.setClient(client);
            // 设置回调
            client.setCallback(mqttAcceptCallback);
            client.connect(options);
        } catch (Exception e) {
            logger.error("MqttAcceptClient connect error,message:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 重新连接
     */
    public void reconnection() {
        try {
            client.connect();
        } catch (MqttException e) {
            logger.error("MqttAcceptClient reconnection error,message:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 订阅某个主题
     *
     * @param topic 主题
     * @param qos   连接方式
     */
    public void subscribe(String topic, int qos) {
        logger.info("========================【开始订阅主题:" + topic + "】========================");
        try {
            client.subscribe(topic, qos);
        } catch (MqttException e) {
            logger.error("MqttAcceptClient subscribe error,message:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 取消订阅某个主题
     *
     * @param topic
     */
    public void unsubscribe(String topic) {
        logger.info("========================【取消订阅主题:" + topic + "】========================");
        try {
            client.unsubscribe(topic);
        } catch (MqttException e) {
            logger.error("MqttAcceptClient unsubscribe error,message:{}", e.getMessage());
            e.printStackTrace();
        }
    }
}

测试服务

package org.hdsoft.controller;


import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

import org.hdsoft.mqtt.MqttSendClient;
import org.hdsoft.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Tag(name = "消息推送")
@RequestMapping("/mqtt")
public class MqttController {

    @Autowired
    private MqttSendClient mqttSendClient;

    @Operation(summary = "推送消息", description = "推送消息")
    @PostMapping("/publishTopic")
    public Result<?> publishTopic(@RequestParam("topic") String topic, @RequestParam("sendMessage") String sendMessage) {
        mqttSendClient.publish(false, topic, sendMessage);
        return Result.ok("topic:" + topic + "\nmessage:" + sendMessage);
    }

}

消息发送成功
在这里插入图片描述
在这里插入图片描述
10, hdsoft-identify-environment 模块中引用mqtt模块实现定时检测推送
在这里插入图片描述

     <dependency>
            <groupId>org.hdsoft</groupId>
            <artifactId>hdsoft-base-mqtt</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

关键代码 TaskApplication.java

package org.hdsoft.tasks;

import com.alibaba.fastjson.JSONObject;
import org.hdsoft.mqtt.MqttSendClient;
import org.hdsoft.service.EnvironmentDetectionService;
import org.hdsoft.utils.ExifInfoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;


@Component
@EnableScheduling
public class TaskApplication {

    @Autowired
    private EnvironmentDetectionService environmentDetectionService;

    @Autowired
    private MqttSendClient mqttSendClient;

    //容器启动后,延迟10秒后再执行一次定时器,以后每60秒再执行一次该定时器。
    @Scheduled(initialDelay = 10000, fixedRate = 60000)
    public void Task1() {
        // 要检测的图片所在目录  大疆的拍照图片一般在minio获取,自行改造对接。我们就当他在本地目录中
        String imagePath = "./hdsoft-identify-environment/src/main/java/org/hdsoft/litterImg";
        Map<String, String> imagePathMap = getImagePathMap(imagePath);
        try {
            ArrayList<JSONObject> arrayList = new ArrayList();
            for (String key : imagePathMap.keySet()) {
                File file = new File(imagePathMap.get(key));
                BufferedImage bufferedImage = ImageIO.read(file);
                JSONObject jsonObject = environmentDetectionService.LitterDetection(bufferedImage);
                if (jsonObject.getInteger("size") != 0 && jsonObject.getString("name").equals("litter")) {
//                    Map<String, String> metadata = ExifInfoUtil.readPicExifInfo(file);
//                    jsonObject.put(metadata.get(""))
                    //应当从metadata 中获取真实的图片信息,因为我这里没有真实数据所有模拟一下 最终把arrayList保存到数据表中,获取直接推送给客户端展示
                    //下面模拟数据
                    BigDecimal Latitude = new BigDecimal(102.55143);
                    BigDecimal Longitude = new BigDecimal(  35.19873);
                    double num = (Math.random() * 10) + 1;
                    BigDecimal i = new BigDecimal(num/10000);

                    jsonObject.put("Latitude", Latitude.add(i).setScale(5, BigDecimal.ROUND_HALF_UP));
                    jsonObject.put("Longitude", Longitude.add(i).setScale(5, BigDecimal.ROUND_HALF_UP));
                    jsonObject.put("Altitude", 2897.44);
                    jsonObject.put("GPSDate", System.currentTimeMillis());

                }
                //先不保存数据直接发送至客户端展示 ,后续可以考虑保存到数据库中作为业务数据使用。
                mqttSendClient.publish(false, "litter", jsonObject.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 读取文件夹下的文件
     *
     * @param imagePath
     * @return
     */
    public static Map<String, String> getImagePathMap(String imagePath) {
        Map<String, String> map = new TreeMap<>();
        File file = new File(imagePath);
        if (file.isFile()) {
            map.put(file.getName(), file.getAbsolutePath());
        } else if (file.isDirectory()) {
            for (File tmpFile : Objects.requireNonNull(file.listFiles())) {
                map.putAll(getImagePathMap(tmpFile.getPath()));
            }
        }
        return map;
    }
}

启动项目后,仔细观察控制台,便能惊喜地发现系统已成功实现定时读取航拍图片,精准检测垃圾,并且推送功能也运转正常。这一系列流畅的自动化操作,是不是让人由衷地感叹科技的神奇力量 ?
在这里插入图片描述

11, 前端接受数据展示,我这里使用cesium 的开源项目。
安装依赖

 npm install mqtt -S

实现接收消息 topic.vue

<template>
  <!--  <div>-->
  <!--    <button @click="unsubscribeTopics">关闭链接</button>-->
  <!--    <button @click="startThis">开始订阅</button>-->
  <!--    <button @click="publishMessage">发布消息</button>-->
  <!--  </div>-->
</template>

<script setup>
import {ref, onMounted, onBeforeUnmount} from 'vue'
import * as mqtt from 'mqtt/dist/mqtt.min';
import {store} from "@/store";

const client = ref(null)
const emits = defineEmits(['close','createESPoi2D'])
const currentTopics = ref([])


const unsubscribeTopics = () => {
  if (client.value && client.value.connected) {
    client.value.unsubscribe(currentTopics.value, (err) => {
      if (!err) {
        console.log('成功取消订阅主题:', currentTopics.value);
        currentTopics.value = []; // 清空当前主题列表
        client.value.end()//彻底断掉连接
      } else {
        console.error('取消订阅失败:', err);
      }
    });
  } else {
    console.log('未连接到 MQTT 服务器或连接已断开');
  }
}

const startThis = () => {
  // 连接到 MQTT 服务器
  client.value = mqtt.connect('ws://192.168.20.20:8083/mqtt', {
    username: 'admin',
    password: 'SKgt@123',
    reconnectPeriod: false,//断掉之后是否重连
    connectTimeout: 5000
  });

  // 监听连接事件
  client.value.on('connect', () => {
    console.log('监听成功');
    // 订阅主题
    client.value.subscribe('server/litter');
    console.log('订阅主题成功!');
    currentTopics.value.push('server/litter');
  });

  // 监听消息事件
  client.value.on('message', (topic, message) => {
    console.log(`监听数据 ${topic}: ${message.toString()}`);
    // 在这里处理接收到的消息
    emits('createESPoi2D', JSON.parse(message.toString()))
  });

  // 监听连接失败事件
  client.value.on('error', (error) => {
    console.log('失败了:', error);
  });

  // 重连
  client.value.on('reconnect', (error) => {
    console.log('正在重连:', error)
  })
}

// 发布
const publishMessage = () => {
  if (client.value && client.value.connected) {
    const messageToSend = 'Hello, MQTT!'; // 要发送的消息内容
    const topicToSend = 'your/topic'; // 替换为你想要发布消息的主题
    client.value.publish(topicToSend, messageToSend, {qos: 2}, (err) => {
      if (!err) {
        console.log(`成功发布消息到主题 ${topicToSend}`);
      } else {
        console.error('发布消息出错:', err);
      }
    });
  } else {
    console.log('未连接到 MQTT 服务器或连接已断开');
  }
}

onMounted(() => {
  startThis();
})

onBeforeUnmount(() => {
  unsubscribeTopics()
})


</script>

<style scoped></style>

打开控制台查看信息 发现已经可以接收消息
在这里插入图片描述
编写cesium 的2d绘制方法


import {ESPoi2D} from 'esobjs-xe2-plugin/dist-node/esobjs-xe2-plugin-main';
const {ESPoi2D} = XE2['esobjs-xe2-plugin-main']
// 创建2D标记
function createESPoi2D(obj) {

  const sceneObject = window.g_xbsjEarthUi.createSceneObject(ESPoi2D)
  if (!sceneObject) return
  // 设置不同样式
  sceneObject.mode = 'CircularH01'
  // sceneObject.mode = 'Linear01'
  sceneObject.position = [obj.Latitude * 1, obj.Longitude * 1, obj.Altitude * 1]
  // 开启编辑模式
  // sceneObject.editing = true
  // 设置显示文本名称
  // sceneObject.name = obj.name
  sceneObject.name = '垃圾点'
}

最终效果
在这里插入图片描述

在这里插入图片描述
通过一步步的努力,我们成功实现了利用无人机画面进行垃圾检测的全流程操作。在这个过程中,我们深入探索并掌握了无人机检测垃圾技术的实现方法。从最初的技术调研、方案设计,到设备调试、算法优化,每一个环节都凝聚着我们的心血。如今,我们已经能够熟练运用这套技术,精准地识别垃圾,为环保工作提供了有力的支持 。

本章源码夸克网盘下载地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值