diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/AdminApplication.java b/sz-service/sz-service-admin/src/main/java/com/sz/AdminApplication.java index 8b44e30..f62e479 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/AdminApplication.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/AdminApplication.java @@ -1,12 +1,19 @@ package com.sz; +import com.sz.admin.monitor.service.CameraService; import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @@ -15,12 +22,14 @@ import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling public class AdminApplication { + private static final Logger log = LoggerFactory.getLogger(AdminApplication.class); @Value("${app.version}") private String appVersion; @Getter private static String version; + @PostConstruct public void init() { setVersion(appVersion); // 通过辅助方法设置静态字段 @@ -45,4 +54,6 @@ public class AdminApplication { System.out.println(result); } + + } \ No newline at end of file diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/config/CameraThreadPoolConfig.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/config/CameraThreadPoolConfig.java new file mode 100644 index 0000000..948884b --- /dev/null +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/config/CameraThreadPoolConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/enums/AlarmReportEnums.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/enums/AlarmReportEnums.java index 03c61bd..c4f6bf3 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/enums/AlarmReportEnums.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/enums/AlarmReportEnums.java @@ -13,7 +13,8 @@ public enum AlarmReportEnums { FIRE_ALARM("Fire",2,"明烟明火报警"), FACE_ID_ALARM("FaceId",3,"人脸识别报警"), CAPTURE_FACE_ALARM("CaptureFace", 4, "抓拍到人脸报警"), - HELMET_ALARM("NoHelmet", 5, "未戴安全帽报警"); + HELMET_ALARM("NoHelmet", 5, "未戴安全帽报警"), + BLUR_ALARM("Blur", 6, "模糊报警"); private final String alarmType; private final Integer alarmCode; private final String alarmDescription; diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/dto/blurDetection/BlurDetectionUpdateDTO.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/dto/blurDetection/BlurDetectionUpdateDTO.java index ae38819..a3a7c54 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/dto/blurDetection/BlurDetectionUpdateDTO.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/dto/blurDetection/BlurDetectionUpdateDTO.java @@ -33,4 +33,5 @@ public class BlurDetectionUpdateDTO { private String url; + } diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/BlurDetection.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/BlurDetection.java index 47d02b9..f5e7dae 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/BlurDetection.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/BlurDetection.java @@ -44,6 +44,7 @@ public class BlurDetection implements Serializable { */ private Integer enable; + /** * 上报的url */ diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/CameraAlarm.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/CameraAlarm.java index 24276ef..78a5236 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/CameraAlarm.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/po/CameraAlarm.java @@ -50,7 +50,7 @@ public class CameraAlarm implements Serializable { @Schema(description = "告警区域id") private Long alarmAreaId; - @Schema(description = "报警类型: 1-移位, 2-明烟明火报警,3-人脸识别,4-脸部抓拍,5-安全帽检测") + @Schema(description = "报警类型: 1-移位, 2-明烟明火报警,3-人脸识别,4-脸部抓拍,5-安全帽检测,6-模糊") private Integer alarmType; @Schema(description = "基准图路径 ") diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/vo/blurDetection/BlurDetectionVO.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/vo/blurDetection/BlurDetectionVO.java index 6ff2045..ea09018 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/vo/blurDetection/BlurDetectionVO.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/pojo/vo/blurDetection/BlurDetectionVO.java @@ -42,6 +42,8 @@ public class BlurDetectionVO { */ private String url; + + /** * 创建时间 */ diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/CameraSnapshotService.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/CameraSnapshotService.java index 27e6f7e..bd78c32 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/CameraSnapshotService.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/CameraSnapshotService.java @@ -26,4 +26,6 @@ public interface CameraSnapshotService extends IService { List getExistSnapshotList(Long id); void deleteByCameraIdAndType(Long cameraId, int i); + + CameraSnapshotVO selectByCameraIdAndType(Long cameraId, int i); } diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/impl/BlurDetectionServiceImpl.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/impl/BlurDetectionServiceImpl.java index 41f4d9f..9574692 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/impl/BlurDetectionServiceImpl.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/service/impl/BlurDetectionServiceImpl.java @@ -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.service.BlurDetectionService; import com.sz.admin.monitor.service.CameraSnapshotService; +import com.sz.core.common.exception.common.BusinessException; import com.sz.core.util.BeanCopyUtils; +import com.sz.platform.enums.AdminResponseEnum; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,7 +33,6 @@ public class BlurDetectionServiceImpl extends ServiceImpl 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 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 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 * * * * ?") - @SneakyThrows - public void executeInspection() { + public void executeInspection() throws InterruptedException { // 从摄像头表中获取设置了抓图对比和在线的摄像头 List cameraList = cameraService.selectListByInspection(); // 然后根据摄像头id查询设置为标准位的预置位, @@ -104,36 +284,53 @@ public class ScheduledTask { } List presetList = presetService.selectByCameraIds(cameraIds); for (Preset preset : presetList) { - // 调用SDK让摄像头回归到该预置位 - boolean ok = manageNVR.ptzPresets(preset.getCameraId().intValue(), 39, String.valueOf(preset.getPresetId()), preset.getPresetName()); - // 延时5秒 + CompletableFuture.runAsync(() -> { + // 调用具体的单次检测逻辑 + 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); if (ok) { - // 抓取一张图片 - String filename = ImageNameUtils.generateFileName("CAPTURE",preset.getCameraId()); - //String image2 = "D:\\work\\images\\CAPTURE_367_20260212092406664_2d117293.jpg"; - // String image2 = "D:\\work\\images\\CAPTURE_19_20260302152457977_db33f5a8.jpg"; - String image2 = manageNVR.capturePic(preset.getCameraId().intValue(), filename); - // 从图片表中,根据预置位,摄像机id,类型为巡检为条件查询出基准图 - CameraSnapshotVO cameraSnapshotVO = cameraSnapshotService.selectByCameraIdAndPresetIndex(preset.getCameraId(), preset.getPresetId()); - if (cameraSnapshotVO == null || cameraSnapshotVO.getImagePath() == null) { - log.warn("摄像机[{}] 未设置基准图,无法对比", preset.getCameraId()); - continue; + String filename = ImageNameUtils.generateFileName("CAPTURE", cameraId); + String image2 = manageNVR.capturePic(cameraId.intValue(), filename); + // String image2="D:\\work\\images\\CAPTURE_258_20260320170313838_4680425d.jpg"; + // 抓图合法性校验 + if (image2 == null || image2.isBlank()) { + log.error("摄像机[{}] 抓拍失败,无法获取当前图片", cameraId); + return; // 结束当前预置位的处理 + } + CameraSnapshotVO cameraSnapshotVO = cameraSnapshotService.selectByCameraIdAndPresetIndex(cameraId, preset.getPresetId()); + if (cameraSnapshotVO == null || cameraSnapshotVO.getImagePath() == null || cameraSnapshotVO.getImagePath().isBlank()) { + log.warn("摄像机[{}] 未设置预置位基准图,无法对比", cameraId); + return; } - // 调用图片对比算法,校验摄像头是否移位 - // 基准图 String image1 = cameraSnapshotVO.getImagePath(); - // 对比 Map map = RobustImageMatcherUtil.calculateOffset(image1, image2); - log.info("图片对比结果:{}", map); - // 判断结果并报警 - handleAnalysisResult(preset, cameraSnapshotVO.getImagePath(), image2, map); + log.info("摄像机[{}] 预置位[{}] 对比结果:{}", cameraId, preset.getPresetId(), map); + if (map != null && map.containsKey("value")) { + 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 map) throws IOException { int value = (int) map.get("value"); if (value == 1) { @@ -147,6 +344,7 @@ public class ScheduledTask { // 设置告警区域 cameraAlarm.setAlarmAreaId(nvr.getStationId()); cameraAlarm.setChannelId(camera.getChannelId()); + cameraAlarm.setCameraNo(camera.getCameraNo()); cameraAlarm.setCameraName(camera.getName()); cameraAlarm.setPresetIndex(preset.getPresetId()); // 标记为移位报警 @@ -185,7 +383,13 @@ public class ScheduledTask { reportDto.setCameraName(camera.getName()); reportDto.setCameraId(camera.getId()); reportDto.setCameraNo(camera.getCameraNo()); - forwardService.enrichAndForward(reportDto); + try { + forwardService.enrichAndForward(reportDto); + }catch (Exception e) + { + log.error("移位报警上报失败,摄像头 ID: {}, 相机编号:{}", + camera.getId(), camera.getCameraNo(), e); + } } } diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BackgroundDetector.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BackgroundDetector.java new file mode 100644 index 0000000..26bad86 --- /dev/null +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BackgroundDetector.java @@ -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 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 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(); + } + } +} \ No newline at end of file diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV2.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV2.java index a0de618..54cfe5f 100644 --- a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV2.java +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV2.java @@ -260,7 +260,7 @@ public class BlurDetectorV2 { public static DetectResult detectBlur(String 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) { - String img = "D:\\work\\images\\CAPTURE_266_20260317123336607_ee638f3b.jpg"; // 基准图提前给定 - String ref = "D:\\work\\images\\CAPTURE_258_20260317115524685_a2c29ddb.jpg"; // 摄像头后面传入图像 - + String img = "D:\\work\\images\\CAPTURE_265_20260319160327655_79b39c57.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 - DetectResult result = doubleDetectBlur(img, ref, 0.85); - System.out.println(result.toString()); + // DetectResult result = doubleDetectBlur(img, ref, 0.85); + // 60 到 70 + DetectResult detectResult = detectBlur(ref); + System.out.println("单图检测:"+detectResult.toString()); + // System.out.println(result.toString()); } } \ No newline at end of file diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV3.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV3.java new file mode 100644 index 0000000..d9c262e --- /dev/null +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV3.java @@ -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()); + } +} \ No newline at end of file diff --git a/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV4.java b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV4.java new file mode 100644 index 0000000..a5b9d74 --- /dev/null +++ b/sz-service/sz-service-admin/src/main/java/com/sz/admin/monitor/utils/BlurDetectorV4.java @@ -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(); + } + } +} \ No newline at end of file