新增抓取历史图片和分析功能

dev_xq_0.0.1-2
刘政 1 day ago
parent c1b99b4d84
commit 65e5982524

@ -1,5 +1,5 @@
ZLMediaKit:
ip: 192.168.2.128
ip: 192.168.2.129
port: 8080
app: live
vhost: __defaultVhost__

@ -55,7 +55,7 @@ public class SaTokenConfig implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// D:/work/images/save/1.jpg
registry.addResourceHandler("/save/**")
.addResourceLocations("file:D:/work/images/");
.addResourceLocations("file:D:/work/images/","file:D:/work/temp/");
}
}

@ -101,6 +101,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.im4java</groupId>
<artifactId>im4java</artifactId>
<version>1.4.0</version>
</dependency>
<!-- jna,ffmpeg的相关依赖-->
<dependency>
<groupId>cc.eguid</groupId>

@ -11,8 +11,8 @@ public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(5000);
factory.setConnectTimeout(10000);
factory.setReadTimeout(10000);
return new RestTemplate(factory);
}
}

@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* ClassName: DeviceController
* Package: com.sz.admin.monitor.controller
@ -30,10 +32,18 @@ public class DeviceController {
return ApiResult.success(deviceService.refresh(id,deviceType));
}
// 设备登录
@Operation(summary = "/设备登录")
@Operation(summary = "设备登录")
@GetMapping("/login")
public ApiResult<?> deviceLogin(@RequestParam("id") Long id)
{
return ApiResult.success(deviceService.deviceLogin(id));
}
// 整个NVR的历史视频的算法分析
@Operation(summary = "整个NVR的历史视频的算法分析")
@GetMapping("/history/analysis")
public ApiResult<?> captureHistoryAndCompose(@RequestParam("ids") List<Long> ids)
{
deviceService.captureHistoryAndCompose(ids);
return ApiResult.success();
}
}

@ -1,6 +1,7 @@
package com.sz.admin.monitor.controller;
import com.sz.admin.monitor.pojo.dto.edgebox.AlgorithmTaskDTO;
import com.sz.admin.monitor.pojo.po.RecordFile;
import com.sz.admin.monitor.pojo.vo.watchful.WatchfulVO;
import com.sz.admin.monitor.service.IpChannelService;
import com.sz.admin.monitor.utils.AlgMediaConfigResponse;
@ -11,6 +12,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* ClassName: IpChannelController
* Package: com.sz.admin.monitor.controller
@ -97,4 +100,21 @@ public class IpChannelController {
return ApiResult.success(ipChannelService.configAlg(algTaskConfigDto));
}
// 抓取历史视频的图片
@Operation(summary = "抓取历史视频的图片并进行对比")
@GetMapping("/capture/history")
public ApiResult<Void> captureHistoryAndCompose(@RequestParam("id") Long id) {
ipChannelService.captureHistoryAndCompose(id);
return ApiResult.success();
}
// 按照时间范围获取录像
@Operation(summary = "按照时间范围获取录像")
@GetMapping("/getVideoByTime")
public ApiResult<List<RecordFile>> getVideoByTime(@RequestParam("id") Long id,
@RequestParam("startTime") String startTime,
@RequestParam("endTime") String endTime) {
return ApiResult.success(ipChannelService.getVideoByTime(id, startTime, endTime));
}
}

@ -23,17 +23,14 @@ public class BoxAlarmReportDto {
private String cameraNo;
@Schema(description = "摄像头ID")
private Long cameraId;
@Schema(description = "报警的url")
private String url;
@Schema(description = "报警的类型")
private String alarmReportType;
private String alarmTypeDesc;
@Schema(description = "报警的枚举值")
private Integer alarmType;
@Schema(description = "报警的时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime alarmReportTime;
private Long alarmTime;
@Schema(description = "基准图")
private String baseImage;
@Schema(description = "抓拍图")
private String captureImage;
@Schema(description = "报警的原因")
private String description;
@Schema(description = "报警图")
private String alarmImage;
}

@ -1,5 +1,7 @@
package com.sz.admin.monitor.sdk;
import com.sz.admin.monitor.sdk.hkSdk.HCNetSDK;
/**
* ClassName: AbstractNVR
* Package: com.sz.admin.monitor.hik
@ -39,4 +41,14 @@ public abstract class AbstractNVR {
* @return true false
*/
public abstract String capturePic(int luerId,int iChannel,String fileName);
/**
*
*/
public abstract boolean captureHistoryImage(int luerId, int iChannel,
HCNetSDK.NET_DVR_TIME startDvrTime,
HCNetSDK.NET_DVR_TIME stopDvrTime,
String tempVideoPath,
String finalImagePath);
}

@ -1,6 +1,8 @@
package com.sz.admin.monitor.sdk;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.mybatisflex.core.query.QueryWrapper;
import com.sz.admin.monitor.pojo.po.Camera;
import com.sz.admin.monitor.pojo.po.Nvr;
@ -34,6 +36,10 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* NVR
@ -56,6 +62,7 @@ public class ManageNVR {
private VcrlUserIdContainer vcrlUserIdContainer;
@Resource
private DataScopeProperties dataScopeProperties;
private final ExecutorService executor = Executors.newFixedThreadPool(5);
/**
* sdknvr
@ -749,4 +756,124 @@ public class ManageNVR {
Map<String, Integer> map = hkNVR.NET_DVR_GetPTZPos(userId, camera.getChannelId());
return BeanCopyUtils.copy(map, PTZVO.class);
}
// 进行历史视频的抓图
public void captureHistoryImage(String taskDir, Long id) {
Camera camera = cameraService.getById(id);
Integer userId = (Integer) vcrlUserIdContainer.getlUserId(camera.getNvrId().intValue());
// 收集前一个月的每个小时的时间点
List<LocalDateTime> points = new ArrayList<>();
LocalDateTime endTime = LocalDateTime.now().withMinute(0).withSecond(0);
LocalDateTime startTime = endTime.minusDays(7);
LocalDateTime current = startTime;
while (current.isBefore(endTime) || current.equals(endTime)) {
points.add(current);
current = current.plusHours(1);
}
int totalSizes = points.size();
// 创建固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 统计进度的原子类
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
AtomicInteger finishedCount = new AtomicInteger(0);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 将任务全部丢进线程池
for (LocalDateTime time : points) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
String fileName = "history_"+camera.getId()+ time.format(fmt) + ".jpg";
File file = captureAtTime(userId, camera.getChannelId(), time, taskDir, fileName);
// 记录当前完成的次数
int currentFinished = finishedCount.incrementAndGet();
if (file != null) {
successCount.incrementAndGet();
// System.out.println(String.format("[进度 %d/%d] 成功: %s", currentFinished, totalSizes, fileName));
} else {
failCount.incrementAndGet();
// System.out.println(String.format("[进度 %d/%d] 失败: %s", currentFinished, totalSizes, fileName));
}
}, threadPool);
// 添加任务
futures.add(future);
}
// 阻塞主线程等待所有720个任务全部跑完
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 关闭线程池
executor.shutdown();
// System.out.println("总任务数量:" + totalSizes);
// System.out.println("成功抓取:" + successCount.get() + "张");
// System.out.println("失败:" + failCount.get() + "张");
// System.out.println("图片已全部保存在:" + "D:\\work\\temp\\");
}
/**
*
*
* @param userId userId
* @param channelId
* @param time (Java 8 LocalDateTime)
* @param dir ( / \\ "D:/temp/")
* @param fileName ( "pic_120000.jpg")
* @return File File null
*/
private File captureAtTime(int userId, int channelId, LocalDateTime time, String dir, String fileName) {
LocalDateTime stopTime = time.plusSeconds(5);
// 将 Java 的 LocalDateTime 翻译成海康 SDK 认识的底层结构体
HCNetSDK.NET_DVR_TIME startDvrTime = convertToDvrTime(time);
HCNetSDK.NET_DVR_TIME stopDvrTime = convertToDvrTime(stopTime);
String tempVideoPath = dir + "temp_slice_" + UUID.randomUUID() + ".mp4";
String finalImagePath = dir + fileName;
try {
hkNVR.captureHistoryImage(
userId,
channelId,
startDvrTime,
stopDvrTime,
tempVideoPath,
finalImagePath
);
// 抓图流程走完后,检查图片是否真的生成成功了
File resultImage = new File(finalImagePath);
if (resultImage.exists() && resultImage.length() > 0) {
return resultImage;
} else {
return null; // 抓图失败(可能是那个时间点设备断网或无录像)
}
} catch (Exception e) {
return null;
} finally {
File tempFile = new File(tempVideoPath);
if (tempFile.exists()) {
tempFile.delete();
}
}
}
private HCNetSDK.NET_DVR_TIME convertToDvrTime(LocalDateTime currentTime) {
HCNetSDK.NET_DVR_TIME netDvrTime = new HCNetSDK.NET_DVR_TIME();
netDvrTime.dwYear = (short) currentTime.getYear();
netDvrTime.dwMonth = (short) currentTime.getMonthValue();
netDvrTime.dwDay = (short) currentTime.getDayOfMonth();
netDvrTime.dwHour = (short) currentTime.getHour();
netDvrTime.dwMinute = (short) currentTime.getMinute();
netDvrTime.dwSecond = (short) currentTime.getSecond();
return netDvrTime;
}
// 按照时间范围获取录像文件
public List<RecordFile> getRecordFileByTime(Long id, LocalDateTime startTime, LocalDateTime endTime) {
Camera camera = cameraService.getById(id);
List<RecordFile> recordFile = new ArrayList<>();
Integer userId = (Integer) vcrlUserIdContainer.getlUserId(camera.getNvrId().intValue());
try {
recordFile = hkNVR.findRecordFile(userId, camera.getChannelId(), startTime, endTime);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return recordFile;
}
}

@ -14,6 +14,8 @@ import com.sz.admin.monitor.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
@ -139,6 +141,81 @@ public class HkNVR extends AbstractNVR {
public String capturePic(int luerId, int iChannel,String fileName) {
return captureP(luerId, iChannel,fileName);
}
@Override
public boolean captureHistoryImage(int luerId, int iChannel,
HCNetSDK.NET_DVR_TIME startDvrTime,
HCNetSDK.NET_DVR_TIME stopDvrTime,
String tempVideoPath,
String finalImagePath) {
// 1. 构建下载条件结构体
HCNetSDK.NET_DVR_PLAYCOND downloadCond = new HCNetSDK.NET_DVR_PLAYCOND();
downloadCond.dwChannel = iChannel;
// 设置开始时间
downloadCond.struStartTime.dwYear = startDvrTime.dwYear;
downloadCond.struStartTime.dwMonth = startDvrTime.dwMonth;
downloadCond.struStartTime.dwDay = startDvrTime.dwDay;
downloadCond.struStartTime.dwHour = startDvrTime.dwHour;
downloadCond.struStartTime.dwMinute = startDvrTime.dwMinute;
downloadCond.struStartTime.dwSecond = startDvrTime.dwSecond;
// 设置结束时间
downloadCond.struStopTime.dwYear = stopDvrTime.dwYear;
downloadCond.struStopTime.dwMonth = stopDvrTime.dwMonth;
downloadCond.struStopTime.dwDay = stopDvrTime.dwDay;
downloadCond.struStopTime.dwHour = stopDvrTime.dwHour;
downloadCond.struStopTime.dwMinute = stopDvrTime.dwMinute;
downloadCond.struStopTime.dwSecond = stopDvrTime.dwSecond;
downloadCond.write();
// 2. 获取下载句柄
int lDownloadHandle = hCNetSDK.NET_DVR_GetFileByTime_V40(luerId, tempVideoPath, downloadCond);
if (lDownloadHandle == -1) {
System.out.println("建立下载通道失败,错误码:" + hCNetSDK.NET_DVR_GetLastError());
return false;
}
// 3. 发送下载控制指令
boolean playControl = hCNetSDK.NET_DVR_PlayBackControl_V40(lDownloadHandle, HCNetSDK.NET_DVR_PLAYSTART, null, 0, null, null);
if (!playControl) {
System.out.println("发送下载控制指令失败,错误码:" + hCNetSDK.NET_DVR_GetLastError());
hCNetSDK.NET_DVR_StopGetFile(lDownloadHandle); //下载用的接口,停止必须用 StopGetFile不能用 StopPlayBack
return false; // 如果指令发送失败,必须 return 终止,不能往下走
}
// 给操作系统 1 秒钟的缓冲时间,确保视频文件彻底写入硬盘并解除占用
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 7. 调用 ffmpeg 进行抽取图片
ProcessBuilder processBuilder = new ProcessBuilder(
"ffmpeg", "-y", "-err_detect", "ignore_err", "-i", tempVideoPath, "-vframes", "1", "-q:v", "2", "-f", "image2", finalImagePath
);
// 重定向 FFmpeg 的输出流,防止缓冲区撑满导致 Java 进程死锁卡住
processBuilder.redirectErrorStream(true);
try {
Process process = processBuilder.start();
java.io.InputStream inputStream = process.getInputStream();
byte[] buffer = new byte[1024];
while (inputStream.read(buffer) != -1) {
// 静默读取,不打印,清空缓冲区
}
// 等待 FFmpeg 执行完毕
int exitCode = process.waitFor();
if (exitCode == 0) {
return true;
} else {
return false;
}
} catch (IOException | InterruptedException e) {
System.out.println("ffmpeg 执行失败:" + e);
return false;
}
}
/**
*
*

@ -3,6 +3,8 @@ package com.sz.admin.monitor.service;
import com.mybatisflex.core.service.IService;
import com.sz.admin.monitor.pojo.po.Nvr;
import java.util.List;
/**
* ClassName: DeviceService
* Package: com.sz.admin.monitor.service
@ -13,4 +15,7 @@ public interface DeviceService extends IService<Nvr> {
// 设备登录
Object deviceLogin(Long id);
// 历史视频的算法分析
void captureHistoryAndCompose(List<Long> ids);
}

@ -13,5 +13,5 @@ public interface ForwardService {
/**
*
*/
void enrichAndForward(BoxAlarmReportDto boxAlarm);
void enrichAndForward(String url,BoxAlarmReportDto boxAlarm);
}

@ -3,9 +3,12 @@ package com.sz.admin.monitor.service;
import com.mybatisflex.core.service.IService;
import com.sz.admin.monitor.pojo.dto.edgebox.AlgorithmTaskDTO;
import com.sz.admin.monitor.pojo.po.Camera;
import com.sz.admin.monitor.pojo.po.RecordFile;
import com.sz.admin.monitor.pojo.vo.watchful.WatchfulVO;
import com.sz.admin.monitor.utils.AlgMediaConfigResponse;
import java.util.List;
/**
* ClassName: IpChannelService
* Package: com.sz.admin.monitor.service
@ -35,5 +38,9 @@ public interface IpChannelService extends IService<Camera> {
// 配置算法任务
AlgMediaConfigResponse configAlg(AlgorithmTaskDTO algTaskConfigDto);
void captureHistoryAndCompose(Long id);
List<RecordFile> getVideoByTime(Long id, String startTime, String endTime);
// void resetSentinel(int channelId, int watchTime, int presetIndex);
}

@ -39,6 +39,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
@ -142,17 +143,22 @@ public class AlgorithmTaskServiceImpl extends ServiceImpl<AlgorithmTaskMapper, A
websocketRedisService.sendServiceToWs(msg);
// 统一处理报警信息后,将摄像头信息+报警信息上报给别人服务器
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.parse(alarmReportDto.getTime(), formatter));
reportDto.setAlarmReportType(alarmReportEnums.getAlarmDescription());
reportDto.setDescription(alarmReportDto.getResult().getDescription());
// 1. 获取你的时间字符串
String timeStr = alarmReportDto.getTime();
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, timeFormatter);
// 转成时间戳(毫秒)
long timestamp = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
reportDto.setAlarmTime(timestamp);
reportDto.setAlarmTypeDesc(alarmReportEnums.getAlarmDescription());
reportDto.setAlarmType(alarmReportEnums.getAlarmCode());
reportDto.setBaseImage(imageData);
reportDto.setCaptureImage(imageDataLabeled);
reportDto.setUrl(algorithmTask.getUrl());
reportDto.setAlarmImage(imageDataLabeled);
reportDto.setCameraName(camera.getName());
reportDto.setCameraId(cameraId);
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
forwardService.enrichAndForward(algorithmTask.getUrl(),reportDto);
}catch ( Exception e)
{
log.error("报警信息上报失败", e);

@ -8,6 +8,7 @@ import com.sz.admin.monitor.pojo.po.Camera;
import com.sz.admin.monitor.pojo.po.Nvr;
import com.sz.admin.monitor.sdk.ManageNVR;
import com.sz.admin.monitor.service.DeviceService;
import com.sz.admin.monitor.service.IpChannelService;
import com.sz.admin.monitor.utils.AiBoxRequestUtil;
import com.sz.admin.monitor.utils.AlgMediaConfigResponse;
import com.sz.admin.monitor.utils.RtspUtil;
@ -30,6 +31,10 @@ import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
/**
@ -49,11 +54,8 @@ public class DeviceServiceImpl extends ServiceImpl<NvrMapper, Nvr> implements De
@Resource
private NvrMapper nvrMapper;
@Resource
private SocketService socketService;
@Resource
private WebsocketRedisService websocketRedisService;
@Resource
private AiBoxRequestUtil aiBoxRequestUtil;
private IpChannelService ipChannelService;
@Override
public Object deviceLogin(Long id) {
@ -68,6 +70,37 @@ public class DeviceServiceImpl extends ServiceImpl<NvrMapper, Nvr> implements De
return result;
}
@Override
public void captureHistoryAndCompose(List<Long> ids) {
if(ids==null || ids.isEmpty())
{
throw new BusinessException(AdminResponseEnum.OPERATION_FAIL,null,"数据为空");
}
ExecutorService threadPool = Executors.newFixedThreadPool(1);
CompletableFuture.runAsync(()->{
try {
List<Nvr> nvrs = nvrMapper.selectListByIds(ids);
// 对每个NVR进行分析
for (Nvr nvr : nvrs) {
// 查询出所有的通道
List<Camera> cameraList = cameraMapper.selectByNvrId(nvr.getId());
// 遍历所有的通道,进行分析
for (Camera camera : cameraList) {
try {
ipChannelService.captureHistoryAndCompose(camera.getId());
}catch (Exception e)
{
log.error("处理camera异常NVRID:{},CameraID:{}",nvr.getId(),camera.getId());
}
}
}
}catch (Exception e)
{
log.error("执行批量分析任务发生全局异常", e);
}
},threadPool);
}
/**
*
* @param id NVRid

@ -19,7 +19,7 @@ public class ForwardServiceImpl implements ForwardService {
@Resource
private RestTemplate restTemplate;
@Override
public void enrichAndForward(BoxAlarmReportDto boxAlarm) {
ResponseEntity<String> response = restTemplate.postForEntity(boxAlarm.getUrl(), boxAlarm, String.class);
public void enrichAndForward(String url,BoxAlarmReportDto boxAlarm) {
ResponseEntity<String> response = restTemplate.postForEntity(url, boxAlarm, String.class);
}
}

@ -1,5 +1,6 @@
package com.sz.admin.monitor.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
@ -8,16 +9,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import com.sz.admin.monitor.enums.HCPlayControlEnum;
import com.sz.admin.monitor.mapper.AlgorithmTaskMapper;
import com.sz.admin.monitor.mapper.CameraMapper;
import com.sz.admin.monitor.mapper.EdgeBoxMapper;
import com.sz.admin.monitor.mapper.NvrMapper;
import com.sz.admin.monitor.mapper.*;
import com.sz.admin.monitor.pojo.dto.Ipchannel.ControlDTO;
import com.sz.admin.monitor.pojo.dto.edgebox.AlgorithmTaskDTO;
import com.sz.admin.monitor.pojo.po.AlgorithmTask;
import com.sz.admin.monitor.pojo.po.Camera;
import com.sz.admin.monitor.pojo.po.EdgeBox;
import com.sz.admin.monitor.pojo.po.Nvr;
import com.sz.admin.monitor.pojo.po.*;
import com.sz.admin.monitor.pojo.vo.camerasnapshot.CameraSnapshotVO;
import com.sz.admin.monitor.pojo.vo.ptz.PTZVO;
import com.sz.admin.monitor.pojo.vo.watchful.WatchfulVO;
@ -25,10 +20,7 @@ import com.sz.admin.monitor.sdk.ManageNVR;
import com.sz.admin.monitor.service.CameraSnapshotService;
import com.sz.admin.monitor.service.IpChannelService;
import com.sz.admin.monitor.service.PresetService;
import com.sz.admin.monitor.utils.AiBoxRequestUtil;
import com.sz.admin.monitor.utils.AlgMediaConfigResponse;
import com.sz.admin.monitor.utils.RtspUtil;
import com.sz.admin.monitor.utils.ZLMediaKitUtils;
import com.sz.admin.monitor.utils.*;
import com.sz.core.common.entity.TransferMessage;
import com.sz.core.common.exception.common.BusinessException;
import com.sz.core.util.BeanCopyUtils;
@ -42,7 +34,9 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
@ -67,7 +61,7 @@ public class IpChannelServiceImpl extends ServiceImpl<CameraMapper, Camera> impl
private AlgorithmTaskMapper algorithmTaskMapper;
@Resource
private PresetService presetService;
private CameraAlarmMapper cameraAlarmMapper;
@Resource
private AiBoxRequestUtil aiBoxRequestUtil;
@ -378,6 +372,98 @@ public class IpChannelServiceImpl extends ServiceImpl<CameraMapper, Camera> impl
}
return response;
}
@Override
public void captureHistoryAndCompose(Long id) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
// 生成本次任务的专属批次号
String batchId=LocalDateTime.now().format(fmt);
// 拼接出本次任务的专属目录
String baseDir="D:/work/temp/";
String taskDir=baseDir+"task_"+batchId+"/";
File taskDirect = new File(taskDir);
if (!taskDirect.exists()) {
boolean created = taskDirect.mkdirs();
if (!created) {
log.error("创建专属工作目录失败,请检查 D 盘权限!路径: {}", taskDir);
return;
}
}
log.info("保存文件的路径:{}",taskDir);
manageNVR.captureHistoryImage(taskDir,id);
startSequentialComparison(taskDir,id);
}
// 执行相邻图片的滑动窗口对比任务
private void startSequentialComparison(String imageDirPath,Long id) {
// 图片存储目录文件夹
File dir = new File(imageDirPath);
Camera camera = this.getById(id);
Nvr nvr = nvrMapper.selectOneById(camera.getNvrId());
// 获取该目录下所有的.jpg图片
File[] imageFiles = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".jpg") && name.contains("history_"+camera.getId()));
if (imageFiles == null || imageFiles.length < 2) {
log.error("图片数据不足,无法对比");
return;
}
// 将图片进行排序
Arrays.sort(imageFiles, Comparator.comparing(File::getName));
List<CameraAlarm> cameraAlarmList=new ArrayList<>();
for (int i = 0; i < imageFiles.length - 1; i++) {
File currentImg = imageFiles[i];
File nextImg = imageFiles[i + 1];
// System.out.println("正在对比:" + currentImg.getName() + "VS " + nextImg.getName());
// 调用算法进行对比
// 1. 移位对比
Map<String, Object> map = RobustImageMatcherUtil.calculateOffset(currentImg.getPath(), nextImg.getPath());
// 如果出现差异化,直接生成一条记录,保存到报警记录表中
int value = (int) map.get("value");
if (value == 1) {
CameraAlarm cameraAlarm = new CameraAlarm();
cameraAlarm.setCameraId(camera.getId());
cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setCameraNo(camera.getCameraNo());
cameraAlarm.setChannelId(camera.getChannelId());
cameraAlarm.setAlarmAreaId(nvr.getStationId());
cameraAlarm.setAlarmType(1);
cameraAlarm.setStatus(0);
cameraAlarm.setBaseImage(currentImg.getPath());
cameraAlarm.setCaptureImage(nextImg.getPath());
cameraAlarm.setAlgoResult((String) map.get("description"));
cameraAlarmList.add(cameraAlarm);
}
// 2. 模糊对比
BlurDetectorV4.DetectResult detectResult = BlurDetectorV4.doubleDetectBlur(currentImg.getPath(), nextImg.getPath());
if ("1".equals(detectResult.value)){
CameraAlarm cameraAlarm = new CameraAlarm();
cameraAlarm.setCameraId(camera.getId());
cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setCameraNo(camera.getCameraNo());
cameraAlarm.setChannelId(camera.getChannelId());
cameraAlarm.setAlarmAreaId(nvr.getStationId());
cameraAlarm.setAlarmType(6);
cameraAlarm.setStatus(0);
cameraAlarm.setBaseImage(currentImg.getPath());
cameraAlarm.setCaptureImage(nextImg.getPath());
cameraAlarm.setAlgoResult((String) map.get("description"));
cameraAlarmList.add(cameraAlarm);
}
}
// 保存到数据库
cameraAlarmMapper.insertBatch(cameraAlarmList);
}
@Override
public List<RecordFile> getVideoByTime(Long id, String startTime, String endTime) {
List<RecordFile> recordFiles = new ArrayList<>();
try {
recordFiles = manageNVR.getRecordFileByTime(id, LocalDateTime.parse(startTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), LocalDateTime.parse(endTime, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
} catch (Exception e) {
throw new RuntimeException("获取视频失败", e);
}
return recordFiles;
}
// 在这里接收透传过来的 websocket信息进行业务处理
@Override
public void handlerMsg(TransferMessage transferMessage) {

@ -33,6 +33,8 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
@ -133,17 +135,24 @@ public class ScheduledTask {
}
count++;
// 进行单一背景检测
BackgroundDetector.DetectResult result = BackgroundDetector.detectBackground(imagePath2);
if("1".equals(result.value))
{
// 当前摄像头面对的是白墙,直接进行报警
handleAnalysisResult(blurDetection, imagePath2, result);
continue;
}
// BackgroundDetector.DetectResult result = BackgroundDetector.detectBackground(imagePath2);
// if("1".equals(result.value))
// {
// // 当前摄像头面对的是白墙,直接进行报警
// handleAnalysisResult(blurDetection, imagePath2, result);
// continue;
// }
// 进行模糊图片对比
BlurDetectorV4.DetectResult detectResult = null;
if (isExists) {
// 基准图存在,使用双图对比功能
// 检查是否对着墙壁或者特征不明显的地方
BlurrinessComparator.DetectResult result = BlurrinessComparator.compareBlurriness(imagePath1, imagePath2);
if (result.value.equals("1")) {
// 当前摄像头面对的是白墙,直接进行报警
handleAnalysisResult(blurDetection, imagePath2, result);
continue;
}
detectResult = BlurDetectorV4.doubleDetectBlur(imagePath1, imagePath2);
} else {
// 基准图不存在,使用单图对比功能
@ -162,7 +171,7 @@ public class ScheduledTask {
public record DetectResult(String desc,double score) {
}
// 分析背景检测结果并且报警
private void handleAnalysisResult(BlurDetection blurDetection, String imagePath, BackgroundDetector.DetectResult result) throws IOException {
private void handleAnalysisResult(BlurDetection blurDetection, String imagePath, BlurrinessComparator.DetectResult result) throws IOException {
Camera camera = cameraService.getById(blurDetection.getCameraId());
CameraAlarm cameraAlarm = new CameraAlarm();
cameraAlarm.setCameraId(camera.getId());
@ -174,7 +183,7 @@ public class ScheduledTask {
cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setCaptureImage(imagePath);
DetectResult backgroundResult = new DetectResult(result.desc, result.score);
cameraAlarm.setAlgoResult(JsonUtils.toJsonString(result));
cameraAlarm.setAlgoResult(JsonUtils.toJsonString(backgroundResult));
cameraAlarm.setStatus(0);
cameraAlarmMapper.insert(cameraAlarm);
SocketMessage bean = new SocketMessage();
@ -190,17 +199,22 @@ public class ScheduledTask {
msg.setToPushAll(true);
websocketRedisService.sendServiceToWs(msg);
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.now());
reportDto.setAlarmReportType(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
reportDto.setDescription("摄像头拍摄的图片出现模糊");
// 获取时间字符串
String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, timeFormatter);
// 转成时间戳(毫秒)
long timestamp = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
reportDto.setAlarmTime(timestamp);
reportDto.setAlarmTypeDesc(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
byte[] CaptureFileContent = Files.readAllBytes(Paths.get(imagePath));
reportDto.setCaptureImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setUrl(blurDetection.getUrl());
reportDto.setAlarmImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setCameraName(camera.getName());
reportDto.setAlarmType(AlarmReportEnums.BLUR_ALARM.getAlarmCode());
reportDto.setCameraId(camera.getId());
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
forwardService.enrichAndForward(blurDetection.getUrl(),reportDto);
}catch (Exception e)
{
log.error("模糊报警上报失败,摄像头 ID: {}, 相机编号:{}",
@ -248,21 +262,26 @@ public class ScheduledTask {
websocketRedisService.sendServiceToWs(msg);
// 统一处理报警信息后,将摄像头信息+报警信息上报给别人服务器
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.now());
reportDto.setAlarmReportType(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
reportDto.setDescription("摄像头拍摄的图片出现模糊");
// 获取时间字符串
String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, timeFormatter);
// 转成时间戳(毫秒)
long timestamp = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
reportDto.setAlarmTime(timestamp);
reportDto.setAlarmTypeDesc(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
if(isExists && imagePath1 !=null) {
byte[] BaseFileContent = Files.readAllBytes(Paths.get(imagePath1));
reportDto.setBaseImage(Base64.getEncoder().encodeToString(BaseFileContent));
}
byte[] CaptureFileContent = Files.readAllBytes(Paths.get(imagePath2));
reportDto.setCaptureImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setUrl(blurDetection.getUrl());
reportDto.setAlarmImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setCameraName(camera.getName());
reportDto.setAlarmType(AlarmReportEnums.BLUR_ALARM.getAlarmCode());
reportDto.setCameraId(camera.getId());
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
forwardService.enrichAndForward(blurDetection.getUrl(),reportDto);
}catch (Exception e)
{
log.error("模糊报警上报失败,摄像头 ID: {}, 相机编号:{}",
@ -304,7 +323,7 @@ public class ScheduledTask {
if (ok) {
String filename = ImageNameUtils.generateFileName("CAPTURE", cameraId);
String image2 = manageNVR.capturePic(cameraId.intValue(), filename);
// String image2="D:\\work\\images\\CAPTURE_258_20260320170313838_4680425d.jpg";
//String image2="D:\\work\\images\\CAPTURE_258_20260331104152476_de86220b.jpg";
// 抓图合法性校验
if (image2 == null || image2.isBlank()) {
log.error("摄像机[{}] 抓拍失败,无法获取当前图片", cameraId);
@ -372,19 +391,24 @@ public class ScheduledTask {
websocketRedisService.sendServiceToWs(msg);
// 统一处理报警信息后,将摄像头信息+报警信息上报给别人服务器
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.now());
reportDto.setAlarmReportType(AlarmReportEnums.DISPLACEMENT_ALARM.getAlarmDescription());
reportDto.setDescription("摄像头发生移位");
// 获取时间字符串
String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(timeStr, timeFormatter);
// 转成时间戳(毫秒)
long timestamp = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
reportDto.setAlarmTime(timestamp);
reportDto.setAlarmTypeDesc(AlarmReportEnums.DISPLACEMENT_ALARM.getAlarmDescription());
byte[] BaseFileContent = Files.readAllBytes(Paths.get(imagePath));
reportDto.setBaseImage(Base64.getEncoder().encodeToString(BaseFileContent));
byte[] CaptureFileContent = Files.readAllBytes(Paths.get(image2));
reportDto.setCaptureImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setUrl(preset.getPresetUrl());
reportDto.setAlarmImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setCameraName(camera.getName());
reportDto.setCameraId(camera.getId());
reportDto.setAlarmType(AlarmReportEnums.DISPLACEMENT_ALARM.getAlarmCode());
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
forwardService.enrichAndForward(preset.getPresetUrl(),reportDto);
}catch (Exception e)
{
log.error("移位报警上报失败,摄像头 ID: {}, 相机编号:{}",

@ -264,7 +264,7 @@ public class BlurDetectorV4 {
}
double ratio = scoreRef / scoreImg;
System.out.println("scoreImg: " + scoreImg + ", scoreRef: " + scoreRef);
DetectResult finalResult;
if (ratio < ratioThreshold) {

@ -0,0 +1,136 @@
package com.sz.admin.monitor.utils;
import org.im4java.core.IMOperation;
import org.im4java.core.ImageCommand;
import java.io.File;
import java.util.UUID;
public class BlurrinessComparator {
// 强制指定你的 IM7 安装路径
private static final String IM_PATH = "D:\\work\\imageMagic\\ImageMagick-7.1.2-Q16-HDRI";
public static void main(String[] args) {
String imagePath1 = "D:\\work\\images\\CAPTURE_258_20260320093827912_bcf0b101.jpg"; // 基准图 (清晰图标准)
String imagePath2 = "D:\\work\\images\\CAPTURE_258_20260319160020647_ffbc4f66.jpg"; // 当前图 (待检测图)
try {
//Result{type='blur_detection', value='1', code='BLURRY', desc='检测到图片模糊',
// conf=1.0, score=580.00, ratio=0.47}
DetectResult result = compareBlurriness(imagePath1, imagePath2);
// 打印最终返回的标准结果对象
System.out.println("检测完成,返回结果: " + result.toString());
} catch (Exception e) {
System.err.println("处理图片时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
/**
*
*/
public static class DetectResult {
public String type; // 检测类型
public String value; // 结果值 (1: 模糊, 0: 正常)
public String code; // 结果代码
public String desc; // 描述信息
public double conf; // 置信度 (此处默认给 1.0)
public double score; // 得分 (此处记录目标图压缩后的大小)
public double ratio; // 比例 (目标图大小 / 基准图大小)
public DetectResult(String type, String value, String code, String desc, double conf, double score, double ratio) {
this.type = type;
this.value = value;
this.code = code;
this.desc = desc;
this.conf = conf;
this.score = score;
this.ratio = ratio;
}
@Override
public String toString() {
return String.format("Result{type='%s', value='%s', code='%s', desc='%s', conf=%.1f, score=%.2f, ratio=%.2f}",
type, value, code, desc, conf, score, ratio);
}
}
/**
* DetectResult
*/
public static DetectResult compareBlurriness(String path1, String path2) {
System.out.println("正在处理ImageMagick 缩放压缩 -> 计算文件体积...");
// 注意:尺寸设为 64x48 非常小,可能会导致压缩后体积差异微弱。如果效果不明显建议调大(如 800x600)。
long size1 = getProcessedImageSize(path1, 64, 48, 80.0);
long size2 = getProcessedImageSize(path2, 64, 48, 80.0);
System.out.println("图片1大小: " + size1 + " 字节, 图片2大小: " + size2 + " 字节");
// x为图片的大小 y = 90.733x - 35.474;
// 90.733*1.234-35.474 = 76.490522
// 90.733*0.58-35.474 = 17.15114
// 90.733*0.695-35.474 = 27.585435
// size1/size2
// size1>size2 则出现了模糊,因为特征点变小了,体积也变小了
// 计算体积比例 (防止除以 0 的异常)
double ratio = (size1 == 0) ? 1.0 : (double) size2 / size1;
String value;
String code;
String desc;
// 判断逻辑:当前图体积小于基准图,说明丢失了细节,判定为模糊
if (size2 < size1) {
value = "1"; // 模糊 Value 为 1
code = "BLURRY";
desc = "检测到图片模糊";
} else {
value = "0"; // 正常 Value 为 0
code = "NORMAL";
desc = "图片清晰度正常";
}
// 构造并返回标准实体类
// type: 设为 "blur_detection"
// conf: 置信度设为 1.0
// score: 存入 size2 (目标图片的压缩字节数) 作为参考得分
return new DetectResult("blur_detection", value, code, desc, 1.0, size2, ratio);
}
/**
* ImageMagick
*/
private static long getProcessedImageSize(String imagePath, int width, int height, double quality) {
// 生成一个临时文件路径,用于存放 ImageMagick 处理后的图片
String tempImagePath = System.getProperty("java.io.tmpdir") + File.separator + "im_temp_" + UUID.randomUUID() + ".jpg";
File tempFile = new File(tempImagePath);
try {
// 构建 ImageMagick 压缩指令
IMOperation op = new IMOperation();
op.addImage(imagePath);
op.strip(); // 剥离 EXIF 等多余元数据信息
op.resize(width, height); // 缩放为标准大小
op.quality(quality); // 设置 JPEG 压缩质量
op.addImage(tempImagePath); // 输出到临时文件
// 执行命令
ImageCommand cmd = new ImageCommand("magick");
cmd.setSearchPath(IM_PATH);
cmd.run(op);
// 获取临时文件的体积 (字节)
if (!tempFile.exists()) {
throw new RuntimeException("ImageMagick 未能成功生成临时文件!");
}
return tempFile.length();
} catch (Exception e) {
throw new RuntimeException("ImageMagick 处理流水线执行失败", e);
} finally {
// 务必清理临时文件
if (tempFile.exists()) {
tempFile.delete();
}
}
}
}

@ -28,7 +28,7 @@ import static org.bytedeco.opencv.global.opencv_video.calcOpticalFlowFarneback;
/**
* ClassName: RobustImageMatcherUtil
* Package: com.sz.admin.monitor.utils
* Description:
* Description:
*/
@Slf4j
public class RobustImageMatcherUtil {
@ -321,7 +321,6 @@ public class RobustImageMatcherUtil {
private static Map<String, Object> formatResult(MatchResult result, Mat img1, Mat img2) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("type", "tx_yzwpy");
if (!result.success || result.confidence < 0.2) {
log.warn("图像配准失败或置信度过低");
map.put("value", 1);
@ -388,10 +387,16 @@ public class RobustImageMatcherUtil {
Mat[] preprocessed = preprocess(smallImg1, smallImg2);
Mat gray1 = preprocessed[0];
Mat gray2 = preprocessed[1];
Map<String, Object> map = new LinkedHashMap<>();
map.put("type", "tx_yzwpy");
if (!isSameScene(gray1, gray2)) {
log.warn("图像差异过大,非同一场景: path1={}, path2={}", path1, path2);
return null;
map.put("value", 1);
map.put("code", "4000");
OffsetVO offsetVO = new OffsetVO();
offsetVO.setDescription("图像存在偏移");
map.put("description", JsonUtils.toJsonString(offsetVO));
return map;
}
MatchResult flowResult = calcOpticalFlow(gray1, gray2);
@ -416,8 +421,8 @@ public class RobustImageMatcherUtil {
// String img1Path = "D:\\work\\images\\CAPTURE_118_20260306140433490_7c323ce1.jpg";
// String img2Path = "D:\\work\\images\\CAPTURE_118_20260306140555121_0a31347e.jpg";
// 出现移位的图片 value 1
String img1Path="D:\\work\\images\\CAPTURE_118_20260306140328178_7f050439.jpg";
String img2Path="D:\\work\\images\\CAPTURE_118_20260306140433490_7c323ce1.jpg";
String img1Path = "D:\\work\\temp\\history_20260324_180000.jpg";
String img2Path = "D:\\work\\temp\\history_20260324_190000.jpg";
Map<String, Object> map = RobustImageMatcherUtil.calculateOffset(img1Path, img2Path);
System.out.println(map);
}

@ -2,24 +2,41 @@ package com.sz.admin.monitor.utils;
import java.io.File;
/**
* ClassName: UrlConvert
* Package: com.sz.admin.monitor.utils
* Description:
*/
public class UrlConvert {
private static final String RESOURCE_PREFIX = "/save/";
// 定义我们配置的两个静态资源根目录 (注意统一用正斜杠 / )
private static final String BASE_DIR_IMAGES = "D:/work/images/";
private static final String BASE_DIR_TEMP = "D:/work/temp/";
/**
*
* @param localPath D:\work\sz-admin\images\test.jpg
* @return /save/test.jpg
* @param localPath : D:\work\temp\task_123\test.jpg
* @return /save/task_123/test.jpg
*/
public static String convertToUrl(String localPath) {
if (localPath == null || localPath.isEmpty()) {
return null;
}
String fileName = new File(localPath).getName();
// 2. 拼接映射前缀
return RESOURCE_PREFIX + fileName;
// 1. 统一路径分隔符:将 Windows 的反斜杠 \ 全部替换为标准的正斜杠 /
String normalizedPath = localPath.replace("\\", "/");
String relativePath = "";
// 2. 剥离根目录,保留后面的子文件夹和文件名
if (normalizedPath.startsWith(BASE_DIR_IMAGES)) {
// 截取掉 D:/work/images/ 这一部分
relativePath = normalizedPath.substring(BASE_DIR_IMAGES.length());
} else if (normalizedPath.startsWith(BASE_DIR_TEMP)) {
// 截取掉 D:/work/temp/ 这一部分,保留 task_123/test.jpg
relativePath = normalizedPath.substring(BASE_DIR_TEMP.length());
} else {
// 兜底方案:如果传进来的路径不在这两个目录里,退回到只取文件名
relativePath = new File(localPath).getName();
}
// 3. 拼接映射前缀
return RESOURCE_PREFIX + relativePath;
}
}

@ -98,6 +98,7 @@ public class ZLMediaKitUtils {
public JSONObject sendPost(String api, Map<String, Object> param, RequestCallback callback) {
OkHttpClient okHttpClient = getClient();
String url = String.format(MediaConstant.HTTP_MEDIA_URL, ip, port, api);
log.info("url:{}", url);
JSONObject responseJSON = new JSONObject();
responseJSON.put("code", CommonResponseEnum.MEDIA_HTTP_FAIL.getCode());
responseJSON.put("msg", CommonResponseEnum.MEDIA_HTTP_FAIL.getMessage());

Loading…
Cancel
Save