2025-09-30 14:23:33 +08:00
|
|
|
|
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;
|
2025-09-30 17:03:41 +08:00
|
|
|
|
|
2025-09-30 14:23:33 +08:00
|
|
|
|
@Autowired
|
|
|
|
|
|
private com.ruoyi.video.mapper.InspectionTaskRecordMapper inspectionTaskRecordMapper;
|
|
|
|
|
|
|
2025-10-08 11:51:28 +08:00
|
|
|
|
// 检测器配置 - 支持环境变量配置
|
|
|
|
|
|
private static final String PYTHON_API_URL = System.getenv().getOrDefault("PYTHON_API_URL", "http://localhost:8000") + "/api/detect/file";
|
|
|
|
|
|
private static final String MODEL_NAME = "smoke"; // 默认使用吸烟检测模型
|
2025-09-30 14:23:33 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分析视频并更新记录(同步调用)
|
|
|
|
|
|
* @param task 巡检任务
|
|
|
|
|
|
* @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");
|
|
|
|
|
|
|
|
|
|
|
|
// 创建检测器
|
2025-10-07 16:07:23 +08:00
|
|
|
|
String chosenModel = (task.getModelName() != null && !task.getModelName().trim().isEmpty())
|
|
|
|
|
|
? task.getModelName().trim()
|
|
|
|
|
|
: MODEL_NAME;
|
|
|
|
|
|
log.info("使用模型进行分析: {} (taskId={})", chosenModel, task.getTaskId());
|
|
|
|
|
|
HttpYoloDetector detector = new HttpYoloDetector("yolov8", PYTHON_API_URL, chosenModel, 0x00FF00);
|
2025-09-30 14:23:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理视频并记录检测结果
|
|
|
|
|
|
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");
|
2025-10-01 22:19:45 +08:00
|
|
|
|
String imagePath = minioService.putObject(bucketName, fileName, multipartFile.getInputStream());
|
2025-09-30 14:23:33 +08:00
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
// 创建告警记录(严格按照DDL)
|
2025-09-30 14:23:33 +08:00
|
|
|
|
AlarmRecord alarmRecord = new AlarmRecord();
|
2025-10-01 22:19:45 +08:00
|
|
|
|
alarmRecord.setTaskId(task.getTaskId());
|
|
|
|
|
|
alarmRecord.setTaskName(task.getTaskName());
|
2025-09-30 14:23:33 +08:00
|
|
|
|
alarmRecord.setDeviceId(task.getDeviceId());
|
2025-10-01 22:19:45 +08:00
|
|
|
|
alarmRecord.setDeviceName(task.getDeviceName());
|
2025-09-30 14:23:33 +08:00
|
|
|
|
alarmRecord.setAlarmType("detection");
|
2025-10-01 22:19:45 +08:00
|
|
|
|
alarmRecord.setAlarmLevel("2"); // 2=中级
|
|
|
|
|
|
alarmRecord.setAlarmDesc(detection.getLabel() + " - 置信度: " + String.format("%.2f", detection.getConfidence()));
|
|
|
|
|
|
alarmRecord.setConfidence(java.math.BigDecimal.valueOf(detection.getConfidence()));
|
|
|
|
|
|
alarmRecord.setImagePath(imagePath);
|
|
|
|
|
|
alarmRecord.setAlarmTime(new Date());
|
|
|
|
|
|
alarmRecord.setHandleStatus("0"); // 0=未处理
|
|
|
|
|
|
alarmRecord.setCreateBy("system");
|
2025-09-30 14:23:33 +08:00
|
|
|
|
alarmRecord.setCreateTime(new Date());
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2025-09-30 18:07:39 +08:00
|
|
|
|
minioObject.setUrl(objectUrl);
|
2025-09-30 14:23:33 +08:00
|
|
|
|
minioObject.setCreateBy("system");
|
|
|
|
|
|
minioObject.setCreateTime(new Date());
|
|
|
|
|
|
Long objectId = vMinioObjectService.insertVMinioObject(minioObject);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新记录的附件信息,添加处理后的视频URL
|
|
|
|
|
|
String currentAccessory = record.getAccessory();
|
2025-10-01 22:19:45 +08:00
|
|
|
|
if (currentAccessory == null || currentAccessory.isEmpty()) {
|
|
|
|
|
|
record.setAccessory(objectUrl);
|
|
|
|
|
|
} else {
|
2025-10-07 11:35:53 +08:00
|
|
|
|
record.setAccessory(currentAccessory + "," + objectUrl);
|
2025-10-01 22:19:45 +08:00
|
|
|
|
}
|
2025-09-30 14:23:33 +08:00
|
|
|
|
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
|
|
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
log.info("处理后视频已上传: 任务ID={}, 记录ID={}, MinIO对象ID={}, URL={}",
|
|
|
|
|
|
task.getTaskId(), record.getRecordId(), objectId, objectUrl);
|
2025-09-30 14:23:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-01 22:19:45 +08:00
|
|
|
|
* 处理视频记录
|
2025-09-30 14:23:33 +08:00
|
|
|
|
* @param inputFile 输入视频文件
|
|
|
|
|
|
* @param outputFile 输出视频文件
|
|
|
|
|
|
* @param detector 检测器
|
|
|
|
|
|
* @param task 巡检任务
|
2025-10-01 22:19:45 +08:00
|
|
|
|
* @param record 巡检记录
|
2025-09-30 14:23:33 +08:00
|
|
|
|
*/
|
2025-10-01 22:19:45 +08:00
|
|
|
|
private void processVideoForRecord(File inputFile, File outputFile, HttpYoloDetector detector,
|
|
|
|
|
|
InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record) throws Exception {
|
2025-09-30 14:23:33 +08:00
|
|
|
|
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());
|
2025-10-01 22:19:45 +08:00
|
|
|
|
if (grabber.getAudioChannels() > 0) {
|
|
|
|
|
|
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
|
|
|
|
|
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
|
|
|
|
|
recorder.setAudioChannels(grabber.getAudioChannels());
|
|
|
|
|
|
recorder.setSampleRate(grabber.getSampleRate());
|
|
|
|
|
|
}
|
2025-09-30 14:23:33 +08:00
|
|
|
|
recorder.start();
|
|
|
|
|
|
|
|
|
|
|
|
OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
|
|
|
|
|
|
|
|
|
|
|
|
// 用于去重的垃圾检测结果缓存
|
|
|
|
|
|
Map<String, Long> detectedGarbageCache = new HashMap<>();
|
|
|
|
|
|
// 跟踪检测到的垃圾ID
|
2025-10-01 22:19:45 +08:00
|
|
|
|
final Long[] detectionId = {1L};
|
2025-09-30 14:23:33 +08:00
|
|
|
|
// 帧计数
|
|
|
|
|
|
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]++);
|
|
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
// 创建告警记录(关联到记录)
|
|
|
|
|
|
createAlarmRecordForRecord(task, record, detection, mat, frameCount);
|
2025-09-30 14:23:33 +08:00
|
|
|
|
} 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();
|
|
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
// 上传处理后的视频(更新记录)
|
|
|
|
|
|
uploadProcessedVideoForRecord(outputFile, task, record);
|
2025-09-30 14:23:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
|
2025-09-30 14:23:33 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成检测结果的唯一键,用于检测结果去重
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-01 22:19:45 +08:00
|
|
|
|
|
2025-09-30 14:23:33 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 在图像上绘制检测框
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|