修复bug

dev_xq_0.0.1-2
刘政 2 weeks ago
parent 9c2f796334
commit c1b99b4d84

@ -1,12 +1,19 @@
package com.sz; package com.sz;
import com.sz.admin.monitor.service.CameraService;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@ -15,12 +22,14 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling @EnableScheduling
public class AdminApplication { public class AdminApplication {
private static final Logger log = LoggerFactory.getLogger(AdminApplication.class);
@Value("${app.version}") @Value("${app.version}")
private String appVersion; private String appVersion;
@Getter @Getter
private static String version; private static String version;
@PostConstruct @PostConstruct
public void init() { public void init() {
setVersion(appVersion); // 通过辅助方法设置静态字段 setVersion(appVersion); // 通过辅助方法设置静态字段
@ -45,4 +54,6 @@ public class AdminApplication {
System.out.println(result); System.out.println(result);
} }
} }

@ -0,0 +1,28 @@
package com.sz.admin.monitor.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class CameraThreadPoolConfig {
@Bean("cameraInspectionExecutor")
public Executor cameraInspectionExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数:
executor.setCorePoolSize(10);
// 最大线程数:并发峰值
executor.setMaxPoolSize(50);
// 队列容量:如果超过核心线程数的任务,先放进队列排队
executor.setQueueCapacity(200);
// 线程前缀名,方便以后看日志排查问题
executor.setThreadNamePrefix("Camera-Inspect-");
// 拒绝策略:如果队列满了,由调用者所在线程(即定时任务主线程)自己去执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

@ -13,7 +13,8 @@ public enum AlarmReportEnums {
FIRE_ALARM("Fire",2,"明烟明火报警"), FIRE_ALARM("Fire",2,"明烟明火报警"),
FACE_ID_ALARM("FaceId",3,"人脸识别报警"), FACE_ID_ALARM("FaceId",3,"人脸识别报警"),
CAPTURE_FACE_ALARM("CaptureFace", 4, "抓拍到人脸报警"), CAPTURE_FACE_ALARM("CaptureFace", 4, "抓拍到人脸报警"),
HELMET_ALARM("NoHelmet", 5, "未戴安全帽报警"); HELMET_ALARM("NoHelmet", 5, "未戴安全帽报警"),
BLUR_ALARM("Blur", 6, "模糊报警");
private final String alarmType; private final String alarmType;
private final Integer alarmCode; private final Integer alarmCode;
private final String alarmDescription; private final String alarmDescription;

@ -33,4 +33,5 @@ public class BlurDetectionUpdateDTO {
private String url; private String url;
} }

@ -44,6 +44,7 @@ public class BlurDetection implements Serializable {
*/ */
private Integer enable; private Integer enable;
/** /**
* url * url
*/ */

@ -50,7 +50,7 @@ public class CameraAlarm implements Serializable {
@Schema(description = "告警区域id") @Schema(description = "告警区域id")
private Long alarmAreaId; private Long alarmAreaId;
@Schema(description = "报警类型: 1-移位, 2-明烟明火报警,3-人脸识别,4-脸部抓拍,5-安全帽检测") @Schema(description = "报警类型: 1-移位, 2-明烟明火报警,3-人脸识别,4-脸部抓拍,5-安全帽检测,6-模糊")
private Integer alarmType; private Integer alarmType;
@Schema(description = "基准图路径 ") @Schema(description = "基准图路径 ")

@ -42,6 +42,8 @@ public class BlurDetectionVO {
*/ */
private String url; private String url;
/** /**
* *
*/ */

@ -26,4 +26,6 @@ public interface CameraSnapshotService extends IService<CameraSnapshot> {
List<CameraSnapshotVO> getExistSnapshotList(Long id); List<CameraSnapshotVO> getExistSnapshotList(Long id);
void deleteByCameraIdAndType(Long cameraId, int i); void deleteByCameraIdAndType(Long cameraId, int i);
CameraSnapshotVO selectByCameraIdAndType(Long cameraId, int i);
} }

@ -9,7 +9,9 @@ import com.sz.admin.monitor.pojo.dto.blurDetection.BlurDetectionDTO;
import com.sz.admin.monitor.pojo.po.BlurDetection; import com.sz.admin.monitor.pojo.po.BlurDetection;
import com.sz.admin.monitor.service.BlurDetectionService; import com.sz.admin.monitor.service.BlurDetectionService;
import com.sz.admin.monitor.service.CameraSnapshotService; import com.sz.admin.monitor.service.CameraSnapshotService;
import com.sz.core.common.exception.common.BusinessException;
import com.sz.core.util.BeanCopyUtils; import com.sz.core.util.BeanCopyUtils;
import com.sz.platform.enums.AdminResponseEnum;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -31,7 +33,6 @@ public class BlurDetectionServiceImpl extends ServiceImpl<BlurDetectionMapper, B
@Transactional @Transactional
public Boolean saveOrUpdateBlur(BlurDetectionDTO blurDetectionDTO) { public Boolean saveOrUpdateBlur(BlurDetectionDTO blurDetectionDTO) {
Long id = blurDetectionDTO.getId(); Long id = blurDetectionDTO.getId();
Long cameraId = blurDetectionDTO.getCameraId();
Integer enable = blurDetectionDTO.getEnable(); Integer enable = blurDetectionDTO.getEnable();
if(ObjUtil.isNotNull(id)) if(ObjUtil.isNotNull(id))
{ {
@ -46,15 +47,18 @@ public class BlurDetectionServiceImpl extends ServiceImpl<BlurDetectionMapper, B
.update(); .update();
}else { }else {
// 删除之前的基准图。抓拍一张最新的基准图保存到数据库 // 删除之前的基准图。抓拍一张最新的基准图保存到数据库
cameraSnapshotService.deleteByCameraIdAndType(cameraId,4); // cameraSnapshotService.deleteByCameraIdAndType(cameraId,4);
cameraSnapshotService.captureAndSave(cameraId, 4); // cameraSnapshotService.captureAndSave(cameraId, 4);
// 保存数据库 // 保存数据库
blurDetectionMapper.update(BeanCopyUtils.copy(blurDetectionDTO, BlurDetection.class)); blurDetectionMapper.update(BeanCopyUtils.copy(blurDetectionDTO, BlurDetection.class));
} }
}else { }else {
// 新增 // 新增
cameraSnapshotService.captureAndSave(cameraId, 4); // cameraSnapshotService.captureAndSave(cameraId, 4);
blurDetectionMapper.insert(BeanCopyUtils.copy(blurDetectionDTO, BlurDetection.class)); BlurDetection detection = BeanCopyUtils.copy(blurDetectionDTO, BlurDetection.class);
detection.setCreateTime(LocalDateTime.now());
detection.setUpdateTime(LocalDateTime.now());
blurDetectionMapper.insert(detection);
} }
return true; return true;
} }

@ -92,6 +92,15 @@ public class CameraSnapshotServiceImpl extends ServiceImpl<CameraSnapshotMapper,
} }
} }
@Override
public CameraSnapshotVO selectByCameraIdAndType(Long cameraId, int i) {
QueryWrapper wrapper = QueryWrapper.create().select().from(CameraSnapshot.class)
.where(CameraSnapshot::getCameraId).eq(cameraId)
.and(CameraSnapshot::getCheckType).eq(i);
CameraSnapshot cameraSnapshot = cameraSnapshotMapper.selectOneByQuery(wrapper);
return BeanCopyUtils.copy(cameraSnapshot, CameraSnapshotVO.class);
}
private void deleteSnapshotByChannelIdAndPresetIndex(Long id, int presetIndex) { private void deleteSnapshotByChannelIdAndPresetIndex(Long id, int presetIndex) {
QueryWrapper wrapper = QueryWrapper.create().select().from(CameraSnapshot.class) QueryWrapper wrapper = QueryWrapper.create().select().from(CameraSnapshot.class)

@ -8,25 +8,18 @@ import com.sz.admin.monitor.mapper.CameraAlarmMapper;
import com.sz.admin.monitor.mapper.CameraMapper; import com.sz.admin.monitor.mapper.CameraMapper;
import com.sz.admin.monitor.mapper.NvrMapper; import com.sz.admin.monitor.mapper.NvrMapper;
import com.sz.admin.monitor.pojo.dto.edgebox.BoxAlarmReportDto; import com.sz.admin.monitor.pojo.dto.edgebox.BoxAlarmReportDto;
import com.sz.admin.monitor.pojo.po.Camera; import com.sz.admin.monitor.pojo.po.*;
import com.sz.admin.monitor.pojo.po.CameraAlarm;
import com.sz.admin.monitor.pojo.po.Nvr;
import com.sz.admin.monitor.pojo.po.Preset;
import com.sz.admin.monitor.pojo.vo.camerasnapshot.CameraSnapshotVO; import com.sz.admin.monitor.pojo.vo.camerasnapshot.CameraSnapshotVO;
import com.sz.admin.monitor.sdk.ManageNVR; import com.sz.admin.monitor.sdk.ManageNVR;
import com.sz.admin.monitor.service.CameraService; import com.sz.admin.monitor.service.*;
import com.sz.admin.monitor.service.CameraSnapshotService; import com.sz.admin.monitor.utils.*;
import com.sz.admin.monitor.service.ForwardService;
import com.sz.admin.monitor.service.PresetService;
import com.sz.admin.monitor.utils.ImageNameUtils;
import com.sz.admin.monitor.utils.OffsetCalculator;
import com.sz.admin.monitor.utils.RobustImageMatcherUtil;
import com.sz.admin.system.pojo.dto.sysmessage.Message; import com.sz.admin.system.pojo.dto.sysmessage.Message;
import com.sz.admin.system.service.SysMessageService; import com.sz.admin.system.service.SysMessageService;
import com.sz.core.common.entity.SocketMessage; import com.sz.core.common.entity.SocketMessage;
import com.sz.core.common.entity.TransferMessage; import com.sz.core.common.entity.TransferMessage;
import com.sz.core.common.enums.MessageTransferScopeEnum; import com.sz.core.common.enums.MessageTransferScopeEnum;
import com.sz.core.common.enums.SocketChannelEnum; import com.sz.core.common.enums.SocketChannelEnum;
import com.sz.core.util.JsonUtils;
import com.sz.redis.WebsocketRedisService; import com.sz.redis.WebsocketRedisService;
import com.sz.security.core.util.LoginUtils; import com.sz.security.core.util.LoginUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -44,6 +37,8 @@ import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/** /**
* ClassName: ScheduleTask * ClassName: ScheduleTask
@ -71,7 +66,10 @@ public class ScheduledTask {
private SysMessageService sysMessageService; private SysMessageService sysMessageService;
@Resource @Resource
private ForwardService forwardService; private ForwardService forwardService;
@Resource
private BlurDetectionService blurDetectionService;
@Resource(name = "cameraInspectionExecutor")
private Executor cameraInspectionExecutor;
// @Scheduled(cron = "0/5 * * * * ?") // @Scheduled(cron = "0/5 * * * * ?")
public void test() public void test()
@ -89,10 +87,192 @@ public class ScheduledTask {
msg.setToPushAll(true); msg.setToPushAll(true);
websocketRedisService.sendServiceToWs(msg); websocketRedisService.sendServiceToWs(msg);
} }
// 定期模糊检测
// @Scheduled(cron = "0/20 * * * * ?")
public void executeFuzzyDetection() {
log.info("执行模糊检测");
// 遍历模糊检测表
List<BlurDetection> blurDetectionList = blurDetectionService.list();
// 过滤掉关闭的状态
blurDetectionList = blurDetectionList.stream().filter(blurDetection -> blurDetection.getEnable() == 1).toList();
if (blurDetectionList.isEmpty()) {
log.info("无启用的模糊检测任务");
return;
}
int count = 0;
for (BlurDetection blurDetection : blurDetectionList) {
Long cameraId = blurDetection.getCameraId();
try {
Camera camera = cameraService.getById(cameraId);
// 获取基准图
String imagePath1 = camera.getHomeImagePath();
// 检查基准图是否存在
boolean isExists = true;
if (imagePath1 == null || imagePath1.trim().isEmpty()) {
log.warn("摄像头[{}] 未设置基准图路径", cameraId);
isExists = false;
} else {
File file1 = new File(imagePath1);
if (!file1.exists() || !file1.isFile()) {
log.error("基准图文件物理路径不存在:{}", imagePath1);
isExists = false;
}
}
// 抓取当前图
String filename = ImageNameUtils.generateFileName("CAPTURE", cameraId);
String imagePath2 = manageNVR.capturePic(cameraId.intValue(), filename);
if (imagePath2 == null || imagePath2.isEmpty()) {
log.error("抓拍图片失败,摄像头 ID: {}", cameraId);
continue;
}
// 检查抓拍图是否存在
File file2 = new File(imagePath2);
if (!file2.exists() || !file2.isFile()) {
log.error("抓拍图不存在:{}", imagePath2);
continue;
}
count++;
// 进行单一背景检测
BackgroundDetector.DetectResult result = BackgroundDetector.detectBackground(imagePath2);
if("1".equals(result.value))
{
// 当前摄像头面对的是白墙,直接进行报警
handleAnalysisResult(blurDetection, imagePath2, result);
continue;
}
// 进行模糊图片对比
BlurDetectorV4.DetectResult detectResult = null;
if (isExists) {
// 基准图存在,使用双图对比功能
detectResult = BlurDetectorV4.doubleDetectBlur(imagePath1, imagePath2);
} else {
// 基准图不存在,使用单图对比功能
detectResult = BlurDetectorV4.detectBlur(imagePath2);
}
log.info("模糊检测结果:{}", detectResult);
handleAnalysisResult(blurDetection, imagePath1, imagePath2, detectResult, isExists);
log.info("模糊检测完成,共检测{}个摄像头", count);
} catch (Exception e) {
log.error("摄像头 ID [{}] 模糊检测过程中发生未知异常", cameraId, e);
}
}
}
//Result{type='bpmh', value='1', code='2000', desc='模糊 score=39.82', conf=1.0, score=39.82, ratio=0.00}
public record DetectResult(String desc,double score) {
}
// 分析背景检测结果并且报警
private void handleAnalysisResult(BlurDetection blurDetection, String imagePath, BackgroundDetector.DetectResult result) throws IOException {
Camera camera = cameraService.getById(blurDetection.getCameraId());
CameraAlarm cameraAlarm = new CameraAlarm();
cameraAlarm.setCameraId(camera.getId());
Nvr nvr = nvrMapper.selectOneById(camera.getNvrId());
cameraAlarm.setAlarmType(6);
cameraAlarm.setAlarmAreaId(nvr.getStationId());
cameraAlarm.setChannelId(camera.getChannelId());
cameraAlarm.setCameraNo(camera.getCameraNo());
cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setCaptureImage(imagePath);
DetectResult backgroundResult = new DetectResult(result.desc, result.score);
cameraAlarm.setAlgoResult(JsonUtils.toJsonString(result));
cameraAlarm.setStatus(0);
cameraAlarmMapper.insert(cameraAlarm);
SocketMessage bean = new SocketMessage();
Map<String, Object> data = new HashMap<>();
data.put("title", "摄像头报警");
data.put("content", "摄像头[" + camera.getId() + "]发生模糊,请及时处理!");
bean.setData(JSON.toJSONString(data));
bean.setChannel(SocketChannelEnum.MESSAGE);
bean.setScope(MessageTransferScopeEnum.SOCKET_CLIENT);
TransferMessage msg = new TransferMessage();
msg.setMessage(bean);
msg.setFromUser("system");
msg.setToPushAll(true);
websocketRedisService.sendServiceToWs(msg);
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.now());
reportDto.setAlarmReportType(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
reportDto.setDescription("摄像头拍摄的图片出现模糊");
byte[] CaptureFileContent = Files.readAllBytes(Paths.get(imagePath));
reportDto.setCaptureImage(Base64.getEncoder().encodeToString(CaptureFileContent));
reportDto.setUrl(blurDetection.getUrl());
reportDto.setCameraName(camera.getName());
reportDto.setCameraId(camera.getId());
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
}catch (Exception e)
{
log.error("模糊报警上报失败,摄像头 ID: {}, 相机编号:{}",
camera.getId(), camera.getCameraNo(), e);
}
}
// 分析模糊算法并且报警
private void handleAnalysisResult(BlurDetection blurDetection, String imagePath1, String imagePath2, BlurDetectorV4.DetectResult detectResult, boolean isExists) throws IOException {
if ("1".equals(detectResult.value)) {
// 发生了模糊,需要保存报警信息等人工处理
Camera camera = cameraService.getById(blurDetection.getCameraId());
CameraAlarm cameraAlarm = new CameraAlarm();
cameraAlarm.setCameraId(camera.getId());
// 设置告警区域
// 根据摄像头查询它对应的nvr
Nvr nvr = nvrMapper.selectOneById(camera.getNvrId());
// 设置告警区域
cameraAlarm.setAlarmType(6);
cameraAlarm.setAlarmAreaId(nvr.getStationId());
cameraAlarm.setChannelId(camera.getChannelId());
cameraAlarm.setCameraNo(camera.getCameraNo());
cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setBaseImage(imagePath1);
cameraAlarm.setCaptureImage(imagePath2);
// 算法的结果
DetectResult result = new DetectResult(detectResult.desc, detectResult.score);
cameraAlarm.setAlgoResult(JsonUtils.toJsonString(result));
// 状态为未处理
cameraAlarm.setStatus(0);
cameraAlarmMapper.insert(cameraAlarm);
// log.info("生成模糊报警: 通道{}, 模糊信息: {}", camera.getChannelId(), detectResult.getDescription());
// 向前端主动推送消息
SocketMessage bean = new SocketMessage();
Map<String, Object> data = new HashMap<>();
data.put("title", "摄像头报警");
data.put("content", "摄像头[" + camera.getId() + "]发生模糊,请及时处理!");
bean.setData(JSON.toJSONString(data));
bean.setChannel(SocketChannelEnum.MESSAGE);
bean.setScope(MessageTransferScopeEnum.SOCKET_CLIENT);
TransferMessage msg = new TransferMessage();
msg.setMessage(bean);
msg.setFromUser("system");
msg.setToPushAll(true);
websocketRedisService.sendServiceToWs(msg);
// 统一处理报警信息后,将摄像头信息+报警信息上报给别人服务器
BoxAlarmReportDto reportDto = new BoxAlarmReportDto();
reportDto.setAlarmReportTime(LocalDateTime.now());
reportDto.setAlarmReportType(AlarmReportEnums.BLUR_ALARM.getAlarmDescription());
reportDto.setDescription("摄像头拍摄的图片出现模糊");
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.setCameraName(camera.getName());
reportDto.setCameraId(camera.getId());
reportDto.setCameraNo(camera.getCameraNo());
try {
forwardService.enrichAndForward(reportDto);
}catch (Exception e)
{
log.error("模糊报警上报失败,摄像头 ID: {}, 相机编号:{}",
camera.getId(), camera.getCameraNo(), e);
}
}
}
// 定期检查 // 定期检查
@Scheduled(cron = "0/20 * * * * ?") @Scheduled(cron = "0/20 * * * * ?")
@SneakyThrows public void executeInspection() throws InterruptedException {
public void executeInspection() {
// 从摄像头表中获取设置了抓图对比和在线的摄像头 // 从摄像头表中获取设置了抓图对比和在线的摄像头
List<Camera> cameraList = cameraService.selectListByInspection(); List<Camera> cameraList = cameraService.selectListByInspection();
// 然后根据摄像头id查询设置为标准位的预置位 // 然后根据摄像头id查询设置为标准位的预置位
@ -104,36 +284,53 @@ public class ScheduledTask {
} }
List<Preset> presetList = presetService.selectByCameraIds(cameraIds); List<Preset> presetList = presetService.selectByCameraIds(cameraIds);
for (Preset preset : presetList) { for (Preset preset : presetList) {
// 调用SDK让摄像头回归到该预置位 CompletableFuture.runAsync(() -> {
boolean ok = manageNVR.ptzPresets(preset.getCameraId().intValue(), 39, String.valueOf(preset.getPresetId()), preset.getPresetName()); // 调用具体的单次检测逻辑
// 延时5秒 processSinglePreset(preset);
}, cameraInspectionExecutor).exceptionally(e -> {
// 捕获线程池级别或执行过程中的严重异常
log.error("预置位[{}]异步巡检任务执行失败", preset.getPresetId(), e);
return null;
});
}
}
private void processSinglePreset(Preset preset) {
Long cameraId = preset.getCameraId();
try {
boolean ok = manageNVR.ptzPresets(cameraId.intValue(), 39, String.valueOf(preset.getPresetId()), preset.getPresetName());
// 这里的 sleep 现在是在独立的异步线程中执行的,不会阻塞主线程!
Thread.sleep(5000); Thread.sleep(5000);
if (ok) { if (ok) {
// 抓取一张图片 String filename = ImageNameUtils.generateFileName("CAPTURE", cameraId);
String filename = ImageNameUtils.generateFileName("CAPTURE",preset.getCameraId()); String image2 = manageNVR.capturePic(cameraId.intValue(), filename);
//String image2 = "D:\\work\\images\\CAPTURE_367_20260212092406664_2d117293.jpg"; // String image2="D:\\work\\images\\CAPTURE_258_20260320170313838_4680425d.jpg";
// String image2 = "D:\\work\\images\\CAPTURE_19_20260302152457977_db33f5a8.jpg"; // 抓图合法性校验
String image2 = manageNVR.capturePic(preset.getCameraId().intValue(), filename); if (image2 == null || image2.isBlank()) {
// 从图片表中根据预置位摄像机id类型为巡检为条件查询出基准图 log.error("摄像机[{}] 抓拍失败,无法获取当前图片", cameraId);
CameraSnapshotVO cameraSnapshotVO = cameraSnapshotService.selectByCameraIdAndPresetIndex(preset.getCameraId(), preset.getPresetId()); return; // 结束当前预置位的处理
if (cameraSnapshotVO == null || cameraSnapshotVO.getImagePath() == null) { }
log.warn("摄像机[{}] 未设置基准图,无法对比", preset.getCameraId()); CameraSnapshotVO cameraSnapshotVO = cameraSnapshotService.selectByCameraIdAndPresetIndex(cameraId, preset.getPresetId());
continue; if (cameraSnapshotVO == null || cameraSnapshotVO.getImagePath() == null || cameraSnapshotVO.getImagePath().isBlank()) {
log.warn("摄像机[{}] 未设置预置位基准图,无法对比", cameraId);
return;
} }
// 调用图片对比算法,校验摄像头是否移位
// 基准图
String image1 = cameraSnapshotVO.getImagePath(); String image1 = cameraSnapshotVO.getImagePath();
// 对比
Map<String, Object> map = RobustImageMatcherUtil.calculateOffset(image1, image2); Map<String, Object> map = RobustImageMatcherUtil.calculateOffset(image1, image2);
log.info("图片对比结果:{}", map); log.info("摄像机[{}] 预置位[{}] 对比结果:{}", cameraId, preset.getPresetId(), map);
// 判断结果并报警 if (map != null && map.containsKey("value")) {
handleAnalysisResult(preset, cameraSnapshotVO.getImagePath(), image2, map); handleAnalysisResult(preset, image1, image2, map);
}
} }
} catch (InterruptedException ie) {
log.error("摄像机[{}] 等待云台转动被中断", cameraId);
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("摄像机[{}] 预置位[{}] 巡检过程中发生业务异常", cameraId, preset.getPresetId(), e);
} }
} }
//判断结果并报警 //判断移位算法结果并报警
private void handleAnalysisResult(Preset preset, String imagePath, String image2, Map<String, Object> map) throws IOException { private void handleAnalysisResult(Preset preset, String imagePath, String image2, Map<String, Object> map) throws IOException {
int value = (int) map.get("value"); int value = (int) map.get("value");
if (value == 1) { if (value == 1) {
@ -147,6 +344,7 @@ public class ScheduledTask {
// 设置告警区域 // 设置告警区域
cameraAlarm.setAlarmAreaId(nvr.getStationId()); cameraAlarm.setAlarmAreaId(nvr.getStationId());
cameraAlarm.setChannelId(camera.getChannelId()); cameraAlarm.setChannelId(camera.getChannelId());
cameraAlarm.setCameraNo(camera.getCameraNo());
cameraAlarm.setCameraName(camera.getName()); cameraAlarm.setCameraName(camera.getName());
cameraAlarm.setPresetIndex(preset.getPresetId()); cameraAlarm.setPresetIndex(preset.getPresetId());
// 标记为移位报警 // 标记为移位报警
@ -185,7 +383,13 @@ public class ScheduledTask {
reportDto.setCameraName(camera.getName()); reportDto.setCameraName(camera.getName());
reportDto.setCameraId(camera.getId()); reportDto.setCameraId(camera.getId());
reportDto.setCameraNo(camera.getCameraNo()); reportDto.setCameraNo(camera.getCameraNo());
forwardService.enrichAndForward(reportDto); try {
forwardService.enrichAndForward(reportDto);
}catch (Exception e)
{
log.error("移位报警上报失败,摄像头 ID: {}, 相机编号:{}",
camera.getId(), camera.getCameraNo(), e);
}
} }
} }

@ -0,0 +1,234 @@
package com.sz.admin.monitor.utils;
import org.bytedeco.javacpp.indexer.FloatIndexer;
import org.bytedeco.javacpp.indexer.IntIndexer;
import org.bytedeco.javacpp.indexer.UByteIndexer;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.MatVector;
import org.bytedeco.opencv.opencv_core.Size;
import org.bytedeco.opencv.opencv_core.TermCriteria;
import java.util.HashMap;
import java.util.Map;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class BackgroundDetector {
// ===============================
// 统一的对外返回结果类 (与 V3/V4 一致)
// ===============================
public static class DetectResult {
public String type;
public String value;
public String code;
public String desc;
public double conf;
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);
}
}
// ===============================
// 内部使用的算法基础数据封装
// ===============================
private static class InternalDetectionResult {
public boolean hasLargeBackground;
public double continuousBackgroundRatio;
public double totalMaxLabelRatio;
public Map<String, Integer> mainBackgroundColor;
}
// ===============================
// 对外暴露的公共访问方法 (默认参数)
// ===============================
public static DetectResult detectBackground(String imagePath) {
// 默认参数设定
int nClusters = 3;
double areaThreshold = 0.55;
int kernelSize = 5;
boolean useGray = false;
double clusterThreshold = 0.55;
return detectBackground(imagePath, nClusters, areaThreshold, kernelSize, useGray, clusterThreshold);
}
// ===============================
// 对外暴露的公共访问方法 (支持自定义参数)
// ===============================
public static DetectResult detectBackground(String imagePath, int nClusters, double areaThreshold,
int kernelSize, boolean useGray, double clusterThreshold) {
Mat img = imread(imagePath);
if (img == null || img.empty()) {
throw new IllegalArgumentException("无法读取图片或图片不存在: " + imagePath);
}
try {
// 调用核心算法
InternalDetectionResult internalResult = detectLargeContinuousBackground(
img, nClusters, areaThreshold, kernelSize, useGray, clusterThreshold);
// 将连续背景占比转化为 0-100 的 score 形式便于理解
double score = internalResult.continuousBackgroundRatio * 100.0;
// 拼接详细描述信息
String desc = String.format("连续背景占比:%.2f%%, 最大聚类占比:%.2f%%, 主颜色:%s",
score,
internalResult.totalMaxLabelRatio * 100.0,
internalResult.mainBackgroundColor.toString());
String detectTypeKey = "background";
if (internalResult.hasLargeBackground) {
// value="1" 表示检测出大片连续背景 (异常状态)code 给 4001
return new DetectResult(detectTypeKey, "1", "4001", "大片连续/纯色背景 [" + desc + "]", 1.0, score, 0.0);
} else {
// value="0" 表示正常图片
return new DetectResult(detectTypeKey, "0", "2000", "正常背景 [" + desc + "]", 1.0, score, 0.0);
}
} finally {
// 确保释放读取的图片内存
img.close();
}
}
// ===============================
// 核心算法实现:基于颜色聚类+连通域
// ===============================
private static InternalDetectionResult detectLargeContinuousBackground(
Mat img, int nClusters, double areaThreshold, int kernelSize, boolean useGray, double clusterThreshold) {
int h = img.rows();
int w = img.cols();
int totalPixels = h * w;
Mat processed = new Mat();
Mat reshaped32f = new Mat();
// 1. 预处理
if (useGray) {
cvtColor(img, processed, COLOR_BGR2GRAY);
Mat kernel = getStructuringElement(MORPH_RECT, new Size(kernelSize, kernelSize));
morphologyEx(processed, processed, MORPH_OPEN, kernel);
} else {
GaussianBlur(img, processed, new Size(3, 3), 0);
}
// 重塑为K-Means输入格式
Mat reshaped = processed.reshape(1, totalPixels);
reshaped.convertTo(reshaped32f, CV_32F);
// 2. K-Means颜色聚类
Mat labels = new Mat();
Mat centers = new Mat();
TermCriteria criteria = new TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 10, 1.0);
kmeans(reshaped32f, nClusters, labels, criteria, 10, KMEANS_PP_CENTERS, centers);
// 3. 统计最大类
int[] labelCounts = new int[nClusters];
IntIndexer labelIndexer = labels.createIndexer();
for (int i = 0; i < totalPixels; i++) {
int label = labelIndexer.get(i, 0);
labelCounts[label]++;
}
int maxLabel = 0;
int maxCount = 0;
for (int i = 0; i < nClusters; i++) {
if (labelCounts[i] > maxCount) {
maxCount = labelCounts[i];
maxLabel = i;
}
}
double maxLabelRatio = (double) maxCount / totalPixels;
// 4. 生成最大类的掩码并分析连通域
Mat mask = new Mat(h, w, CV_8UC1);
UByteIndexer maskIndexer = mask.createIndexer();
int pixelIdx = 0;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int label = labelIndexer.get(pixelIdx++, 0);
maskIndexer.put(y, x, label == maxLabel ? 255 : 0);
}
}
MatVector contours = new MatVector();
Mat hierarchy = new Mat();
findContours(mask, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
double largestContourArea = 0;
for (int i = 0; i < contours.size(); i++) {
double area = contourArea(contours.get(i));
if (area > largestContourArea) {
largestContourArea = area;
}
}
double continuousBackgroundRatio = largestContourArea / totalPixels;
boolean hasLargeBackground = (continuousBackgroundRatio >= areaThreshold) && (maxLabelRatio >= clusterThreshold);
// 5. 提取主颜色
FloatIndexer centersIndexer = centers.createIndexer();
Map<String, Integer> mainColor = new HashMap<>();
if (useGray) {
mainColor.put("gray", (int) centersIndexer.get(maxLabel, 0));
} else {
mainColor.put("B", (int) centersIndexer.get(maxLabel, 0));
mainColor.put("G", (int) centersIndexer.get(maxLabel, 1));
mainColor.put("R", (int) centersIndexer.get(maxLabel, 2));
}
// 6. 构造内部返回结果
InternalDetectionResult result = new InternalDetectionResult();
result.hasLargeBackground = hasLargeBackground;
result.continuousBackgroundRatio = continuousBackgroundRatio;
result.totalMaxLabelRatio = maxLabelRatio;
result.mainBackgroundColor = mainColor;
// 释放临时对象
processed.close();
reshaped32f.close();
reshaped.close();
labels.close();
centers.close();
mask.close();
hierarchy.close();
return result;
}
// ===============================
// 测试入口
// ===============================
public static void main(String[] args) {
// 替换为你的真实测试图片路径
String path = "D:\\work\\ceshi\\bai.jpg";
try {
System.out.println("===== 开始检测 =====");
// 直接调用极简的封装方法
DetectResult result = detectBackground(path);
System.out.println(result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

@ -260,7 +260,7 @@ public class BlurDetectorV2 {
public static DetectResult detectBlur(String imagePath) { public static DetectResult detectBlur(String imagePath) {
try (Mat img = imread(imagePath)) { try (Mat img = imread(imagePath)) {
return detectBlur(img, "bpmh", 60.0); return detectBlur(img, "bpmh", 90.0);
} }
} }
@ -312,11 +312,15 @@ public class BlurDetectorV2 {
// 测试入口 // 测试入口
// =============================== // ===============================
public static void main(String[] args) { public static void main(String[] args) {
String img = "D:\\work\\images\\CAPTURE_266_20260317123336607_ee638f3b.jpg"; // 基准图提前给定 String img = "D:\\work\\images\\CAPTURE_265_20260319160327655_79b39c57.jpg"; // 基准图提前给定
String ref = "D:\\work\\images\\CAPTURE_258_20260317115524685_a2c29ddb.jpg"; // 摄像头后面传入图像 String ref = "D:\\work\\images\\CAPTURE_266_20260319160512854_13b52e7e.jpg"; // 摄像头后面传入图像
//Result{value='0', code='2000', desc='正常 ratio=1.01'}
// Result{value='1', code='2000', desc='模糊 ratio=0.69'}
// 默认 ratio_threshold=0.85 // 默认 ratio_threshold=0.85
DetectResult result = doubleDetectBlur(img, ref, 0.85); // DetectResult result = doubleDetectBlur(img, ref, 0.85);
System.out.println(result.toString()); // 60 到 70
DetectResult detectResult = detectBlur(ref);
System.out.println("单图检测:"+detectResult.toString());
// System.out.println(result.toString());
} }
} }

@ -0,0 +1,324 @@
package com.sz.admin.monitor.utils;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_imgproc.CLAHE;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class BlurDetectorV3 {
// ===============================
// 数据返回类 (替代 Python 中的 dict)
// ===============================
public static class DetectResult {
public String type;
public String value;
public String code;
public String desc;
public double conf;
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{value='%s', code='%s', desc='%s'}", value, code, desc);
}
}
// ===============================
// 光照归一化
// ===============================
private static Mat normalizeLighting(Mat gray) {
Mat dst = new Mat();
try (CLAHE clahe = createCLAHE(1.5, new Size(8, 8))) {
clahe.apply(gray, dst);
}
return dst;
}
// ===============================
// Laplacian 清晰度
// ===============================
private static double laplacianScore(Mat gray) {
try (Mat lap = new Mat(); Mat mean = new Mat(); Mat stddev = new Mat()) {
Laplacian(gray, lap, CV_64F);
meanStdDev(lap, mean, stddev);
double sd = stddev.createIndexer().getDouble();
return sd * sd; // 方差
}
}
// ===============================
// Tenengrad 梯度
// ===============================
private static double tenengradScore(Mat gray) {
try (Mat gx = new Mat(); Mat gy = new Mat();
Mat gx2 = new Mat(); Mat gy2 = new Mat();
Mat g = new Mat()) {
Sobel(gray, gx, CV_64F, 1, 0, 3, 1.0, 0.0, BORDER_DEFAULT);
Sobel(gray, gy, CV_64F, 0, 1, 3, 1.0, 0.0, BORDER_DEFAULT);
multiply(gx, gx, gx2);
multiply(gy, gy, gy2);
add(gx2, gy2, g);
return mean(g).get(0);
}
}
// ===============================
// Edge Density边缘密度
// ===============================
private static double edgeDensity(Mat gray) {
try (Mat edges = new Mat()) {
Canny(gray, edges, 80, 150);
int nonZero = countNonZero(edges);
return (double) nonZero / (edges.rows() * edges.cols());
}
}
// ===============================
// FFT 高频能量 (最终完美版)
// ===============================
private static double fftHighFrequency(Mat gray) {
// 第一层:初始化所有基础操作矩阵,这些变量在此块内不可被重新赋值
try (Mat f32 = new Mat();
Mat zeros = Mat.zeros(gray.size(), CV_32F).asMat();
MatVector planes = new MatVector(2);
Mat complexI = new Mat();
Mat mag = new Mat();
Mat ones = Mat.ones(gray.size(), CV_32F).asMat();
Mat tmp = new Mat()) {
// 1. 转为浮点并合并为双通道复数矩阵
gray.convertTo(f32, CV_32F);
planes.put(0, f32);
planes.put(1, zeros);
merge(planes, complexI);
// 2. 傅里叶变换
dft(complexI, complexI);
// 3. 计算幅度: magnitude = log(abs(fshift) + 1)
split(complexI, planes);
magnitude(planes.get(0), planes.get(1), mag);
add(mag, ones, mag);
log(mag, mag);
// 4. fftshift 平移 (将低频移到中心)
// 声明新的变量 croppedMag 来接收裁剪结果,避免给 mag 重新赋值引发报错
try (Mat croppedMag = new Mat(mag, new Rect(0, 0, mag.cols() & -2, mag.rows() & -2))) {
int cx = croppedMag.cols() / 2;
int cy = croppedMag.rows() / 2;
// 第三层:声明所有的子矩阵 (ROI) 和 mask确保它们用完后被自动释放
try (Mat q0 = new Mat(croppedMag, new Rect(0, 0, cx, cy));
Mat q1 = new Mat(croppedMag, new Rect(cx, 0, cx, cy));
Mat q2 = new Mat(croppedMag, new Rect(0, cy, cx, cy));
Mat q3 = new Mat(croppedMag, new Rect(cx, cy, cx, cy));
Mat mask = new Mat(croppedMag.size(), CV_32F, new Scalar(1.0))) {
// 象限交换 (Shift)
q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3);
q1.copyTo(tmp); q2.copyTo(q1); tmp.copyTo(q2);
// 5. Mask 屏蔽中心低频
int r = Math.min(cx, cy) / 4;
rectangle(mask,
new Point(cx - r, cy - r),
new Point(cx + r, cy + r),
new Scalar(0.0),
-1, 8, 0);
// 6. 对应元素相乘 (只保留高频部分)
multiply(croppedMag, mask, croppedMag);
// 7. 计算整体高频均值
return mean(croppedMag).get(0);
}
}
}
}
// ===============================
// 白墙和纯色背景图检测
// ===============================
private static boolean isWhiteWall(Mat image) {
try (Mat hsv = new Mat();
Mat mask = new Mat();
Mat lower = new Mat(1, 1, CV_8UC3, new Scalar(0, 0, 80, 0));
Mat upper = new Mat(1, 1, CV_8UC3, new Scalar(180, 50, 255, 0));
Mat gray = new Mat()) {
cvtColor(image, hsv, COLOR_BGR2HSV);
inRange(hsv, lower, upper, mask);
double ratio = (double) countNonZero(mask) / (mask.rows() * mask.cols());
cvtColor(image, gray, COLOR_BGR2GRAY);
double lap = laplacianScore(gray);
double edge = edgeDensity(gray);
if (ratio > 0.7 && lap < 30) {
return true;
}
if (lap < 15 && edge < 0.03) {
return true;
}
return false;
}
}
// ===============================
// ROI中心区域 (切片等价实现)
// ===============================
private static Mat extractRoi(Mat image) {
int h = image.rows();
int w = image.cols();
int y = h / 5;
int x = w / 5;
int roiH = (4 * h / 5) - y;
int roiW = (4 * w / 5) - x;
return new Mat(image, new Rect(x, y, roiW, roiH));
}
// ===============================
// 模糊检测单图主函数
// ===============================
public static DetectResult detectBlur(Mat image, String detectTypeKey, double threshold) {
if (image == null || image.empty()) {
throw new IllegalArgumentException("image read error");
}
// ===============================
// 白墙过滤
// ===============================
if (isWhiteWall(image)) {
return new DetectResult(detectTypeKey, "1", "4001", "白墙/纯色背景 score=0", 0.0, 0.0, 0.0);
}
try (Mat roiImage = extractRoi(image);
Mat gray = new Mat()) {
cvtColor(roiImage, gray, COLOR_BGR2GRAY);
try (Mat normGray = normalizeLighting(gray);
Mat smoothedGray = new Mat()) {
GaussianBlur(normGray, smoothedGray, new Size(3, 3), 0);
// ===============================
// 计算指标
// ===============================
double lap = laplacianScore(normGray);
double ten = tenengradScore(normGray);
double edge = edgeDensity(normGray);
double fft = fftHighFrequency(normGray);
// ===============================
// 归一化
// ===============================
double lap_n = Math.min(lap / 2000.0, 1.0);
double ten_n = Math.min(ten / 10000.0, 1.0);
double edge_n = Math.min(edge * 10.0, 1.0);
double fft_n = Math.min(fft / 10.0, 1.0);
// ===============================
// 综合评分
// ===============================
double score = (0.35 * lap_n + 0.30 * ten_n + 0.20 * edge_n + 0.15 * fft_n) * 100.0;
String descStr = String.format("score=%.2f", score);
// ===============================
// 判断
// ===============================
if (score < threshold) {
return new DetectResult(detectTypeKey, "1", "2000", "模糊 " + descStr, 1.0, score, 0.0);
} else {
return new DetectResult(detectTypeKey, "0", "2000", "正常 " + descStr, 1.0, score, 0.0);
}
}
}
}
public static DetectResult detectBlur(String imagePath) {
try (Mat img = imread(imagePath)) {
return detectBlur(img, "bpmh", 70.0);
}
}
// ===============================
// 双图检测
// ===============================
public static DetectResult doubleDetectBlur(String imgPath, String refPath, double ratioThreshold) {
boolean useRef = true;
if (refPath == null || refPath.isEmpty()) {
useRef = false;
}
try (Mat imgMat = imread(imgPath);
Mat refMat = useRef ? imread(refPath) : null) {
if (refMat == null || refMat.empty()) {
useRef = false;
}
// 单图模式兜底
if (!useRef) {
return detectBlur(imgMat, "bpmh", 60.0);
}
// 双图模式
DetectResult imgResult = detectBlur(imgMat, "bpmh", 60.0);
DetectResult refResult = detectBlur(refMat, "bpmh", 60.0);
double scoreImg = imgResult.score;
double scoreRef = refResult.score;
if (scoreRef < 1e-6) {
return new DetectResult("bpmh", "0", "2000", "参考图异常", 0.0, scoreImg, 0.0);
}
double ratio = scoreRef / scoreImg;
System.out.println(scoreImg + ", " + scoreRef);
if (ratio < ratioThreshold) {
return new DetectResult("bpmh", "1", "2000", String.format("模糊 ratio=%.2f", ratio), 1.0, scoreImg, ratio);
} else {
return new DetectResult("bpmh", "0", "2000", String.format("正常 ratio=%.2f", ratio), 1.0, scoreImg, ratio);
}
}
}
// ===============================
// 测试入口
// ===============================
public static void main(String[] args) {
String img = "D:\\work\\images\\CAPTURE_258_20260320093827912_bcf0b101.jpg"; // 基准图提前给定
String ref = "D:\\work\\images\\CAPTURE_258_20260319160231459_89b5826f.jpg"; // 摄像头后面传入图像
// 默认 ratio_threshold=0.85
DetectResult result = doubleDetectBlur(img, ref, 0.85);
System.out.println(result.toString());
// String path="D:\\work\\imgs\\CAPTURE_258_20260319160154580_4d8abac8.jpg";
// DetectResult detectResult = detectBlur(path);
// System.out.println(detectResult.toString());
}
}

@ -0,0 +1,297 @@
package com.sz.admin.monitor.utils;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.opencv.opencv_imgproc.CLAHE;
import static org.bytedeco.opencv.global.opencv_core.*;
import static org.bytedeco.opencv.global.opencv_imgcodecs.imread;
import static org.bytedeco.opencv.global.opencv_imgproc.*;
public class BlurDetectorV4 {
// ===============================
// 数据返回类 (从 V3 移植)
// ===============================
public static class DetectResult {
public String type;
public String value;
public String code;
public String desc;
public double conf;
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);
}
}
// ===============================
// 光照归一化 (保留 V4 的 3.0 参数)
// ===============================
public static Mat normalizeLighting(Mat gray) {
CLAHE clahe = createCLAHE(1.5, new Size(8, 8));
Mat dst = new Mat();
clahe.apply(gray, dst);
clahe.close();
return dst;
}
// ===============================
// Laplacian 清晰度
// ===============================
public static double laplacianScore(Mat gray) {
Mat lap = new Mat();
Laplacian(gray, lap, CV_64F);
Mat mean = new Mat();
Mat stddev = new Mat();
meanStdDev(lap, mean, stddev);
double std = stddev.createIndexer().getDouble(0);
lap.close();
mean.close();
stddev.close();
return std * std;
}
// ===============================
// Tenengrad 梯度
// ===============================
public static double tenengradScore(Mat gray) {
Mat gx = new Mat();
Mat gy = new Mat();
Sobel(gray, gx, CV_64F, 1, 0, 3, 1.0, 0.0, BORDER_DEFAULT);
Sobel(gray, gy, CV_64F, 0, 1, 3, 1.0, 0.0, BORDER_DEFAULT);
Mat gx2 = new Mat();
Mat gy2 = new Mat();
Mat g = new Mat();
multiply(gx, gx, gx2);
multiply(gy, gy, gy2);
add(gx2, gy2, g);
double score = mean(g).get(0);
gx.close(); gy.close();
gx2.close(); gy2.close(); g.close();
return score;
}
// ===============================
// Edge Density边缘密度
// ===============================
public static double edgeDensity(Mat gray) {
Mat edges = new Mat();
Canny(gray, edges, 80, 150);
int nonZero = countNonZero(edges);
long totalPixels = edges.total();
edges.close();
return (double) nonZero / totalPixels;
}
// ===============================
// FFT 高频能量 (保留 V4 优秀的内存管理)
// ===============================
private static double fftHighFrequency(Mat gray) {
try (Mat f32 = new Mat();
Mat zeros = Mat.zeros(gray.size(), CV_32F).asMat();
MatVector planes = new MatVector(2);
Mat complexI = new Mat();
Mat mag = new Mat();
Mat ones = Mat.ones(gray.size(), CV_32F).asMat();
Mat tmp = new Mat()) {
gray.convertTo(f32, CV_32F);
planes.put(0, f32);
planes.put(1, zeros);
merge(planes, complexI);
dft(complexI, complexI);
split(complexI, planes);
magnitude(planes.get(0), planes.get(1), mag);
add(mag, ones, mag);
log(mag, mag);
try (Mat croppedMag = new Mat(mag, new Rect(0, 0, mag.cols() & -2, mag.rows() & -2))) {
int cx = croppedMag.cols() / 2;
int cy = croppedMag.rows() / 2;
try (Mat q0 = new Mat(croppedMag, new Rect(0, 0, cx, cy));
Mat q1 = new Mat(croppedMag, new Rect(cx, 0, cx, cy));
Mat q2 = new Mat(croppedMag, new Rect(0, cy, cx, cy));
Mat q3 = new Mat(croppedMag, new Rect(cx, cy, cx, cy));
Mat mask = new Mat(croppedMag.size(), CV_32F, new Scalar(1.0))) {
q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3);
q1.copyTo(tmp); q2.copyTo(q1); tmp.copyTo(q2);
int r = Math.min(cx, cy) / 4;
rectangle(mask,
new Point(cx - r, cy - r),
new Point(cx + r, cy + r),
new Scalar(0.0),
-1, 8, 0);
multiply(croppedMag, mask, croppedMag);
return mean(croppedMag).get(0);
}
}
}
}
// ===============================
// ROI中心区域
// ===============================
public static Mat extractRoi(Mat image) {
int h = image.rows();
int w = image.cols();
int x = w / 5;
int y = h / 5;
int roiW = (4 * w / 5) - x;
int roiH = (4 * h / 5) - y;
return new Mat(image, new Rect(x, y, roiW, roiH));
}
// ===============================
// 模糊检测主函数 (改为返回 DetectResult)
// ===============================
public static DetectResult detectBlur(Object imageObj, String detectTypeKey, double threshold) {
Mat image = null;
boolean needsRelease = false;
if (imageObj instanceof String) {
image = imread((String) imageObj);
needsRelease = true;
} else if (imageObj instanceof Mat) {
image = (Mat) imageObj;
}
if (image == null || image.empty()) {
throw new IllegalArgumentException("Image read error");
}
Mat roiImage = extractRoi(image);
Mat gray = new Mat();
cvtColor(roiImage, gray, COLOR_BGR2GRAY);
Mat normalizedGray = normalizeLighting(gray);
double lap = laplacianScore(normalizedGray);
double ten = tenengradScore(normalizedGray);
double edge = edgeDensity(normalizedGray);
double fft = fftHighFrequency(normalizedGray);
double lapN = Math.min(lap / 2000.0, 1.0);
double tenN = Math.min(ten / 10000.0, 1.0);
double edgeN = Math.min(edge * 10.0, 1.0);
double fftN = Math.min(fft / 10.0, 1.0);
double score = (0.35 * lapN + 0.30 * tenN + 0.20 * edgeN + 0.15 * fftN) * 100;
String descStr = String.format("score=%.2f", score);
DetectResult result;
if (score < threshold) {
result = new DetectResult(detectTypeKey, "1", "2000", "模糊 " + descStr, 1.0, score, 0.0);
} else {
result = new DetectResult(detectTypeKey, "0", "2000", "正常 " + descStr, 1.0, score, 0.0);
}
roiImage.close();
gray.close();
normalizedGray.close();
if (needsRelease) image.close();
return result;
}
public static DetectResult detectBlur(Object imageObj) {
return detectBlur(imageObj, "bpmh", 70.0);
}
// ===============================
// 双图检测 (改为返回 DetectResult)
// ===============================
public static DetectResult doubleDetectBlur(Object imgObj, Object refObj, double ratioThreshold) {
boolean useRef = true;
Mat refMat = null;
boolean refNeedsRelease = false;
if (refObj == null || (refObj instanceof String && ((String) refObj).isEmpty())) {
useRef = false;
} else if (refObj instanceof String) {
refMat = imread((String) refObj);
if (refMat == null || refMat.empty()) useRef = false;
else refNeedsRelease = true;
} else if (refObj instanceof Mat) {
refMat = (Mat) refObj;
if (refMat.empty()) useRef = false;
}
// 单图模式兜底
if (!useRef) {
return detectBlur(imgObj);
}
// 双图模式
DetectResult imgResult = detectBlur(imgObj);
DetectResult refResult = detectBlur(refMat);
double scoreImg = imgResult.score;
double scoreRef = refResult.score;
if (scoreRef < 1e-6) {
if (refNeedsRelease) refMat.close();
return new DetectResult("bpmh", "0", "2000", "参考图异常", 0.0, scoreImg, 0.0);
}
double ratio = scoreRef / scoreImg;
System.out.println("scoreImg: " + scoreImg + ", scoreRef: " + scoreRef);
DetectResult finalResult;
if (ratio < ratioThreshold) {
finalResult = new DetectResult("bpmh", "1", "2000", String.format("模糊 ratio=%.2f", ratio), 1.0, scoreImg, ratio);
} else {
finalResult = new DetectResult("bpmh", "0", "2000", String.format("正常 ratio=%.2f", ratio), 1.0, scoreImg, ratio);
}
if (refNeedsRelease) refMat.close();
return finalResult;
}
public static DetectResult doubleDetectBlur(Object imgObj, Object refObj) {
return doubleDetectBlur(imgObj, refObj, 0.85);
}
// ===============================
// 测试
// ===============================
public static void main(String[] args) {
String path = "D:\\work\\imgs\\CAPTURE_266_20260317123416804_ac0f924c.jpg";
try {
DetectResult result = detectBlur(path);
System.out.println(result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Loading…
Cancel
Save