序号 | 名称 |
---|---|
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客户端调用方式实现该系统。
- ONNX Runtime Server
ONNX(Open Neural Network Exchange)是一种开放格式,允许模型在不同的深度学习框架间自由转换。通过ONNX Runtime Server,您可以部署经ONNX转换的模型,该服务器支持多种硬件后端(CPU、GPU及特定AI芯片加速器),从而实现实时高效的模型推理。ONNX Runtime Server还拥有跨平台支持、多语言客户端、性能优化以及广泛模型兼容性的优点,使得Java Web应用仅需通过标准HTTP接口就能调用模型,而无需关注底层深度学习框架的具体实现。 - 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 = '垃圾点'
}
最终效果
通过一步步的努力,我们成功实现了利用无人机画面进行垃圾检测的全流程操作。在这个过程中,我们深入探索并掌握了无人机检测垃圾技术的实现方法。从最初的技术调研、方案设计,到设备调试、算法优化,每一个环节都凝聚着我们的心血。如今,我们已经能够熟练运用这套技术,精准地识别垃圾,为环保工作提供了有力的支持 。