Compare commits

...

3 Commits

Author SHA1 Message Date
Bingkun Li
7451b5e60a Add image enhancement without clahe 2026-03-03 10:39:55 +08:00
Bingkun Li
6725634e8e Save the changes of testing splicing image solution 2026-03-03 10:03:21 +08:00
Bingkun Li
c7f9482883 Test for image enhancement 2026-03-02 16:03:04 +08:00
24 changed files with 889 additions and 390 deletions

View File

@ -5,6 +5,7 @@ import com.zhangy.skyeye.common.extend.anno.IgnoreAuth;
import com.zhangy.skyeye.common.extend.enums.EnumUtil;
import com.zhangy.skyeye.common.extend.exception.ServiceException;
import com.zhangy.skyeye.common.extend.util.DateUtil;
import com.zhangy.skyeye.common.extend.util.ObjectUtil;
import com.zhangy.skyeye.common.pojo.result.Result;
import com.zhangy.skyeye.jm.consts.JmJobModeEnum;
import com.zhangy.skyeye.jm.dto.JmJobDTO;
@ -77,6 +78,10 @@ public class JmJobController {
@PostMapping("/save")
public Object insert(@Valid @RequestBody JmJobDTO e) {
JmJobModeEnum mode = EnumUtil.parseEx(JmJobModeEnum.class, e.getMode());
if (mode != JmJobModeEnum.CRUISE && ObjectUtil.isEmpty(e.getPointList())) {
throw ServiceException.noLog("任务点不能为空");
}
clearId(e);
// 默认执行一次性任务
if (e.getType() == null) {

View File

@ -52,4 +52,14 @@ public class JmAirlineStatusDTO {
// 前一张右上右下
private Double[] beforeRight;
/** 航线终点经度 */
private Double endLon;
/** 航线终点纬度 */
private Double endLat;
private Integer headingDiff;
private double[] values;
}

View File

@ -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当前状态
*

View File

@ -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<JmImageItem> itemList;
/** 转图像识别算法参数 */

View File

@ -45,6 +45,15 @@ public interface JmImageMapper {
*/
List<JmImage> 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);
/**
* 按主键查询
*/

View File

@ -41,6 +41,11 @@ public interface JmImageService {
*/
List<JmImage> selectLowByAirline(Long airlineExecId);
/**
* 查询航线最后一张图
*/
JmImage selectLastByAirline(FileTypeEnum fileType, Long airlineExecId);
/**
* 按主键查询详情
*/

View File

@ -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<JmImage> selectById(Long... id) {
return imageMapper.selectById(id);

View File

@ -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;
}
}

View File

@ -9,11 +9,11 @@ import java.nio.ByteOrder;
public class ChecksumUtil {
/**
* 计算校验和表82排除最后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

View File

@ -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<Mat> 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();

View File

@ -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

View File

@ -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();

View File

@ -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<char>
* 包数据
*/
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;

View File

@ -31,6 +31,9 @@ public class SarImagePacketGroupDTO extends SarBackPacketGroupDTO<SarImagePacket
/** 分包数据 */
private Map<Integer, SarImagePacketDTO> packets = new HashMap<>();
/** 整包数据 */
private byte[] groupData;
public SarImagePacketDTO getLastPacket() {
if (packets.size() == 0) {
return null;
@ -68,7 +71,7 @@ public class SarImagePacketGroupDTO extends SarBackPacketGroupDTO<SarImagePacket
SarImagePacketGroupDTO group = new SarImagePacketGroupDTO();
group.setSourceIp(sourceIp);
group.setSessionId(packet.getSessionId());
// group.setPacketNum(packet.getPacketNum());
// group.setPacketNum(packet.getPacketNum());
group.setLastArrayTime(System.currentTimeMillis());
group.put(packet);
group.setMaxNo(packet.getPacketNo());

View File

@ -0,0 +1,152 @@
package com.zhangy.skyeye.sar.listen;
import com.zhangy.skyeye.jm.dto.JmAirlineStatusDTO;
import com.zhangy.skyeye.jm.service.JmJobStatusService;
import com.zhangy.skyeye.sar.dto.SarImagePacketDTO;
import com.zhangy.skyeye.sar.dto.SarImagePacketGroupDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 接收udp和处理异步进行可靠传输
*/
@Slf4j
public abstract class SarAsynAbstractUdpProcessor extends SarAbstractUdpProcessor<SarImagePacketGroupDTO, SarImagePacketDTO> {
// 任务队列容量
private final int QUEUE_CAPACITY = 1000;
// 任务队列
private BlockingQueue<SarImagePacketGroupDTO> 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<Integer, SarImagePacketDTO> 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<Integer, SarImagePacketDTO> 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<Integer, SarImagePacketDTO> 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;
}
}

View File

@ -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<SarImagePacketGroupDTO, SarImagePacketDTO> {
public class SarImageUdpProcessor extends SarAsynAbstractUdpProcessor {
@Autowired
private ISarImageService sarImageService;
@ -32,62 +35,52 @@ public class SarImageUdpProcessor extends SarAbstractUdpProcessor<SarImagePacket
@Autowired
private JmJobStatusService jobStatusService;
protected void afterJoin(byte[] framePacketData, SarImagePacketGroupDTO group) {
@Setter
@Value("${ld.sar.image.default-level:15}")
private Integer defaultTailLevel;
@Override
protected void afterJoin(SarImagePacketGroupDTO group) {
boolean hasFirstPacket = group.getPackets().get(0) != null;// 如果有首包则加头校验8字节
boolean hasLastPacket = group.getLastPacket().isLast(); // 如果有尾包则加尾校验8字节
SarBackFramePackDTO framePack = new SarBackFramePackDTO(framePacketData, hasLastPacket);
byte[] frameData = framePack.getFrameData();
SarBackImageFrameDTO imageFram = new SarBackImageFrameDTO(frameData);
if (!imageFram.check()) {
log.warn("图像帧 " + group.getSessionId() + " 已丢弃,因为图像信息不完整。数据:" + imageFram);
if (!hasLastPacket) {
log.warn("sessionId=" + group.getSessionId() + ")缺失尾包,丢弃帧");
}
byte[] framePacketData = group.getGroupData();
SarBackFramePackDTO framePack = SarBackFramePackDTO.parse(framePacketData, hasLastPacket);
if (framePack == null) {
log.warn("图像帧 " + group.getSessionId() + " 已丢弃,帧数据校验失败");
return;
}
log.debug("图像帧数据:" + imageFram);
// 生成图片并更新数据库
JmImage imageInfo = sarImageService.parseImage(group.getSourceIp(), group.getAirlineExecId(), frameData, imageFram);
// 推送仅当前有任务时jobId uavId 不能为空
if (imageInfo != null) {
log.info("推送图片:" + JsonUtil.toString(imageInfo));
sarBackWsService.sendImg(imageInfo);
byte[] frameData = framePack.getFrameData();
SarBackImageFrameDTO frameDTO = new SarBackImageFrameDTO(frameData);
if (!frameDTO.check()) {
log.warn("图像帧 " + group.getSessionId() + " 已丢弃,因为图像信息不完整。数据:" + frameDTO);
return;
}
}
log.debug("图像帧数据:" + frameDTO);
/**
* 判断是否可以合并回图帧首包不能丢
*
* @param group
* @return
*/
public boolean canJoin(SarImagePacketGroupDTO group) {
Map<Integer, SarImagePacketDTO> 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<Integer, SarImagePacketDTO> 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<SarImagePacket
return "雷达回图";
}
@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);
}
// 2.判断是否全部包到达是则合并
SarImagePacketDTO last = group.getLastPacket();
// 若所有包已收到则合并
if (last != null && last.isLast() && group.getPackets().size() == group.getMaxNo() + 1) {
log.info("[UDP] sesionid=" + packet.getSessionId() + "全部到达合并!");
byte[] framePacketData = join(group);
afterJoin(framePacketData, group);
remove(sourceIp, sessionId);
}
return group;
}
@Override
protected String getProcessorName() {
return "sarImageProcessor";
}
@Override
protected void expireProcess(SarImagePacketGroupDTO group) {
// 定时判断超过 PACKET_TIMEOUT 时间未收到新包且符合生成条件则合成图
if (canJoin(group)) {
log.info("[UDP] sesionid {} 超时合并!", group.getSessionId());
byte[] framePacketData = join(group);
afterJoin(framePacketData, group);
} else {
// 超时且无法合并丢弃
Map<Integer, SarImagePacketDTO> packets = group.getPackets();
if (packets.size() == 1 && packets.values().iterator().next().isLast()) {
// 雷达传图有bug每帧的尾包会多发一个丢弃且不打印日志
} else {
log.warn("{}-移除超时帧: {}", getText(), group.getSessionId());
}
}
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<JmImage> 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,107 +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);
// RadarDisplayOptions options = new RadarDisplayOptions();
// SarImageToneAdjuster.processForDisplayMat(baseMat, options);
// SarImageToneAdjuster.applyPseudoColorMat(baseMat, options);
}
// }
// 5.保存后处理图异步用于前端显示
generateAfterMat(currAirline, baseMat, 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;
}
/**
@ -284,13 +182,52 @@ public class SarImageServiceImpl implements ISarImageService {
OpenCVUtil.multiply(currImage , currMax / baseMax);
}
}
Mat baseMat = ImageUtil.join(baseImage, baseNo, currImage, currNo); // 会释放 currImage baseImage 资源
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;
}
/**
* 生成后处理图将拼接好的基准图转置并调整对比度
*
@ -299,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) { // 14用前一张图58坐标
// 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]);
}
/**
@ -313,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]);
@ -348,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;
}
}

View File

@ -2,18 +2,24 @@ package com.zhangy.skyeye.sar.util;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.CLAHE;
import org.opencv.imgproc.Imgproc;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.photo.Photo;
import org.opencv.photo.Tonemap;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
/**
* SAR图像亮度修正在拼接图片后食用
*/
@Slf4j
public class SarImageToneAdjuster {
// 示例
@ -643,4 +649,261 @@ public class SarImageToneAdjuster {
image.put(0, 0, data);
}
/**
* Reinhard 局部色调映射
*/
public static Mat reinhardToneMapping(Mat src,
float gamma,
float intensity,
float lightAdapt,
float colorAdapt) {
Mat srcFloat = new Mat();
src.convertTo(srcFloat, CvType.CV_32FC3, 1.0 / 255.0);
// 注意这里的类型
Tonemap tonemap = Photo.createTonemapReinhard(
gamma,
intensity,
lightAdapt,
colorAdapt
);
Mat dstFloat = new Mat();
tonemap.process(srcFloat, dstFloat);
Mat dst = new Mat();
dstFloat.convertTo(dst, CvType.CV_8UC3, 255.0);
return dst;
}
/**
* 对输入图像做 CLAHE ( LAB 空间)
*/
public static Mat applyCLAHEGray(Mat src,
double clipLimit,
Size tileGridSize) {
CLAHE clahe = Imgproc.createCLAHE();
clahe.setClipLimit(clipLimit);
clahe.setTilesGridSize(tileGridSize);
Mat dst = new Mat();
clahe.apply(src, dst);
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();
// 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)
src.convertTo(floatMat, CvType.CV_32F, 1.0 / 255.0);
else
throw new IllegalArgumentException("Unsupported type");
// 2 log
Core.add(floatMat, Scalar.all(1e-6), floatMat);
Core.log(floatMat, floatMat);
// 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);
// 4 8bit
Mat img8 = new Mat();
floatMat.convertTo(img8, CvType.CV_8U, 255.0);
// 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();
// 自适应 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);
// 7 轻锐化减弱避免放大缝
Mat blur = new Mat();
Imgproc.GaussianBlur(claheOut, blur, new Size(0,0), 1.0);
Mat sharpened = new Mat();
Core.addWeighted(claheOut, 1.1, blur, -0.1, 0, sharpened);
return sharpened;
}
public static Mat enhanceSarIndustrialNoClahe(Mat src) {
if (src.channels() != 1)
throw new IllegalArgumentException("Only single channel supported");
Mat floatMat = new Mat();
// 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)
src.convertTo(floatMat, CvType.CV_32F, 1.0 / 255.0);
else
throw new IllegalArgumentException("Unsupported type");
// 2 log 压缩
Core.add(floatMat, Scalar.all(1e-6), floatMat);
Core.log(floatMat, floatMat);
// 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);
// ==============================
// 4 局部自适应对比增强替代 CLAHE
// ==============================
Mat localMean = new Mat();
double sigma = src.cols() / 30.0;
Imgproc.GaussianBlur(floatMat, localMean, new Size(0,0), sigma);
Mat localContrast = new Mat();
Core.divide(floatMat, localMean, localContrast);
// 压缩避免过增强
Core.min(localContrast, new Scalar(3.0), localContrast);
Core.max(localContrast, new Scalar(0.3), localContrast);
// 再归一
Core.normalize(localContrast, localContrast, 0, 1, Core.NORM_MINMAX);
// ==============================
// 5 gamma 提升
// ==============================
Core.pow(localContrast, 0.8, localContrast);
// ==============================
// 6 8bit
// ==============================
Mat img8 = new Mat();
localContrast.convertTo(img8, CvType.CV_8U, 255.0);
// ==============================
// 7 speckle 抑制
// ==============================
Imgproc.medianBlur(img8, img8, 3);
// ==============================
// 8 轻锐化
// ==============================
Mat blur = new Mat();
Imgproc.GaussianBlur(img8, blur, new Size(0,0), 1.0);
Mat sharpened = new Mat();
Core.addWeighted(img8, 1.15, blur, -0.15, 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;
}
}

View File

@ -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

View File

@ -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图像';

View File

@ -3,33 +3,41 @@
<mapper namespace="com.zhangy.skyeye.jm.mapper.JmImageMapper">
<sql id="selectSql">
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
</sql>
<select id="selectPage" resultType="com.zhangy.skyeye.jm.entity.JmImage">
@ -47,11 +55,13 @@
<if test="name != null and name != ''">
and f.name like concat('%', #{name}, '%')
</if>
order by j.create_time desc, m.create_time
</select>
<select id="selectByJob" resultType="com.zhangy.skyeye.jm.entity.JmImage">
<include refid="selectSql"/>
where m.job_id in
where
m.job_id in
<foreach item="item" collection="array" open="(" separator="," close=")">
#{item}
</foreach>
@ -64,7 +74,7 @@
<include refid="selectSql"/>
where m.job_id = #{jobId}
<if test="array != null and array.length > 0">
and m.uav_id in
and m.uav_id in
<foreach item="item" collection="array" open="(" separator="," close=")">
#{item}
</foreach>
@ -86,6 +96,19 @@
order by m.image_no
</select>
<select id="selectLastByAirline" resultType="com.zhangy.skyeye.jm.entity.JmImage">
<include refid="selectSql"/>
where m.airline_id in
<foreach item="item" collection="array" open="(" separator="," close=")">
#{item}
</foreach>
<if test="type != null">
and f.type = #{type}
</if>
order by m.image_no desc
limit 0,1
</select>
<select id="selectById" resultType="com.zhangy.skyeye.jm.entity.JmImage">
<include refid="selectSql"/>
where m.id in
@ -98,7 +121,7 @@
<include refid="selectSql"/>
where f.type = #{type}
<if test="array != null and array.length > 0">
and f.name in
and f.name in
<foreach item="item" collection="array" open="(" separator="," close=")">
#{item}
</foreach>
@ -110,28 +133,33 @@
<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into jm_image(
id,
name,
job_id,
uav_id,
payload_id,
airline_id,
file_id,
image_no,
max,
left1_lon,
left1_lat,
left2_lon,
left2_lat,
right1_lon,
right1_lat,
right2_lon,
right2_lat,
image_time,
create_time
id,
name,
job_id,
uav_id,
payload_id,
airline_id,
file_id,
rotate_angle,
max,
left1_lon,
left1_lat,
left2_lon,
left2_lat,
right1_lon,
right1_lat,
right2_lon,
right2_lat,
tail_level,
image_time,
create_time,
image_no,
frame_from,
frame_to,
brightness
) values
<foreach item="item" index="index" collection="array" separator=",">
(
<foreach item="item" index="index" collection="array" separator=",">
(
#{item.id},
#{item.name},
#{item.jobId},
@ -139,7 +167,7 @@
#{item.payloadId},
#{item.airlineId},
#{item.fileId},
#{item.imageNo},
#{item.rotateAngle},
#{item.max},
#{item.left1Lon},
#{item.left1Lat},
@ -149,9 +177,14 @@
#{item.right1Lat},
#{item.right2Lon},
#{item.right2Lat},
#{item.tailLevel},
#{item.imageTime},
#{item.createTime}
)
#{item.createTime},
#{item.imageNo},
#{item.frameFrom},
#{item.frameTo},
#{item.brightness}
)
</foreach>
</insert>
@ -165,6 +198,7 @@
<if test="airlineId != null">airline_id = #{airlineId},</if>
<if test="fileId != null">file_id = #{fileId},</if>
<if test="max != null">max = #{max},</if>
<if test="rotateAngle != null">rotate_angle = #{rotateAngle},</if>
<if test="left1Lon != null">left1_lon = #{left1Lon},</if>
<if test="left1Lat != null">left1_lat = #{left1Lat},</if>
<if test="left2Lon != null">left2_lon = #{left2Lon},</if>
@ -175,6 +209,11 @@
<if test="right2Lat != null">right2_lat = #{right2Lat},</if>
<if test="imageTime != null">image_time = #{imageTime},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="tailLevel != null">tail_level = #{tailLevel},</if>
<if test="imageNo != null">image_no = #{imageNo},</if>
<if test="frameFrom != null">frame_from = #{frameFrom},</if>
<if test="frameTo != null">frame_to = #{frameTo},</if>
<if test="brightness != null">brightness = #{brightness},</if>
</trim>
where id = #{id}
</update>
@ -189,6 +228,7 @@
airline_id = #{airlineId},
file_id = #{fileId},
max = #{max},
rotate_angle = #{rotateAngle},
left1_lon = #{left1Lon},
left1_lat = #{left1Lat},
left2_lon = #{left2Lon},
@ -199,10 +239,12 @@
right2_lat = #{right2Lat},
image_time = #{imageTime},
create_time = #{createTime},
tail_level = #{tailLevel},
brightness = #{brightness},
</trim>
where id = #{id}
</update>
<delete id="delete">
delete from jm_image where id in
<foreach item="item" collection="array" open="(" separator="," close=")">
@ -222,7 +264,7 @@
from jm_image img
where img.job_id = #{jobId}
<if test="array != null and array.length > 0">
and img.uav_id in
and img.uav_id in
<foreach item="item" collection="array" open="(" separator="," close=")">
#{item}
</foreach>

View File

@ -1,9 +1,9 @@
window.config = {
env: 'offline', //online
//api: 'http://127.0.0.1:9116/', // 外网服务器,
api: 'http://182.92.203.107:9116',
socket: 'http://182.92.203.107:9116', //外网服务器,
imagePath: 'http://182.92.203.107:8080/files',
api: 'http://127.0.0.1:9116',
socket: 'http://127.0.0.1:9116', //外网服务器,
imagePath: 'http://127.0.0.1:8080/files',
arithmeticPath: 'http://127.0.0.1:18090/ktkx/UavPlanning/SAR',
tokenKey: 'accessToken',
refreshTokenKey: 'refreshToken',