From 6725634e8e76fad5d0d2445b853093335ef8ab81 Mon Sep 17 00:00:00 2001 From: Bingkun Li Date: Tue, 3 Mar 2026 10:03:21 +0800 Subject: [PATCH] Save the changes of testing splicing image solution --- .../skyeye/jm/dto/JmAirlineStatusDTO.java | 10 + .../zhangy/skyeye/jm/dto/JmUavStatusDTO.java | 4 + .../com/zhangy/skyeye/jm/entity/JmImage.java | 31 +- .../skyeye/jm/mapper/JmImageMapper.java | 9 + .../skyeye/jm/service/JmImageService.java | 5 + .../jm/service/impl/JmImageServiceImpl.java | 5 + .../zhangy/skyeye/publics/dto/GeoTiffDTO.java | 12 + .../skyeye/publics/utils/ChecksumUtil.java | 19 +- .../skyeye/publics/utils/ImageUtil.java | 69 +++- .../skyeye/sar/dto/SarBackFramePackDTO.java | 29 +- .../skyeye/sar/dto/SarControlPackDTO.java | 2 +- .../skyeye/sar/dto/SarImagePacketDTO.java | 26 +- .../sar/dto/SarImagePacketGroupDTO.java | 5 +- .../listen/SarAsynAbstractUdpProcessor.java | 152 ++++++++ .../sar/listen/SarImageUdpProcessor.java | 140 +++----- .../skyeye/sar/service/ISarImageService.java | 4 +- .../service/impl/SarBackWsServiceImpl.java | 1 + .../sar/service/impl/SarImageServiceImpl.java | 336 ++++++++---------- .../skyeye/sar/util/SarImageToneAdjuster.java | 120 +++++-- .../src/main/resources/application-dev.yml | 6 +- .../src/main/resources/db.sql | 8 +- .../resources/mapping/jm/JmImageMapping.xml | 152 +++++--- frontend/Skyeye-sys-ui/public/config.js | 6 +- 23 files changed, 722 insertions(+), 429 deletions(-) create mode 100644 backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/listen/SarAsynAbstractUdpProcessor.java diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmAirlineStatusDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmAirlineStatusDTO.java index 418bc45..96f8f4f 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmAirlineStatusDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmAirlineStatusDTO.java @@ -52,4 +52,14 @@ public class JmAirlineStatusDTO { // 前一张右上、右下 private Double[] beforeRight; + + /** 航线终点经度 */ + private Double endLon; + + /** 航线终点纬度 */ + private Double endLat; + + private Integer headingDiff; + + private double[] values; } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmUavStatusDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmUavStatusDTO.java index 4da8562..b5f3163 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmUavStatusDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/dto/JmUavStatusDTO.java @@ -1,6 +1,7 @@ package com.zhangy.skyeye.jm.dto; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.zhangy.skyeye.jm.consts.JmJobModeEnum; import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO; import com.zhangy.skyeye.publics.consts.ExecStatusEnum; import lombok.Data; @@ -62,6 +63,9 @@ public class JmUavStatusDTO { /** sar 图片亮度 */ private volatile Integer sarImageLight; + /** 任务模式 */ + private JmJobModeEnum jobMode; + /** * 获取sar当前状态 * diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/entity/JmImage.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/entity/JmImage.java index 1963116..c0662cb 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/entity/JmImage.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/entity/JmImage.java @@ -1,6 +1,7 @@ package com.zhangy.skyeye.jm.entity; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.zhangy.skyeye.publics.dto.GeoTiffDTO; import com.zhangy.skyeye.py.dto.PyImageParamDTO; import lombok.Getter; @@ -14,13 +15,16 @@ import java.util.List; @Getter @Setter public class JmImage extends GeoTiffDTO { - + /** 主键 */ private Long id; - + /** 任务执行ID */ private Long jobId; + /** 任务配置ID */ + private Long jobConfId; + /** 无人机 */ private Long uavId; @@ -39,19 +43,32 @@ public class JmImage extends GeoTiffDTO { /** 归一化前最大值 */ private Float max; - /** 航线的图片序号 */ + /** 层级 */ + private Integer tailLevel; + + /** 图片序号 */ private Integer imageNo; + /** 起始帧 */ + private Integer frameFrom; + + /** 结束帧 */ + private Integer frameTo; + + /** 旋转角 */ + private Double rotateAngle; + + /* 非数据库字段 */ /** 文件名 */ private String fileName; - + /** 图像类型 */ private Integer fileType; - + /** 存储路径 */ private String filePath; @@ -69,7 +86,7 @@ public class JmImage extends GeoTiffDTO { /** 无人机名称 */ private String uavName; - + /** * 文件上传时间 * sar实时影像:航线的第一张图片在服务器生成的时间 @@ -85,7 +102,7 @@ public class JmImage extends GeoTiffDTO { /** 图像亮度 */ private Integer brightness; - /** 图像识别结果 */ + /** 识别结果 */ private List itemList; /** 转图像识别算法参数 */ diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/mapper/JmImageMapper.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/mapper/JmImageMapper.java index 861c859..c74985e 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/mapper/JmImageMapper.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/mapper/JmImageMapper.java @@ -45,6 +45,15 @@ public interface JmImageMapper { */ List selectByAirline(@Param("type") Integer fileType, @Param("array") Long... airlineExecId); + /** + * 查询航线最后一张图 + * + * @param fileType 航线执行ID,必需 + * @param airlineExecId 文件类型,非必需 + * @return + */ + JmImage selectLastByAirline(@Param("type") Integer fileType, @Param("array") Long... airlineExecId); + /** * 按主键查询 */ diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/JmImageService.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/JmImageService.java index 8cace46..95396a8 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/JmImageService.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/JmImageService.java @@ -41,6 +41,11 @@ public interface JmImageService { */ List selectLowByAirline(Long airlineExecId); + /** + * 查询航线最后一张图 + */ + JmImage selectLastByAirline(FileTypeEnum fileType, Long airlineExecId); + /** * 按主键查询详情 */ diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/impl/JmImageServiceImpl.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/impl/JmImageServiceImpl.java index 8353fda..040dafa 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/impl/JmImageServiceImpl.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/jm/service/impl/JmImageServiceImpl.java @@ -119,6 +119,11 @@ public class JmImageServiceImpl implements JmImageService { return list; } + @Override + public JmImage selectLastByAirline(FileTypeEnum fileType, Long airlineExecId) { + return imageMapper.selectLastByAirline(fileType.getValue(), airlineExecId); + } + @Override public List selectById(Long... id) { return imageMapper.selectById(id); diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/dto/GeoTiffDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/dto/GeoTiffDTO.java index 3afcda7..68694b8 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/dto/GeoTiffDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/dto/GeoTiffDTO.java @@ -67,4 +67,16 @@ public class GeoTiffDTO implements Serializable { right2Lon = right1Lon; right2Lat = left2Lat; } + + public void updateCoord(double left1Lon, double left1Lat, double left2Lon, double left2Lat, + double right1Lon, double right1Lat, double right2Lon, double right2Lat) { + this.left1Lon = left1Lon; + this.left1Lat = left1Lat; + this.left2Lon = left2Lon; + this.left2Lat = left2Lat; + this.right1Lon = right1Lon; + this.right1Lat = right1Lat; + this.right2Lon = right2Lon; + this.right2Lat = right2Lat; + } } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ChecksumUtil.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ChecksumUtil.java index d1ca03f..5bfe64e 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ChecksumUtil.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ChecksumUtil.java @@ -9,11 +9,11 @@ import java.nio.ByteOrder; public class ChecksumUtil { /** - * 计算校验和(表8、2),排除最后8个字节 + * 表4,排除最后8个字节 * @param data * @return */ - public static int checksum8(byte[] data) { + public static int checksum32(byte[] data) { long checksum = 0; ByteBuffer checksumBuffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); int u32Count = data.length / 4; @@ -23,6 +23,21 @@ public class ChecksumUtil { return (int) (checksum & 0xFFFFFFFFL); } + /** + * 表14,排除最后8个字节 + * @param data + * @return + */ + public static int checksum8(byte[] data) { + int checksum = 0; + ByteBuffer checksumBuffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); + int u32Count = data.length; + for (int i = 0; i < u32Count - 8; i++) { // 不检查校验和、帧尾 + checksum += (Byte.toUnsignedInt(checksumBuffer.get())); + } + return checksum; + } + /** * 判断校验和(表9),排除第13字节 * @param data 如果分包数据不足1472,有效长度后面的是前一个分包留下的数据,并非是0 diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ImageUtil.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ImageUtil.java index 03b9f1c..197b679 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ImageUtil.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/publics/utils/ImageUtil.java @@ -4,6 +4,7 @@ import com.zhangy.skyeye.common.extend.exception.ServiceException; import com.zhangy.skyeye.common.extend.util.FileUtil; import com.zhangy.skyeye.jm.dto.JmImageRotateDTO; import com.zhangy.skyeye.publics.dto.GeoTiffDTO; +import com.zhangy.skyeye.sar.dto.SarBackImageFrameDTO; import lombok.extern.slf4j.Slf4j; import org.geotools.api.geometry.Bounds; import org.geotools.coverage.grid.GridCoverage2D; @@ -38,15 +39,18 @@ public class ImageUtil { public static Mat afterCreate(Mat image, JmImageRotateDTO rotateDTO) { if (rotateDTO != null) { OpenCVUtil.flip(image, rotateDTO.getType()); - Mat rgbaImage = OpenCVUtil.rotate(image, rotateDTO.getAngle() * -1); - image.release(); - image = rgbaImage; - } - int lightRate = rotateDTO.getLightRate(); - if (lightRate != 0) { - //OpenCVUtil.multiply(image, lightRate); - // OpenCVUtil.enhanceContrast(image, lightRate, 0); + double angle = rotateDTO.getAngle(); + if (angle != 0) { + Mat rgbaImage = OpenCVUtil.rotate(image, angle * -1); + image.release(); + image = rgbaImage; + } } +// int lightRate = rotateDTO.getLightRate(); +// if (lightRate != 0) { +// //OpenCVUtil.multiply(image, lightRate); +// OpenCVUtil.enhanceContrast(image, lightRate, 0); +// } return image; } @@ -69,6 +73,39 @@ public class ImageUtil { return OpenCVUtil.write(outPath, image, true); } + /** + * 加载回传图像,先转置再转为Mat + * + * @param frameData 图像帧数据,包含参数信息和未转置的8位灰度图像数据 + * @param imageFrame 图像帧对象 + * @return 转置后的4通道图像Mat + */ + public static Mat loadCurrImageMat(byte[] frameData, SarBackImageFrameDTO imageFrame) { + Mat currImage = null; + int offset = imageFrame.IMAGE_OFFSET; + + switch (imageFrame.getImgType()) { + case 0: // tif + currImage = ImageUtil.createImage(frameData, offset, imageFrame.getWidth(), + imageFrame.getHeight(), imageFrame.getImageBitDeep()); + break; + case 1: // jpg,先去掉参数信息,只保留图像数据 + case 4: { // png + byte[] validData = new byte[frameData.length - offset]; + System.arraycopy(frameData, offset, validData, 0, validData.length); + currImage = ImageUtil.createImageFromJpg(validData); + break; + } + default: + log.error("不支持的图片类型imgType=" + imageFrame.getImgType() + ",丢弃图片"); + } + if (currImage != null && currImage.empty()) { + log.error("图片数据是空的,数据长度:" + frameData.length); + currImage.release(); + } + return currImage; + } + /** * 创建图像对象 * @param rawData 原始像素数据 @@ -213,12 +250,11 @@ public class ImageUtil { * @param currGray 新接收的单通道灰度图(右部分),会被释放资源 * @return 拼接后的4通道Mat矩阵 */ - public static Mat join(Mat baseMat, Integer baseNo, Mat currGray, int currNo) { + public static Mat join(Mat baseMat, Integer baseNo, Mat currGray, int currNo, boolean isFirst) { int width = currGray.cols(); - // 将当前灰度图转换为4通道(BGRA),支持16位 Mat curr4 = convertToRGBA(currGray); - if (baseNo == null || baseMat == null) { + if (baseNo == null || baseMat == null || baseMat.empty()) { // 首图无需拼接,直接返回 return curr4; } // 统一数据类型:如果基准图是8位而当前是16位,需要转换 @@ -241,14 +277,13 @@ public class ImageUtil { Mat result = null; // 检查帧号是否连续 int missingFrames = currNo - baseNo - 1; - if (missingFrames > 0) { + log.info("--------> 帧" + currNo +"开始拼接,填充" + missingFrames + ",基准图:" + baseMat.rows() + "," + baseMat.type() + "," + baseMat.dims() + + ";当前图:" + curr4.rows() + "," + curr4.type() + "," + curr4.dims()); + if (!isFirst && missingFrames > 0) { // 创建缺失的透明图像(4通道) Mat transparent = new Mat(baseHeight, width, baseMat.type(), new Scalar(0, 0, 0, 0)); // 准备要拼接的图像列表 - log.info("当前帧" + currNo +"准备拼接图片,基准图:" + baseMat.rows() + "," + baseMat.type() + "," + baseMat.dims() + - ";填充图:" + transparent.rows() + "," + transparent.type() + "," + transparent.dims() + - ";当前图:" + curr4.rows() + "," + curr4.type() + "," + curr4.dims()); List imagesToConcat = new ArrayList<>(); imagesToConcat.add(baseMat); @@ -272,12 +307,10 @@ public class ImageUtil { imagesToConcat.add(curr4); result = new Mat(); - log.info("当前帧" + currNo +"准备拼接图片,基准图:" + baseMat.rows() + "," + baseMat.type() + "," + baseMat.dims() + - ";当前图:" + curr4.rows() + "," + curr4.type() + "," + curr4.dims()); Core.hconcat(imagesToConcat, result); baseMat.release(); } - log.info("当前帧" + currNo +"拼接完成"); + log.info("<-------- 帧" + currNo +"拼接完成"); // 释放资源,若missingFrames < 0 则说明当前图是前面的图迟到了,无法拼接,按丢图处理 curr4.release(); diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarBackFramePackDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarBackFramePackDTO.java index a0400fc..0a60e18 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarBackFramePackDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarBackFramePackDTO.java @@ -1,5 +1,6 @@ package com.zhangy.skyeye.sar.dto; +import com.zhangy.skyeye.publics.utils.ChecksumUtil; import lombok.Data; import java.nio.ByteBuffer; @@ -26,8 +27,12 @@ public class SarBackFramePackDTO { /** 帧尾标志 0x7EFFDC02 */ private int tailFlag; + public SarBackFramePackDTO() { + + } + public boolean check() { - return headFlag == 0x7EFFDC01; + return headFlag == 0x7EFFDC01 && tailFlag == 0x7EFFDC02; } /** @@ -57,23 +62,31 @@ public class SarBackFramePackDTO { * @param data 实际接收的帧数据 * @param hasLastPacket 是否包含尾包 */ - public SarBackFramePackDTO(byte[] data, boolean hasLastPacket) { + public static SarBackFramePackDTO parse(byte[] data, boolean hasLastPacket) { + SarBackFramePackDTO dto = new SarBackFramePackDTO(); ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); // 首包包含的头校验信息 - this.headFlag = buffer.getInt(); - this.size = buffer.getInt(); + dto.headFlag = buffer.getInt(); + if (dto.headFlag != 0x7EFFDC01) { + return null; + } + dto.size = buffer.getInt(); // 图像数据 = 帧数据 - 头尾校验数据 int frameSize = data.length - 8 - (hasLastPacket ? 8 : 0); - this.frameData = new byte[frameSize]; - buffer.get(this.frameData, 0, frameSize); + dto.frameData = new byte[frameSize]; + buffer.get(dto.frameData, 0, frameSize); // 尾包包含的尾校验信息 if (hasLastPacket) { - this.checksum = buffer.getInt(); - this.tailFlag = buffer.getInt(); + dto.checksum = (int) (buffer.getInt() & 0xFFFFFFFFL); + dto.tailFlag = buffer.getInt(); + if (dto.tailFlag != 0x7EFFDC02 || ChecksumUtil.checksum8(data) != dto.checksum) { + return null; + } } + return dto; } @Override diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarControlPackDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarControlPackDTO.java index 325b856..701889e 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarControlPackDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarControlPackDTO.java @@ -50,7 +50,7 @@ public class SarControlPackDTO { buffer.put(sarControl.toBytes()); // 尾 byte[] checkData = buffer.array(); - this.checksum = ChecksumUtil.checksum8(checkData); + this.checksum = ChecksumUtil.checksum32(checkData); buffer.putInt(checksum); buffer.putInt(endFlag); return buffer.array(); diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketDTO.java index 8d912b2..0375e60 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketDTO.java @@ -15,32 +15,32 @@ import java.util.zip.CRC32; public class SarImagePacketDTO extends SarBackPacketDTO { /** - * 图像帧ID,随机不连续 uint32_t + * 图像帧ID,随机不连续 */ long sessionId; /** - * 帧序号,从0开始 uint32_t + * 帧序号,从0开始 */ int packetNo; /** - * 帧数据大小(字节) uint32_t + * 帧数据大小(字节) */ int size; /** - * 是否为最后一个包 uint8_t + * 是否为最后一个包 */ boolean isLast; /** - * 校验值 uint32_t + * 校验值 */ long crc32; /** - * 包数据 std::vector + * 包数据 */ byte[] data; @@ -55,12 +55,11 @@ public class SarImagePacketDTO extends SarBackPacketDTO { bb.order(ByteOrder.BIG_ENDIAN); // 网络字节序 // 解析数据包头部 - pkt.sessionId = bb.getInt() & 0xFFFFFFFFL; // uint32_t - pkt.packetNo = bb.getInt(); // uint32_t - pkt.size = bb.getInt(); // uint32_t - pkt.isLast = bb.get() != 0; // uint8_t -> boolean - pkt.crc32 = bb.getInt() & 0xFFFFFFFFL; // uint32_t - + pkt.sessionId = bb.getInt() & 0xFFFFFFFFL; + pkt.packetNo = bb.getInt(); + pkt.size = bb.getInt(); + pkt.isLast = bb.get() != 0; + pkt.crc32 = bb.getInt() & 0xFFFFFFFFL; // 提取数据部分 byte[] pktData = new byte[pkt.size]; bb.get(pktData); @@ -68,10 +67,9 @@ public class SarImagePacketDTO extends SarBackPacketDTO { // CRC校验 long crcCalc = calculateCrc32(pktData); if (pkt.crc32 != crcCalc) { - log.warn("CRC不匹配,序号=" + pkt.packetNo + ",丢弃"); + log.warn("CRC不匹配,序号=" + pkt.packetNo + ",丢弃包"); return null; } - // 创建Packet对象 pkt.data = pktData; return pkt; diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketGroupDTO.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketGroupDTO.java index 62d621a..5b33f74 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketGroupDTO.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/dto/SarImagePacketGroupDTO.java @@ -31,6 +31,9 @@ public class SarImagePacketGroupDTO extends SarBackPacketGroupDTO packets = new HashMap<>(); + /** 整包数据 */ + private byte[] groupData; + public SarImagePacketDTO getLastPacket() { if (packets.size() == 0) { return null; @@ -68,7 +71,7 @@ public class SarImagePacketGroupDTO extends SarBackPacketGroupDTO { + + // 任务队列容量 + private final int QUEUE_CAPACITY = 1000; + + // 任务队列 + private BlockingQueue packetQueue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); + + // 异步线程 + private Thread processThread = new Thread(this::startProcessing); + + @Autowired + private JmJobStatusService jobStatusService; + + /** + * 初始化 + */ + public SarAsynAbstractUdpProcessor() { + super(); + running = true; + processThread.start(); + } + + /** + * 异步处理 + * @param group + */ + protected abstract void afterJoin(SarImagePacketGroupDTO group); + + /** + * 启动处理线程从队列消费数据 + */ + private void startProcessing() { + while (running) { + try { + SarImagePacketGroupDTO group = packetQueue.poll(100, TimeUnit.MILLISECONDS); + if (group != null) { + try { + afterJoin(group); + } catch (Exception ex) { + log.error(getText() + "异步线程处理图像包错误:" + ex.getMessage(), ex); + } + } + } catch (Throwable e) { + log.error(getText() + "异步线程处理异常", e); + } + } + } + + /** + * 判断是否可以合并回图帧,首包不能丢 + * @param group + * @return + */ + public boolean canJoin(SarImagePacketGroupDTO group) { + Map packets = group.getPackets(); + //log.info("定时判断合并udp:" + group.getSessionId() + "," + packets.keySet()); + // 首包包含图片格式信息,不能丢,否则图片无法打开 + return packets.get(0) != null && group.getLastPacket().isLast(); + } + + @Override + protected void expireProcess(SarImagePacketGroupDTO group) { + // 定时判断:超过 PACKET_TIMEOUT 时间未收到新包且符合生成条件则合成图 + if (canJoin(group)) { + log.info("[UDP] sesionid="+ group.getSessionId() + "超时合并!"); + join(group); + this.packetQueue.offer(group); + } else { // 超时且无法合并,丢弃 + Map packets = group.getPackets(); + if (packets.size() == 1 && packets.values().iterator().next().isLast()) { + // 雷达传图有bug,每帧的尾包会多发一个,丢弃且不打印日志 + } else { + log.warn(getText() + "-移除超时帧: {}", group.getSessionId()); + } + } + } + + /** + * 将upd分包数据合并为完整帧(帧数据 + 头尾校验),因为无法确定分包大小,分包缺失无法补充0 + * 合并后删除frameMap的分包数据 + * 使用ByteBuffer替代ByteArrayOutputStream,以减少内存分配。ByteArrayOutputStream 的 toByteArray()底层用Arrays.copyOf实现, + * 有性能开销;ByteBuffer的array()无需拷贝数组 + * @param group + * @return + */ + private void join(SarImagePacketGroupDTO group) { + ByteBuffer buffer = ByteBuffer.allocate(group.getTotalSize()); + buffer.order(ByteOrder.BIG_ENDIAN); + + Map packets = group.getPackets(); + for (int i = 0; i <= group.getLastPacket().getPacketNo(); i++) { + SarImagePacketDTO packet = packets.get(i); + if (packet != null) { + buffer.put(packet.getData(), 0, packet.getSize()); + } + } + group.setGroupData(buffer.array()); + } + + @Override + public SarImagePacketGroupDTO putPacket(String sourceIp, SarImagePacketDTO packet) { + // 1.包放入缓冲区 + long sessionId = packet.getSessionId(); + SarImagePacketGroupDTO group; + if (contains(sourceIp, sessionId)) { + group = get(sourceIp, sessionId); + group.put(packet); + } else { + group = SarImagePacketGroupDTO.init(sourceIp, packet); + // 将帧与航线绑定,当航线2开机但航线1的图片未传完时,用航线ID判断生成的图片是属于哪条航线 + JmAirlineStatusDTO currAirline = jobStatusService.getCurrAirline(sourceIp); + if (currAirline == null) { + return null; // 没有在执行的任务或航线 + } + group.setAirlineExecId(currAirline.getExecId()); + put(sourceIp, sessionId, group); + } + //System.out.println(packet); + // 2.判断是否全部包到达,是则合并 + SarImagePacketDTO last = group.getLastPacket(); + // 若所有包已收到则合并 + if (last != null && last.isLast() && group.getPackets().size() == group.getMaxNo() + 1) { + log.info("[UDP] sesionid="+ packet.getSessionId() + "全部到达合并!"); + join(group); + this.packetQueue.offer(group); + remove(sourceIp, sessionId); + } + return group; + } +} diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/listen/SarImageUdpProcessor.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/listen/SarImageUdpProcessor.java index ea74e79..22e5e88 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/listen/SarImageUdpProcessor.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/listen/SarImageUdpProcessor.java @@ -2,6 +2,7 @@ package com.zhangy.skyeye.sar.listen; import com.zhangy.skyeye.common.extend.util.JsonUtil; import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO; +import com.zhangy.skyeye.jm.dto.JmUavStatusDTO; import com.zhangy.skyeye.jm.entity.JmImage; import com.zhangy.skyeye.jm.service.JmJobStatusService; import com.zhangy.skyeye.sar.dto.SarBackFramePackDTO; @@ -9,8 +10,10 @@ import com.zhangy.skyeye.sar.dto.SarBackImageFrameDTO; import com.zhangy.skyeye.sar.dto.SarImagePacketDTO; import com.zhangy.skyeye.sar.dto.SarImagePacketGroupDTO; import com.zhangy.skyeye.sar.service.ISarImageService; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @@ -24,7 +27,7 @@ import java.util.Map; @Primary @Slf4j @Service -public class SarImageUdpProcessor extends SarAbstractUdpProcessor { +public class SarImageUdpProcessor extends SarAsynAbstractUdpProcessor { @Autowired private ISarImageService sarImageService; @@ -32,62 +35,52 @@ public class SarImageUdpProcessor extends SarAbstractUdpProcessor packets = group.getPackets(); - //log.info("定时判断合并udp:" + group.getSessionId() + "," + packets.keySet()); - // 首包包含图片格式信息,不能丢,否则图片无法打开 - return packets.get(0) != null; - } - - /** - * 将upd分包数据合并为完整帧(帧数据 + 头尾校验),因为无法确定分包大小,分包缺失无法补充0 - * 合并后删除frameMap的分包数据 - * 使用ByteBuffer替代ByteArrayOutputStream,以减少内存分配。ByteArrayOutputStream 的 toByteArray()底层用Arrays.copyOf实现, - * 有性能开销;ByteBuffer的array()无需拷贝数组 - * - * @param group - * @return - */ - private byte[] join(SarImagePacketGroupDTO group) { - ByteBuffer buffer = ByteBuffer.allocate(group.getTotalSize()); - buffer.order(ByteOrder.BIG_ENDIAN); - - Map packets = group.getPackets(); - // 使用ByteBuffer的批量操作,减少系统调用 - for (int i = 0; i <= group.getLastPacket().getPacketNo(); i++) { - SarImagePacketDTO packet = packets.get(i); - if (packet != null) { - buffer.put(packet.getData(), 0, packet.getSize()); - } + // 1.取航线 + Long airlineExecId = group.getAirlineExecId(); + if (airlineExecId == null) { + return; + } + JmUavStatusDTO uav = jobStatusService.getCurrUav(group.getSourceIp()); + if (uav == null) { + return; + } + // 查询图像对应的航线,不能查当前航线,因为可能是前一航线没传完的图 + JmAirlineStatusDTO currAirline = uav.getAirline(airlineExecId); + if (currAirline == null) { + return; + } + // 2.拼接图片 + JmImage base = sarImageService.parseImage(uav, currAirline, airlineExecId, frameData, frameDTO); + if (base == null) { + return; } - return buffer.array(); } @Override @@ -95,58 +88,9 @@ public class SarImageUdpProcessor extends SarAbstractUdpProcessor packets = group.getPackets(); - if (packets.size() == 1 && packets.values().iterator().next().isLast()) { - // 雷达传图有bug,每帧的尾包会多发一个,丢弃且不打印日志 - } else { - log.warn("{}-移除超时帧: {}", getText(), group.getSessionId()); - } - } - } - } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/ISarImageService.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/ISarImageService.java index b1bc616..6376957 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/ISarImageService.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/ISarImageService.java @@ -1,5 +1,7 @@ package com.zhangy.skyeye.sar.service; +import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO; +import com.zhangy.skyeye.jm.dto.JmUavStatusDTO; import com.zhangy.skyeye.jm.entity.JmImage; import com.zhangy.skyeye.sar.dto.SarBackImageFrameDTO; @@ -15,5 +17,5 @@ public interface ISarImageService { * @param frameData 图像帧数据 * @param imageFrame 图像帧 */ - JmImage parseImage(String sourceIp, Long airlineExecId, byte[] frameData, SarBackImageFrameDTO imageFrame); + JmImage parseImage(JmUavStatusDTO uav, JmAirlineStatusDTO currAirline, Long airlineExecId, byte[] frameData, SarBackImageFrameDTO imageFrame); } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarBackWsServiceImpl.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarBackWsServiceImpl.java index 71e07b4..444df11 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarBackWsServiceImpl.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarBackWsServiceImpl.java @@ -48,6 +48,7 @@ public class SarBackWsServiceImpl implements ISarBackWsService { @Override public void sendImg(JmImage imageInfo) { + log.info("===send image back ======"); simpMessageingTemplate.convertAndSend(WebSocketKey.SAR_BACK_IMAGE, imageInfo); } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarImageServiceImpl.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarImageServiceImpl.java index 939fd01..d39a66f 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarImageServiceImpl.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/service/impl/SarImageServiceImpl.java @@ -1,6 +1,7 @@ package com.zhangy.skyeye.sar.service.impl; import com.zhangy.skyeye.common.extend.util.MathUtil; +import com.zhangy.skyeye.jm.consts.JmJobModeEnum; import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO; import com.zhangy.skyeye.common.extend.util.ObjectUtil; import com.zhangy.skyeye.redis.utils.RedisUtil; @@ -14,6 +15,7 @@ import com.zhangy.skyeye.publics.service.SysFileTypeService; import com.zhangy.skyeye.publics.utils.ImageUtil; import com.zhangy.skyeye.publics.utils.OpenCVUtil; import com.zhangy.skyeye.sar.dto.SarBackImageFrameDTO; +import com.zhangy.skyeye.sar.service.ISarBackWsService; import com.zhangy.skyeye.sar.service.ISarImageService; import com.zhangy.skyeye.sar.service.SarWsAsyncService; import com.zhangy.skyeye.sar.util.RadarDisplayOptions; @@ -44,20 +46,15 @@ public class SarImageServiceImpl implements ISarImageService { @Autowired private SarWsAsyncService sarWsAsyncService; + @Autowired + protected ISarBackWsService sarBackWsService; @Autowired private RedisUtil redisUtil; - // 图片最大宽度,前端说和电脑有关,4096保险点,一般是4096 8192 16384 - @Value("${skyeye.sar.image.max:4096}") - @Setter - private int IMG_MAX_WITH; - // 起始帧号 - private final String CACHE_FIELD_START_FRAME_NO = "startFrameNo"; - // 当前帧号 - private final String CACHE_FIELD_CURR_FRAME_NO = "currFrameNo"; - // 缓存超时(秒) - private final long CACHE_EXPIRE_SECOND = 24 * 3600; + // 拼图数量 + @Value("${skyeye.sar.image.count:5}") + private long count; /** * 获取基准图像信息 @@ -65,91 +62,40 @@ public class SarImageServiceImpl implements ISarImageService { * @param airlineId 航线执行ID * @param singleWidth 单条图片宽度 * @param frameNo 当前帧号 - * @return 返回非空的图像信息,其字段 imageNo 一定有值 + * @return 返回非空的图像信息 */ - private JmImage getBaseImage(Long airlineId, int singleWidth, int frameNo) {IMG_MAX_WITH=1; - List imageList = imageService.selectLowByAirline(airlineId); + private JmImage getBaseImage(JmUavStatusDTO uav, Long airlineId, int singleWidth, int frameNo) { + JmImage last = imageService.selectLastByAirline(FileTypeEnum.SAR_IMAGE_LOW, airlineId); + + // last = null; + String cacheKey = "jmImgJoin-" + airlineId; JmImage base = null; // 情况1:航线第一张图 - if (ObjectUtil.isEmpty(imageList)) { + if (last == null) { base = new JmImage(); base.setImageNo(1); - redisUtil.hset(cacheKey, CACHE_FIELD_START_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND); + base.setFrameFrom(frameNo); + return base; + } else if (uav.getJobMode() == JmJobModeEnum.CRUISE) { // 手动模式不拼接 + base = new JmImage(); + base.setImageNo(last.getImageNo() + 1); + base.setFrameFrom(frameNo); return base; } // 情况2:如果最后一张还能拼图,则直接返回继续拼 - JmImage last = imageList.get(imageList.size() - 1); - Integer startFrameNo = (Integer) redisUtil.hget(cacheKey, CACHE_FIELD_START_FRAME_NO); - int currWidth = startFrameNo == null ? 0 : singleWidth * (frameNo - startFrameNo + 1); // 图宽(当前图+基准图) - int surplusNum = (IMG_MAX_WITH - currWidth) / singleWidth; // 还可以拼图片数 - Integer baseNo = (Integer) redisUtil.hget("jmImgJoin-" + airlineId, CACHE_FIELD_CURR_FRAME_NO); - if (startFrameNo == null || currWidth < IMG_MAX_WITH || - baseNo == null || (frameNo - baseNo + 1 <= surplusNum)) { // 当前图+填充 不能超过允许拼接数 - log.info("当前宽度:" + currWidth + " < " + IMG_MAX_WITH + " 可以继续拼接"); + File lastFile = new File(last.getFilePath()); + if (frameNo - last.getFrameFrom() < count) { // 当前图+填充 不能超过允许拼接数 + log.info("当前帧:" + last.getFrameFrom() + "," + frameNo + " 可以继续拼接"); return last; } // 情况3:已经拼接到最大数量,或者当前图+填充数量超过允许拼接数量,创建新图像文件 - log.info("当前宽度:" + currWidth + " > " + IMG_MAX_WITH + " 重新拼接,当前帧号" + frameNo + "作为首帧"); + log.info("当前帧:" + last.getFrameFrom() + "," + frameNo + " 重新拼接,当前帧号" + frameNo + "作为首帧"); base = new JmImage(); - redisUtil.hset(cacheKey, CACHE_FIELD_START_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND); base.setImageNo(last.getImageNo() + 1); + base.setFrameFrom(frameNo); return base; } - - private void modCoord(SarBackImageFrameDTO imageFrame, boolean lostImage, JmAirlineStatusDTO currAirline) { - JmImageRotateDTO rotateDTO = JmImageRotateDTO.rotate(currAirline.getTargetHeading(), currAirline.getDirection()); - Double[] before = currAirline.getBeforeRight(); - boolean isFirst = before == null; - if (isFirst) { - before = new Double[4]; - currAirline.setBeforeRight(before); - } - // 使用前一张图的右侧坐标作为后一张图的左侧,前提是没丢图 - if (!isFirst && !lostImage) { - /*imageFrame.setLon1(before[0]); - imageFrame.setLat1(before[1]); - imageFrame.setLon4(before[2]); - imageFrame.setLat4(before[3]);*/ - } - before[0] = imageFrame.getLon5(); - before[1] = imageFrame.getLat5(); - before[2] = imageFrame.getLon8(); - before[3] = imageFrame.getLat8(); - /*switch (rotateDTO.getType()) { - case 0: - case 1: - // 使用前一张图的右侧坐标作为后一张图的左侧,前提是没丢图 - if (!isFirst && !lostImage) { - imageFrame.setLon1(before[0]); - imageFrame.setLat1(before[1]); - imageFrame.setLon4(before[2]); - imageFrame.setLat4(before[3]); - } else if (before == null) { - before = new Double[4]; - currAirline.setBeforeRight(before); - } - before[0] = imageFrame.getLon5(); - before[1] = imageFrame.getLat5(); - before[2] = imageFrame.getLon8(); - before[3] = imageFrame.getLat8(); - break; - case 2: - case 3: - if (!isFirst && !lostImage) { - imageFrame.setLon5(before[0]); - imageFrame.setLat5(before[1]); - imageFrame.setLon8(before[2]); - imageFrame.setLat8(before[3]); - } - before[0] = imageFrame.getLon1(); - before[1] = imageFrame.getLat1(); - before[2] = imageFrame.getLon4(); - before[3] = imageFrame.getLat4(); - break; - } */ - } - /** * 将原始图像数据保存为dat文件,图像转png并将路径推送给前端 * @@ -159,109 +105,59 @@ public class SarImageServiceImpl implements ISarImageService { * @param imageFrame 图像帧 */ @Override - public JmImage parseImage(String sourceIp, Long airlineExecId, byte[] frameData, SarBackImageFrameDTO imageFrame) { - if (airlineExecId == null) { - return null; - } + public JmImage parseImage(JmUavStatusDTO uav, JmAirlineStatusDTO currAirline, Long airlineExecId, byte[] frameData, SarBackImageFrameDTO imageFrame) { int frameNo = imageFrame.getFrameNo(); - JmUavStatusDTO uav = jobStatusService.getCurrUav(sourceIp); - // 若载荷任务结束,sar仍然回传图像则 uav为空。此场景仅限sardemo,实际sar不会发生 - if (uav == null) { - return null; - } - // 查询图像对应的航线,不能查当前航线,因为可能是前一航线没传完的图 - JmAirlineStatusDTO currAirline = uav.getAirline(airlineExecId); - if (currAirline == null) { - return null; - } Long jobExecId = uav.getJobExecId(); //----------------------------------- 处理图像 -------------------------------------------------------- long start = System.currentTimeMillis(); // 1.保存dat(异步) sarWsAsyncService.saveImageDat(jobExecId, airlineExecId + "-" + frameNo + ".dat", frameData); - // 2.生成当前图像矩阵 - Mat currImage = loadCurrImageMat(frameData, imageFrame); + Mat currImage = ImageUtil.loadCurrImageMat(frameData, imageFrame); if (currImage == null) { return null; } - // 3.保存图像png,用航线ID+序号命名 - JmImage base = getBaseImage(airlineExecId, currImage.width(), imageFrame.getFrameNo()); - String imageName = airlineExecId + "-" + base.getImageNo() +".png"; - String[] imagePath = sysFileTypeService.getFilePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId, imageName); - String currPath = imagePath[0]; + JmImage base = getBaseImage(uav, airlineExecId, currImage.width(), imageFrame.getFrameNo()); + String baseName = airlineExecId + "-" + base.getImageNo() +"-base.png"; + String[] basePath = sysFileTypeService.getFilePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId, baseName); - System.out.println("帧:" + frameNo); + // 4.保存基准图(同步),并拼接 + Integer baseNo = base.getFrameTo(); - // 4.保存基准图(同步),用于下次拼接 - Integer baseNo = (Integer) redisUtil.hget("jmImgJoin-" + airlineExecId, CACHE_FIELD_CURR_FRAME_NO); - boolean lostImage = baseNo != null && (frameNo - baseNo > 1); // 判断是否丢图 - String basePath = sysFileTypeService.getAbsolutePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId, - airlineExecId + "-" + base.getImageNo() +"-base.png"); - Mat baseMat = generateBaseMat(base, currImage, frameNo, imageFrame.getMax(), basePath, baseNo); + Mat baseMat = generateBaseMat(base, currImage, frameNo, imageFrame.getMax(), basePath[0], baseNo); if (baseMat == null) { // 拼接失败 或 基准图生成失败则跳过,按丢图处理 return null; } - if (lostImage) { - log.warn("丢图"+(frameNo - baseNo)+"张!当前帧" + frameNo + ",前帧" + baseNo); - } - //modCoord(imageFrame, lostImage, currAirline); - redisUtil.hset("jmImgJoin-" + airlineExecId, CACHE_FIELD_CURR_FRAME_NO, frameNo, CACHE_EXPIRE_SECOND);// 更新帧号 - // ### 亮度调整,用于可靠udp版本图像,固定使用系数0.5 - // 拆分多张图片,去掉自适应调整 - // if (IMG_MAX_WITH > 20000) { + // 更新坐标 + boolean isFirst = base.getId() == null; + // ### 亮度调整,用于可靠udp版本图像,固定使用系数0.5 手动模式不调整 + // enhance image + // if (uav.getJobMode() != JmJobModeEnum.CRUISE) { SarImageToneAdjuster.autoToneWithClipping(baseMat, 0.5f); - // Mat enhancedImage = SarImageToneAdjuster.enhanceSarImage(baseMat); -// RadarDisplayOptions options = new RadarDisplayOptions(); -// SarImageToneAdjuster.processForDisplayMat(baseMat, options); -// SarImageToneAdjuster.applyPseudoColorMat(baseMat, options); - //} + // } // 5.保存后处理图(异步),用于前端显示 - generateAfterMat(currAirline, baseMat, currPath, uav.getSarImageLight()); - // generateAfterMat(currAirline, enhancedImage, currPath, uav.getSarImageLight()); + String afterName = airlineExecId + "-" + base.getImageNo() + ".png"; + String[] afterPath = sysFileTypeService.getFilePath(FileTypeEnum.SAR_IMAGE_LOW, jobExecId, afterName); + JmImageRotateDTO rotateDTO = JmImageRotateDTO.rotate(currAirline.getTargetHeading(), currAirline.getDirection()); + rotateDTO.setLightRate(uav.getSarImageLight()); + base.setRotateAngle(rotateDTO.getAngle()); + rotateDTO.setAngle(0); // 不旋转 + generateAfterMat(baseMat, afterPath[0], rotateDTO); + saveImage(uav, airlineExecId, base, afterPath, imageFrame, frameNo, currAirline); // 6.更新基准图坐标和帧号 - JmImage imageInfo = saveImage(uav, airlineExecId, base, imagePath, imageFrame, frameNo, lostImage); + //updateCoord(airlineExecId, currAirline, imageFrame); + long end = System.currentTimeMillis(); - log.info("生成" + imageFrame.getImageBitDeep()+"位雷达回传图像:帧序号" + frameNo + "," + - imageInfo.getRelativePath() + ",耗时" + (end - start)/1000 + "秒"); - return imageInfo; - } - - /** - * 加载回传图像,先转置再转为Mat - * - * @param frameData 图像帧数据,包含参数信息和未转置的8位灰度图像数据 - * @param imageFrame 图像帧对象 - * @return 转置后的4通道图像Mat - */ - private Mat loadCurrImageMat(byte[] frameData, SarBackImageFrameDTO imageFrame) { - Mat currImage = null; - int offset = imageFrame.IMAGE_OFFSET; - - switch (imageFrame.getImgType()) { - case 0: // tif - currImage = ImageUtil.createImage(frameData, offset, imageFrame.getWidth(), - imageFrame.getHeight(), imageFrame.getImageBitDeep()); - break; - case 1: // jpg,先去掉参数信息,只保留图像数据 - case 4: { // png - byte[] validData = new byte[frameData.length - offset]; - System.arraycopy(frameData, offset, validData, 0, validData.length); - currImage = ImageUtil.createImageFromJpg(validData); - break; - } - default: - log.error("不支持的图片类型imgType=" + imageFrame.getImgType() + ",丢弃图片"); - } - if (currImage != null && currImage.empty()) { - log.error("图片数据是空的,数据长度:" + frameData.length); - currImage.release(); - } - return currImage; + log.info("生成" + imageFrame.getImageBitDeep() + "位雷达回传图像:帧序号" + frameNo + "," + + base.getRelativePath() + ",耗时" + (end - start) / 1000 + "秒"); + // 向前端推送 + base.setJobId(base.getJobConfId()); + sarBackWsService.sendImg(base); + return base; } /** @@ -277,22 +173,61 @@ public class SarImageServiceImpl implements ISarImageService { private Mat generateBaseMat(JmImage base, Mat currImage, int currNo, float currMax, String imagePath, Integer baseNo) { Mat baseImage = OpenCVUtil.read(imagePath); // 从硬盘加载基准图 // 归一化调整 -// if (base.getId() != null) { -// float baseMax = base.getMax(); -// //System.out.println("基准图:" + baseMax + ",当前图:" + currMax); -// if (baseMax < currMax) { -// OpenCVUtil.multiply(baseImage, baseMax / currMax); -// } else if (baseMax > currMax) { -// OpenCVUtil.multiply(currImage , currMax / baseMax); -// } -// } - Mat baseMat = ImageUtil.join(baseImage, baseNo, currImage, currNo); // 会释放 currImage baseImage 资源 + if (base.getId() != null) { + float baseMax = base.getMax(); + //System.out.println("基准图:" + baseMax + ",当前图:" + currMax); + if (baseMax < currMax) { + OpenCVUtil.multiply(baseImage, baseMax / currMax); + } else if (baseMax > currMax) { + OpenCVUtil.multiply(currImage , currMax / baseMax); + } + } + Mat baseMat = ImageUtil.join(baseImage, baseNo, currImage, currNo, base.getId() == null); // 会释放 currImage baseImage 资源 if (baseMat != null) { // 若拼接成功则回写硬盘,拼接失败按丢图处理,不改动基准图 OpenCVUtil.write(imagePath, baseMat, false); } return baseMat; } + private Mat generateBaseMat2(JmImage base, Mat currImage, int currNo, float currMax, String imagePath, Integer baseNo) { + Mat baseImage = OpenCVUtil.read(imagePath); + + // ============================== + // 分位数匹配逻辑 + // ============================== + if (base != null && base.getId() != null + && baseImage != null + && currImage != null) { + + double baseP = SarImageToneAdjuster.computePercentile(baseImage, 0.99); + double currP = SarImageToneAdjuster.computePercentile(currImage, 0.99); + + if (baseP > 0 && currP > 0) { + + double scale = baseP / currP; + + // 限制比例,防止跳变 + double MIN_SCALE = 0.7; + double MAX_SCALE = 1.5; + + scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); + + OpenCVUtil.multiply(currImage, scale); + } + } + + // ============================== + // 原拼接逻辑不变 + // ============================== + Mat baseMat = ImageUtil.join(baseImage, baseNo, currImage, currNo, base.getId() == null); + + if (baseMat != null) { + OpenCVUtil.write(imagePath, baseMat, false); + } + + return baseMat; + } + /** * 生成后处理图,将拼接好的基准图转置并调整对比度 * @@ -301,12 +236,45 @@ public class SarImageServiceImpl implements ISarImageService { * @param imagePath 后处理图路径,每条航线对应一张图,每次生成会覆盖 * @param imageLight 图像亮度倍数,0是不调整 */ - private void generateAfterMat(JmAirlineStatusDTO currAirline, Mat baseMat, String imagePath, int imageLight) { + private void generateAfterMat(Mat baseMat, String imagePath, JmImageRotateDTO rotateDTO) { // 后处理参数 - JmImageRotateDTO rotateDTO = JmImageRotateDTO.rotate(currAirline.getTargetHeading(), currAirline.getDirection()); - rotateDTO.setLightRate(imageLight); Mat afterMat = ImageUtil.afterCreate(baseMat, rotateDTO); // 会释放 baseMat 资源 - sarWsAsyncService.write(imagePath, afterMat, true); + OpenCVUtil.write(imagePath, afterMat, true); + } + + /** + * 加载图像坐标 + * @param currAirline + * @param image + * @param imageFrame + * @param isFirst + */ + private void loadCoord(JmJobModeEnum jobMode, JmAirlineStatusDTO currAirline, JmImage image, SarBackImageFrameDTO imageFrame, boolean isFirst) { + double[] values = currAirline.getValues(); + if (isFirst) { + if (values == null) { + values = new double[9]; + currAirline.setValues(values); + } + if (values[4] != image.getImageNo() && image.getImageNo() > 1 && jobMode != JmJobModeEnum.CRUISE) { // 1、4用前一张图5、8坐标 + // System.out.println("mode:" + jobMode.getText()); + values[0] = values[5]; + values[1] = values[6]; + values[2] = values[7]; + values[3] = values[8]; + } else { + values[0] = imageFrame.getLon1(); + values[1] = imageFrame.getLat1(); + values[2] = imageFrame.getLon4(); + values[3] = imageFrame.getLat4(); + } + } + values[4] = image.getImageNo(); + values[5] = imageFrame.getLon5(); + values[6] = imageFrame.getLat5(); + values[7] = imageFrame.getLon8(); + values[8] = imageFrame.getLat8(); + image.updateCoord(values[0], values[1], values[2], values[3], values[5], values[6], values[7], values[8]); } /** @@ -315,24 +283,16 @@ public class SarImageServiceImpl implements ISarImageService { * @param airlineId 航线执行ID */ private JmImage saveImage(JmUavStatusDTO uav, Long airlineId, JmImage image, String[] imagePath, - SarBackImageFrameDTO imageFrame, int frameNo, boolean lostImage) { + SarBackImageFrameDTO imageFrame, int frameNo, JmAirlineStatusDTO currAirline) { boolean isFirst = image.getId() == null; - - // 拼接后的图片的四角坐标 - double minLon = isFirst ? MathUtil.min(imageFrame.getLon1(), imageFrame.getLon4(), imageFrame.getLon5(), imageFrame.getLon8()) - : MathUtil.min(imageFrame.getLon1(), imageFrame.getLon4(), imageFrame.getLon5(), imageFrame.getLon8(), image.getLeft1Lon(), image.getRight1Lon()); - double maxLon = isFirst ? MathUtil.max(imageFrame.getLon1(), imageFrame.getLon4(), imageFrame.getLon5(), imageFrame.getLon8()) - : MathUtil.max(imageFrame.getLon1(), imageFrame.getLon4(), imageFrame.getLon5(), imageFrame.getLon8(), image.getLeft1Lon(), image.getRight1Lon()); - double minLat = isFirst ? MathUtil.min(imageFrame.getLat1(), imageFrame.getLat4(), imageFrame.getLat5(), imageFrame.getLat8()) - : MathUtil.min(imageFrame.getLat1(), imageFrame.getLat4(), imageFrame.getLat5(), imageFrame.getLat8(), image.getLeft1Lat(), image.getLeft2Lat()); - double maxLat = isFirst ? MathUtil.max(imageFrame.getLat1(), imageFrame.getLat4(), imageFrame.getLat5(), imageFrame.getLat8()) - : MathUtil.max(imageFrame.getLat1(), imageFrame.getLat4(), imageFrame.getLat5(), imageFrame.getLat8(), image.getLeft1Lat(), image.getLeft2Lat()); - // 更新坐标和序号 - image.updateCoord(minLon, maxLon, minLat, maxLat); + // loadCoord(uav.getJobMode(), currAirline, image, imageFrame, isFirst); + loadCoord(JmJobModeEnum.CREATE, currAirline, image, imageFrame, isFirst); Float currMax = !isFirst && image.getMax() > imageFrame.getMax() ? image.getMax() : imageFrame.getMax(); image.setMax(currMax); image.setImageTime(imageFrame.getDate()); + image.setFrameTo(frameNo); + image.setBrightness(uav.getSarImageLight()); if (isFirst) { File file = new File(imagePath[0]); @@ -350,11 +310,11 @@ public class SarImageServiceImpl implements ISarImageService { imageService.updateNotNull(image); } // 前端需要 - image.setJobId(uav.getJobId()); // 前端通过任务配置ID关联任务 + image.setJobConfId(uav.getJobId()); image.setJobName(uav.getJobName()); image.setUavName(uav.getUavName()); image.setPayloadName(uav.getSarName()); - image.setBrightness(uav.getSarImageLight()); + return image; } } diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/util/SarImageToneAdjuster.java b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/util/SarImageToneAdjuster.java index 096f80f..e1ff3b7 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/util/SarImageToneAdjuster.java +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/java/com/zhangy/skyeye/sar/util/SarImageToneAdjuster.java @@ -14,9 +14,12 @@ import org.opencv.photo.Tonemap; import java.util.ArrayList; import java.util.List; +import lombok.extern.slf4j.Slf4j; + /** * SAR图像亮度修正,在拼接图片后食用 */ +@Slf4j public class SarImageToneAdjuster { // 示例 @@ -693,69 +696,130 @@ public class SarImageToneAdjuster { return dst; } + public static Mat enhanceSarAuto(Mat src) { + + Mat gray = new Mat(); + + // ========================= + // 转单通道 + // ========================= + if (src.channels() == 1) { + gray = src; + } + else if (src.channels() == 3) { + Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY); + } + else if (src.channels() == 4) { + Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGRA2GRAY); + } + else { + throw new IllegalArgumentException("Unsupported channel number"); + } + + // ========================= + // 调用增强 + // ========================= + return enhanceSarImage(gray); + } + public static Mat enhanceSarImage(Mat src) { if (src.channels() != 1) { + log.info("====channels: " + src.channels() + "======"); throw new IllegalArgumentException("Only single channel supported"); } Mat floatMat = new Mat(); - // ========================= - // 转 float - // ========================= - if (src.type() == CvType.CV_16U) { + // 1️⃣ 转 float + if (src.type() == CvType.CV_16U) src.convertTo(floatMat, CvType.CV_32F, 1.0 / 65535.0); - } else if (src.type() == CvType.CV_8U) { + else if (src.type() == CvType.CV_8U) src.convertTo(floatMat, CvType.CV_32F, 1.0 / 255.0); - } else { + else throw new IllegalArgumentException("Unsupported type"); - } - // ========================= - // log 压缩 - // ========================= + // 2️⃣ log Core.add(floatMat, Scalar.all(1e-6), floatMat); Core.log(floatMat, floatMat); - // ========================= - // 固定裁剪 - // ========================= - double low = -8; // 根据你的数据调 - double high = 0; + // 3️⃣ 百分位裁剪(稳定) + Mat flat = floatMat.reshape(1,1); + Mat sorted = flat.clone(); + Core.sort(sorted, sorted, Core.SORT_ASCENDING); + + int total = sorted.cols(); + double low = sorted.get(0, (int)(total * 0.01))[0]; + double high = sorted.get(0, (int)(total * 0.995))[0]; Core.min(floatMat, new Scalar(high), floatMat); Core.max(floatMat, new Scalar(low), floatMat); Core.subtract(floatMat, new Scalar(low), floatMat); Core.divide(floatMat, new Scalar(high - low), floatMat); - // Core.normalize(floatMat, floatMat, 0.0, 1.0, Core.NORM_MINMAX); - // ========================= - // 转 8bit - // ========================= + // 4️⃣ 转 8bit Mat img8 = new Mat(); floatMat.convertTo(img8, CvType.CV_8U, 255.0); - // ========================= - // CLAHE - // ========================= + // 5️⃣ 去 speckle + Imgproc.medianBlur(img8, img8, 3); + + // 6️⃣ 动态 tile 计算(关键) + int targetTileSize = 512; // 每块目标大小 + int tilesX = Math.max(1, src.cols() / targetTileSize); + int tilesY = Math.max(1, src.rows() / targetTileSize); + CLAHE clahe = Imgproc.createCLAHE(); - clahe.setClipLimit(3.0); - clahe.setTilesGridSize(new Size(8, 8)); + + // 自适应 clip + MatOfDouble mean = new MatOfDouble(); + MatOfDouble std = new MatOfDouble(); + Core.meanStdDev(img8, mean, std); + + double s = std.get(0,0)[0]; + double clip; + + if (s < 20) + clip = 3.0; + else if (s < 40) + clip = 2.5; + else + clip = 2.0; + + clahe.setClipLimit(clip); + clahe.setTilesGridSize(new Size(tilesX, tilesY)); Mat claheOut = new Mat(); clahe.apply(img8, claheOut); - // ========================= - // Unsharp Mask - // ========================= + // 7️⃣ 轻锐化(减弱避免放大缝) Mat blur = new Mat(); Imgproc.GaussianBlur(claheOut, blur, new Size(0,0), 1.0); Mat sharpened = new Mat(); - Core.addWeighted(claheOut, 1.3, blur, -0.3, 0, sharpened); + Core.addWeighted(claheOut, 1.1, blur, -0.1, 0, sharpened); return sharpened; } + + static public double computePercentile(Mat mat, double percentile) { + + if (mat.empty()) + return 0; + + Mat flat = mat.reshape(1, 1); // 拉平成1行 + Mat sorted = new Mat(); + + Core.sort(flat, sorted, Core.SORT_ASCENDING); + + int total = sorted.cols(); + int index = (int)(percentile * total); + + index = Math.min(Math.max(index, 0), total - 1); + + double[] value = sorted.get(0, index); + + return value != null ? value[0] : 0; + } } \ No newline at end of file diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/application-dev.yml b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/application-dev.yml index 583287c..ede83e2 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/application-dev.yml +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/application-dev.yml @@ -61,7 +61,7 @@ mybatis-plus: table-underline: true configuration: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 - #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl call-setters-on-nulls: true # Jasypt 配置 jasypt: @@ -76,7 +76,9 @@ skyeye: sar: image: type: 1 - max: 409600000 + default-level: 15 + max-level: 18 + count: 5 udp: status: answer-timeout: 2 diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/db.sql b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/db.sql index 3ee4e88..a41a706 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/db.sql +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/db.sql @@ -98,13 +98,15 @@ CREATE TABLE `jm_image` ( `right1_lat` decimal(11,8) DEFAULT NULL COMMENT '右上纬度', `right2_lon` decimal(11,8) DEFAULT NULL COMMENT '右下经度', `right2_lat` decimal(11,8) DEFAULT NULL COMMENT '右下纬度', - `frame_no` int DEFAULT NULL COMMENT '图像帧序号', `max` decimal(20,6) DEFAULT NULL COMMENT '归一化前最大值', `image_time` datetime DEFAULT NULL COMMENT '图像生成时间', `create_time` datetime DEFAULT NULL COMMENT '文件上传时间', + `tail_level` int DEFAULT NULL COMMENT '层级', + `rotate_angle` decimal(11,8) DEFAULT NULL COMMENT '旋转角度', `image_no` int DEFAULT '1' COMMENT '图像编号', - `start_frame_no` int DEFAULT NULL, - `end_frame_no` int DEFAULT NULL, + `frame_from` int DEFAULT NULL COMMENT '起始帧', + `frame_to` int DEFAULT NULL COMMENT '结束帧', + `brightness` int DEFAULT NULL COMMENT '亮度', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10542 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='sar图像'; diff --git a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/mapping/jm/JmImageMapping.xml b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/mapping/jm/JmImageMapping.xml index 2d79c65..f5e3226 100644 --- a/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/mapping/jm/JmImageMapping.xml +++ b/backend/Skyeye-sys-dev/skyeye-service-manager/src/main/resources/mapping/jm/JmImageMapping.xml @@ -3,33 +3,41 @@ - select - m.id, - m.name, - m.job_id, j.name as job_name, - m.uav_id, - m.payload_id, - m.airline_id, - m.file_id, - m.image_no, - m.max, - m.left1_lon, - m.left1_lat, - m.left2_lon, - m.left2_lat, - m.right1_lon, - m.right1_lat, - m.right2_lon, - m.right2_lat, - m.image_time, - m.create_time, - f.name as file_name, - f.type as file_type, - f.path as file_path, - f.relative_path as relative_path + select m.id, + m.name, + m.image_no, + m.frame_from, + m.frame_to, + m.job_id, + j.name as job_name, + m.uav_id, + m.payload_id, + m.airline_id, + m.file_id, + m.max, + m.left1_lon, + m.left1_lat, + m.left2_lon, + m.left2_lat, + m.right1_lon, + m.right1_lat, + m.right2_lon, + m.right2_lat, + m.image_time, + m.create_time, + m.tail_level, + m.rotate_angle, + m.brightness, + f.name as file_name, + f.type as file_type, + f.path as file_path, + f.relative_path as relative_path, + j.id as job_conf_id, + j.name as job_name from jm_image m - left join sys_file f on f.id = m.file_id - left join jm_job j on j.id = m.job_id + left join sys_file f on f.id = m.file_id + left join jm_job_exec je on je.id = m.job_id + left join jm_job j on j.id = je.conf_id + +