增加了一个将点云转为图片,再计算面积的方法

泸州-视觉+扫码-昆船
LAPTOP-S9HJSOEB\昊天 9 months ago
parent c306be50b2
commit 12cf1008cb

Binary file not shown.

Binary file not shown.

@ -18,6 +18,16 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis 连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>

@ -36,6 +36,8 @@ public class ConfigProperties {
private String userUrl; private String userUrl;
private boolean sendSignal = false;
private KSEC ksec; private KSEC ksec;
private LightSource lightSource; private LightSource lightSource;

@ -24,6 +24,10 @@ import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; 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 @Service
@Slf4j @Slf4j
@ -46,7 +50,61 @@ public class KuKouService extends ServiceImpl<KukouMapper,KuKou> implements IS
return kuKou; 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<IndustrialCameraReqVO> result = Result.success(industrialCameraReqVO, "图像识别完成");
HttpEntity<Result<IndustrialCameraReqVO>> entity = new HttpEntity<>(result, headers);
ResponseEntity<String> response = restTemplate.exchange(
configProperties.getKsec().getReportHttp(),
HttpMethod.POST,
entity,
new ParameterizedTypeReference<String>() {}
);
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 { try {

@ -58,7 +58,151 @@ public class IntervalPolygonArea {
max = Math.max(max, value); max = Math.max(max, value);
} }
} }
public static double calculateAreaByXInterval(List<double[]> points, int xBinSize, int yGapThreshold) {
if (points == null || points.isEmpty()) return 0;
// Step 1: 按 x 分桶
Map<Integer, List<Double>> 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<double[]> currentPolygon = new ArrayList<>();
// 未跳变的记录
Map<Integer,List<double[]>> polygonMap = new HashMap<>();
// 最新一次记录
Map<Integer,double[]> polygonMapLest = new HashMap<>();
//面积
double totalArea = 0;
// 记录跳变变化,当跳变和当前记录不同时,计算面积
int assJumpCount = 0;
Integer lastX = null;
for (Map.Entry<Integer, List<Double>> entry : xBinnedYValues.entrySet()) {
int x = entry.getKey();
List<Double> 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<double[]> 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<Double> 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<double[]> polygon : polygons) {
//
// }
}
if (currentPolygon.size() >= 3) {
totalArea += polygonArea(currentPolygon);
}
return totalArea;
}
private static double polygonArea(Map<Integer, List<double[]>> polygon) {
double totalArea = 0;
// 按 key 排序,确保顺序处理
List<Integer> 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<double[]> upperContour = polygon.get(upperKey);
List<double[]> lowerContour = polygon.get(lowerKey);
if (upperContour == null || lowerContour == null || upperContour.isEmpty() || lowerContour.isEmpty()) {
continue;
}
// 反转下轮廓以形成闭合路径
List<double[]> combined = new ArrayList<>(upperContour);
List<double[]> reversedLower = new ArrayList<>(lowerContour);
Collections.reverse(reversedLower);
combined.addAll(reversedLower);
// 计算这个组合多边形的面积
totalArea += calculateSinglePolygonArea(combined);
}
return totalArea;
}
/**
* 使 Shoelace
*/
private static double calculateSinglePolygonArea(List<double[]> 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 // 多边形面积公式Shoelace
private static double polygonArea(List<double[]> polygon) { private static double polygonArea(List<double[]> polygon) {
double area = 0; double area = 0;

@ -7,13 +7,18 @@ import com.zhehekeji.web.entity.Interval3D;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.io.*; import java.io.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.zhehekeji.web.service.algorithm.PointToImageMapper.drawPointsAndCalculateArea;
@Slf4j @Slf4j
public class PointCloudProcessor { public class PointCloudProcessor {
@ -672,7 +677,7 @@ public class PointCloudProcessor {
} }
for (int h =5; h>0; h--){ for (int h =5; h>0; h--){
if (map.containsKey(h) && map.get(h).size()>1000){ 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){ if (area>0){
System.out.println("面积:"+area); System.out.println("面积:"+area);
System.out.println("个数:"+area/(double) (l*w)); System.out.println("个数:"+area/(double) (l*w));
@ -686,9 +691,9 @@ public class PointCloudProcessor {
} }
public static void main(String[] args) { public static void main(String[] args) {
String path ="E:\\泸州\\pcd\\实际53识别出54\\58ed9ebf-6956-427d-b248-50f254d652ef--192.168.32.11.pcd"; String path ="E:\\泸州\\27\\40016743\\30,31\\d970c134-98ff-4e7a-b951-b2cbdf9acfd4--192.168.56.11.pcd";
String configPath = "E:\\工作\\泸州测试\\24\\27.json"; String configPath = "E:\\泸州\\27\\27.json";
String typeConfPath = "E:\\工作\\泸州测试\\24\\40016741.json"; String typeConfPath = "E:\\泸州\\27\\temlent\\40016743.json";
List<double[]> points = readPCD(path); List<double[]> points = readPCD(path);
PcdPojo pojo = new PcdPojo(); PcdPojo pojo = new PcdPojo();
PcdPojo pcdPojo = new PcdPojo(); PcdPojo pcdPojo = new PcdPojo();
@ -713,7 +718,10 @@ public class PointCloudProcessor {
e.printStackTrace(); 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); System.out.println(i);
} }

@ -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<double[]> 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<Integer, List<double[]>> 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<double[]> 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<PointColorMapping> 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<Color, Integer> 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<double[]> 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<double[], Color> colorFunc = point -> Color.RED;
//
// Map<Color, Double> result = drawPointsAndCalculateArea(points,"" );
//
// for (Map.Entry<Color, Double> entry : result.entrySet()) {
// System.out.println("颜色: " + entry.getKey() + ", 面积: " + entry.getValue());
// }
// }
}

@ -17,6 +17,7 @@ spring:
url: jdbc:mysql://127.0.0.1:3306/lia_duoji?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 url: jdbc:mysql://127.0.0.1:3306/lia_duoji?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: root username: root
validationQuery: SELECT 1 FROM DUAL validationQuery: SELECT 1 FROM DUAL
# --------本服务端口号 # --------本服务端口号
server: server:
Port: 8099 Port: 8099
@ -52,11 +53,11 @@ cameraConfig:
# 单位毫秒 # 单位毫秒
delayDownloadMp4: 10000 delayDownloadMp4: 10000
#工业相机编码sn #工业相机编码sn
industrialCamera: # industrialCamera:
- 1 # - 1
- 2 # - 2
#camera3D(ip来标注 #camera3D(ip来标注
camera3D: 114 # camera3D: 114
sickIp: 127.0.0.1 sickIp: 127.0.0.1
sickPort: 2001 sickPort: 2001
# ------------ # ------------
@ -86,6 +87,10 @@ IP: 127.0.0.1
# 服务端TCP端口 # 服务端TCP端口
serverPort: 3001 serverPort: 3001
#是否启用发送信号
sendSignal: true
# ------------ 实时视频流 全部页面的格式 行列数量 # ------------ 实时视频流 全部页面的格式 行列数量
videoStyleConfig: videoStyleConfig:
videoStyleRow: 4 videoStyleRow: 4

Loading…
Cancel
Save