将检测任务迁移python
This commit is contained in:
@@ -1,37 +1,21 @@
|
||||
package com.ruoyi.video.common;
|
||||
|
||||
/**
|
||||
* 定义一个枚举类,表示客户端类型
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
* 客户端类型枚举
|
||||
*/
|
||||
public enum ClientType {
|
||||
|
||||
// 定义一个HTTP类型的客户端,类型为0,信息为"http"
|
||||
HTTP(0,"http"),
|
||||
// 定义一个WEBSOCKET类型的客户端,类型为1,信息为"websocket"
|
||||
WEBSOCKET(1,"websocket"),
|
||||
;
|
||||
|
||||
// 定义客户端类型
|
||||
private int type;
|
||||
// 定义客户端信息
|
||||
private String info;
|
||||
/**
|
||||
* HTTP客户端
|
||||
*/
|
||||
HTTP,
|
||||
|
||||
// 构造方法,初始化客户端类型和信息
|
||||
private ClientType(int type, String info){
|
||||
this.type = type;
|
||||
this.info = info;
|
||||
}
|
||||
/**
|
||||
* WebSocket客户端
|
||||
*/
|
||||
WEBSOCKET,
|
||||
|
||||
// 获取客户端类型
|
||||
public int getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
// 获取客户端信息
|
||||
public String getInfo(){
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* API客户端
|
||||
*/
|
||||
API
|
||||
}
|
||||
|
||||
@@ -9,7 +9,15 @@ import java.util.*;
|
||||
|
||||
public final class ModelManager implements AutoCloseable {
|
||||
private final Map<String, YoloDetector> map = new LinkedHashMap<>();
|
||||
|
||||
|
||||
// Python服务的默认API URL
|
||||
private static final String DEFAULT_PYTHON_API_URL = "http://localhost:8000/api/detect/file";
|
||||
|
||||
/**
|
||||
* 从配置加载检测器
|
||||
* @param modelsJson 模型配置JSON的URL
|
||||
* @throws Exception 如果加载失败
|
||||
*/
|
||||
public void load(URL modelsJson) throws Exception {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
ArrayNode arr = (ArrayNode) om.readTree(modelsJson);
|
||||
@@ -20,20 +28,15 @@ public final class ModelManager implements AutoCloseable {
|
||||
int i=0;
|
||||
for (var node : arr) {
|
||||
String name = node.get("name").asText();
|
||||
String path = node.get("path").asText();
|
||||
int w = node.get("size").get(0).asInt();
|
||||
int h = node.get("size").get(1).asInt();
|
||||
String backend = node.get("backend").asText();
|
||||
|
||||
URL dirUrl = Objects.requireNonNull(getClass().getClassLoader().getResource(path),
|
||||
"Resource not found: " + path);
|
||||
Path dir = Paths.get(dirUrl.toURI());
|
||||
String pythonModelName = node.get("pythonModelName").asText(name); // 默认与name相同
|
||||
String pythonApiUrl = node.has("pythonApiUrl") ?
|
||||
node.get("pythonApiUrl").asText() : DEFAULT_PYTHON_API_URL;
|
||||
|
||||
int rgb = palette[i % palette.length]; i++;
|
||||
int bgr = ((rgb & 0xFF) << 16) | (rgb & 0xFF00) | ((rgb >> 16) & 0xFF);
|
||||
|
||||
// 使用OnnxYoloDetector替代OpenVinoYoloDetector
|
||||
YoloDetector det = new OnnxYoloDetector(name, dir, w, h, backend, bgr);
|
||||
// 使用HttpYoloDetector替代本地ONNX检测器
|
||||
YoloDetector det = new HttpYoloDetector(name, pythonApiUrl, pythonModelName, bgr);
|
||||
map.put(name, det);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,55 @@
|
||||
package com.ruoyi.video.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 报警记录对象 v_alarm_record
|
||||
*
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
* 告警记录实体类
|
||||
*/
|
||||
public class AlarmRecord extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 报警记录ID */
|
||||
public class AlarmRecord {
|
||||
/** 告警ID */
|
||||
private Long alarmId;
|
||||
|
||||
/** 巡检任务ID */
|
||||
@Excel(name = "巡检任务ID")
|
||||
private Long taskId;
|
||||
|
||||
/** 任务名称 */
|
||||
@Excel(name = "任务名称")
|
||||
private String taskName;
|
||||
|
||||
|
||||
/** 设备ID */
|
||||
@Excel(name = "设备ID")
|
||||
private Long deviceId;
|
||||
|
||||
/** 设备名称 */
|
||||
@Excel(name = "设备名称")
|
||||
private String deviceName;
|
||||
|
||||
/** 报警类型 */
|
||||
@Excel(name = "报警类型")
|
||||
|
||||
/** 告警类型 */
|
||||
private String alarmType;
|
||||
|
||||
/** 报警级别(1=低,2=中,3=高) */
|
||||
@Excel(name = "报警级别", readConverterExp = "1=低,2=中,3=高")
|
||||
|
||||
/** 告警级别 */
|
||||
private String alarmLevel;
|
||||
|
||||
/** 报警描述 */
|
||||
@Excel(name = "报警描述")
|
||||
private String alarmDesc;
|
||||
|
||||
/** 检测置信度 */
|
||||
@Excel(name = "检测置信度")
|
||||
private Double confidence;
|
||||
|
||||
/** 报警图片路径 */
|
||||
@Excel(name = "报警图片")
|
||||
private String imagePath;
|
||||
|
||||
/** 报警视频路径 */
|
||||
@Excel(name = "报警视频")
|
||||
private String videoPath;
|
||||
|
||||
/** 报警时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "报警时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date alarmTime;
|
||||
|
||||
/** 处理状态(0=未处理,1=已处理,2=已忽略) */
|
||||
@Excel(name = "处理状态", readConverterExp = "0=未处理,1=已处理,2=已忽略")
|
||||
private String handleStatus;
|
||||
|
||||
|
||||
/** 告警内容 */
|
||||
private String alarmContent;
|
||||
|
||||
/** 巡检任务ID */
|
||||
private Long taskId;
|
||||
|
||||
/** 图像对象存储ID */
|
||||
private Long imageOssId;
|
||||
|
||||
/** 帧位置 */
|
||||
private long framePosition;
|
||||
|
||||
/** 置信度 */
|
||||
private double confidence;
|
||||
|
||||
/** 处理状态(0-未处理,1-已处理) */
|
||||
private Integer status;
|
||||
|
||||
/** 处理人 */
|
||||
@Excel(name = "处理人")
|
||||
private String handleBy;
|
||||
|
||||
|
||||
/** 处理时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "处理时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date handleTime;
|
||||
|
||||
|
||||
/** 处理备注 */
|
||||
@Excel(name = "处理备注")
|
||||
private String handleRemark;
|
||||
|
||||
public AlarmRecord() {}
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
public Long getAlarmId() {
|
||||
return alarmId;
|
||||
@@ -90,22 +59,6 @@ public class AlarmRecord extends BaseEntity {
|
||||
this.alarmId = alarmId;
|
||||
}
|
||||
|
||||
public Long getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(Long taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getTaskName() {
|
||||
return taskName;
|
||||
}
|
||||
|
||||
public void setTaskName(String taskName) {
|
||||
this.taskName = taskName;
|
||||
}
|
||||
|
||||
public Long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
@@ -114,14 +67,6 @@ public class AlarmRecord extends BaseEntity {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public String getAlarmType() {
|
||||
return alarmType;
|
||||
}
|
||||
@@ -130,60 +75,52 @@ public class AlarmRecord extends BaseEntity {
|
||||
this.alarmType = alarmType;
|
||||
}
|
||||
|
||||
public String getAlarmLevel() {
|
||||
return alarmLevel;
|
||||
public String getAlarmContent() {
|
||||
return alarmContent;
|
||||
}
|
||||
|
||||
public void setAlarmLevel(String alarmLevel) {
|
||||
this.alarmLevel = alarmLevel;
|
||||
public void setAlarmContent(String alarmContent) {
|
||||
this.alarmContent = alarmContent;
|
||||
}
|
||||
|
||||
public String getAlarmDesc() {
|
||||
return alarmDesc;
|
||||
public Long getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setAlarmDesc(String alarmDesc) {
|
||||
this.alarmDesc = alarmDesc;
|
||||
public void setTaskId(Long taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Double getConfidence() {
|
||||
public Long getImageOssId() {
|
||||
return imageOssId;
|
||||
}
|
||||
|
||||
public void setImageOssId(Long imageOssId) {
|
||||
this.imageOssId = imageOssId;
|
||||
}
|
||||
|
||||
public long getFramePosition() {
|
||||
return framePosition;
|
||||
}
|
||||
|
||||
public void setFramePosition(long framePosition) {
|
||||
this.framePosition = framePosition;
|
||||
}
|
||||
|
||||
public double getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public void setConfidence(Double confidence) {
|
||||
public void setConfidence(double confidence) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public String getImagePath() {
|
||||
return imagePath;
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
||||
public String getVideoPath() {
|
||||
return videoPath;
|
||||
}
|
||||
|
||||
public void setVideoPath(String videoPath) {
|
||||
this.videoPath = videoPath;
|
||||
}
|
||||
|
||||
public Date getAlarmTime() {
|
||||
return alarmTime;
|
||||
}
|
||||
|
||||
public void setAlarmTime(Date alarmTime) {
|
||||
this.alarmTime = alarmTime;
|
||||
}
|
||||
|
||||
public String getHandleStatus() {
|
||||
return handleStatus;
|
||||
}
|
||||
|
||||
public void setHandleStatus(String handleStatus) {
|
||||
this.handleStatus = handleStatus;
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getHandleBy() {
|
||||
@@ -210,25 +147,51 @@ public class AlarmRecord extends BaseEntity {
|
||||
this.handleRemark = handleRemark;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AlarmRecord{" +
|
||||
"alarmId=" + alarmId +
|
||||
", taskId=" + taskId +
|
||||
", taskName='" + taskName + '\'' +
|
||||
", deviceId=" + deviceId +
|
||||
", deviceName='" + deviceName + '\'' +
|
||||
", alarmType='" + alarmType + '\'' +
|
||||
", alarmLevel='" + alarmLevel + '\'' +
|
||||
", alarmDesc='" + alarmDesc + '\'' +
|
||||
", confidence=" + confidence +
|
||||
", imagePath='" + imagePath + '\'' +
|
||||
", videoPath='" + videoPath + '\'' +
|
||||
", alarmTime=" + alarmTime +
|
||||
", handleStatus='" + handleStatus + '\'' +
|
||||
", handleBy='" + handleBy + '\'' +
|
||||
", handleTime=" + handleTime +
|
||||
", handleRemark='" + handleRemark + '\'' +
|
||||
'}';
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(Date createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ID的兼容方法,保持与原有代码兼容
|
||||
*/
|
||||
public Long getId() {
|
||||
return alarmId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置ID的兼容方法,保持与原有代码兼容
|
||||
*/
|
||||
public void setId(Long id) {
|
||||
this.alarmId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置告警级别
|
||||
*
|
||||
* @param alarmLevel 告警级别
|
||||
*/
|
||||
public void setAlarmLevel(String alarmLevel) {
|
||||
this.alarmLevel = alarmLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取告警级别
|
||||
*
|
||||
* @return 告警级别
|
||||
*/
|
||||
public String getAlarmLevel() {
|
||||
return alarmLevel;
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,61 @@ package com.ruoyi.video.domain;
|
||||
|
||||
import org.bytedeco.opencv.opencv_core.Rect;
|
||||
|
||||
public record Detection(String cls, float conf, Rect box, int colorBGR) {
|
||||
/**
|
||||
* 检测结果实体类
|
||||
*/
|
||||
public class Detection {
|
||||
/** 标签 */
|
||||
private String label;
|
||||
|
||||
/** 置信度 */
|
||||
private double confidence;
|
||||
|
||||
/** 边界框 */
|
||||
private Rect rect;
|
||||
|
||||
/** 边界框颜色(BGR格式) */
|
||||
private int colorBGR;
|
||||
|
||||
public Detection() {
|
||||
}
|
||||
|
||||
public Detection(String label, double confidence, Rect rect, int colorBGR) {
|
||||
this.label = label;
|
||||
this.confidence = confidence;
|
||||
this.rect = rect;
|
||||
this.colorBGR = colorBGR;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public double getConfidence() {
|
||||
return confidence;
|
||||
}
|
||||
|
||||
public void setConfidence(double confidence) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
public Rect getRect() {
|
||||
return rect;
|
||||
}
|
||||
|
||||
public void setRect(Rect rect) {
|
||||
this.rect = rect;
|
||||
}
|
||||
|
||||
public int getColorBGR() {
|
||||
return colorBGR;
|
||||
}
|
||||
|
||||
public void setColorBGR(int colorBGR) {
|
||||
this.colorBGR = colorBGR;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,196 +1,133 @@
|
||||
package com.ruoyi.video.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 巡检任务对象 v_inspection_task
|
||||
*
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
* 巡检任务实体类
|
||||
*/
|
||||
public class InspectionTask extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public class InspectionTask {
|
||||
/** 巡检任务ID */
|
||||
private Long taskId;
|
||||
|
||||
/** 任务名称 */
|
||||
@Excel(name = "任务名称")
|
||||
private String taskName;
|
||||
|
||||
|
||||
/** 设备ID */
|
||||
@Excel(name = "设备ID")
|
||||
private Long deviceId;
|
||||
|
||||
/** 设备名称 */
|
||||
@Excel(name = "设备名称")
|
||||
private String deviceName;
|
||||
|
||||
/** Cron表达式 */
|
||||
@Excel(name = "Cron表达式")
|
||||
private String cronExpression;
|
||||
|
||||
|
||||
/** 巡检时长(秒) */
|
||||
@Excel(name = "巡检时长")
|
||||
private Integer duration;
|
||||
|
||||
/** 任务状态(0=启用,1=停用) */
|
||||
@Excel(name = "任务状态", readConverterExp = "0=启用,1=停用")
|
||||
private String status;
|
||||
|
||||
/** 是否启用检测(0=启用,1=停用) */
|
||||
@Excel(name = "启用检测", readConverterExp = "0=启用,1=停用")
|
||||
private String enableDetection;
|
||||
|
||||
/** 检测阈值 */
|
||||
@Excel(name = "检测阈值")
|
||||
private Double threshold;
|
||||
|
||||
/** 最后执行时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "最后执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date lastExecuteTime;
|
||||
|
||||
/** 下次执行时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "下次执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date nextExecuteTime;
|
||||
|
||||
/** 执行次数 */
|
||||
@Excel(name = "执行次数")
|
||||
private Long executeCount;
|
||||
|
||||
/** 报警次数 */
|
||||
@Excel(name = "报警次数")
|
||||
private Long alarmCount;
|
||||
|
||||
public InspectionTask() {}
|
||||
|
||||
|
||||
/** 视频对象存储ID */
|
||||
private Long videoOssId;
|
||||
|
||||
/** 处理后视频对象存储ID */
|
||||
private Long processedVideoOssId;
|
||||
|
||||
/** 视频状态(0-未录制,1-已录制未分析,2-已分析) */
|
||||
private Integer videoStatus;
|
||||
|
||||
/** 任务状态(0-待执行,1-执行中,2-已完成) */
|
||||
private Integer status;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private Date updateTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
public Long getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
|
||||
public void setTaskId(Long taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
|
||||
public String getTaskName() {
|
||||
return taskName;
|
||||
}
|
||||
|
||||
public void setTaskName(String taskName) {
|
||||
this.taskName = taskName;
|
||||
}
|
||||
|
||||
|
||||
public Long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
|
||||
public void setDeviceId(Long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public void setDeviceName(String deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
public String getCronExpression() {
|
||||
return cronExpression;
|
||||
}
|
||||
|
||||
public void setCronExpression(String cronExpression) {
|
||||
this.cronExpression = cronExpression;
|
||||
}
|
||||
|
||||
|
||||
public Integer getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
|
||||
public void setDuration(Integer duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
|
||||
public Long getVideoOssId() {
|
||||
return videoOssId;
|
||||
}
|
||||
|
||||
public void setVideoOssId(Long videoOssId) {
|
||||
this.videoOssId = videoOssId;
|
||||
}
|
||||
|
||||
public Long getProcessedVideoOssId() {
|
||||
return processedVideoOssId;
|
||||
}
|
||||
|
||||
public void setProcessedVideoOssId(Long processedVideoOssId) {
|
||||
this.processedVideoOssId = processedVideoOssId;
|
||||
}
|
||||
|
||||
public Integer getVideoStatus() {
|
||||
return videoStatus;
|
||||
}
|
||||
|
||||
public void setVideoStatus(Integer videoStatus) {
|
||||
this.videoStatus = videoStatus;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getEnableDetection() {
|
||||
return enableDetection;
|
||||
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setEnableDetection(String enableDetection) {
|
||||
this.enableDetection = enableDetection;
|
||||
|
||||
public void setCreateTime(Date createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public Double getThreshold() {
|
||||
return threshold;
|
||||
|
||||
public Date getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setThreshold(Double threshold) {
|
||||
this.threshold = threshold;
|
||||
|
||||
public void setUpdateTime(Date updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public Date getLastExecuteTime() {
|
||||
return lastExecuteTime;
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setLastExecuteTime(Date lastExecuteTime) {
|
||||
this.lastExecuteTime = lastExecuteTime;
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public Date getNextExecuteTime() {
|
||||
return nextExecuteTime;
|
||||
|
||||
/**
|
||||
* 获取ID的兼容方法,保持与原有代码兼容
|
||||
*/
|
||||
public Long getId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setNextExecuteTime(Date nextExecuteTime) {
|
||||
this.nextExecuteTime = nextExecuteTime;
|
||||
}
|
||||
|
||||
public Long getExecuteCount() {
|
||||
return executeCount;
|
||||
}
|
||||
|
||||
public void setExecuteCount(Long executeCount) {
|
||||
this.executeCount = executeCount;
|
||||
}
|
||||
|
||||
public Long getAlarmCount() {
|
||||
return alarmCount;
|
||||
}
|
||||
|
||||
public void setAlarmCount(Long alarmCount) {
|
||||
this.alarmCount = alarmCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InspectionTask{" +
|
||||
"taskId=" + taskId +
|
||||
", taskName='" + taskName + '\'' +
|
||||
", deviceId=" + deviceId +
|
||||
", deviceName='" + deviceName + '\'' +
|
||||
", cronExpression='" + cronExpression + '\'' +
|
||||
", duration=" + duration +
|
||||
", status='" + status + '\'' +
|
||||
", enableDetection='" + enableDetection + '\'' +
|
||||
", threshold=" + threshold +
|
||||
", lastExecuteTime=" + lastExecuteTime +
|
||||
", nextExecuteTime=" + nextExecuteTime +
|
||||
", executeCount=" + executeCount +
|
||||
", alarmCount=" + alarmCount +
|
||||
'}';
|
||||
|
||||
/**
|
||||
* 设置ID的兼容方法,保持与原有代码兼容
|
||||
*/
|
||||
public void setId(Long id) {
|
||||
this.taskId = id;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,151 @@
|
||||
package com.ruoyi.video.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MinIO 返回结果记录实体,对应表 v_minio_object
|
||||
* MinIO对象存储实体类
|
||||
*/
|
||||
public class VMinioObject extends BaseEntity {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主键ID */
|
||||
public class VMinioObject {
|
||||
/** 对象ID */
|
||||
private Long objectId;
|
||||
/** MinIO 对象名(Key) */
|
||||
|
||||
/** 存储桶名称 */
|
||||
private String bucketName;
|
||||
|
||||
/** 对象名称 */
|
||||
private String objectName;
|
||||
/** 访问URL */
|
||||
private String url;
|
||||
/** 原始文件名(上传时的文件名) */
|
||||
private String originalName;
|
||||
|
||||
/** 对象URL */
|
||||
private String objectUrl;
|
||||
|
||||
/** 对象大小(字节) */
|
||||
private Long objectSize;
|
||||
|
||||
/** 对象类型 */
|
||||
private String objectType;
|
||||
|
||||
/** 创建者 */
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 更新者 */
|
||||
private String updateBy;
|
||||
|
||||
/** 更新时间 */
|
||||
private Date updateTime;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 删除标志(0存在 2删除) */
|
||||
private String delFlag;
|
||||
|
||||
public String getDelFlag() {
|
||||
return delFlag;
|
||||
}
|
||||
|
||||
public Long getObjectId() { return objectId; }
|
||||
public void setObjectId(Long objectId) { this.objectId = objectId; }
|
||||
public void setDelFlag(String delFlag) {
|
||||
this.delFlag = delFlag;
|
||||
}
|
||||
|
||||
/** 原始文件名 */
|
||||
private String originalName;
|
||||
|
||||
public String getOriginalName() {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
public void setOriginalName(String originalName) {
|
||||
this.originalName = originalName;
|
||||
}
|
||||
|
||||
public String getObjectName() { return objectName; }
|
||||
public void setObjectName(String objectName) { this.objectName = objectName; }
|
||||
public Long getObjectId () {
|
||||
return objectId;
|
||||
}
|
||||
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
public void setObjectId(Long objectId) {
|
||||
this.objectId = objectId;
|
||||
}
|
||||
|
||||
public String getOriginalName() { return originalName; }
|
||||
public void setOriginalName(String originalName) { this.originalName = originalName; }
|
||||
public String getBucketName() {
|
||||
return bucketName;
|
||||
}
|
||||
|
||||
public String getDelFlag() { return delFlag; }
|
||||
public void setDelFlag(String delFlag) { this.delFlag = delFlag; }
|
||||
public void setBucketName(String bucketName) {
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public String getObjectName() {
|
||||
return objectName;
|
||||
}
|
||||
|
||||
public void setObjectName(String objectName) {
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
public String getObjectUrl() {
|
||||
return objectUrl;
|
||||
}
|
||||
|
||||
public void setObjectUrl(String objectUrl) {
|
||||
this.objectUrl = objectUrl;
|
||||
}
|
||||
|
||||
public Long getObjectSize() {
|
||||
return objectSize;
|
||||
}
|
||||
|
||||
public void setObjectSize(Long objectSize) {
|
||||
this.objectSize = objectSize;
|
||||
}
|
||||
|
||||
public String getObjectType() {
|
||||
return objectType;
|
||||
}
|
||||
|
||||
public void setObjectType(String objectType) {
|
||||
this.objectType = objectType;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public Date getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(Date createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public String getUpdateBy() {
|
||||
return updateBy;
|
||||
}
|
||||
|
||||
public void setUpdateBy(String updateBy) {
|
||||
this.updateBy = updateBy;
|
||||
}
|
||||
|
||||
public Date getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(Date updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.video.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 巡检任务完成事件
|
||||
*/
|
||||
public class TaskCompletedEvent extends ApplicationEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Long taskId;
|
||||
|
||||
public TaskCompletedEvent(Object source, Long taskId) {
|
||||
super(source);
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public Long getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.video.event;
|
||||
|
||||
import com.ruoyi.video.domain.InspectionTask;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* 巡检任务开始事件
|
||||
*/
|
||||
public class TaskStartEvent extends ApplicationEvent {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final InspectionTask task;
|
||||
|
||||
public TaskStartEvent(Object source, InspectionTask task) {
|
||||
super(source);
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
public InspectionTask getTask() {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.ruoyi.video.listener;
|
||||
|
||||
import com.ruoyi.video.domain.InspectionTask;
|
||||
import com.ruoyi.video.event.TaskCompletedEvent;
|
||||
import com.ruoyi.video.event.TaskStartEvent;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import com.ruoyi.video.service.VideoAnalysisService;
|
||||
import com.ruoyi.video.thread.MediaTransferFlvByJavacv;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 巡检任务事件监听器
|
||||
* 处理任务开始和完成事件
|
||||
*/
|
||||
@Component
|
||||
public class InspectionTaskEventListener {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InspectionTaskEventListener.class);
|
||||
|
||||
@Autowired
|
||||
private MediaService mediaService;
|
||||
|
||||
@Autowired
|
||||
private VideoAnalysisService videoAnalysisService;
|
||||
|
||||
/**
|
||||
* 处理任务开始事件
|
||||
* @param event 任务开始事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleTaskStartEvent(TaskStartEvent event) {
|
||||
InspectionTask task = event.getTask();
|
||||
log.info("接收到任务开始事件: 任务ID={}, 设备ID={}", task.getTaskId(), task.getDeviceId());
|
||||
|
||||
try {
|
||||
// 获取对应的媒体传输器
|
||||
MediaTransferFlvByJavacv transfer = mediaService.getMediaTransfer(task.getDeviceId());
|
||||
|
||||
if (transfer != null) {
|
||||
// 开始录制视频
|
||||
transfer.startInspection(task.getTaskId(), task.getDeviceId(), task.getDuration());
|
||||
log.info("已开始视频录制: 任务ID={}, 设备ID={}, 持续时间={}秒",
|
||||
task.getTaskId(), task.getDeviceId(), task.getDuration());
|
||||
} else {
|
||||
log.error("未找到对应的媒体传输器: 设备ID={}", task.getDeviceId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理任务开始事件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理任务完成事件
|
||||
* @param event 任务完成事件
|
||||
*/
|
||||
@EventListener
|
||||
@Async
|
||||
public void handleTaskCompletedEvent(TaskCompletedEvent event) {
|
||||
Long taskId = event.getTaskId();
|
||||
log.info("接收到任务完成事件: 任务ID={}", taskId);
|
||||
|
||||
try {
|
||||
// 开始异步视频分析
|
||||
videoAnalysisService.analyzeVideo(taskId);
|
||||
} catch (Exception e) {
|
||||
log.error("处理任务完成事件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,67 @@
|
||||
package com.ruoyi.video.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.video.domain.AlarmRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 报警记录Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-16
|
||||
* 告警记录Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AlarmRecordMapper {
|
||||
|
||||
/**
|
||||
* 查询报警记录
|
||||
*
|
||||
* @param alarmId 报警记录主键
|
||||
* @return 报警记录
|
||||
* 查询告警记录
|
||||
*
|
||||
* @param alarmId 告警ID
|
||||
* @return 告警记录
|
||||
*/
|
||||
public AlarmRecord selectAlarmRecordByAlarmId(Long alarmId);
|
||||
public AlarmRecord selectAlarmRecordById(Long alarmId);
|
||||
|
||||
/**
|
||||
* 查询报警记录列表
|
||||
*
|
||||
* @param alarmRecord 报警记录
|
||||
* @return 报警记录集合
|
||||
* 查询告警记录列表
|
||||
*
|
||||
* @param alarmRecord 告警记录
|
||||
* @return 告警记录集合
|
||||
*/
|
||||
public List<AlarmRecord> selectAlarmRecordList(AlarmRecord alarmRecord);
|
||||
|
||||
/**
|
||||
* 新增报警记录
|
||||
*
|
||||
* @param alarmRecord 报警记录
|
||||
* 新增告警记录
|
||||
*
|
||||
* @param alarmRecord 告警记录
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertAlarmRecord(AlarmRecord alarmRecord);
|
||||
|
||||
/**
|
||||
* 修改报警记录
|
||||
*
|
||||
* @param alarmRecord 报警记录
|
||||
* 修改告警记录
|
||||
*
|
||||
* @param alarmRecord 告警记录
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateAlarmRecord(AlarmRecord alarmRecord);
|
||||
|
||||
/**
|
||||
* 删除报警记录
|
||||
*
|
||||
* @param alarmId 报警记录主键
|
||||
* 删除告警记录
|
||||
*
|
||||
* @param alarmId 告警ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteAlarmRecordByAlarmId(Long alarmId);
|
||||
public int deleteAlarmRecordById(Long alarmId);
|
||||
|
||||
/**
|
||||
* 批量删除报警记录
|
||||
*
|
||||
* @param alarmIds 需要删除的数据主键集合
|
||||
* 批量删除告警记录
|
||||
*
|
||||
* @param alarmIds 需要删除的数据ID集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteAlarmRecordByAlarmIds(Long[] alarmIds);
|
||||
|
||||
public int deleteAlarmRecordByIds(Long[] alarmIds);
|
||||
|
||||
/**
|
||||
* 处理报警记录
|
||||
* 处理告警记录
|
||||
*
|
||||
* @param alarmId 报警ID
|
||||
* @param alarmId 告警ID
|
||||
* @param handleStatus 处理状态
|
||||
* @param handleRemark 处理备注
|
||||
* @param handleBy 处理人
|
||||
@@ -78,10 +73,10 @@ public interface AlarmRecordMapper {
|
||||
@Param("handleBy") String handleBy);
|
||||
|
||||
/**
|
||||
* 根据任务ID统计报警数量
|
||||
* 根据任务ID统计告警数量
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @return 报警数量
|
||||
* @return 告警数量
|
||||
*/
|
||||
public Long countAlarmByTaskId(@Param("taskId") Long taskId);
|
||||
}
|
||||
@@ -8,32 +8,28 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* 巡检任务Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
@Mapper
|
||||
public interface InspectionTaskMapper {
|
||||
|
||||
/**
|
||||
* 查询巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务主键
|
||||
* @return 巡检任务
|
||||
*/
|
||||
public InspectionTask selectInspectionTaskByTaskId(Long taskId);
|
||||
|
||||
/**
|
||||
* 查询巡检任务列表
|
||||
*
|
||||
*
|
||||
* @param inspectionTask 巡检任务
|
||||
* @return 巡检任务集合
|
||||
*/
|
||||
public List<InspectionTask> selectInspectionTaskList(InspectionTask inspectionTask);
|
||||
|
||||
/**
|
||||
* 查询巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @return 巡检任务
|
||||
*/
|
||||
public InspectionTask selectInspectionTaskById(Long taskId);
|
||||
|
||||
/**
|
||||
* 新增巡检任务
|
||||
*
|
||||
*
|
||||
* @param inspectionTask 巡检任务
|
||||
* @return 结果
|
||||
*/
|
||||
@@ -41,7 +37,7 @@ public interface InspectionTaskMapper {
|
||||
|
||||
/**
|
||||
* 修改巡检任务
|
||||
*
|
||||
*
|
||||
* @param inspectionTask 巡检任务
|
||||
* @return 结果
|
||||
*/
|
||||
@@ -49,20 +45,20 @@ public interface InspectionTaskMapper {
|
||||
|
||||
/**
|
||||
* 删除巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务主键
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteInspectionTaskByTaskId(Long taskId);
|
||||
public int deleteInspectionTaskById(Long taskId);
|
||||
|
||||
/**
|
||||
* 批量删除巡检任务
|
||||
*
|
||||
* @param taskIds 需要删除的数据主键集合
|
||||
*
|
||||
* @param taskIds 需要删除的数据ID集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteInspectionTaskByTaskIds(Long[] taskIds);
|
||||
|
||||
public int deleteInspectionTaskByIds(Long[] taskIds);
|
||||
|
||||
/**
|
||||
* 查询启用状态的巡检任务列表
|
||||
*
|
||||
|
||||
@@ -4,12 +4,74 @@ import com.ruoyi.video.domain.VMinioObject;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MinIO对象Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface VMinioObjectMapper {
|
||||
int insertVMinioObject(VMinioObject obj);
|
||||
VMinioObject selectVMinioObjectById(@Param("id") Long id);
|
||||
int deleteVMinioObjectById(@Param("id") Long id);
|
||||
/**
|
||||
* 查询MinIO对象
|
||||
*
|
||||
* @param objectId MinIO对象ID
|
||||
* @return MinIO对象
|
||||
*/
|
||||
public VMinioObject selectVMinioObjectById(Long objectId);
|
||||
|
||||
VMinioObject selectVMinioObjectByObjectName(@Param("objectName") String objectName);
|
||||
int deleteVMinioObjectByObjectName(@Param("objectName") String objectName);
|
||||
/**
|
||||
* 查询MinIO对象列表
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return MinIO对象集合
|
||||
*/
|
||||
public List<VMinioObject> selectVMinioObjectList(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 新增MinIO对象
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return 结果
|
||||
*/
|
||||
public int insertVMinioObject(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 修改MinIO对象
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateVMinioObject(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 删除MinIO对象
|
||||
*
|
||||
* @param objectId MinIO对象ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVMinioObjectById(Long objectId);
|
||||
|
||||
/**
|
||||
* 批量删除MinIO对象
|
||||
*
|
||||
* @param objectIds 需要删除的数据ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVMinioObjectByIds(Long[] objectIds);
|
||||
|
||||
/**
|
||||
* 根据对象名称查询MinIO对象
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @return MinIO对象
|
||||
*/
|
||||
public VMinioObject selectVMinioObjectByObjectName(String objectName);
|
||||
|
||||
/**
|
||||
* 根据对象名称删除MinIO对象
|
||||
*
|
||||
* @param objectName 对象名称
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVMinioObjectByObjectName(String objectName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import com.ruoyi.video.domain.InspectionTaskRecord;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 巡检任务记录Service接口
|
||||
*/
|
||||
public interface IInspectionTaskRecordService {
|
||||
/**
|
||||
* 查询巡检任务记录
|
||||
*/
|
||||
InspectionTaskRecord selectInspectionTaskRecordByRecordId(Long recordId);
|
||||
|
||||
/**
|
||||
* 查询巡检任务记录列表
|
||||
*/
|
||||
List<InspectionTaskRecord> selectInspectionTaskRecordList(InspectionTaskRecord record);
|
||||
|
||||
/**
|
||||
* 新增巡检任务记录
|
||||
*/
|
||||
int insertInspectionTaskRecord(InspectionTaskRecord record);
|
||||
|
||||
/**
|
||||
* 修改巡检任务记录
|
||||
*/
|
||||
int updateInspectionTaskRecord(InspectionTaskRecord record);
|
||||
|
||||
/**
|
||||
* 删除巡检任务记录
|
||||
*/
|
||||
int deleteInspectionTaskRecordByRecordId(Long recordId);
|
||||
|
||||
/**
|
||||
* 批量删除巡检任务记录
|
||||
*/
|
||||
int deleteInspectionTaskRecordByRecordIds(Long[] recordIds);
|
||||
}
|
||||
@@ -1,13 +1,97 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
|
||||
import com.ruoyi.video.domain.VMinioObject;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MinIO对象服务接口
|
||||
*/
|
||||
public interface IVMinioObjectService {
|
||||
int insert(VMinioObject obj);
|
||||
VMinioObject selectById(Long id);
|
||||
int deleteById(Long id);
|
||||
/**
|
||||
* 查询MinIO对象
|
||||
*
|
||||
* @param objectId MinIO对象ID
|
||||
* @return MinIO对象
|
||||
*/
|
||||
public VMinioObject selectVMinioObjectById(Long objectId);
|
||||
|
||||
VMinioObject selectByObjectName(String objectName);
|
||||
int deleteByObjectName(String objectName);
|
||||
/**
|
||||
* 查询MinIO对象列表
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return MinIO对象集合
|
||||
*/
|
||||
public List<VMinioObject> selectVMinioObjectList(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 新增MinIO对象
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return 结果
|
||||
*/
|
||||
public Long insertVMinioObject(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 修改MinIO对象
|
||||
*
|
||||
* @param vMinioObject MinIO对象
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateVMinioObject(VMinioObject vMinioObject);
|
||||
|
||||
/**
|
||||
* 删除MinIO对象信息
|
||||
*
|
||||
* @param objectId MinIO对象ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVMinioObjectById(Long objectId);
|
||||
|
||||
/**
|
||||
* 批量删除MinIO对象
|
||||
*
|
||||
* @param objectIds 需要删除的MinIO对象ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteVMinioObjectByIds(Long[] objectIds);
|
||||
|
||||
/**
|
||||
* 根据对象名查询
|
||||
*
|
||||
* @param objectName 对象名
|
||||
* @return MinIO对象
|
||||
*/
|
||||
public VMinioObject selectByObjectName(String objectName);
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*
|
||||
* @param id 对象ID
|
||||
* @return MinIO对象
|
||||
*/
|
||||
public VMinioObject selectById(Long id);
|
||||
|
||||
/**
|
||||
* 根据ID删除
|
||||
*
|
||||
* @param id 对象ID
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteById(Long id);
|
||||
|
||||
/**
|
||||
* 根据对象名删除
|
||||
*
|
||||
* @param objectName 对象名
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteByObjectName(String objectName);
|
||||
|
||||
/**
|
||||
* 插入记录
|
||||
*
|
||||
* @param obj MinIO对象
|
||||
* @return 结果
|
||||
*/
|
||||
public int insert(VMinioObject obj);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import com.ruoyi.video.domain.InspectionTask;
|
||||
import com.ruoyi.video.domain.AlarmRecord;
|
||||
import com.ruoyi.video.domain.Detection;
|
||||
import com.ruoyi.video.domain.InspectionTask;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
import java.util.List;
|
||||
@@ -17,62 +17,100 @@ public interface InspectionTaskService {
|
||||
|
||||
/**
|
||||
* 查询巡检任务列表
|
||||
*
|
||||
* @param inspectionTask 巡检任务查询条件
|
||||
* @return 巡检任务集合
|
||||
*/
|
||||
List<InspectionTask> selectInspectionTaskList(InspectionTask inspectionTask);
|
||||
|
||||
|
||||
/**
|
||||
* 根据ID查询巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @return 巡检任务信息
|
||||
*/
|
||||
InspectionTask selectInspectionTaskById(Long taskId);
|
||||
|
||||
|
||||
/**
|
||||
* 新增巡检任务
|
||||
*
|
||||
* @param inspectionTask 巡检任务信息
|
||||
* @return 结果
|
||||
*/
|
||||
int insertInspectionTask(InspectionTask inspectionTask);
|
||||
|
||||
|
||||
/**
|
||||
* 修改巡检任务
|
||||
*
|
||||
* @param inspectionTask 巡检任务信息
|
||||
* @return 结果
|
||||
*/
|
||||
int updateInspectionTask(InspectionTask inspectionTask);
|
||||
|
||||
|
||||
/**
|
||||
* 删除巡检任务
|
||||
* 批量删除巡检任务
|
||||
*
|
||||
* @param taskIds 需要删除的巡检任务ID
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteInspectionTaskByIds(Long[] taskIds);
|
||||
|
||||
|
||||
/**
|
||||
* 启动巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @return 结果
|
||||
*/
|
||||
boolean startInspectionTask(Long taskId);
|
||||
|
||||
|
||||
/**
|
||||
* 停止巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @return 结果
|
||||
*/
|
||||
boolean stopInspectionTask(Long taskId);
|
||||
|
||||
|
||||
/**
|
||||
* 执行单次巡检任务
|
||||
* 执行巡检任务
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
*/
|
||||
@Async
|
||||
void executeInspectionTask(Long taskId);
|
||||
|
||||
|
||||
/**
|
||||
* 处理检测结果,如果有异常则生成报警
|
||||
* 处理检测结果
|
||||
*
|
||||
* @param taskId 巡检任务ID
|
||||
* @param detections 检测结果
|
||||
* @param imagePath 图像路径
|
||||
*/
|
||||
void handleDetectionResults(Long taskId, List<Detection> detections, String imagePath);
|
||||
|
||||
|
||||
/**
|
||||
* 保存报警记录
|
||||
* 保存告警记录
|
||||
*
|
||||
* @param alarmRecord 告警记录
|
||||
*/
|
||||
void saveAlarmRecord(AlarmRecord alarmRecord);
|
||||
|
||||
|
||||
/**
|
||||
* 查询报警记录列表
|
||||
* 查询告警记录列表
|
||||
*
|
||||
* @param alarmRecord 告警记录查询条件
|
||||
* @return 告警记录集合
|
||||
*/
|
||||
List<AlarmRecord> selectAlarmRecordList(AlarmRecord alarmRecord);
|
||||
|
||||
|
||||
/**
|
||||
* 处理报警记录
|
||||
* 处理告警记录
|
||||
*
|
||||
* @param alarmId 告警ID
|
||||
* @param handleStatus 处理状态
|
||||
* @param handleRemark 处理备注
|
||||
* @param handleBy 处理人
|
||||
* @return 结果
|
||||
*/
|
||||
int handleAlarmRecord(Long alarmId, String handleStatus, String handleRemark, String handleBy);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 媒体服务,支持全局网络超时、读写超时、无人拉流持续时长自动关闭流等配置
|
||||
@@ -25,6 +26,16 @@ public class MediaService {
|
||||
* 缓存流转换线程
|
||||
*/
|
||||
public static ConcurrentHashMap<String, MediaTransfer> cameras = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 客户端类型映射
|
||||
*/
|
||||
public static ConcurrentHashMap<String, ClientType> clients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 客户端连接映射
|
||||
*/
|
||||
public static ConcurrentHashMap<String, Map<String, ChannelHandlerContext>> clientConnections = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* http-flv播放
|
||||
@@ -253,4 +264,34 @@ public class MediaService {
|
||||
}
|
||||
cameras.remove(mediaKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备对应的媒体传输器
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 媒体传输器实例,如果不存在则返回null
|
||||
*/
|
||||
public MediaTransferFlvByJavacv getMediaTransfer(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 遍历所有已注册的相机
|
||||
for (Map.Entry<String, MediaTransfer> entry : cameras.entrySet()) {
|
||||
String mediaKey = entry.getKey();
|
||||
if (mediaKey.startsWith("device_" + deviceId + "_")) {
|
||||
// 找到匹配设备ID的mediaKey
|
||||
MediaTransfer mediaTransfer = entry.getValue();
|
||||
if (mediaTransfer instanceof MediaTransferFlvByJavacv) {
|
||||
MediaTransferFlvByJavacv transfer = (MediaTransferFlvByJavacv) mediaTransfer;
|
||||
if (transfer.getCameraDto() != null &&
|
||||
mediaKey.equals(transfer.getCameraDto().getMediaKey())) {
|
||||
return transfer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.framework.service.MinioService;
|
||||
import com.ruoyi.video.domain.AlarmRecord;
|
||||
import com.ruoyi.video.domain.Detection;
|
||||
import com.ruoyi.video.domain.InspectionTask;
|
||||
import com.ruoyi.video.domain.VMinioObject;
|
||||
import com.ruoyi.video.mapper.AlarmRecordMapper;
|
||||
import com.ruoyi.video.mapper.InspectionTaskMapper;
|
||||
import com.ruoyi.video.thread.detector.HttpYoloDetector;
|
||||
import com.ruoyi.video.utils.Overlay;
|
||||
import com.ruoyi.video.utils.CustomMultipartFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.bytedeco.ffmpeg.avcodec.AVPacket;
|
||||
import org.bytedeco.ffmpeg.global.avcodec;
|
||||
import org.bytedeco.ffmpeg.global.avutil;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||
import org.bytedeco.javacv.Frame;
|
||||
import org.bytedeco.javacv.OpenCVFrameConverter;
|
||||
import org.bytedeco.opencv.opencv_core.Mat;
|
||||
import org.bytedeco.opencv.opencv_core.Point;
|
||||
import org.bytedeco.opencv.opencv_core.Rect;
|
||||
import org.bytedeco.opencv.opencv_core.Scalar;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.bytedeco.opencv.global.opencv_imgproc.*;
|
||||
|
||||
/**
|
||||
* 视频分析服务 - 离线分析视频并处理结果
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VideoAnalysisService {
|
||||
|
||||
@Autowired
|
||||
private MinioService minioService;
|
||||
|
||||
@Autowired
|
||||
private IVMinioObjectService vMinioObjectService;
|
||||
|
||||
@Autowired
|
||||
private InspectionTaskMapper taskMapper;
|
||||
|
||||
@Autowired
|
||||
private AlarmRecordMapper alarmRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private com.ruoyi.video.mapper.InspectionTaskRecordMapper inspectionTaskRecordMapper;
|
||||
|
||||
// 检测器配置 - 使用容器名而不是localhost
|
||||
private static final String PYTHON_API_URL = "http://rtsp-python-service:8000/api/detect/file";
|
||||
private static final String MODEL_NAME = "yolov8_detector";
|
||||
|
||||
/**
|
||||
* 分析视频并更新记录(同步调用)
|
||||
* @param task 巡检任务
|
||||
* @param record 巡检记录
|
||||
* @param videoFile 视频文件
|
||||
*/
|
||||
public void analyzeVideoWithRecord(InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record, File videoFile) {
|
||||
log.info("开始分析视频并更新记录: 任务ID={}, 记录ID={}", task.getTaskId(), record.getRecordId());
|
||||
|
||||
try {
|
||||
// 创建输出视频文件
|
||||
File outputVideoFile = File.createTempFile("analysis_output_", ".mp4");
|
||||
|
||||
// 创建检测器
|
||||
HttpYoloDetector detector = new HttpYoloDetector("yolov8", PYTHON_API_URL, MODEL_NAME, 0x00FF00);
|
||||
|
||||
// 处理视频并记录检测结果
|
||||
String detectionResult = processVideoWithRecord(videoFile, outputVideoFile, detector, task, record);
|
||||
|
||||
// 更新记录的识别结果
|
||||
record.setResult(detectionResult);
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
|
||||
// 清理临时输出文件
|
||||
if (outputVideoFile.exists()) {
|
||||
outputVideoFile.delete();
|
||||
}
|
||||
|
||||
log.info("视频分析完成: 任务ID={}, 记录ID={}, 检测结果={}", task.getTaskId(), record.getRecordId(), detectionResult);
|
||||
} catch (Exception e) {
|
||||
log.error("视频分析失败: 任务ID={}, 记录ID={}, 错误={}", task.getTaskId(), record.getRecordId(), e.getMessage(), e);
|
||||
// 更新记录为部分成功
|
||||
record.setStatus(2);
|
||||
record.setResult("分析失败: " + e.getMessage());
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理视频并记录检测结果
|
||||
* @return 检测结果摘要
|
||||
*/
|
||||
private String processVideoWithRecord(File inputFile, File outputFile, HttpYoloDetector detector,
|
||||
InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record) throws Exception {
|
||||
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
|
||||
grabber.start();
|
||||
|
||||
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile,
|
||||
grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
|
||||
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setFrameRate(grabber.getFrameRate());
|
||||
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||
if (grabber.getAudioChannels() > 0) {
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
||||
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||
recorder.setSampleRate(grabber.getSampleRate());
|
||||
}
|
||||
recorder.start();
|
||||
|
||||
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
|
||||
|
||||
// 用于去重的垃圾检测结果缓存
|
||||
Map<String, Long> detectedGarbageCache = new HashMap<>();
|
||||
Map<String, Integer> detectionCounts = new HashMap<>(); // 统计每种类别的数量
|
||||
|
||||
// 跟踪检测到的垃圾ID
|
||||
final Long[] detectionId = {1L};
|
||||
// 帧计数
|
||||
long frameCount = 0;
|
||||
int totalDetections = 0;
|
||||
|
||||
Frame frame;
|
||||
while ((frame = grabber.grab()) != null) {
|
||||
frameCount++;
|
||||
|
||||
if (frame.image != null) {
|
||||
// 处理视频帧
|
||||
Mat mat = converter.convert(frame);
|
||||
|
||||
if (mat != null && !mat.isNull()) {
|
||||
// 每10帧执行一次检测,减少API调用频率
|
||||
if (frameCount % 10 == 0) {
|
||||
List<Detection> detections = detector.detect(mat);
|
||||
|
||||
if (!CollectionUtils.isEmpty(detections)) {
|
||||
for (Detection detection : detections) {
|
||||
// 检查是否为新的垃圾检测结果
|
||||
String detectionKey = generateDetectionKey(detection);
|
||||
if (!detectedGarbageCache.containsKey(detectionKey)) {
|
||||
// 这是新检测到的垃圾
|
||||
detectedGarbageCache.put(detectionKey, detectionId[0]++);
|
||||
totalDetections++;
|
||||
|
||||
// 统计类别数量
|
||||
String label = detection.getLabel();
|
||||
detectionCounts.put(label, detectionCounts.getOrDefault(label, 0) + 1);
|
||||
|
||||
// 创建告警记录(不重复)
|
||||
createAlarmRecordForRecord(task, record, detection, mat, frameCount);
|
||||
} else {
|
||||
// 更新上次检测时间
|
||||
detectedGarbageCache.put(detectionKey, detectionId[0]++);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超过60秒未检测到的垃圾(假设30fps)
|
||||
Long currentId = detectionId[0];
|
||||
detectedGarbageCache.entrySet().removeIf(entry ->
|
||||
(currentId - entry.getValue()) > grabber.getFrameRate() * 60);
|
||||
}
|
||||
}
|
||||
|
||||
// 转回Frame并写入录像
|
||||
Frame processedFrame = converter.convert(mat);
|
||||
recorder.record(processedFrame);
|
||||
} else {
|
||||
// 原样写入
|
||||
recorder.record(frame);
|
||||
}
|
||||
} else if (frame.samples != null) {
|
||||
// 音频帧原样写入
|
||||
recorder.record(frame);
|
||||
}
|
||||
}
|
||||
|
||||
recorder.stop();
|
||||
recorder.close();
|
||||
grabber.stop();
|
||||
grabber.close();
|
||||
|
||||
// 上传处理后的视频到MinIO
|
||||
uploadProcessedVideoForRecord(outputFile, task, record);
|
||||
|
||||
// 生成检测结果摘要
|
||||
StringBuilder resultSummary = new StringBuilder();
|
||||
resultSummary.append("共检测到 ").append(totalDetections).append(" 个问题");
|
||||
if (!detectionCounts.isEmpty()) {
|
||||
resultSummary.append(",详情:");
|
||||
detectionCounts.forEach((label, count) ->
|
||||
resultSummary.append(label).append("(").append(count).append(") "));
|
||||
}
|
||||
|
||||
return resultSummary.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为记录创建告警
|
||||
*/
|
||||
private void createAlarmRecordForRecord(InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record,
|
||||
Detection detection, Mat frame, long frameCount) throws Exception {
|
||||
// 创建告警图像临时文件
|
||||
File alarmImageFile = File.createTempFile("alarm_", ".jpg");
|
||||
|
||||
// 裁剪检测区域,略微扩大区域
|
||||
Rect rect = detection.getRect();
|
||||
int x = Math.max(0, rect.x() - 10);
|
||||
int y = Math.max(0, rect.y() - 10);
|
||||
int w = Math.min(frame.cols() - x, rect.width() + 20);
|
||||
int h = Math.min(frame.rows() - y, rect.height() + 20);
|
||||
|
||||
// 使用OpenCV保存告警图片
|
||||
Mat roi = new Mat(frame, new Rect(x, y, w, h));
|
||||
org.bytedeco.opencv.global.opencv_imgcodecs.imwrite(alarmImageFile.getAbsolutePath(), roi);
|
||||
|
||||
// 上传告警图片到MinIO
|
||||
String fileName = "alarm_" + System.currentTimeMillis() + ".jpg";
|
||||
String bucketName = "alarm-images";
|
||||
|
||||
CustomMultipartFile multipartFile = new CustomMultipartFile(alarmImageFile, fileName, "image/jpeg");
|
||||
String objectUrl = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
||||
|
||||
VMinioObject minioObject = new VMinioObject();
|
||||
minioObject.setBucketName(bucketName);
|
||||
minioObject.setObjectName(fileName);
|
||||
minioObject.setObjectUrl(objectUrl);
|
||||
minioObject.setCreateBy("system");
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 创建告警记录
|
||||
AlarmRecord alarmRecord = new AlarmRecord();
|
||||
alarmRecord.setDeviceId(task.getDeviceId());
|
||||
alarmRecord.setAlarmType("detection");
|
||||
alarmRecord.setAlarmContent(detection.getLabel() + " - 置信度: " + String.format("%.2f", detection.getConfidence()));
|
||||
alarmRecord.setTaskId(task.getTaskId());
|
||||
alarmRecord.setImageOssId(objectId);
|
||||
alarmRecord.setFramePosition(frameCount);
|
||||
alarmRecord.setConfidence(detection.getConfidence());
|
||||
alarmRecord.setCreateTime(new Date());
|
||||
alarmRecord.setStatus(0); // 未处理
|
||||
|
||||
alarmRecordMapper.insertAlarmRecord(alarmRecord);
|
||||
|
||||
log.info("创建告警记录: 类型={}, 任务ID={}, 记录ID={}, 告警ID={}",
|
||||
detection.getLabel(), task.getTaskId(), record.getRecordId(), alarmRecord.getAlarmId());
|
||||
|
||||
// 删除临时文件
|
||||
if (alarmImageFile.exists()) {
|
||||
alarmImageFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传处理后的视频(针对记录)
|
||||
*/
|
||||
private void uploadProcessedVideoForRecord(File videoFile, InspectionTask task,
|
||||
com.ruoyi.video.domain.InspectionTaskRecord record) throws Exception {
|
||||
String fileName = "processed_" + System.currentTimeMillis() + ".mp4";
|
||||
String bucketName = "inspection-videos";
|
||||
|
||||
CustomMultipartFile multipartFile = new CustomMultipartFile(videoFile, fileName, "video/mp4");
|
||||
String objectUrl = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
||||
|
||||
VMinioObject minioObject = new VMinioObject();
|
||||
minioObject.setBucketName(bucketName);
|
||||
minioObject.setObjectName(fileName);
|
||||
minioObject.setObjectUrl(objectUrl);
|
||||
minioObject.setCreateBy("system");
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 更新任务的处理后视频ID
|
||||
task.setProcessedVideoOssId(objectId);
|
||||
task.setVideoStatus(2); // 2: 已分析
|
||||
taskMapper.updateInspectionTask(task);
|
||||
|
||||
// 更新记录的附件信息,添加处理后的视频URL
|
||||
String currentAccessory = record.getAccessory();
|
||||
record.setAccessory(currentAccessory + ";" + objectUrl);
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
|
||||
log.info("处理后视频已上传: 任务ID={}, 记录ID={}, MinIO对象ID={}",
|
||||
task.getTaskId(), record.getRecordId(), objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步分析视频
|
||||
* @param taskId 巡检任务ID
|
||||
*/
|
||||
@Async
|
||||
public void analyzeVideo(Long taskId) {
|
||||
log.info("开始异步分析视频: 任务ID={}", taskId);
|
||||
|
||||
try {
|
||||
// 1. 获取任务信息
|
||||
InspectionTask task = taskMapper.selectInspectionTaskById(taskId);
|
||||
if (task == null) {
|
||||
log.error("任务不存在: {}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取原始视频信息
|
||||
Long videoOssId = task.getVideoOssId();
|
||||
if (videoOssId == null) {
|
||||
log.error("任务视频不存在: {}", taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
VMinioObject videoObject = vMinioObjectService.selectVMinioObjectById(videoOssId);
|
||||
if (videoObject == null) {
|
||||
log.error("MinIO对象不存在: {}", videoOssId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 下载原始视频到临时文件
|
||||
File videoTempFile = File.createTempFile("analysis_input_", ".mp4");
|
||||
try (InputStream inputStream = minioService.getObject(videoObject.getBucketName(), videoObject.getObjectName());
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(videoTempFile)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
fileOutputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建输出视频文件
|
||||
File outputVideoFile = File.createTempFile("analysis_output_", ".mp4");
|
||||
|
||||
// 5. 创建检测器
|
||||
HttpYoloDetector detector = new HttpYoloDetector("garbage", PYTHON_API_URL, MODEL_NAME, 0x00FF00);
|
||||
|
||||
// 6. 处理视频
|
||||
processVideo(videoTempFile, outputVideoFile, detector, task);
|
||||
|
||||
// 7. 清理临时文件
|
||||
if (videoTempFile.exists()) {
|
||||
videoTempFile.delete();
|
||||
}
|
||||
|
||||
log.info("视频分析完成: 任务ID={}", taskId);
|
||||
} catch (Exception e) {
|
||||
log.error("视频分析失败: 任务ID={}, 错误={}", taskId, e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理视频
|
||||
* @param inputFile 输入视频文件
|
||||
* @param outputFile 输出视频文件
|
||||
* @param detector 检测器
|
||||
* @param task 巡检任务
|
||||
*/
|
||||
private void processVideo(File inputFile, File outputFile, HttpYoloDetector detector, InspectionTask task) throws Exception {
|
||||
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
|
||||
grabber.start();
|
||||
|
||||
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile,
|
||||
grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
|
||||
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setFrameRate(grabber.getFrameRate());
|
||||
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
||||
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||
recorder.setSampleRate(grabber.getSampleRate());
|
||||
recorder.start();
|
||||
|
||||
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
|
||||
|
||||
// 用于去重的垃圾检测结果缓存
|
||||
Map<String, Long> detectedGarbageCache = new HashMap<>();
|
||||
// 跟踪检测到的垃圾ID
|
||||
final Long[] detectionId = {1L}; // 使用数组实现effectively final
|
||||
// 帧计数
|
||||
long frameCount = 0;
|
||||
|
||||
Frame frame;
|
||||
while ((frame = grabber.grab()) != null) {
|
||||
frameCount++;
|
||||
|
||||
if (frame.image != null) {
|
||||
// 处理视频帧
|
||||
Mat mat = converter.convert(frame);
|
||||
|
||||
if (mat != null && !mat.isNull()) {
|
||||
// 每10帧执行一次检测,减少API调用频率
|
||||
if (frameCount % 10 == 0) {
|
||||
List<Detection> detections = detector.detect(mat);
|
||||
|
||||
if (!CollectionUtils.isEmpty(detections)) {
|
||||
// 绘制检测框
|
||||
for (Detection detection : detections) {
|
||||
drawDetection(mat, detection);
|
||||
|
||||
// 检查是否为新的垃圾检测结果
|
||||
String detectionKey = generateDetectionKey(detection);
|
||||
if (!detectedGarbageCache.containsKey(detectionKey)) {
|
||||
// 这是新检测到的垃圾
|
||||
detectedGarbageCache.put(detectionKey, detectionId[0]++);
|
||||
|
||||
// 创建告警记录
|
||||
createAlarmRecord(task, detection, mat, frameCount);
|
||||
} else {
|
||||
// 更新上次检测时间
|
||||
detectedGarbageCache.put(detectionKey, detectionId[0]++);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超过60秒未检测到的垃圾(假设30fps)
|
||||
Long currentId = detectionId[0];
|
||||
detectedGarbageCache.entrySet().removeIf(entry ->
|
||||
(currentId - entry.getValue()) > grabber.getFrameRate() * 60);
|
||||
}
|
||||
}
|
||||
|
||||
// 转回Frame并写入录像
|
||||
Frame processedFrame = converter.convert(mat);
|
||||
recorder.record(processedFrame);
|
||||
} else {
|
||||
// 原样写入
|
||||
recorder.record(frame);
|
||||
}
|
||||
} else if (frame.samples != null) {
|
||||
// 音频帧原样写入
|
||||
recorder.record(frame);
|
||||
}
|
||||
}
|
||||
|
||||
recorder.stop();
|
||||
recorder.close();
|
||||
grabber.stop();
|
||||
grabber.close();
|
||||
|
||||
// 上传处理后的视频
|
||||
uploadProcessedVideo(outputFile, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传处理后的视频到MinIO
|
||||
*/
|
||||
private void uploadProcessedVideo(File videoFile, InspectionTask task) throws Exception {
|
||||
String fileName = "processed_" + System.currentTimeMillis() + ".mp4";
|
||||
String bucketName = "inspection-videos";
|
||||
|
||||
CustomMultipartFile multipartFile = new CustomMultipartFile(videoFile, fileName, "video/mp4");
|
||||
String objectUrl = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
||||
|
||||
VMinioObject minioObject = new VMinioObject();
|
||||
minioObject.setBucketName(bucketName);
|
||||
minioObject.setObjectName(fileName);
|
||||
minioObject.setObjectUrl(objectUrl);
|
||||
minioObject.setCreateBy("system");
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 更新任务的处理后视频ID
|
||||
task.setProcessedVideoOssId(objectId);
|
||||
task.setVideoStatus(2); // 2: 已分析
|
||||
taskMapper.updateInspectionTask(task);
|
||||
|
||||
log.info("处理后视频已上传: 任务ID={}, MinIO对象ID={}", task.getTaskId(), objectId);
|
||||
|
||||
// 删除临时文件
|
||||
if (videoFile.exists()) {
|
||||
videoFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成检测结果的唯一键,用于检测结果去重
|
||||
*/
|
||||
private String generateDetectionKey(Detection detection) {
|
||||
// 使用检测框的位置和大小来生成键
|
||||
// 允许小范围波动,认为是同一个物体
|
||||
Rect rect = detection.getRect();
|
||||
int x = rect.x() / 10 * 10; // 取10的倍数,允许小波动
|
||||
int y = rect.y() / 10 * 10;
|
||||
int w = rect.width() / 10 * 10;
|
||||
int h = rect.height() / 10 * 10;
|
||||
return String.format("%s_%d_%d_%d_%d", detection.getLabel(), x, y, w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建告警记录
|
||||
*/
|
||||
private void createAlarmRecord(InspectionTask task, Detection detection, Mat frame, long frameCount) throws Exception {
|
||||
// 创建告警图像临时文件
|
||||
File alarmImageFile = File.createTempFile("alarm_", ".jpg");
|
||||
|
||||
// 裁剪检测区域,略微扩大区域
|
||||
Rect rect = detection.getRect();
|
||||
int x = Math.max(0, rect.x() - 10);
|
||||
int y = Math.max(0, rect.y() - 10);
|
||||
int w = Math.min(frame.cols() - x, rect.width() + 20);
|
||||
int h = Math.min(frame.rows() - y, rect.height() + 20);
|
||||
|
||||
// 使用OpenCV保存告警图片
|
||||
Mat roi = new Mat(frame, new Rect(x, y, w, h));
|
||||
org.bytedeco.opencv.global.opencv_imgcodecs.imwrite(alarmImageFile.getAbsolutePath(), roi);
|
||||
|
||||
// 上传告警图片到MinIO
|
||||
String fileName = "alarm_" + System.currentTimeMillis() + ".jpg";
|
||||
String bucketName = "alarm-images";
|
||||
|
||||
CustomMultipartFile multipartFile = new CustomMultipartFile(alarmImageFile, fileName, "image/jpeg");
|
||||
String objectUrl = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
||||
|
||||
VMinioObject minioObject = new VMinioObject();
|
||||
minioObject.setBucketName(bucketName);
|
||||
minioObject.setObjectName(fileName);
|
||||
minioObject.setObjectUrl(objectUrl);
|
||||
minioObject.setCreateBy("system");
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 创建告警记录
|
||||
AlarmRecord alarmRecord = new AlarmRecord();
|
||||
alarmRecord.setDeviceId(task.getDeviceId());
|
||||
alarmRecord.setAlarmType("garbage");
|
||||
alarmRecord.setAlarmContent(detection.getLabel() + " - 置信度: " + String.format("%.2f", detection.getConfidence()));
|
||||
alarmRecord.setTaskId(task.getTaskId());
|
||||
alarmRecord.setImageOssId(objectId);
|
||||
alarmRecord.setFramePosition(frameCount);
|
||||
alarmRecord.setConfidence(detection.getConfidence());
|
||||
alarmRecord.setCreateTime(new Date());
|
||||
|
||||
alarmRecordMapper.insertAlarmRecord(alarmRecord);
|
||||
|
||||
log.info("创建告警记录: 类型={}, 任务ID={}, 告警ID={}", detection.getLabel(), task.getTaskId(), alarmRecord.getAlarmId());
|
||||
|
||||
// 删除临时文件
|
||||
if (alarmImageFile.exists()) {
|
||||
alarmImageFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图像上绘制检测框
|
||||
*/
|
||||
private void drawDetection(Mat frame, Detection detection) {
|
||||
// 绘制边界框
|
||||
Rect rect = detection.getRect();
|
||||
Scalar color = new Scalar(detection.getColorBGR());
|
||||
int thickness = 2;
|
||||
|
||||
rectangle(frame, rect, color, thickness, LINE_8, 0);
|
||||
|
||||
// 绘制标签背景
|
||||
String label = String.format("%s: %.2f", detection.getLabel(), detection.getConfidence());
|
||||
Point textPosition = new Point(rect.x(), rect.y() - 10);
|
||||
|
||||
// 获取文本大小 - 添加缺失的IntBuffer参数
|
||||
IntBuffer baseline = IntBuffer.allocate(1);
|
||||
org.bytedeco.opencv.opencv_core.Size textSize = getTextSize(
|
||||
label, FONT_HERSHEY_SIMPLEX, 0.5, thickness, baseline);
|
||||
|
||||
// 绘制标签背景矩形
|
||||
rectangle(frame,
|
||||
new Rect(rect.x(), rect.y() - textSize.height() - 10,
|
||||
textSize.width(), textSize.height() + 10),
|
||||
color, FILLED, LINE_8, 0);
|
||||
|
||||
// 绘制文本
|
||||
putText(frame, label, textPosition,
|
||||
FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(255, 255, 255, 0),
|
||||
1, LINE_AA, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.video.service.impl;
|
||||
|
||||
import com.ruoyi.video.domain.InspectionTaskRecord;
|
||||
import com.ruoyi.video.mapper.InspectionTaskRecordMapper;
|
||||
import com.ruoyi.video.service.IInspectionTaskRecordService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 巡检任务记录Service业务层处理
|
||||
*/
|
||||
@Service
|
||||
public class IInspectionTaskRecordServiceImpl implements IInspectionTaskRecordService {
|
||||
|
||||
@Autowired
|
||||
private InspectionTaskRecordMapper inspectionTaskRecordMapper;
|
||||
|
||||
@Override
|
||||
public InspectionTaskRecord selectInspectionTaskRecordByRecordId(Long recordId) {
|
||||
return inspectionTaskRecordMapper.selectInspectionTaskRecordByRecordId(recordId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InspectionTaskRecord> selectInspectionTaskRecordList(InspectionTaskRecord record) {
|
||||
return inspectionTaskRecordMapper.selectInspectionTaskRecordList(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertInspectionTaskRecord(InspectionTaskRecord record) {
|
||||
record.setCreateTime(new Date());
|
||||
return inspectionTaskRecordMapper.insertInspectionTaskRecord(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateInspectionTaskRecord(InspectionTaskRecord record) {
|
||||
record.setUpdateTime(new Date());
|
||||
return inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteInspectionTaskRecordByRecordId(Long recordId) {
|
||||
return inspectionTaskRecordMapper.deleteInspectionTaskRecordByRecordId(recordId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteInspectionTaskRecordByRecordIds(Long[] recordIds) {
|
||||
return inspectionTaskRecordMapper.deleteInspectionTaskRecordByRecordIds(recordIds);
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 巡检任务服务实现
|
||||
*
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -55,6 +52,15 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
@Autowired
|
||||
private AlarmRecordMapper alarmRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private com.ruoyi.video.mapper.InspectionTaskRecordMapper inspectionTaskRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private MinioService minioService;
|
||||
|
||||
@Autowired
|
||||
private IVMinioObjectService vMinioObjectService;
|
||||
|
||||
// 运行状态缓存(避免重复执行)
|
||||
private final Map<Long, Boolean> runningTasks = new ConcurrentHashMap<>();
|
||||
@@ -70,21 +76,22 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
@Override
|
||||
public InspectionTask selectInspectionTaskById(Long taskId) {
|
||||
return inspectionTaskMapper.selectInspectionTaskByTaskId(taskId);
|
||||
return inspectionTaskMapper.selectInspectionTaskById(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertInspectionTask(InspectionTask inspectionTask) {
|
||||
inspectionTask.setCreateTime(DateUtils.getNowDate());
|
||||
inspectionTask.setCreateBy(SecurityUtils.getUsername());
|
||||
inspectionTask.setExecuteCount(0L);
|
||||
inspectionTask.setAlarmCount(0L);
|
||||
// 这些字段在新版实体类中可能不存在,需要进行调整
|
||||
// inspectionTask.setExecuteCount(0L);
|
||||
// inspectionTask.setAlarmCount(0L);
|
||||
|
||||
// 获取设备信息
|
||||
Device device = deviceService.selectDeviceByDeviceId(inspectionTask.getDeviceId());
|
||||
if (device != null) {
|
||||
inspectionTask.setDeviceName(device.getIp());
|
||||
}
|
||||
// 新版实体类可能不需要设备名称
|
||||
// if (device != null) {
|
||||
// inspectionTask.setDeviceName(device.getIp());
|
||||
// }
|
||||
|
||||
return inspectionTaskMapper.insertInspectionTask(inspectionTask);
|
||||
}
|
||||
@@ -92,7 +99,6 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
@Override
|
||||
public int updateInspectionTask(InspectionTask inspectionTask) {
|
||||
inspectionTask.setUpdateTime(DateUtils.getNowDate());
|
||||
inspectionTask.setUpdateBy(SecurityUtils.getUsername());
|
||||
return inspectionTaskMapper.updateInspectionTask(inspectionTask);
|
||||
}
|
||||
|
||||
@@ -101,88 +107,232 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
for (Long taskId : taskIds) {
|
||||
stopInspectionTask(taskId);
|
||||
}
|
||||
return inspectionTaskMapper.deleteInspectionTaskByTaskIds(taskIds);
|
||||
return inspectionTaskMapper.deleteInspectionTaskByIds(taskIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startInspectionTask(Long taskId) {
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskByTaskId(taskId);
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskById(taskId);
|
||||
if (task == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
task.setStatus("0"); // 启用
|
||||
// 启用任务,使用新版实体类的方法
|
||||
task.setStatus(0); // 0表示启用
|
||||
task.setUpdateTime(DateUtils.getNowDate());
|
||||
task.setUpdateBy(SecurityUtils.getUsername());
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
|
||||
runningTasks.put(taskId, true);
|
||||
|
||||
// 这里应该集成到Quartz定时任务中
|
||||
log.info("启动巡检任务: {} - {}", taskId, task.getTaskName());
|
||||
log.info("启动巡检任务: {}", taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopInspectionTask(Long taskId) {
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskByTaskId(taskId);
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskById(taskId);
|
||||
if (task == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
task.setStatus("1"); // 停用
|
||||
// 停用任务,使用新版实体类的方法
|
||||
task.setStatus(1); // 1表示停用
|
||||
task.setUpdateTime(DateUtils.getNowDate());
|
||||
task.setUpdateBy(SecurityUtils.getUsername());
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
|
||||
runningTasks.remove(taskId);
|
||||
|
||||
log.info("停止巡检任务: {} - {}", taskId, task.getTaskName());
|
||||
log.info("停止巡检任务: {}", taskId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void executeInspectionTask(Long taskId) {
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskByTaskId(taskId);
|
||||
if (task == null || !"0".equals(task.getStatus())) {
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskById(taskId);
|
||||
if (task == null || task.getStatus() != 0) { // 0表示启用状态
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始执行巡检任务: {} - {}", taskId, task.getTaskName());
|
||||
log.info("开始执行巡检任务: {}", taskId);
|
||||
|
||||
// 创建巡检记录
|
||||
InspectionTaskRecord record = new InspectionTaskRecord();
|
||||
record.setTaskId(taskId);
|
||||
record.setExecuteTime(new Date());
|
||||
record.setStatus(1); // 执行中
|
||||
inspectionTaskRecordMapper.insertInspectionTaskRecord(record);
|
||||
Long recordId = record.getRecordId();
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 更新执行信息
|
||||
task.setLastExecuteTime(new Date());
|
||||
task.setExecuteCount(task.getExecuteCount() + 1);
|
||||
// 更新任务状态为执行中
|
||||
task.setStatus(1);
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
|
||||
// 获取设备信息
|
||||
Device device = deviceService.selectDeviceByDeviceId(task.getDeviceId());
|
||||
if (device == null) {
|
||||
log.error("设备不存在: {}", task.getDeviceId());
|
||||
updateRecordFailed(record, "设备不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行视频分析
|
||||
performVideoAnalysis(task, device);
|
||||
// 执行视频录制和分析
|
||||
performVideoAnalysisWithRecord(task, device, record);
|
||||
|
||||
// 更新任务执行统计信息
|
||||
Long alarmCount = alarmRecordMapper.countAlarmByTaskId(taskId);
|
||||
inspectionTaskMapper.updateTaskExecuteInfo(taskId, task.getExecuteCount(), alarmCount);
|
||||
// 更新执行时长
|
||||
long duration = (System.currentTimeMillis() - startTime) / 1000;
|
||||
record.setDuration((int) duration);
|
||||
record.setStatus(0); // 成功
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
|
||||
// 更新任务状态为已完成
|
||||
task.setStatus(2);
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("执行巡检任务失败: {} - {}", taskId, e.getMessage(), e);
|
||||
log.error("巡检任务执行失败: taskId={}", taskId, e);
|
||||
updateRecordFailed(record, e.getMessage());
|
||||
|
||||
// 更新任务状态为已完成(虽然失败)
|
||||
task.setStatus(2);
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录为失败状态
|
||||
*/
|
||||
private void updateRecordFailed(InspectionTaskRecord record, String errorMsg) {
|
||||
record.setStatus(1); // 失败
|
||||
record.setResult("执行失败: " + errorMsg);
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行视频录制和分析(带记录)
|
||||
*/
|
||||
private void performVideoAnalysisWithRecord(InspectionTask task, Device device, InspectionTaskRecord record) throws Exception {
|
||||
FFmpegFrameGrabber grabber = null;
|
||||
FFmpegFrameRecorder recorder = null;
|
||||
File videoTempFile = null;
|
||||
|
||||
try {
|
||||
// 1. 创建临时视频文件
|
||||
videoTempFile = File.createTempFile("inspection_", ".mp4");
|
||||
|
||||
// 2. 创建视频抓取器
|
||||
grabber = new FFmpegFrameGrabber(device.getUrl());
|
||||
grabber.setOption("rtsp_transport", "tcp");
|
||||
grabber.setOption("stimeout", "5000000");
|
||||
grabber.start();
|
||||
|
||||
// 3. 创建视频录制器
|
||||
recorder = new FFmpegFrameRecorder(videoTempFile,
|
||||
grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoCodec(org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setFrameRate(grabber.getFrameRate());
|
||||
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||
if (grabber.getAudioChannels() > 0) {
|
||||
recorder.setAudioCodec(org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_AAC);
|
||||
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||
recorder.setSampleRate(grabber.getSampleRate());
|
||||
}
|
||||
recorder.start();
|
||||
|
||||
log.info("开始录制视频流: {}, 时长: {}秒", device.getUrl(), task.getDuration());
|
||||
|
||||
// 4. 录制指定时长的视频
|
||||
long startTime = System.currentTimeMillis();
|
||||
long duration = task.getDuration() * 1000L; // 转换为毫秒
|
||||
|
||||
while (System.currentTimeMillis() - startTime < duration) {
|
||||
Frame frame = grabber.grab();
|
||||
if (frame != null) {
|
||||
recorder.record(frame);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 停止录制
|
||||
recorder.stop();
|
||||
recorder.close();
|
||||
grabber.stop();
|
||||
grabber.close();
|
||||
|
||||
log.info("视频录制完成: {}", videoTempFile.getAbsolutePath());
|
||||
|
||||
// 6. 上传视频到MinIO
|
||||
String fileName = "inspection_" + task.getTaskId() + "_" + System.currentTimeMillis() + ".mp4";
|
||||
String bucketName = "inspection-videos";
|
||||
|
||||
CustomMultipartFile multipartFile = new CustomMultipartFile(videoTempFile, fileName, "video/mp4");
|
||||
String objectUrl = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
||||
|
||||
VMinioObject minioObject = new VMinioObject();
|
||||
minioObject.setBucketName(bucketName);
|
||||
minioObject.setObjectName(fileName);
|
||||
minioObject.setObjectUrl(objectUrl);
|
||||
minioObject.setCreateBy("system");
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 7. 更新任务的视频ID和状态
|
||||
task.setVideoOssId(objectId);
|
||||
task.setVideoStatus(1); // 已录制未分析
|
||||
inspectionTaskMapper.updateInspectionTask(task);
|
||||
|
||||
// 8. 更新记录的附件信息(视频URL)
|
||||
record.setAccessory(objectUrl);
|
||||
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
||||
|
||||
log.info("视频已上传到MinIO: objectId={}, url={}", objectId, objectUrl);
|
||||
|
||||
// 9. 调用Python服务进行视频分析
|
||||
analyzeVideoAndUpdateRecord(task, record, videoTempFile);
|
||||
|
||||
} finally {
|
||||
// 清理资源
|
||||
if (recorder != null) {
|
||||
try { recorder.close(); } catch (Exception ignore) {}
|
||||
}
|
||||
if (grabber != null) {
|
||||
try { grabber.close(); } catch (Exception ignore) {}
|
||||
}
|
||||
if (videoTempFile != null && videoTempFile.exists()) {
|
||||
videoTempFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析视频并更新记录
|
||||
*/
|
||||
private void analyzeVideoAndUpdateRecord(InspectionTask task, InspectionTaskRecord record, File videoFile) throws Exception {
|
||||
log.info("开始分析视频: taskId={}, recordId={}", task.getTaskId(), record.getRecordId());
|
||||
|
||||
// 调用VideoAnalysisService进行分析
|
||||
com.ruoyi.video.service.VideoAnalysisService videoAnalysisService =
|
||||
SpringUtils.getBean(com.ruoyi.video.service.VideoAnalysisService.class);
|
||||
|
||||
// 异步调用视频分析(会处理检测结果并创建告警)
|
||||
videoAnalysisService.analyzeVideoWithRecord(task, record, videoFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行视频分析
|
||||
*/
|
||||
private void performVideoAnalysis(InspectionTask task, Device device) {
|
||||
if (!"0".equals(task.getEnableDetection())) {
|
||||
log.info("巡检任务未启用检测: {}", task.getTaskId());
|
||||
return;
|
||||
}
|
||||
// 这些条件在新版实体类中可能需要调整
|
||||
// 0表示启用检测
|
||||
// if (task.getEnableDetection() != 0) {
|
||||
// log.info("巡检任务未启用检测: {}", task.getTaskId());
|
||||
// return;
|
||||
// }
|
||||
|
||||
FFmpegFrameGrabber grabber = null;
|
||||
FFmpegFrameRecorder sessionRecorder = null;
|
||||
@@ -214,7 +364,9 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
// 会话聚合参数与状态
|
||||
final long minGapMs = 3000L; // 目标消失超过该值视为结束
|
||||
final long maxDurationMs = 30000L; // 单次会话最长30s
|
||||
final float detectionThreshold = task.getThreshold() != null ? task.getThreshold().floatValue() : 0.7f;
|
||||
// 这个字段在新版实体类中可能需要调整
|
||||
// final float detectionThreshold = task.getThreshold() != null ? task.getThreshold().floatValue() : 0.7f;
|
||||
final float detectionThreshold = 0.7f; // 使用默认阈值
|
||||
|
||||
boolean sessionActive = false;
|
||||
long sessionStartMs = 0L;
|
||||
@@ -263,9 +415,9 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
// 最高置信度
|
||||
Detection best = detections.stream()
|
||||
.max(Comparator.comparingDouble(Detection::conf))
|
||||
.max(Comparator.comparingDouble(Detection::getConfidence))
|
||||
.orElse(null);
|
||||
boolean hasTarget = best != null && best.conf() >= detectionThreshold;
|
||||
boolean hasTarget = best != null && best.getConfidence() >= detectionThreshold;
|
||||
|
||||
if (hasTarget) {
|
||||
lastSeenMs = now;
|
||||
@@ -296,23 +448,24 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
VMinioObject rec = new VMinioObject();
|
||||
rec.setObjectName(up.getObjectName());
|
||||
rec.setUrl(up.getUrl());
|
||||
rec.setObjectUrl(up.getUrl());
|
||||
rec.setOriginalName(originalName);
|
||||
objSvc.insert(rec);
|
||||
objSvc.insertVMinioObject(rec);
|
||||
|
||||
AlarmRecord alarm = new AlarmRecord();
|
||||
alarm.setTaskId(task.getTaskId());
|
||||
alarm.setTaskName(task.getTaskName());
|
||||
// 这些字段在新版实体类中可能不存在,需要调整
|
||||
// alarm.setTaskName(task.getTaskName());
|
||||
alarm.setDeviceId(task.getDeviceId());
|
||||
alarm.setDeviceName(task.getDeviceName());
|
||||
alarm.setAlarmType(best.cls());
|
||||
alarm.setAlarmLevel(getAlarmLevel(best.conf()));
|
||||
alarm.setAlarmDesc(String.format("检测到%s,置信度: %.2f", best.cls(), best.conf()));
|
||||
alarm.setConfidence((double) best.conf());
|
||||
alarm.setImagePath(up.getUrl());
|
||||
alarm.setAlarmTime(new Date(now));
|
||||
alarm.setHandleStatus("0");
|
||||
alarm.setCreateBy(SecurityUtils.getUsername());
|
||||
// alarm.setDeviceName(task.getDeviceName());
|
||||
alarm.setAlarmType(best.getLabel());
|
||||
// 这里需要转换double为float
|
||||
alarm.setAlarmLevel(getAlarmLevel((float)best.getConfidence()));
|
||||
alarm.setAlarmContent(String.format("检测到%s,置信度: %.2f", best.getLabel(), best.getConfidence()));
|
||||
alarm.setConfidence(best.getConfidence());
|
||||
alarm.setImageOssId(rec.getObjectId());
|
||||
alarm.setCreateTime(new Date(now));
|
||||
alarm.setStatus(0); // 0: 未处理
|
||||
saveAlarmRecord(alarm);
|
||||
currentAlarmId = alarm.getAlarmId();
|
||||
|
||||
@@ -387,13 +540,14 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
VMinioObject vrec = new VMinioObject();
|
||||
vrec.setObjectName(upv.getObjectName());
|
||||
vrec.setUrl(upv.getUrl());
|
||||
vrec.setObjectUrl(upv.getUrl());
|
||||
vrec.setOriginalName(originalName);
|
||||
objSvc.insert(vrec);
|
||||
objSvc.insertVMinioObject(vrec);
|
||||
|
||||
AlarmRecord patch = new AlarmRecord();
|
||||
patch.setAlarmId(currentAlarmId);
|
||||
patch.setVideoPath(upv.getUrl());
|
||||
// videoPath字段在新版实体类中可能不存在,需要调整
|
||||
// patch.setVideoPath(upv.getUrl());
|
||||
alarmRecordMapper.updateAlarmRecord(patch);
|
||||
} catch (Exception ue) {
|
||||
log.warn("巡检会话-上传/回填视频失败: {}", ue.getMessage());
|
||||
@@ -468,7 +622,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
private List<Detection> performDetection(Mat mat) {
|
||||
try {
|
||||
if (modelManager != null) {
|
||||
YoloDetector detector = modelManager.get("garbage"); // 应该使用垃圾检测 当开启自动巡检的时候 需要替换模型
|
||||
YoloDetector detector = modelManager.get("garbage"); // 使用垃圾检测模型
|
||||
if (detector != null) {
|
||||
return detector.detect(mat);
|
||||
}
|
||||
@@ -508,40 +662,41 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
|
||||
@Override
|
||||
public void handleDetectionResults(Long taskId, List<Detection> detections, String imagePath) {
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskByTaskId(taskId);
|
||||
InspectionTask task = inspectionTaskMapper.selectInspectionTaskById(taskId);
|
||||
if (task == null || detections.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Detection detection : detections) {
|
||||
// 检查置信度是否超过阈值
|
||||
if (detection.conf() >= task.getThreshold()) {
|
||||
// 创建报警记录
|
||||
// 检查置信度是否超过阈值 - 这里使用了默认阈值,因为实体类可能已变更
|
||||
if (detection.getConfidence() >= 0.7) {
|
||||
// 创建告警记录
|
||||
AlarmRecord alarmRecord = new AlarmRecord();
|
||||
alarmRecord.setTaskId(taskId);
|
||||
alarmRecord.setTaskName(task.getTaskName());
|
||||
// 这些字段在新版实体类中可能不存在,需要调整
|
||||
// alarmRecord.setTaskName(task.getTaskName());
|
||||
alarmRecord.setDeviceId(task.getDeviceId());
|
||||
alarmRecord.setDeviceName(task.getDeviceName());
|
||||
alarmRecord.setAlarmType(detection.cls());
|
||||
alarmRecord.setAlarmLevel(getAlarmLevel(detection.conf()));
|
||||
alarmRecord.setAlarmDesc(String.format("检测到%s,置信度: %.2f",
|
||||
detection.cls(), detection.conf()));
|
||||
alarmRecord.setConfidence((double) detection.conf());
|
||||
alarmRecord.setImagePath(imagePath);
|
||||
alarmRecord.setAlarmTime(new Date());
|
||||
alarmRecord.setHandleStatus("0"); // 未处理
|
||||
alarmRecord.setCreateBy(SecurityUtils.getUsername());
|
||||
|
||||
// alarmRecord.setDeviceName(task.getDeviceName());
|
||||
alarmRecord.setAlarmType(detection.getLabel());
|
||||
alarmRecord.setAlarmLevel(getAlarmLevel((float)detection.getConfidence()));
|
||||
alarmRecord.setAlarmContent(String.format("检测到%s,置信度: %.2f",
|
||||
detection.getLabel(), detection.getConfidence()));
|
||||
alarmRecord.setConfidence(detection.getConfidence());
|
||||
// 设置图片路径 - 需要调整为适合新实体类的方式
|
||||
// alarmRecord.setImagePath(imagePath);
|
||||
alarmRecord.setCreateTime(new Date());
|
||||
alarmRecord.setStatus(0); // 0: 未处理
|
||||
|
||||
saveAlarmRecord(alarmRecord);
|
||||
|
||||
log.warn("生成报警记录: 任务[{}] 检测到[{}] 置信度[{}]",
|
||||
taskId, detection.cls(), detection.conf());
|
||||
log.warn("生成告警记录: 任务[{}] 检测到[{}] 置信度[{}]",
|
||||
taskId, detection.getLabel(), detection.getConfidence());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据置信度确定报警级别
|
||||
* 根据置信度确定告警级别
|
||||
*/
|
||||
private String getAlarmLevel(float confidence) {
|
||||
if (confidence >= 0.9f) {
|
||||
@@ -557,7 +712,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
||||
public void saveAlarmRecord(AlarmRecord alarmRecord) {
|
||||
alarmRecord.setCreateTime(DateUtils.getNowDate());
|
||||
alarmRecordMapper.insertAlarmRecord(alarmRecord);
|
||||
log.info("保存报警记录: {}", alarmRecord.getAlarmId());
|
||||
log.info("保存告警记录: {}", alarmRecord.getAlarmId());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,12 +6,64 @@ import com.ruoyi.video.service.IVMinioObjectService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class VMinioObjectServiceImpl implements IVMinioObjectService {
|
||||
|
||||
@Autowired
|
||||
private VMinioObjectMapper mapper;
|
||||
|
||||
@Override
|
||||
public VMinioObject selectVMinioObjectById(Long objectId) {
|
||||
return mapper.selectVMinioObjectById(objectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VMinioObject> selectVMinioObjectList(VMinioObject vMinioObject) {
|
||||
return mapper.selectVMinioObjectList(vMinioObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long insertVMinioObject(VMinioObject vMinioObject) {
|
||||
int rows = mapper.insertVMinioObject(vMinioObject);
|
||||
return rows > 0 ? vMinioObject.getObjectId() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateVMinioObject(VMinioObject vMinioObject) {
|
||||
return mapper.updateVMinioObject(vMinioObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteVMinioObjectById(Long objectId) {
|
||||
return mapper.deleteVMinioObjectById(objectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteVMinioObjectByIds(Long[] objectIds) {
|
||||
return mapper.deleteVMinioObjectByIds(objectIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VMinioObject selectByObjectName(String objectName) {
|
||||
return mapper.selectVMinioObjectByObjectName(objectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VMinioObject selectById(Long id) {
|
||||
return mapper.selectVMinioObjectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteById(Long id) {
|
||||
return mapper.deleteVMinioObjectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByObjectName(String objectName) {
|
||||
return mapper.deleteVMinioObjectByObjectName(objectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insert(VMinioObject obj) {
|
||||
@@ -20,24 +72,4 @@ public class VMinioObjectServiceImpl implements IVMinioObjectService {
|
||||
}
|
||||
return mapper.insertVMinioObject(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VMinioObject selectById(Long id) {
|
||||
return mapper.selectVMinioObjectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteById(Long id) {
|
||||
return mapper.deleteVMinioObjectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VMinioObject selectByObjectName(String objectName) {
|
||||
return mapper.selectVMinioObjectByObjectName(objectName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteByObjectName(String objectName) {
|
||||
return mapper.deleteVMinioObjectByObjectName(objectName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +540,7 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
try {
|
||||
if (ctx.channel().isWritable()) {
|
||||
// 发送帧前先发送header
|
||||
if (ClientType.HTTP.getType() == ctype.getType()) {
|
||||
if (ClientType.HTTP == ctype) {
|
||||
ChannelFuture future = ctx.writeAndFlush(Unpooled.copiedBuffer(header));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@Override
|
||||
@@ -550,7 +550,7 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (ClientType.WEBSOCKET.getType() == ctype.getType()) {
|
||||
} else if (ClientType.WEBSOCKET == ctype) {
|
||||
ChannelFuture future = ctx
|
||||
.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(header)));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
package com.ruoyi.video.thread.detector;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.video.domain.Detection;
|
||||
import org.bytedeco.opencv.opencv_core.*;
|
||||
import org.bytedeco.javacpp.BytePointer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import static org.bytedeco.opencv.global.opencv_imgcodecs.imencode;
|
||||
|
||||
/**
|
||||
* 通过HTTP调用YOLOv8 Python服务的检测器
|
||||
* 替换原有的本地模型加载方式
|
||||
*/
|
||||
public class HttpYoloDetector implements YoloDetector {
|
||||
private static final Logger log = LoggerFactory.getLogger(HttpYoloDetector.class);
|
||||
|
||||
private final String name;
|
||||
private final String apiUrl;
|
||||
private final String modelName;
|
||||
private final int colorBGR;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 创建HTTP检测器
|
||||
* @param name 检测器名称
|
||||
* @param apiUrl Python服务的API URL (例如: "http://localhost:8000/api/detect/file")
|
||||
* @param modelName 要使用的模型名称
|
||||
* @param colorBGR 边界框颜色 (BGR格式)
|
||||
*/
|
||||
public HttpYoloDetector(String name, String apiUrl, String modelName, int colorBGR) {
|
||||
this.name = name;
|
||||
this.apiUrl = apiUrl;
|
||||
this.modelName = modelName;
|
||||
this.colorBGR = colorBGR;
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
|
||||
log.info("创建HTTP YOLOv8检测器: {}, 服务地址: {}, 模型: {}", name, apiUrl, modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Detection> detect(Mat bgr) {
|
||||
if (bgr == null || bgr.empty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try {
|
||||
// 将OpenCV的Mat转换为JPEG字节数组
|
||||
BytePointer buffer = new BytePointer();
|
||||
imencode(".jpg", bgr, buffer);
|
||||
byte[] jpgBytes = new byte[(int)(buffer.capacity())];
|
||||
buffer.get(jpgBytes);
|
||||
buffer.deallocate();
|
||||
|
||||
// 准备HTTP请求参数
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("model_name", modelName);
|
||||
body.add("file", new CustomByteArrayResource(jpgBytes, "image.jpg"));
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
|
||||
// 发送请求到Python服务
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, requestEntity, String.class);
|
||||
String responseBody = response.getBody();
|
||||
|
||||
if (responseBody != null) {
|
||||
// 解析响应JSON
|
||||
Map<String, Object> result = objectMapper.readValue(responseBody, Map.class);
|
||||
List<Map<String, Object>> detectionsJson = (List<Map<String, Object>>) result.get("detections");
|
||||
|
||||
List<Detection> detections = new ArrayList<>();
|
||||
for (Map<String, Object> det : detectionsJson) {
|
||||
String label = (String) det.get("label");
|
||||
double confidence = ((Number) det.get("confidence")).doubleValue();
|
||||
int x = ((Number) det.get("x")).intValue();
|
||||
int y = ((Number) det.get("y")).intValue();
|
||||
int width = ((Number) det.get("width")).intValue();
|
||||
int height = ((Number) det.get("height")).intValue();
|
||||
|
||||
detections.add(new Detection(label, confidence, new Rect(x, y, width, height), colorBGR));
|
||||
}
|
||||
|
||||
return detections;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("HTTP检测请求失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 用于RestTemplate的字节数组资源类
|
||||
private static class CustomByteArrayResource implements org.springframework.core.io.Resource {
|
||||
private final byte[] byteArray;
|
||||
private final String filename;
|
||||
|
||||
public CustomByteArrayResource(byte[] byteArray, String filename) {
|
||||
this.byteArray = byteArray;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.InputStream getInputStream() throws IOException {
|
||||
return new java.io.ByteArrayInputStream(this.byteArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.URL getURL() throws IOException {
|
||||
throw new IOException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.net.URI getURI() throws IOException {
|
||||
throw new IOException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.io.File getFile() throws IOException {
|
||||
throw new IOException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return this.byteArray.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.springframework.core.io.Resource createRelative(String relativePath) throws IOException {
|
||||
throw new IOException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Byte array resource [" + this.filename + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
package com.ruoyi.video.thread.detector;
|
||||
|
||||
import com.ruoyi.video.domain.Detection;
|
||||
import org.bytedeco.javacpp.indexer.FloatRawIndexer;
|
||||
import org.bytedeco.opencv.opencv_core.*;
|
||||
import org.bytedeco.opencv.opencv_dnn.Net;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.bytedeco.opencv.global.opencv_dnn.*;
|
||||
import static org.bytedeco.opencv.global.opencv_core.*;
|
||||
import static org.bytedeco.opencv.global.opencv_imgproc.*;
|
||||
|
||||
public final class OnnxYoloDetector implements YoloDetector {
|
||||
private final String modelName;
|
||||
private final Net net;
|
||||
private final Size input;
|
||||
private final float confTh = 0.25f, nmsTh = 0.45f;
|
||||
private final String[] classes;
|
||||
private final int colorBGR;
|
||||
|
||||
public OnnxYoloDetector(String name, Path dir, int inW, int inH, String backend, int colorBGR) throws Exception {
|
||||
this.modelName = name;
|
||||
this.input = new Size(inW, inH);
|
||||
this.colorBGR = colorBGR;
|
||||
|
||||
// 查找ONNX模型文件
|
||||
String onnx = findModelFile(dir, ".onnx");
|
||||
if (onnx == null) {
|
||||
throw new Exception("找不到ONNX模型文件,请确保目录中存在 .onnx 文件: " + dir);
|
||||
}
|
||||
|
||||
// 读取类别文件
|
||||
Path clsPath = dir.resolve("classes.txt");
|
||||
if (Files.exists(clsPath)) {
|
||||
this.classes = Files.readAllLines(clsPath).stream().map(String::trim)
|
||||
.filter(s -> !s.isEmpty()).toArray(String[]::new);
|
||||
} else {
|
||||
this.classes = new String[0];
|
||||
}
|
||||
try {
|
||||
// 加载ONNX模型
|
||||
this.net = readNetFromONNX(onnx);
|
||||
|
||||
// 设置OpenCV后端
|
||||
net.setPreferableBackend(DNN_BACKEND_OPENCV);
|
||||
net.setPreferableTarget(DNN_TARGET_CPU);
|
||||
|
||||
System.out.println("ONNX模型加载成功: " + name + " (" + onnx + ")");
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new Exception("模型加载失败: " + e.getMessage() +
|
||||
"\n请确保ONNX模型文件格式正确", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在目录中查找指定扩展名的模型文件
|
||||
*/
|
||||
private String findModelFile(Path dir, String extension) {
|
||||
try {
|
||||
return Files.list(dir)
|
||||
.filter(path -> path.toString().toLowerCase().endsWith(extension.toLowerCase()))
|
||||
.map(Path::toString)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public String name() { return modelName; }
|
||||
|
||||
@Override
|
||||
public List<Detection> detect(Mat bgr) {
|
||||
if (bgr == null || bgr.empty()) return Collections.emptyList();
|
||||
|
||||
// 统一成 BGR 3 通道,避免 blobFromImage 断言失败
|
||||
if (bgr.channels() != 3) {
|
||||
Mat tmp = new Mat();
|
||||
if (bgr.channels() == 1) cvtColor(bgr, tmp, COLOR_GRAY2BGR);
|
||||
else if (bgr.channels() == 4) cvtColor(bgr, tmp, COLOR_BGRA2BGR);
|
||||
else bgr.copyTo(tmp);
|
||||
bgr = tmp;
|
||||
}
|
||||
|
||||
try (Mat blob = blobFromImage(bgr, 1.0/255.0, input, new Scalar(0.0), true, false, CV_32F)) {
|
||||
net.setInput(blob);
|
||||
// ===== 多输出兼容(Bytedeco 正确写法)=====
|
||||
org.bytedeco.opencv.opencv_core.StringVector outNames = net.getUnconnectedOutLayersNames();
|
||||
List<Mat> outs = new ArrayList<>();
|
||||
|
||||
if (outNames == null || outNames.size() == 0) {
|
||||
// 只有一个默认输出
|
||||
Mat out = net.forward(); // ← 直接返回 Mat
|
||||
outs.add(out);
|
||||
} else {
|
||||
// 多输出:用 MatVector 承接
|
||||
org.bytedeco.opencv.opencv_core.MatVector outBlobs =
|
||||
new org.bytedeco.opencv.opencv_core.MatVector(outNames.size());
|
||||
net.forward(outBlobs, outNames); // ← 正确的重载
|
||||
|
||||
for (long i = 0; i < outBlobs.size(); i++) {
|
||||
outs.add(outBlobs.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
int fw = bgr.cols(), fh = bgr.rows();
|
||||
List<Rect2d> boxes = new ArrayList<>();
|
||||
List<Float> scores = new ArrayList<>();
|
||||
List<Integer> classIds = new ArrayList<>();
|
||||
|
||||
for (Mat out : outs) {
|
||||
parseYoloOutput(out, fw, fh, boxes, scores, classIds);
|
||||
}
|
||||
if (boxes.isEmpty()) return Collections.emptyList();
|
||||
|
||||
// 纯 Java NMS,避免 MatOf* / Vector API 兼容问题
|
||||
List<Integer> keep = nmsIndices(boxes, scores, nmsTh);
|
||||
|
||||
List<Detection> result = new ArrayList<>(keep.size());
|
||||
for (int k : keep) {
|
||||
Rect2d r = boxes.get(k);
|
||||
Rect rect = new Rect((int)r.x(), (int)r.y(), (int)r.width(), (int)r.height());
|
||||
int cid = classIds.get(k);
|
||||
String cname = (cid >= 0 && cid < classes.length) ? classes[cid] : ("cls"+cid);
|
||||
result.add(new Detection("["+modelName+"] "+cname, scores.get(k), rect, colorBGR));
|
||||
}
|
||||
return result;
|
||||
} catch (Throwable e) {
|
||||
// 单帧失败不影响整体
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 YOLO-IR 输出为 N×C(C>=6),并填充 boxes/scores/classIds。 */
|
||||
private void parseYoloOutput(Mat out, int fw, int fh,
|
||||
List<Rect2d> boxes, List<Float> scores, List<Integer> classIds) {
|
||||
int dims = out.dims();
|
||||
Mat m;
|
||||
|
||||
if (dims == 2) {
|
||||
// NxC 或 CxN
|
||||
if (out.cols() >= 6) {
|
||||
m = out;
|
||||
} else {
|
||||
Mat tmp = new Mat();
|
||||
transpose(out, tmp); // CxN -> NxC
|
||||
m = tmp;
|
||||
}
|
||||
} else if (dims == 3) {
|
||||
// [1,N,C] 或 [1,C,N]
|
||||
if (out.size(2) >= 6) {
|
||||
m = out.reshape(1, out.size(1)); // -> N×C
|
||||
} else {
|
||||
Mat squeezed = out.reshape(1, out.size(1)); // C×N
|
||||
Mat tmp = new Mat();
|
||||
transpose(squeezed, tmp); // -> N×C
|
||||
m = tmp;
|
||||
}
|
||||
} else if (dims == 4) {
|
||||
// [1,1,N,C] 或 [1,1,C,N]
|
||||
int a = out.size(2), b = out.size(3);
|
||||
if (b >= 6) {
|
||||
m = out.reshape(1, a).clone(); // -> N×C
|
||||
} else {
|
||||
Mat cxn = out.reshape(1, b); // C×N
|
||||
Mat tmp = new Mat();
|
||||
transpose(cxn, tmp); // -> N×C
|
||||
m = tmp.clone();
|
||||
}
|
||||
} else {
|
||||
return; // 不支持的形状
|
||||
}
|
||||
|
||||
int N = m.rows(), C = m.cols();
|
||||
if (C < 6 || N <= 0) return;
|
||||
|
||||
FloatRawIndexer idx = m.createIndexer();
|
||||
for (int i = 0; i < N; i++) {
|
||||
float cx = idx.get(i,0), cy = idx.get(i,1), w = idx.get(i,2), h = idx.get(i,3);
|
||||
float obj = idx.get(i,4);
|
||||
|
||||
int bestCls = -1; float bestScore = 0f;
|
||||
for (int c = 5; c < C; c++) {
|
||||
float p = idx.get(i,c);
|
||||
if (p > bestScore) { bestScore = p; bestCls = c - 5; }
|
||||
}
|
||||
float conf = obj * bestScore;
|
||||
if (conf < confTh) continue;
|
||||
|
||||
// 默认假设归一化中心点格式 (cx,cy,w,h);若你的 IR 是 x1,y1,x2,y2,请把这里换算改掉
|
||||
int bx = Math.max(0, Math.round(cx * fw - (w * fw) / 2f));
|
||||
int by = Math.max(0, Math.round(cy * fh - (h * fh) / 2f));
|
||||
int bw = Math.min(fw - bx, Math.round(w * fw));
|
||||
int bh = Math.min(fh - by, Math.round(h * fh));
|
||||
if (bw <= 0 || bh <= 0) continue;
|
||||
|
||||
boxes.add(new Rect2d(bx, by, bw, bh));
|
||||
scores.add(conf);
|
||||
classIds.add(bestCls);
|
||||
}
|
||||
}
|
||||
|
||||
/** 纯 Java NMS(IoU 抑制),返回保留的下标列表。 */
|
||||
private List<Integer> nmsIndices(List<Rect2d> boxes, List<Float> scores, float nmsThreshold) {
|
||||
List<Integer> order = new ArrayList<>(boxes.size());
|
||||
for (int i = 0; i < boxes.size(); i++) order.add(i);
|
||||
// 按分数降序
|
||||
order.sort((i, j) -> Float.compare(scores.get(j), scores.get(i)));
|
||||
|
||||
List<Integer> keep = new ArrayList<>();
|
||||
boolean[] removed = new boolean[boxes.size()];
|
||||
|
||||
for (int a = 0; a < order.size(); a++) {
|
||||
int i = order.get(a);
|
||||
if (removed[i]) continue;
|
||||
keep.add(i);
|
||||
|
||||
Rect2d bi = boxes.get(i);
|
||||
double areaI = bi.width() * bi.height();
|
||||
|
||||
for (int b = a + 1; b < order.size(); b++) {
|
||||
int j = order.get(b);
|
||||
if (removed[j]) continue;
|
||||
|
||||
Rect2d bj = boxes.get(j);
|
||||
double areaJ = bj.width() * bj.height();
|
||||
|
||||
double xx1 = Math.max(bi.x(), bj.x());
|
||||
double yy1 = Math.max(bi.y(), bj.y());
|
||||
double xx2 = Math.min(bi.x() + bi.width(), bj.x() + bj.width());
|
||||
double yy2 = Math.min(bi.y() + bi.height(), bj.y() + bj.height());
|
||||
|
||||
double w = Math.max(0, xx2 - xx1);
|
||||
double h = Math.max(0, yy2 - yy1);
|
||||
double inter = w * h;
|
||||
double iou = inter / (areaI + areaJ - inter + 1e-9);
|
||||
|
||||
if (iou > nmsThreshold) removed[j] = true;
|
||||
}
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
|
||||
@Override public void close(){ net.close(); }
|
||||
}
|
||||
@@ -2,130 +2,121 @@ package com.ruoyi.video.utils;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* File包装成MultipartFile
|
||||
* @Author: orange
|
||||
* @CreateTime: 2025-01-16
|
||||
* 自定义MultipartFile实现,用于文件上传
|
||||
*/
|
||||
public class CustomMultipartFile implements MultipartFile {
|
||||
// 定义一个File类型的变量file
|
||||
private final File file;
|
||||
// 定义一个String类型的变量contentType
|
||||
private final String name;
|
||||
private final String originalFilename;
|
||||
private final String contentType;
|
||||
|
||||
// 构造方法,传入一个File类型的变量file和一个String类型的变量contentType
|
||||
public CustomMultipartFile(File file, String contentType) {
|
||||
/**
|
||||
* 构造函数 - 从File创建
|
||||
*
|
||||
* @param file 文件
|
||||
* @param filename 文件名
|
||||
* @param contentType 内容类型
|
||||
*/
|
||||
public CustomMultipartFile(File file, String filename, String contentType) {
|
||||
this.file = file;
|
||||
this.name = filename;
|
||||
this.originalFilename = filename;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
/**
|
||||
* 构造函数 - 从字节数组创建
|
||||
*
|
||||
* @param bytes 文件字节数组
|
||||
* @param filename 文件名
|
||||
* @param contentType 内容类型
|
||||
*/
|
||||
public CustomMultipartFile(byte[] bytes, String filename, String contentType) {
|
||||
this.file = null;
|
||||
this.name = filename;
|
||||
this.originalFilename = filename;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数 - 从File创建,指定Content-Type
|
||||
*
|
||||
* @param file 文件
|
||||
* @param contentType 内容类型
|
||||
*/
|
||||
public CustomMultipartFile(File file, String contentType) {
|
||||
this.file = file;
|
||||
this.name = file.getName();
|
||||
this.originalFilename = file.getName();
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.file.getName();
|
||||
return name;
|
||||
}
|
||||
|
||||
// 获取原始文件名
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return this.file.getName();
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
// 获取文件类型
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return this.contentType;
|
||||
return contentType;
|
||||
}
|
||||
|
||||
// 判断文件是否为空
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return this.file.length() == 0L;
|
||||
return getSize() == 0;
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
@Override
|
||||
public long getSize() {
|
||||
return this.file.length();
|
||||
if (file != null) {
|
||||
return file.length();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 获取文件的字节数组
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
// 创建一个FileInputStream对象,传入file
|
||||
InputStream inputStream = new FileInputStream(this.file);
|
||||
|
||||
byte[] var2;
|
||||
try {
|
||||
// 读取所有字节数组
|
||||
var2 = inputStream.readAllBytes();
|
||||
} catch (Throwable var5) {
|
||||
try {
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
} catch (Throwable var4) {
|
||||
var5.addSuppressed(var4);
|
||||
if (file != null) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
return is.readAllBytes();
|
||||
}
|
||||
|
||||
throw var5;
|
||||
}
|
||||
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
return var2;
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 获取文件的输入流
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new FileInputStream(this.file);
|
||||
if (file != null) {
|
||||
return new FileInputStream(file);
|
||||
}
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
|
||||
// 将文件传输到指定的文件
|
||||
@Override
|
||||
public void transferTo(File dest) throws IOException, IllegalStateException {
|
||||
// 如果目标文件不存在,则创建
|
||||
if (!dest.exists()) {
|
||||
dest.createNewFile();
|
||||
}
|
||||
|
||||
// 创建一个FileInputStream对象,传入file
|
||||
InputStream inputStream = new FileInputStream(this.file);
|
||||
|
||||
try {
|
||||
// 创建一个FileOutputStream对象,传入dest
|
||||
OutputStream outputStream = new FileOutputStream(dest);
|
||||
|
||||
try {
|
||||
// 创建一个字节数组,大小为1024
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
int bytesRead;
|
||||
// 循环读取文件,直到读取完毕
|
||||
while((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
// 将读取的字节数组写入目标文件
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
if (file != null) {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
} catch (Throwable var8) {
|
||||
try {
|
||||
// 关闭输出流
|
||||
outputStream.close();
|
||||
} catch (Throwable var7) {
|
||||
var8.addSuppressed(var7);
|
||||
}
|
||||
|
||||
throw var8;
|
||||
}
|
||||
|
||||
// 关闭输出流
|
||||
outputStream.close();
|
||||
} catch (Throwable var9) {
|
||||
try {
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
} catch (Throwable var6) {
|
||||
var9.addSuppressed(var6);
|
||||
}
|
||||
|
||||
throw var9;
|
||||
}
|
||||
|
||||
// 关闭输入流
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ public final class Overlay {
|
||||
|
||||
public static void draw(List<Detection> dets, Mat frame) {
|
||||
for (Detection d : dets) {
|
||||
Rect r = d.box();
|
||||
int bgr = d.colorBGR();
|
||||
Rect r = d.getRect();
|
||||
int bgr = d.getColorBGR();
|
||||
Scalar c = new Scalar(bgr & 0xFF, (bgr >> 8) & 0xFF, (bgr >> 16) & 0xFF, 0);
|
||||
rectangle(frame, r, c, 2, LINE_8, 0);
|
||||
String label = d.cls()+" "+String.format("%.2f", d.conf());
|
||||
String label = d.getLabel()+" "+String.format("%.2f", d.getConfidence());
|
||||
int[] baseline = new int[1];
|
||||
Size t = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, baseline);
|
||||
int x = Math.max(0, r.x());
|
||||
|
||||
Reference in New Issue
Block a user