file-type

PCL与OpenCV实现点云体积精确计算方法

ZIP文件

4星 · 超过85%的资源 | 下载需积分: 48 | 5.71MB | 更新于2025-01-18 | 181 浏览量 | 204 下载量 举报 34 收藏
download 立即下载
本资源将探讨如何利用PCL库与OpenCV相结合,进行点云数据的体积求取,这在三维空间分析、物体尺寸测量等场景中非常有用。整个过程将覆盖点云数据的获取、预处理、分析和计算等步骤。" ### 点云获取 点云数据的获取通常通过激光扫描仪、深度摄像头等设备获得。获取的原始点云数据往往含有噪声和不相关的点,需要预处理以提高后续处理的准确性和效率。 ### 滤波 滤波是处理点云数据的重要步骤之一。由于噪声和环境因素的干扰,获取的点云中可能包含大量的噪声点和背景点。PCL提供了多种滤波器,如VoxelGrid滤波器、PassThrough滤波器等,可以去除噪声,提取感兴趣的区域(Region of Interest,ROI)内的点云数据。 ### 分割 分割是将点云分为几个部分或提取出感兴趣的物体的过程。PCL提供了基于法线、颜色、模型拟合等多种分割算法。通过分割,可以将点云中的目标物体与背景分离,为体积计算做准备。 ### 求长宽高 在分割后,可以对物体进行长、宽、高三个维度的测量。这通常需要先对点云数据进行配准,使得物体的点云处于一个统一的坐标系下。通过计算点云边界盒(bounding box)的尺寸,可以得到物体的长、宽、高。 ### 计算物体的体积 求取物体体积的方法之一是使用凸包(convex hull)算法,它能够生成包围所有点的最小凸多面体,进而计算其体积。另外,通过体素化(voxelization)方法将点云转换为体素网格,也能计算占据体素的数目,从而得到物体的体积。PCL库中已经封装好了这些算法,可以直接调用。 ### 配准 为了更准确地测量长宽高和体积,有时需要对多个不同视角获取的点云数据进行配准。点云配准是将不同时间或不同角度获取的点云数据对齐的过程。在PCL库中,可以使用迭代最近点(Iterative Closest Point,ICP)算法进行配准。 ### 检索与特征提取 为了识别和检索特定类型的物体,需要提取点云的特征。这些特征可以是几何特征,如表面法线、曲率等,也可以是其他描述符,如FPFH(Fast Point Feature Histograms)等。PCL库提供了丰富的特征提取算法。 ### 识别与追踪 在点云数据处理中,物体的识别与追踪也是常见任务。通过对点云进行识别,可以确定点云中物体的类别。而追踪则是在一系列点云数据中跟踪物体的运动。 ### 曲面重建与可视化 最后,曲面重建将点云转化为连续的曲面,用于进一步分析或可视化展示。可视化是帮助我们理解三维数据的重要手段,PCL提供了与OpenCV结合的方法来实现点云的可视化。 ### OpenCV集成 OpenCV是一个专注于实时计算机视觉的库,它提供了一些基本的三维重建和可视化功能。在实际应用中,将PCL与OpenCV结合使用,可以发挥各自的优势,如使用OpenCV进行图像数据处理和可视化,而使用PCL进行点云数据处理。 ### 实际应用 在实际应用中,如机器视觉、自动驾驶车辆、三维建模等领域,点云数据的获取、处理、分析和可视化是核心技术之一。正确求取点云体积对于物体尺寸检测、空间占用分析等具有重要意义。 总结以上,利用PCL和OpenCV进行点云体积求取涉及了点云数据处理的多个步骤,包括数据获取、预处理、分析和计算等。掌握这些技能不仅要求对PCL库有深入了解,也需要熟悉相关的三维数据处理和计算机视觉知识。

相关推荐

filetype

/// /// 加载点云数据 /// /// <param name="matDepth"></param> /// <param name="CameraIntrinsics"></param> private void Load3DPointCloud(Mat? matDepth, CameraIntrinsics? CameraIntrinsics) { //try //{ if (CameraIntrinsics == null) { MessageBox.Show("相机内参未设置,请先配置相机内参。"); return; } //ResetGlControl(); // 重置OpenGL控件 if (matDepth == null) { string rootDir = GetSavePath("统筹数据集"); string dateFolder = DateTime.Now.ToString("yyyyMMdd"); string directory = Path.Combine(rootDir, "深度图Tiff", dateFolder); // 选择directory下时间排序的最新的tiff文件 string[] files = Directory.GetFiles(directory, "*.tiff"); if (files.Length == 0) { MessageBox.Show("没有找到深度图文件,请先拍摄深度图。"); return; } // 按照文件创建时间排序 Array.Sort(files, (x, y) => File.GetCreationTime(x).CompareTo(File.GetCreationTime(y))); // 获取最新的文件路径(不带扩展名) string filePath = Path.GetFileNameWithoutExtension(files[files.Length - 1]); // 拿到filePath完整路径 filePath = Path.Combine(directory, filePath); // 读取深度图像,格式为 TIFF,支持任意深度 matDepth = Cv2.ImRead($"{filePath}.tiff", ImreadModes.AnyDepth); } matDepth = RepairDepth(matDepth); // 将 Mat 转换为二维数组 matDepth.GetRectangularArray(out ushort[,] depthElements); DepthImage = matDepth.Clone(); userDrawnOuterContour = false; // 重置用户绘制的外轮廓标志 // 假设您已经有了相机内参(焦距和光心) double fx = CameraIntrinsics.Fx; // 相机焦距 (x) double fy = CameraIntrinsics.Fy; // 相机焦距 (y) double cx = CameraIntrinsics.Ppx; // 光心 x 坐标 double cy = CameraIntrinsics.Ppy; // 光心 y 坐标 camFx = fx; camFy = fy; camCx = cx; camCy = cy; var width = CameraIntrinsics.Width; var height = CameraIntrinsics.Height; // 创建 PCLSharp 点云对象 cloud = new PointCloudXYZ(); cloud.ReSize(width * height); // 预分配内存 #if true p3d_list.Clear(); innerPoints.Clear(); Mat filteredDepth = matDepth.Threshold(binMaximalDepth, 0, ThresholdTypes.TozeroInv); // 使用 ExtractBinInnerMask 提取掩码 var (success, innerMask, innerRect) = ExtractBinInnerMask(filteredDepth); if (!success) { MessageBox.Show("料箱内轮廓提取失败"); return; } BinInnerRect = innerRect; int erosionSize = 10; Mat erodedMask = new Mat(); Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(erosionSize, erosionSize)); Cv2.Erode(innerMask, erodedMask, kernel); matDepth.GetRectangularArray(out ushort[,] depthData); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (innerMask.At<byte>(y, x) == 0) continue; ushort d = depthData[y, x]; if (d == 0) continue; double z = d / 1000.0; double px = (x - cx) * z / fx; double py = (y - cy) * z / fy; cloud.Push(px, py, z); var pt = ((float)px, (float)py, (float)z); p3d_list.Add(pt); innerPoints.Add(pt); } } // 根据腐蚀后的掩码再次筛选 innerPoints = FilterPointsByErodedMask(p3d_list, erodedMask, width, height); //binPlane = FitPlaneUsingRansac(DepthImage, innerRect.BoundingRect()); // if (binPlane == null) // { // MessageBox.Show("平面拟合失败,无法计算容积率。"); // return; // } var nz = erodedMask.FindNonZero(); Rect roi = Cv2.BoundingRect(nz); Mat depthForFit = new Mat(); matDepth.CopyTo(depthForFit, erodedMask); // 使用新的 FitPlaneUsingRansac(会把像素坐标转成相机坐标——米) boxTopPlane = FitPlaneUsingRansac(depthForFit, roi, camFx, camFy, camCx, camCy); if (boxTopPlane == null) { MessageBox.Show("箱口平面拟合失败(点数太少或拟合不稳定)!"); return; } #elif false // 遍历深度图像的每个像素 for (int v = 0; v < height; v++) { for (int u = 0; u < width; u++) { // 获取当前像素的深度值(单位:毫米) ushort depthValue = depthElements[v, u]; // 跳过无效深度值(通常为0) if (depthValue == 0) continue; // 将深度转为米(PCL 通常使用米为单位) double z = depthValue / 1000.0f; // 计算三维坐标(相机坐标系) double x = (u - cx) * z / fx; double y = (v - cy) * z / fy; // 将点添加到点云 cloud.Push(x, y, z); } } #endif // 计算Z轴着色 CalculateAxisColors('z'); // 重置平移位置(新增) cameraPanX = 0.0f; cameraPanY = 0.0f; // 直接访问指针属性 IntPtr cloudPointer = cloud.PointCloudXYZPointer; // 请求重绘 glControl.Invalidate(); //} //catch (Exception ex) //{ // MessageBox.Show($"加载点云失败: {ex.Message}"); //} } /// /// 重置glc /// private void ResetGlControl() { // 重新计算Z轴着色 CalculateAxisColors('z'); // 重置旋转角度 cameraAngleX = 180.0f; cameraAngleY = 0.0f; // 重新计算相机距离 cameraDistance = Math.Max(Math.Max(maxX - minX, maxY - minY), maxZ - minZ) * 2.0f; // 重置平移位置(新增) cameraPanX = 0.0f; cameraPanY = 0.0f; // 请求重绘 glControl.Invalidate(); } #region 容积率 // 点云点列表(用于分析) private List<(float x, float y, float z)> p3d_list = new(); private List<(float x, float y, float z)> innerPoints = new(); // 深度图 public Mat DepthImage { get; set; } ushort binMaximalDepth = 900; // 平面拟合结果 private Plane binPlane; // 空箱容积 private List<double> emptyVolumes = new(); // 空箱容积平均值 private double avgBinVolume = -1; private RotatedRect? BinInnerRect = null; private OpenCvSharp.Point[] OuterHullContour = null; private bool userDrawnOuterContour = false; // 默认未绘制 private Plane boxTopPlane = null; // 箱口平面 private Plane emptyBoxTopPlane = null; // 存储空箱时的箱口平面 #if true // 计算容积 //private void ComputeVolumeRate() //{ // if (innerPoints == null || innerPoints.Count == 0) // { // MessageBox.Show("未提取内轮廓点,请先加载并筛选点云数据。"); // return; // } // if (emptyBoxTopPlane == null) // { // MessageBox.Show("请先完成空箱体积采集,确保有空箱的箱口平面数据。"); // return; // } // if (avgBinVolume <= 0) // { // MessageBox.Show("请先点击空箱体积测量按钮并完成 3 次采集。"); // return; // } // double totalVolume = 0; // int validPointCount = 0; // foreach (var pt in innerPoints) // { // if (pt.z <= 0) continue; // double z_plane = (-emptyBoxTopPlane.A * pt.x - emptyBoxTopPlane.B * pt.y - emptyBoxTopPlane.Offset) / emptyBoxTopPlane.C; // double height = z_plane - pt.z; // if (height > 0.002) // { // //double pixelArea = 1.0; // double pixelArea = (pt.z * pt.z) / (camFx * camFy); // totalVolume += height * pixelArea; // validPointCount++; // } // } // double fillRate = 1 - totalVolume / avgBinVolume; // MessageBox.Show($"有效点数: {validPointCount}\n" + // $"已装体积: {totalVolume:F6} m³\n" + // $"容积率: {fillRate * 100:F2}%"); //} private void ComputeVolumeRate() { if (innerPoints == null || innerPoints.Count == 0) { MessageBox.Show("未提取内轮廓点,请先加载并筛选点云数据。"); return; } if (boxTopPlane == null) { MessageBox.Show("箱口平面未拟合,请先进行空箱体积测量。"); return; } if (avgBinVolume <= 0) { MessageBox.Show("请先完成空箱体积测量(3次)。"); return; } double totalVolume = 0; int validPointCount = 0; foreach (var point in innerPoints) { //if (point.z <= 0) continue; //double z_top = (-boxTopPlane.A * point.x - boxTopPlane.B * point.y - boxTopPlane.Offset) / boxTopPlane.C; //double height = z_top - point.z; ////double height = point.z - z_top; //if (height > 0.002) //{ // totalVolume += height; // validPointCount++; //} // 计算箱口平面在该点 (x,y) 上的 z 值(米) double z_top = (-boxTopPlane.A * point.x - boxTopPlane.B * point.y - boxTopPlane.Offset) / boxTopPlane.C; double height = z_top - point.z; // 从点到箱口的高度(米) if (height <= 0.002) continue; // 忽略微小值或点高于箱口(负高度) // 单像素在 X-Y 平面上的面积近似: (Z^2) / (fx * fy) double areaPerPixel = (point.z * point.z) / (camFx * camFy); // m^2 //double areaPerPixel = 1.0; totalVolume += height * areaPerPixel; // m^3 validPointCount++; } double fillRate = 1 - (totalVolume / avgBinVolume); MessageBox.Show($"点数: {validPointCount}\n" + $"当前剩余体积: {totalVolume:F6}\n" + $"容积率: {fillRate * 100:F2}%"); } #elif true // 计算容积率(外轮廓) private void ComputeVolumeRate() { if (!CheckOuterContourValid()) return; if (innerPoints == null || innerPoints.Count == 0) { MessageBox.Show("未提取内轮廓点,请先加载并筛选点云数据。"); return; } if (binPlane == null) { MessageBox.Show("平面拟合失败,无法计算容积率。"); return; } if (avgBinVolume <= 0) { MessageBox.Show("请先点击空箱体积测量按钮并完成 3 次采集。"); return; } double totalVolume = 0; int validPointCount = 0; foreach (var point in innerPoints) { if (point.z <= 0) continue; double z_plane = (-binPlane.A * point.x - binPlane.B * point.y - binPlane.Offset) / binPlane.C; double height = point.z - z_plane; if (height < 0.002) continue; totalVolume += height; validPointCount++; } double fillRate = 1 - totalVolume / avgBinVolume; MessageBox.Show($"点数: {validPointCount}\n" + $"体积: {totalVolume:F2} (像素单位)\n" + $"容积率: {fillRate * 100:F2}%"); } #endif /// /// 基于箱内轮廓拟合箱口平面 /// //private Plane FitBoxTopPlane(Mat depthImage, RotatedRect innerRect) //{ // // 创建内轮廓掩码 // Mat topMask = Mat.Zeros(depthImage.Size(), MatType.CV_8UC1); // OpenCvSharp.Point[] polyPoints = Array.ConvertAll(innerRect.Points(), p => new OpenCvSharp.Point((int)p.X, (int)p.Y)); // Cv2.FillConvexPoly(topMask, polyPoints, Scalar.White); // // ROI 区域 // Rect roi = Cv2.BoundingRect(topMask.FindNonZero()); // // 阈值化:保留最浅的区域(箱口边缘) // Mat shallowDepth = depthImage.Threshold(0, 0, ThresholdTypes.Tozero); // 保留非零 // double minVal, maxVal; // Cv2.MinMaxLoc(shallowDepth, out minVal, out maxVal); // shallowDepth = shallowDepth.Threshold(minVal + 5, 0, ThresholdTypes.TozeroInv); // // 拟合平面 // return FitPlaneUsingRansac(shallowDepth, roi); //} // 腐蚀掩码 private List<(float x, float y, float z)> FilterPointsByErodedMask(List<(float x, float y, float z)> allPoints,Mat erodedMask,int width,int height) { List<(float x, float y, float z)> filtered = new(); for (int i = 0; i < allPoints.Count; i++) { int x = i % width; int y = i / width; if (y >= height || x >= width) continue; if (erodedMask.At<byte>(y, x) > 0) { filtered.Add(allPoints[i]); } } return filtered; } // 平面拟合 private Plane FitPlaneUsingRansac(Mat data, Rect roi) { //List<Point3> points = new();// 存储有效点的列表 List<Point3> points = new List<Point3>(); // 防护性裁剪 roi 到深度图范围内 int top = Math.Max(0, roi.Top); int left = Math.Max(0, roi.Left); int bottom = Math.Min(data.Rows, roi.Bottom); int right = Math.Min(data.Cols, roi.Right); for (int y = roi.Top; y < roi.Bottom; y++) { for (int x = roi.Left; x < roi.Right; x++) { ushort d = data.At<ushort>(y, x);// 获取深度值 if (d > 0) { // 将二维图像坐标和深度转换为三维点 (x, y, depth) points.Add(new Point3(x, y, d)); //points.Add(new Point3(x, y, d / 1000)); } } } if (points.Count < 10) { return null; } var estimator = new RansacPlane(2, 0.99);// 创建 RANSAC 平面估计器 return estimator.Estimate(points.ToArray()); } // FitPlaneUsingRansac:用的是相机坐标系下的 (X,Y,Z in meters) private Plane FitPlaneUsingRansac(Mat depth, Rect roi, double fx, double fy, double cx, double cy) { List<Point3> points = new List<Point3>(); // 防护性裁剪 roi 到深度图范围内 int top = Math.Max(0, roi.Top); int left = Math.Max(0, roi.Left); int bottom = Math.Min(depth.Rows, roi.Bottom); int right = Math.Min(depth.Cols, roi.Right); for (int y = top; y < bottom; y++) { for (int x = left; x < right; x++) { ushort d = depth.At<ushort>(y, x); if (d == 0) continue; double z = d / 1000.0; // 转为米 double px = (x - cx) * z / fx; double py = (y - cy) * z / fy; points.Add(new Point3((float)px, (float)py, (float)z)); } } // 如果有效点太少,返回 null(避免误拟合) if (points.Count < 30) { Console.WriteLine($"Plane fit aborted: only {points.Count} points available in ROI."); return null; } var estimator = new RansacPlane(2, 0.99); return estimator.Estimate(points.ToArray()); } // 计算容积率 private void buttonVolume_Click(object sender, EventArgs e) { ComputeVolumeRate(); } #if true //// 采集空箱体积(自动获取轮廓) //private void buttonEmptyVolumn_Click(object sender, EventArgs e) //{ // if (avgBinVolume > 0) // { // MessageBox.Show($"空箱体积已采集完成,平均值为: {avgBinVolume:F6} m³"); // return; // } // if (DepthImage == null || DepthImage.Empty()) // { // MessageBox.Show("深度图未加载!"); // return; // } // if (boxTopPlane == null) // { // MessageBox.Show("箱口平面未拟合,请先加载点云。"); // return; // } // // 第一次采集时保存空箱的箱口平面 // if (emptyBoxTopPlane == null) // emptyBoxTopPlane = boxTopPlane; // double totalVolume = 0; // int validCount = 0; // foreach (var pt in innerPoints) // { // if (pt.z <= 0) continue; // double z_plane = (-emptyBoxTopPlane.A * pt.x - emptyBoxTopPlane.B * pt.y - emptyBoxTopPlane.Offset) / emptyBoxTopPlane.C; // double height = z_plane - pt.z; // if (height > 0.002) // { // //double pixelArea = 1.0; // double pixelArea = (pt.z * pt.z) / (camFx * camFy); // totalVolume += height * pixelArea; // validCount++; // } // } // emptyVolumes.Add(totalVolume); // MessageBox.Show($"当前第 {emptyVolumes.Count} 次测量:\n" + // $"体积: {totalVolume:F6} m³\n有效点数: {validCount}"); // if (emptyVolumes.Count >= 3) // { // avgBinVolume = emptyVolumes.Average(); // MessageBox.Show($"✅ 空箱体积测量完成,平均值: {avgBinVolume:F6} m³"); // } //} private void buttonEmptyVolumn_Click(object sender, EventArgs e) { if (avgBinVolume > 0) { MessageBox.Show($"空箱体积已采集完成,平均值为: {avgBinVolume:F2}"); return; } if (DepthImage == null || DepthImage.Empty()) { MessageBox.Show("深度图未加载!"); return; } if (boxTopPlane == null) { MessageBox.Show("箱口平面未拟合,请先加载点云。"); return; } double totalVolume = 0; int validCount = 0; foreach (var point in innerPoints) { //if (point.z <= 0) continue; //double z_top = (-boxTopPlane.A * point.x - boxTopPlane.B * point.y - boxTopPlane.Offset) / boxTopPlane.C; ////double height = point.z - z_top; //double height = z_top - point.z; //if (height > 0.002) //{ // totalVolume += height; // validCount++; //} if (point.z <= 0) continue; // 计算当前点对应的箱口平面高度 double z_plane = (-boxTopPlane.A * point.x - boxTopPlane.B * point.y - boxTopPlane.Offset) / boxTopPlane.C; //double height = z_plane - point.z; // 从箱口往下的高度 double height = z_plane - point.z; if (height > 0.002) // 忽略 2mm 以内的噪声 { // 每个点的面积(像素面积投影到实际尺寸) double pixelArea = (point.z * point.z) / (camFx * camFy); //double pixelArea = 1.0; totalVolume += height * pixelArea; validCount++; } } emptyVolumes.Add(totalVolume); MessageBox.Show($"第 {emptyVolumes.Count} 次测量: {totalVolume:F6}\n有效点数: {validCount}"); if (emptyVolumes.Count >= 3) { avgBinVolume = emptyVolumes.Average(); MessageBox.Show($"✅ 空箱体积测量完成,平均值为: {avgBinVolume:F6}"); } } #elif true private bool CheckOuterContourValid() { if (OuterHullContour == null || OuterHullContour.Length < 3) { MessageBox.Show("请先绘制或提取箱体外轮廓!"); return false; } return true; } // 采集空箱体积(使用外轮廓) private void buttonEmptyVolumn_Click(object sender, EventArgs e) { if (avgBinVolume > 0) { MessageBox.Show($"空箱体积已采集完成,平均值为: {avgBinVolume:F2}"); return; } if (DepthImage == null || DepthImage.Empty()) { MessageBox.Show("深度图未加载!"); return; } if (!CheckOuterContourValid()) return; // 修复深度图 Mat repairedDepth = RepairDepth(DepthImage); // 使用外轮廓生成掩码 Mat outerMask = Mat.Zeros(repairedDepth.Size(), MatType.CV_8UC1); Cv2.FillConvexPoly(outerMask, OuterHullContour, Scalar.White); // 拟合平面(使用轮廓内区域) Plane plane = FitPlaneUsingRansac(repairedDepth, Cv2.BoundingRect(OuterHullContour)); if (plane == null) { MessageBox.Show("平面拟合失败!"); return; } // 准备内参 repairedDepth.GetRectangularArray(out ushort[,] depthData); double fx = 500, fy = 500; double cx = 640 / 2.0, cy = 480 / 2.0; double totalVolume = 0; int validCount = 0; for (int y = 0; y < 480; y++) { for (int x = 0; x < 640; x++) { if (outerMask.At<byte>(y, x) == 0) continue; ushort depth = depthData[y, x]; if (depth == 0) continue; double z = depth / 1000.0; double px = (x - cx) * z / fx; double py = (y - cy) * z / fy; double z_plane = (-plane.A * px - plane.B * py - plane.Offset) / plane.C; double height = z_plane - z; if (height > 0.002) { totalVolume += height; validCount++; } } } emptyVolumes.Add(totalVolume); MessageBox.Show($"当前第 {emptyVolumes.Count} 次测量: {totalVolume:F2}\n有效点数: {validCount}"); if (emptyVolumes.Count >= 3) { avgBinVolume = emptyVolumes.Average(); MessageBox.Show($"✅ 空箱体积测量完成,平均值为: {avgBinVolume:F2}"); } } #endif // 深度图修复方法 private Mat RepairDepth(Mat matDepth) { Mat repaired = matDepth.Clone();// 克隆原始深度图 int kernelSize = 3;// 邻域大小 int half = kernelSize / 2; // 使用中值滤波修复深度图中的噪点 // 找深度值为0的点,使用周围邻域的中值填充 for (int y = 0; y < repaired.Rows; y++) { for (int x = 0; x < repaired.Cols; x++) { ushort d = repaired.At<ushort>(y, x); if (d == 0) { int sum = 0, count = 0; for (int dy = -half; dy <= half; dy++) { for (int dx = -half; dx <= half; dx++) { int ny = y + dy; int nx = x + dx; if (ny >= 0 && ny < repaired.Rows && nx >= 0 && nx < repaired.Cols) { ushort n = repaired.At<ushort>(ny, nx); if (n > 0) { sum += n; count++; } } } } if (count > 0) repaired.Set(y, x, (ushort)(sum / count)); } } } return repaired; } // 自动提取内轮廓掩码 private (bool success, Mat innerMask, RotatedRect innerRect) ExtractBinInnerMask(Mat data) { //用梯度算法计算梯度 Mat laplacian = data.Laplacian(data.Type(), 1, 5).ConvertScaleAbs(); //从梯度图查找轮廓 var contours = laplacian.FindContoursAsArray(RetrievalModes.External, ContourApproximationModes.ApproxSimple); laplacian.Dispose(); // 如果没有找到任何轮廓,则返回 if (contours == null || contours.Length == 0) { return (false, null, new RotatedRect()); } // 有效点 IEnumerable<OpenCvSharp.Point> validPoints = null; // 合并轮廓,找到料箱的边框轮廓 foreach (var contour in contours) { if (Cv2.ContourArea(contour) < 400) continue; validPoints = validPoints == null ? contour : validPoints.Concat(contour); } // 如果没有有效点,则返回 if (validPoints == null || !validPoints.Any()) { return (false, null, new RotatedRect()); } // 计算凸包,找到料箱的外轮廓 var hull = Cv2.ConvexHull(validPoints); if (hull == null) return (false, null, new RotatedRect()); // 找到料箱轮廓的最小外接矩形 RotatedRect minAreaRect = Cv2.MinAreaRect(hull); // 创建外轮廓掩码 Mat outerMask = Mat.Zeros(480, 640, MatType.CV_8UC1); Cv2.FillConvexPoly(outerMask, hull, Scalar.White); // 对外轮廓掩码进行腐蚀操作,去除小噪点 int erosionSize = (int)(Math.Min(minAreaRect.Size.Width, minAreaRect.Size.Height) * 0.15);// 调整参数参数越大,轮廓越大。 Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(erosionSize, erosionSize));// 腐蚀核大小 Mat innerMask = new(); Cv2.Erode(outerMask, innerMask, kernel); // 获取内轮廓 var innerContours = Cv2.FindContoursAsArray(innerMask, RetrievalModes.External, ContourApproximationModes.ApproxSimple); if (innerContours.Length == 0) return (false, null, new RotatedRect()); var largest = innerContours.OrderByDescending(c => Cv2.ContourArea(c)).First(); RotatedRect innerRect = Cv2.MinAreaRect(largest); return (true, innerMask, innerRect); } // 获取外轮廓掩码 private bool ExtractBinOuterMask(Mat data, out Mat mask, out RotatedRect outRect) { mask = null; outRect = new RotatedRect(); // 使用拉普拉斯算子计算梯度 Mat laplacian = data.Laplacian(data.Type(), 1, 5).ConvertScaleAbs(); var contours = laplacian.FindContoursAsArray(RetrievalModes.External, ContourApproximationModes.ApproxSimple); laplacian.Dispose(); if (contours == null || contours.Length == 0) return false;// 如果没有找到任何轮廓,则返回 IEnumerable<OpenCvSharp.Point> validPoints = null;// 有效点集合 foreach (var contour in contours) { if (Cv2.ContourArea(contour) < 400) continue;// 忽略小轮廓 validPoints = validPoints == null ? contour : validPoints.Concat(contour);// 合并轮廓 } if (validPoints == null || !validPoints.Any()) return false; var hull = Cv2.ConvexHull(validPoints);// 计算凸包 if (hull == null) return false;// 如果没有有效的凸包,则返回 OuterHullContour = hull;// 保存外轮廓点 outRect = Cv2.MinAreaRect(hull); mask = Mat.Zeros(data.Size(), MatType.CV_8UC1); Cv2.FillConvexPoly(mask, hull, Scalar.White); return true; } // 渲染外轮廓轮廓线 private void RenderOuterContour() { // 检查是否需要绘制外轮廓 if (!userDrawnOuterContour || OuterHullContour == null || OuterHullContour.Length < 2 || DepthImage == null || currentSKData.CameraIntrinsics == null) return; double fx = currentSKData.CameraIntrinsics.Fx; double fy = currentSKData.CameraIntrinsics.Fy; double cx = currentSKData.CameraIntrinsics.Ppx; double cy = currentSKData.CameraIntrinsics.Ppy; GL.Color3(1.0f, 1.0f, 1.0f);// 设置颜色为白色 GL.LineWidth(2.0f);// 设置线宽 GL.Begin(PrimitiveType.LineLoop);// 开始绘制线环 foreach (var pt in OuterHullContour) { if (pt.X < 0 || pt.X >= DepthImage.Width || pt.Y < 0 || pt.Y >= DepthImage.Height)// 检查点是否在深度图范围内 continue; ushort depth = DepthImage.At<ushort>(pt.Y, pt.X); if (depth == 0) continue; double z = depth / 1000.0; double x = (pt.X - cx) * z / fx; double y = (pt.Y - cy) * z / fy; GL.Vertex3(x, y, z); } GL.End(); GL.LineWidth(1.0f); } // 绘制处理后的点云 private void buttonCreate_Click(object sender, EventArgs e) { userDrawnOuterContour = true;// 标记用户已绘制外轮廓 if (contourPolygons == null || contourPolygons.Count == 0 || DepthImage == null) { MessageBox.Show("请先通过界面提取箱体外轮廓!"); return; } List<OpenCvSharp.Point> allPoints = new();// 存储所有轮廓点 foreach (var polygon in contourPolygons) { allPoints.AddRange(polygon.Select(p => new OpenCvSharp.Point((int)p.X, (int)p.Y))); } OpenCvSharp.Point[] contour = Cv2.ConvexHull(allPoints);// 计算凸包轮廓 Mat mask = Mat.Zeros(DepthImage.Size(), MatType.CV_8UC1);// 创建掩码 Cv2.FillConvexPoly(mask, contour, Scalar.White);// 填充掩码 OuterHullContour = contour;// 保存外轮廓点 DepthImage.GetRectangularArray(out ushort[,] depthElements); double fx = currentSKData.CameraIntrinsics.Fx; double fy = currentSKData.CameraIntrinsics.Fy; double cx = currentSKData.CameraIntrinsics.Ppx; double cy = currentSKData.CameraIntrinsics.Ppy; int width = DepthImage.Width; int height = DepthImage.Height; cloud = new PointCloudXYZ(); //cloud.ReSize(width * height);--------------------------------------- p3d_list.Clear(); innerPoints.Clear(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (mask.At<byte>(y, x) == 0) continue;// 检查掩码是否为白色(有效点) ushort d = depthElements[y, x]; if (d == 0) continue; double z = d / 1000.0; double px = (x - cx) * z / fx; double py = (y - cy) * z / fy; double pz = z; cloud.Push(px, py, pz); var point = ((float)px, (float)py, (float)pz); p3d_list.Add(point); innerPoints.Add(point); } } // 重新拟合平面 binPlane = FitPlaneUsingRansac(DepthImage, Cv2.BoundingRect(contour),camFx, camFy, camCx, camCy); if (binPlane == null) { MessageBox.Show("平面拟合失败,无法计算容积率"); return; } CalculateAxisColors('z'); cameraPanX = 0.0f; cameraPanY = 0.0f; glControl.Invalidate(); } 为什么我这个代码,buttonEmptyVolumn_Click我是获得空箱子的容积。然后放了物品之后,buttonVolume_Click我再触发这个,这样子就可以获得箱子装有物品之后,箱子内现在的容积率是多少了。但是我 发现我在实际测试当中,放了物品之后,我的这个 体积变大了。比空箱子反而要大了这很不合理,为什么

djboy1021
  • 粉丝: 2
上传资源 快速赚钱