diff --git a/libs/lx/LxCameraApi.dll b/libs/lx/LxCameraApi.dll index f3599e6..4ea38c4 100644 Binary files a/libs/lx/LxCameraApi.dll and b/libs/lx/LxCameraApi.dll differ diff --git a/libs/lx/LxDataProcess.dll b/libs/lx/LxDataProcess.dll index 5222376..923c8f5 100644 Binary files a/libs/lx/LxDataProcess.dll and b/libs/lx/LxDataProcess.dll differ diff --git a/web/pom.xml b/web/pom.xml index 85e460b..4a0212b 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -18,6 +18,16 @@ + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.apache.commons + commons-pool2 + cn.hutool hutool-all diff --git a/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java b/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java index c424d89..5e078aa 100644 --- a/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java +++ b/web/src/main/java/com/zhehekeji/web/config/ConfigProperties.java @@ -36,6 +36,8 @@ public class ConfigProperties { private String userUrl; + private boolean sendSignal = false; + private KSEC ksec; private LightSource lightSource; diff --git a/web/src/main/java/com/zhehekeji/web/service/KuKouService.java b/web/src/main/java/com/zhehekeji/web/service/KuKouService.java index 0d19da7..9ffa1e3 100644 --- a/web/src/main/java/com/zhehekeji/web/service/KuKouService.java +++ b/web/src/main/java/com/zhehekeji/web/service/KuKouService.java @@ -24,6 +24,10 @@ import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @Service @Slf4j @@ -46,7 +50,61 @@ public class KuKouService extends ServiceImpl implements IS return kuKou; } - public void setHttp(IndustrialCameraVO scTransmission,Boolean flag) + private ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10); // 初始化一个包含2个线程的调度池; // 可以通过配置注入或使用 @Async 的 scheduler + + public void setHttp(IndustrialCameraVO scTransmission, Boolean flag) { + sendWithRetry(scTransmission, flag, 0); + } + + private void sendWithRetry(IndustrialCameraVO scTransmission, Boolean flag, int retryCount) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Content-Type", "application/json"); + headers.set("User-Agent", "Mozilla/5.0"); + + scTransmission.setTrayCode(setTrayCode(scTransmission.getTrayCode())); + IndustrialCameraReqVO industrialCameraReqVO = new IndustrialCameraReqVO(); + BeanUtil.copyProperties(scTransmission, industrialCameraReqVO); + industrialCameraReqVO.setFlag(1); + + log.info("发送盘点请求: {}", scTransmission.toString()); + Result result = Result.success(industrialCameraReqVO, "图像识别完成"); + + HttpEntity> entity = new HttpEntity<>(result, headers); + + ResponseEntity response = restTemplate.exchange( + configProperties.getKsec().getReportHttp(), + HttpMethod.POST, + entity, + new ParameterizedTypeReference() {} + ); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info("请求成功,响应:{}", response.getBody()); + } else { + log.warn("请求未成功,状态码:{}", response.getStatusCode()); + scheduleRetry(scTransmission, flag, retryCount); + } + + } catch (Exception e) { + log.error("第 {} 次请求失败", retryCount + 1, e); + scheduleRetry(scTransmission, flag, retryCount); + } + } + + private void scheduleRetry(IndustrialCameraVO scTransmission, Boolean flag, int retryCount) { + if (retryCount >= 20) { + log.error("已达到最大重试次数,放弃请求"); + return; + } + + int nextRetry = retryCount + 1; + long delay = 590 * nextRetry; // 延迟时间递增 + + scheduledExecutor.schedule(() -> sendWithRetry(scTransmission, flag, nextRetry), delay, TimeUnit.MILLISECONDS); + } + + public void setHttpSync(IndustrialCameraVO scTransmission,Boolean flag) { try { diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/IntervalPolygonArea.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/IntervalPolygonArea.java index ec8bb63..835b8c6 100644 --- a/web/src/main/java/com/zhehekeji/web/service/algorithm/IntervalPolygonArea.java +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/IntervalPolygonArea.java @@ -58,7 +58,151 @@ public class IntervalPolygonArea { max = Math.max(max, value); } } + public static double calculateAreaByXInterval(List points, int xBinSize, int yGapThreshold) { + if (points == null || points.isEmpty()) return 0; + // Step 1: 按 x 分桶 + Map> xBinnedYValues = new TreeMap<>(); + for (double[] point : points) { + int x = (int) Math.floor(point[0] / xBinSize); + xBinnedYValues.computeIfAbsent(x, k -> new ArrayList<>()).add(point[1]); + } + + // Step 2: 构造轮廓点 + List currentPolygon = new ArrayList<>(); +// 未跳变的记录 + Map> polygonMap = new HashMap<>(); +// 最新一次记录 + Map polygonMapLest = new HashMap<>(); +//面积 + double totalArea = 0; +// 记录跳变变化,当跳变和当前记录不同时,计算面积 + int assJumpCount = 0; + Integer lastX = null; + for (Map.Entry> entry : xBinnedYValues.entrySet()) { + int x = entry.getKey(); + List yList = entry.getValue(); +// 将上次的记录登记到map中 + + double xCenter = x * xBinSize + xBinSize / 2.0; + if (yList.isEmpty()) continue; + if (yList.size()<20) continue; + + // Step 3: 排序 y 并检测空隙 + Collections.sort(yList); + List yLayers = new ArrayList<>(); + int jumpCount = 0; + for (int i = 1; i < yList.size(); i++) { + // 第一个添加到轮廓 + if (i==1){ +// yLayers.add(new double[]{x * xBinSize, yList.get(i-1)}); //添加上下轮廓 + polygonMapLest.put(0,new double[]{x * xBinSize, yList.get(0)}); + } + double prev = yList.get(i - 1); + double curr = yList.get(i); +// 当出现跳变时,添加到轮廓 + if (curr - prev >= yGapThreshold) { + jumpCount ++; + polygonMapLest.put(jumpCount*2-1,new double[]{x * xBinSize, yList.get(i-1)}); + polygonMapLest.put(jumpCount*2,new double[]{x * xBinSize, yList.get(i-1)}); + } + if (i == yList.size()-1){ +// yLayers.add(new double[]{x * xBinSize, yList.get(i-1)}); //添加上下轮廓 + polygonMapLest.put(jumpCount*2+1,new double[]{x * xBinSize, yList.get(i-1)}); + } + } + if (assJumpCount == 0) { + assJumpCount = jumpCount; + } + + + // 若y跳变有变化,或者x有跳变,则将记录map值所记录的轮廓信息计算轮廓面积,并且重置记录map + if ((lastX != null && x - lastX > 1 && !currentPolygon.isEmpty()) || jumpCount != assJumpCount) { +// polygons.add(currentPolygon); + + + assJumpCount = jumpCount; + currentPolygon = new ArrayList<>(); + } + +// 若为最后一次,先登记轮廓信息,在计算轮廓面积,最后一次若y有跳变则不登记,直接计算面积 + currentPolygon.addAll(yLayers); + + // Step 5: 构造上、下轮廓点(取 y 最大和最小值) +// for (List layer : yLayers) { +// double minY = Collections.min(layer); +// double maxY = Collections.max(layer); +// +// currentPolygon.add(new double[]{xCenter, maxY}); // 上轮廓 +// currentPolygon.add(new double[]{xCenter, minY}); // 下轮廓 +// } + + + lastX = x; +// // Step 7: 计算每个轮廓的面积 +// for (List polygon : polygons) { +// +// } + } + + + if (currentPolygon.size() >= 3) { + totalArea += polygonArea(currentPolygon); + } + return totalArea; + } + + private static double polygonArea(Map> polygon) { + double totalArea = 0; + + // 按 key 排序,确保顺序处理 + List keys = new ArrayList<>(polygon.keySet()); + Collections.sort(keys); + + // 每两个为一组进行面积计算 + for (int i = 0; i < keys.size(); i += 2) { + Integer upperKey = keys.get(i); + if (i + 1 >= keys.size()) break; // 如果只剩一个 key,跳过 + + Integer lowerKey = keys.get(i + 1); + + List upperContour = polygon.get(upperKey); + List lowerContour = polygon.get(lowerKey); + + if (upperContour == null || lowerContour == null || upperContour.isEmpty() || lowerContour.isEmpty()) { + continue; + } + + // 反转下轮廓以形成闭合路径 + List combined = new ArrayList<>(upperContour); + List reversedLower = new ArrayList<>(lowerContour); + Collections.reverse(reversedLower); + combined.addAll(reversedLower); + + // 计算这个组合多边形的面积 + totalArea += calculateSinglePolygonArea(combined); + } + + return totalArea; + } + + /** + * 计算单个闭合多边形的面积(使用 Shoelace 公式) + */ + private static double calculateSinglePolygonArea(List polygon) { + if (polygon.size() < 3) return 0; + + double area = 0; + int n = polygon.size(); + + for (int i = 0; i < n; i++) { + double[] p1 = polygon.get(i); + double[] p2 = polygon.get((i + 1) % n); + area += (p1[0] * p2[1] - p2[0] * p1[1]); + } + + return Math.abs(area) / 2.0; + } // 多边形面积公式(Shoelace) private static double polygonArea(List polygon) { double area = 0; diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java index aa53844..083e2f8 100644 --- a/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointCloudProcessor.java @@ -7,13 +7,18 @@ import com.zhehekeji.web.entity.Interval3D; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.awt.*; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.text.DecimalFormat; import java.util.*; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; + +import static com.zhehekeji.web.service.algorithm.PointToImageMapper.drawPointsAndCalculateArea; + @Slf4j public class PointCloudProcessor { @@ -672,7 +677,7 @@ public class PointCloudProcessor { } for (int h =5; h>0; h--){ if (map.containsKey(h) && map.get(h).size()>1000){ - double area =IntervalPolygonArea.calculateArea(map.get(h),pojo.minBounds[0],10); + double area =IntervalPolygonArea.calculateArea(map.get(h),pojo.getMinBounds()[0],20); if (area>0){ System.out.println("面积:"+area); System.out.println("个数:"+area/(double) (l*w)); @@ -686,9 +691,9 @@ public class PointCloudProcessor { } public static void main(String[] args) { - String path ="E:\\泸州\\pcd\\实际53识别出54\\58ed9ebf-6956-427d-b248-50f254d652ef--192.168.32.11.pcd"; - String configPath = "E:\\工作\\泸州测试\\24\\27.json"; - String typeConfPath = "E:\\工作\\泸州测试\\24\\40016741.json"; + String path ="E:\\泸州\\27\\40016743\\30,31\\d970c134-98ff-4e7a-b951-b2cbdf9acfd4--192.168.56.11.pcd"; + String configPath = "E:\\泸州\\27\\27.json"; + String typeConfPath = "E:\\泸州\\27\\temlent\\40016743.json"; List points = readPCD(path); PcdPojo pojo = new PcdPojo(); PcdPojo pcdPojo = new PcdPojo(); @@ -713,7 +718,10 @@ public class PointCloudProcessor { e.printStackTrace(); } - int i = getLongitudinalType(points,pojo, pcdPojo.getWidth(), pcdPojo.getLength(), pcdPojo.getHigh(),pcdPojo.getArrangeType()); +// int i = getLongitudinalType(points,pojo, pcdPojo.getWidth(), pcdPojo.getLength(), pcdPojo.getHigh(),pcdPojo.getArrangeType()); + + int i = PointToImageMapper.getLongitudinalType(points,pojo, pcdPojo.getWidth(), pcdPojo.getLength(), pcdPojo.getHigh(),pcdPojo.getArrangeType()); + System.out.println(i); } diff --git a/web/src/main/java/com/zhehekeji/web/service/algorithm/PointToImageMapper.java b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointToImageMapper.java new file mode 100644 index 0000000..421482b --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/algorithm/PointToImageMapper.java @@ -0,0 +1,255 @@ +package com.zhehekeji.web.service.algorithm; +import org.springframework.stereotype.Service; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.imageio.ImageIO; + +import static com.zhehekeji.web.service.algorithm.PointCloudProcessor.clipPoints; +public class PointToImageMapper { + + public static class PointColorMapping { + public double[] point; + public Color color; + + public PointColorMapping(double[] point, Color color) { + this.point = point; + this.color = color; + } + } + + public static int getLongitudinalType(List points, PcdPojo pojo, int l, int w, int height,String type){ +//pojo里面的floorHeight为地板的值,以后所有的height都将根据地板值进行计算地板值减去当前点的z轴值,为高度,且当为height的倍数的时候,认为是有效的点,其中1倍的冗余在50mm,每高一层,冗余增加20mm +// 计算 + System.out.println("新方法调用"); + double baseTolerance = 50; // 初始冗余 50mm + double additionalTolerancePerLevel = 20; // 每层增加 20mm 冗余 + Map> map = new HashMap<>(); + + points = points.stream() + .filter(point -> clipPoints(point, pojo.getMinBounds(), pojo.getMaxBounds())) + .filter(point -> { + // 计算当前点的高度(地板值减去 z 轴值) + double currentHeight = pojo.getFloorHeight() - point[2]; + + // 确保高度为正值 + if (currentHeight < 0) { + return false; + } + + // 计算当前高度是 height 的几倍 + int level = (int) Math.round(currentHeight / height); + + // 计算允许的冗余范围 + double tolerance = baseTolerance + level * additionalTolerancePerLevel; + + // 判断当前高度是否在允许的范围内 + if (Math.abs(currentHeight - level * height) <= tolerance) { + if (!map.containsKey(level)) { + map.put(level, new ArrayList<>()); + } + map.get(level).add(point); + return true; + }else return false; + }) + .peek(point -> point[2] = pojo.getFloorHeight() - point[2]) + + .collect(Collectors.toList()); + String[] types = type.split(" "); + + int layersCount = 0; + //计算最大值 + for (String s : types){ + if (s.endsWith("w")){ + int maxW= Integer.parseInt(s.substring(0,1)); + layersCount +=maxW; + }else if (s.endsWith("h")){ + int maxL= Integer.parseInt(s.substring(0,1)); + layersCount +=maxL; + } + } + for (int h =5; h>0; h--){ + if (map.containsKey(h) && map.get(h).size()>1000){ + double area = 0; + try { + + area = drawPointsAndCalculateArea(map.get(h), String.valueOf(h),pojo); + + } catch (Exception e) { + throw new RuntimeException(e); + } + if (area>0){ + System.out.println("面积:"+area); + System.out.println("个数:"+area/(double) (l*w)); + int i =(layersCount*(h-1))+(int) Math.min(Math.round(area/(double) (l*w)),layersCount); + return i; + } + } + } + return 0; + } + + + /** + * 根据点集合生成图像,并计算不同颜色区域的面积 + */ + public static Double drawPointsAndCalculateArea( + List points,String taskId,PcdPojo pojo + ) throws Exception { + int imageWidth = 600; + int imageHeight = 600; + String drawMode= "point"; + + if (points == null || points.isEmpty()) { + return 0d; + } + + // Step 1: 找出 x 和 y 的最大最小值 + double minX = pojo.getMinBounds()[0] -20; + double maxX = pojo.getMaxBounds()[0]+20; + + double minY = pojo.getMinBounds()[1]-20; + double maxY = pojo.getMaxBounds()[1]+20; + + for (double[] point : points) { + minX = Math.min(minX, point[0]); + maxX = Math.max(maxX, point[0]); + minY = Math.min(minY, point[1]); + maxY = Math.max(maxY, point[1]); + } + + // Step 2: 计算偏移量,使得所有坐标非负 + double xOffset = minX < 0 ? -minX : 0; + double yOffset = minY < 0 ? -minY : 0; + + // Step 3: 缩放比例 + double scaleX = imageWidth / (maxX - minX + 1e-9); + double scaleY = imageHeight / (maxY - minY + 1e-9); + + // Step 4: 创建图像 + BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.setBackground(Color.WHITE); + g.clearRect(0, 0, imageWidth, imageHeight); + + // Step 5: 绘制点或连线 + List mappedPoints = new ArrayList<>(); + + for (double[] point : points) { + Color color = Color.RED; + double x = point[0]; + double y = point[1]; + + int px = (int) ((x + xOffset) * scaleX); + int py = (int) ((y + yOffset) * scaleY); + + mappedPoints.add(new PointColorMapping(new double[]{x, y}, color)); + + if ("point".equalsIgnoreCase(drawMode)) { + g.setColor(color); + g.fillRect(px, py, 8, 8); // 点模式 + } else if ("line".equalsIgnoreCase(drawMode)) { + // 只有在连线模式下才记录点用于后续连线 + } + } + + // 如果是连线模式,按 x 排序后连接相邻点 + if ("line".equalsIgnoreCase(drawMode)) { + mappedPoints.sort(Comparator.comparingDouble(p -> p.point[0])); + + for (int i = 0; i < mappedPoints.size() - 1; i++) { + PointColorMapping p1 = mappedPoints.get(i); + PointColorMapping p2 = mappedPoints.get(i + 1); + + int x1 = (int) ((p1.point[0] + xOffset) * scaleX); + int y1 = (int) ((p1.point[1] + yOffset) * scaleY); + int x2 = (int) ((p2.point[0] + xOffset) * scaleX); + int y2 = (int) ((p2.point[1] + yOffset) * scaleY); + + g.setColor(p1.color); + g.drawLine(x1, y1, x2, y2); + } + } + + g.dispose(); + + // Step 6: 统计每种颜色的像素数量 + Map colorCount = new HashMap<>(); + Integer totalPixels = 0; + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + int rgb = image.getRGB(x, y); + if (rgb == Color.WHITE.getRGB()) continue; + + Color color = new Color(rgb); + totalPixels++; + } + } + + // Step 7: 换算像素数为实际面积 + double pixelToAreaRatio = (maxX - minX) * (maxY - minY) / (imageWidth * imageHeight); + double area = totalPixels * pixelToAreaRatio; + + // 可选:保存图像 + ImageIO.write(image, "PNG", generateImageFilePath().toFile()); + + return area; + } + /** + * 生成 E:/data/pcdImage/yyyy-MM-dd/HHmmssSSS.png 格式的路径 + * 并创建父目录(如果不存在) + */ + public static Path generateImageFilePath() throws IOException { + // 定义基础路径 + String basePath = "E:/data/pcdImage"; + + // 获取当前日期和时间 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + SimpleDateFormat timeFormat = new SimpleDateFormat("HH_mm_ss_SSS"); + + String dateFolder = dateFormat.format(new Date()); + String fileName = timeFormat.format(new Date()) + ".png"; + + // 构建完整路径 + Path fullPath = Paths.get(basePath, dateFolder, fileName); + + // 创建父目录(如果不存在) + Path parentDir = fullPath.getParent(); + if (parentDir != null && !Files.exists(parentDir)) { + Files.createDirectories(parentDir); + System.out.println("创建目录: " + parentDir); + } + + return fullPath; + } +// +// // 示例调用方法 +// public static void main(String[] args) throws Exception { +// List points = new ArrayList<>(); +// // 示例数据 +// for (int i = 0; i < 100; i++) { +// double x = Math.sin(i * 0.1) * 10; +// double y = Math.cos(i * 0.1) * 10; +// points.add(new double[]{x, y}); +// } +// +// // 使用 lambda 设置颜色规则(示例:红色) +// Function colorFunc = point -> Color.RED; +// +// Map result = drawPointsAndCalculateArea(points,"" ); +// +// for (Map.Entry entry : result.entrySet()) { +// System.out.println("颜色: " + entry.getKey() + ", 面积: " + entry.getValue()); +// } +// } +} diff --git a/web/src/main/resources/application-prod.yml b/web/src/main/resources/application-prod.yml index 237e8a2..776e2b5 100644 --- a/web/src/main/resources/application-prod.yml +++ b/web/src/main/resources/application-prod.yml @@ -17,6 +17,7 @@ spring: url: jdbc:mysql://127.0.0.1:3306/lia_duoji?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 username: root validationQuery: SELECT 1 FROM DUAL + # --------本服务端口号 server: Port: 8099 @@ -52,11 +53,11 @@ cameraConfig: # 单位毫秒 delayDownloadMp4: 10000 #工业相机编码sn - industrialCamera: - - 1 - - 2 +# industrialCamera: +# - 1 +# - 2 #camera3D(ip来标注 - camera3D: 114 +# camera3D: 114 sickIp: 127.0.0.1 sickPort: 2001 # ------------ @@ -86,6 +87,10 @@ IP: 127.0.0.1 # 服务端TCP端口 serverPort: 3001 +#是否启用发送信号 +sendSignal: true + + # ------------ 实时视频流 全部页面的格式 行列数量 videoStyleConfig: videoStyleRow: 4