pcl 平面点云拟合矩形

时间: 2025-03-05 21:36:07 浏览: 71
### 使用PCL库对平面点云数据进行矩形拟合 为了使用PCL库对平面点云数据进行矩形拟合,通常先通过PCA方法或其他手段找到最佳拟合平面。一旦获得了该平面,则可以在平面上寻找边界框或轮廓以定义矩形区域。 #### 平面拟合过程概述 利用主成分分析(PCA),可以从点云中提取出描述其分布的主要方向,进而确定一个最能代表这些点的最佳拟合平面[^1]。此过程中计算出来的特征向量提供了坐标轴的方向信息,而对应的特征值则反映了沿各个方向上的方差大小;其中最大的两个特征值所对应的方向构成了目标平面内的基底矢量。 对于更精确和平稳的结果,在某些场景下可能还需要调整参数如迭代次数和收敛条件来优化平面拟合的质量[^2]。 #### 实现矩形拟合的具体步骤 完成上述平面拟合之后,下一步就是识别并提取位于同一水平面上的对象边缘以便构建矩形模型: - **分割**:从原始点云中分离出属于特定平面的部分。 - **降维投影**:将三维空间中的点映射至二维平面上,简化后续操作。 - **轮廓检测/聚类**:应用诸如RANSAC等算法查找物体外接多边形或者直接运用形态学运算获取闭合曲线。 - **最小面积包围盒**:基于所得轮廓计算最小旋转矩形作为最终拟合结果的一部分。 以下是Python代码片段展示如何执行这一系列任务: ```cpp #include <pcl/io/pcd_io.h> #include <pcl/point_types.h> #include <pcl/features/normal_3d.h> #include <pcl/filters/passthrough.h> #include <pcl/sample_consensus/method_types.h> #include <pcl/sample_consensus/model_types.h> #include <pcl/segmentation/sac_segmentation.h> #include <pcl/filters/project_inliers.h> // ... 初始化 PointCloud 对象 ... pcl::SACSegmentation<pcl::PointXYZ> seg; seg.setOptimizeCoefficients(true); seg.setModelType(pcl::SACMODEL_PLANE); // 设置模型为平面 seg.setMethodType(pcl::SAC_RANSAC); // RANSAC 方法 seg.setMaxIterations(1000); // 迭代上限 seg.setDistanceThreshold(0.01); // 距离阈值 pcl::ModelCoefficients coefficients; pcl::PointIndices inliers; seg.setInputCloud(cloud.makeShared()); seg.segment(inliers, coefficients); if (inliers.indices.size() != 0){ pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_projected(new pcl::PointCloud<pcl::PointXYZ>); pcl::ProjectInliers<pcl::PointXYZ> proj; proj.setModelType(pcl::SACMODEL_PLANE); proj.setInputCloud(cloud.makeShared()); proj.setModelCoefficients(coefficients); proj.filter(*cloud_projected); // 继续处理 projected_cloud... } ``` 注意这段C++代码主要用于说明目的,并未完全覆盖整个流程特别是最后一步即求解最小外包矩形的过程。这可以通过OpenCV库或者其他几何工具包进一步实现。
阅读全文

相关推荐

/// /// 加载点云数据 /// /// /// 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 points = new();// 存储有效点的列表 List points = new List(); // 防护性裁剪 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 points = new List(); // 防护性裁剪 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我再触发这个,这样子就可以获得箱子装有物品之后,箱子内现在的容积率是多少了。但是我 发现我在实际测试当中,放了物品之后,我的这个 体积变大了。比空箱子反而要大了这很不合理,为什么

大家在看

recommend-type

.NET frxamework v2.0 64位

Microsoft .NET framework 2.0 64位可再发行组件包将安装 .NET framework 运行库,以及运行面向 .NET framework 2.0 版开发的 64 位应用程序所需的相关文件。
recommend-type

服务质量管理-NGBOSS能力架构

服务质量管理 二级能力名称 服务质量管理 二级能力编号 CMCM.5.4 概述 监测、分析和控制客户感知的服务表现 相关子能力描述 能够主动的将网络性能数据通告给前端客服人员; 能够根据按照客户价值来划分的客户群来制定特殊的SLA指标; 能够为最有价值的核心客户群进行网络优化; 对于常规的维护问题,QoS能够由网元设备自动完成,比如,对于网络故障的自恢复能力和优先客户的使用权; 能够把潜在的网络问题与客户进行主动的沟通; 能够分析所有的服务使用的质量指标; 能够根据关键的服务质量指标检测与实际的差距,提出改进建议; Service request 服务请求---请求管理。 客户的分析和报告:对关闭的请求、用户联系和相关的报告进行分析。 Marketing collateral的散发和marketing Collateral 的散发后的线索跟踪
recommend-type

AUTOSAR_MCAL_WDG.zip

This User Manual describes NXP Semiconductors AUTOSAR Watchdog ( Wdg ) for S32K14X . AUTOSAR Wdg driver configuration parameters and deviations from the specification are described in Wdg Driver chapter of this document. AUTOSAR Wdg driver requirements and APIs are described in the AUTOSAR Wdg driver software specification document.
recommend-type

MATLABSimulinkCommunicationSystemmaster_matlab_matlabsimulink_

MATLAB通信系统仿真历程,基于参考书《详解MATLAB/Simulink通信系统建模仿真》。都是里面的例子
recommend-type

multisim 实现四位二进制密码锁功能密码锁.rar

1、在锁的控制电路中储存一个可修改的四位二进制代码作为密码,当输入代码与锁的密码相等时,进入开锁状态使锁打开。开锁状态时绿灯亮。 2、从第一个按键触动后的5秒内未将锁打开,则电路进入自锁状态,使之无法再打开,并由扬声器发出持续10秒的报警信号。自锁状态时红灯亮。

最新推荐

recommend-type

信捷XC系列PLC主从通讯程序设计与实现——工业自动化控制核心技术

信捷XC系列PLC主从通讯程序的设计与实现方法。信捷XC系列PLC是一款高性能、高可靠性的可编程逻辑控制器,在工业自动化领域广泛应用。文中阐述了主从通讯的基本概念及其重要性,具体讲解了配置网络参数、编写程序、数据交换以及调试与测试四个主要步骤。此外,还探讨了该技术在生产线控制、仓储物流、智能交通等多个领域的应用实例,强调了其对系统效率和稳定性的提升作用。 适合人群:从事工业自动化控制的技术人员、工程师及相关专业学生。 使用场景及目标:适用于需要多台PLC协同工作的复杂工业控制系统,旨在提高系统的效率和稳定性,确保各设备间的数据交换顺畅无误。 其他说明:随着工业自动化的快速发展,掌握此类通信协议和技术对于优化生产流程至关重要。
recommend-type

Qt 5.12.4与Halcon构建视觉流程框架:编译与测试的成功实践

如何将Halcon视觉技术和Qt框架相结合,构建一个强大的视觉处理流程框架。文中首先阐述了选择Qt 5.12.4的原因及其优势,接着描述了框架的具体构建方法,包括利用Qt的跨平台特性和界面设计工具创建用户界面,以及用Halcon进行图像处理与识别。随后,文章讲解了编译过程中需要注意的关键步骤,如正确引入Halcon的头文件和库文件,并提供了一个简单的代码示例。最后,作者分享了测试阶段的经验,强调了确保系统在各种情况下都能正常工作的必要性。通过这次实践,证明了两者结合可以带来更高效、更稳定的视觉处理解决方案。 适合人群:具有一定编程经验的技术人员,尤其是对计算机视觉和图形界面开发感兴趣的开发者。 使用场景及目标:适用于希望深入了解Qt与Halcon集成应用的开发者,旨在帮助他们掌握从框架搭建到实际部署的全过程,从而提升自身技能水平。 阅读建议:读者可以在阅读过程中跟随作者的步伐逐步尝试相关操作,以便更好地理解和吸收所介绍的知识点和技术细节。
recommend-type

【CAD入门基础课程】1.4 AutoCAD2016 功能介绍.avi

一、AutoCAD 2016的新增功能 版本演进: 从V1.0到2016版已更新数十次,具备绘图、编辑、图案填充、尺寸标注、三维造型等完整功能体系 界面优化: 新增暗黑色调界面:使界面协调深沉利于工作 底部状态栏整体优化:操作更实用便捷 硬件加速:效果显著提升运行效率 核心新增功能: 新标签页和功能区库:改进工作空间管理 命令预览功能:增强操作可视化 地理位置和实景计算:拓展设计应用场景 Exchange应用程序和计划提要:提升协同效率 1. 几何中心捕捉功能 新增选项: 在对象捕捉模式组中新增"几何中心"选项 可捕捉任意多边形的几何中心点 启用方法: 通过草图设置对话框→对象捕捉选项卡 勾选"几何中心(G)"复选框(F3快捷键启用) 2. 标注功能增强 DIM命令改进: 智能标注创建:基于选择的对象类型自动适配标注方式 选项显示优化:同时在命令行和快捷菜单中显示标注选项 应用场景: 支持直线、圆弧、圆等多种图形元素的智能标注 3. 打印输出改进 PDF输出增强: 新增专用PDF选项按钮 支持为位图对象添加超链接 可连接到外部网站和本地文件 操作路径: 快速访问工具栏→打印按钮→PDF选项对话框 4. 其他重要更新 修订云线增强: 支持创建矩形和多边形云线 新增虚拟线段编辑选项 系统变量: 新增多个控制新功能的系统变量
recommend-type

电力电子领域单相PWM整流模型的主电路与控制模块实现研究

内容概要:本文详细探讨了单相PWM整流模型的设计与实现,重点介绍了主电路和控制模块的具体实现方法。主电路作为整流模型的核心部分,涉及功率因数、电流稳定性和调节范围等因素,常采用全桥或多级整流等拓扑结构。控制模块则由PWM控制器、传感器和执行器组成,负责调节交流电源的输入,实现所需电压和电流波形。文中还强调了算法设计、硬件接口设计和参数调整与优化的重要性,指出这些因素对于提高整流效果和效率至关重要。 适合人群:从事电力电子领域的研究人员、工程师及相关专业的学生。 使用场景及目标:适用于希望深入了解单相PWM整流模型工作原理和技术细节的人群,旨在帮助他们掌握主电路和控制模块的设计要点,提升电力系统的稳定性和效率。 其他说明:文中提到的相关技术可以通过进一步查阅专业文献和技术资料获得更深入的理解。
recommend-type

这是sanllin的第一次尝试开发app,调用kimi的文本识别和图像识别

资源下载链接为: https://pan.quark.cn/s/39ff61edf06f 这是sanllin的第一次尝试开发app,调用kimi的文本识别和图像识别(最新、最全版本!打开链接下载即可用!)
recommend-type

基于Debian Jessie的Kibana Docker容器部署指南

Docker是一种开源的容器化平台,它允许开发者将应用及其依赖打包进一个可移植的容器中。Kibana则是由Elastic公司开发的一款开源数据可视化插件,主要用于对Elasticsearch中的数据进行可视化分析。Kibana与Elasticsearch以及Logstash一起通常被称为“ELK Stack”,广泛应用于日志管理和数据分析领域。 在本篇文档中,我们看到了关于Kibana的Docker容器化部署方案。文档提到的“Docker-kibana:Kibana 作为基于 Debian Jessie 的Docker 容器”实际上涉及了两个版本的Kibana,即Kibana 3和Kibana 4,并且重点介绍了它们如何被部署在Docker容器中。 Kibana 3 Kibana 3是一个基于HTML和JavaScript构建的前端应用,这意味着它不需要复杂的服务器后端支持。在Docker容器中运行Kibana 3时,容器实际上充当了一个nginx服务器的角色,用以服务Kibana 3的静态资源。在文档中提及的配置选项,建议用户将自定义的config.js文件挂载到容器的/kibana/config.js路径。这一步骤使得用户能够将修改后的配置文件应用到容器中,以便根据自己的需求调整Kibana 3的行为。 Kibana 4 Kibana 4相较于Kibana 3,有了一个质的飞跃,它基于Java服务器应用程序。这使得Kibana 4能够处理更复杂的请求和任务。文档中指出,要通过挂载自定义的kibana.yml文件到容器的/kibana/config/kibana.yml路径来配置Kibana 4。kibana.yml是Kibana的主要配置文件,它允许用户配置各种参数,比如Elasticsearch服务器的地址,数据索引名称等等。通过Docker容器部署Kibana 4,用户可以很轻松地利用Docker提供的环境隔离和可复制性特点,使得Kibana应用的部署和运维更为简洁高效。 Docker容器化的优势 使用Docker容器化技术部署Kibana,有几个显著的优势: - **一致性**:Docker容器确保应用在开发、测试和生产环境中的行为保持一致。 - **轻量级**:相比传统虚拟机,Docker容器更加轻量,启动快速,资源占用更少。 - **隔离性**:容器之间的环境隔离,确保应用之间互不干扰。 - **可移植性**:容器可以在任何支持Docker的环境中运行,提高了应用的可移植性。 - **易于维护**:通过Dockerfile可以轻松构建和分发应用镜像,便于维护和升级。 在文档中,我们还看到了文件名“docker-kibana-master”。这个名称很可能是指向了存放Docker相关文件的源代码仓库,其中可能包含Dockerfile、构建和运行脚本以及可能的配置模板文件等。开发者可以从这个仓库中克隆或下载所需的Docker相关文件,并根据这些文件来构建和部署Kibana的Docker容器。 根据以上信息,对于希望利用Docker容器部署和管理Kibana应用的用户,需要掌握Docker的基本使用方法,包括Docker的安装、镜像管理、容器的创建和配置等。同时,还需要了解Kibana的基本原理和配置方法,这样才能充分利用Docker容器化带来的便利,优化Kibana应用的部署和管理流程。
recommend-type

Coze智能体工作流:打造钦天监视频内容的创新与实践

# 1. Coze智能体工作流概述 Coze智能体工作流是业界领先的智能工作流程管理系统,它通过整合最新的智能技术与人工智能算法,旨在为视频内容创造与优化带来革命性的变革。本章将对Coze智能体工作流进行全面的概述,揭示其在IT行业中的应用范围、流程结构以及技术基础。通过了解Coze智能体工作流,读者将能够把握它在当前和未
recommend-type

使用git仓库的利与弊

<think>好的,我需要分析Git仓库的优缺点。首先,根据用户提供的引用,看看有哪些点需要覆盖。引用[1]提到了Git有本地和远程仓库,需要push命令来更新远程,这应该属于优点还是缺点呢?可能优点在于本地操作灵活,缺点是需要额外命令。 引用[2]指出Git的权限管理薄弱,学习曲线陡峭,这显然是缺点。另外,引用[4]提到Git使用SHA-1哈希,内容完整性更好,属于优点。引用[5]比较了Git的分支管理简单,而缺少全局版本号,这可以作为优点和缺点分别说明。 现在需要将这些信息整理成结构化的优缺点,可能分点列出。同时,用户要求回答结构清晰,逐步解决问题,所以可能需要先介绍Git的基本概念,
recommend-type

TextWorld:基于文本游戏的强化学习环境沙箱

在给出的文件信息中,我们可以提取到以下IT知识点: ### 知识点一:TextWorld环境沙箱 **标题**中提到的“TextWorld”是一个专用的学习环境沙箱,专为强化学习(Reinforcement Learning,简称RL)代理的训练和测试而设计。在IT领域中,尤其是在机器学习的子领域中,环境沙箱是指一个受控的计算环境,允许实验者在隔离的条件下进行软件开发和测试。强化学习是一种机器学习方法,其中智能体(agent)通过与环境进行交互来学习如何在某个特定环境中执行任务,以最大化某种累积奖励。 ### 知识点二:基于文本的游戏生成器 **描述**中说明了TextWorld是一个基于文本的游戏生成器。在计算机科学中,基于文本的游戏(通常被称为文字冒险游戏)是一种游戏类型,玩家通过在文本界面输入文字指令来与游戏世界互动。TextWorld生成器能够创建这类游戏环境,为RL代理提供训练和测试的场景。 ### 知识点三:强化学习(RL) 强化学习是**描述**中提及的关键词,这是一种机器学习范式,用于训练智能体通过尝试和错误来学习在给定环境中如何采取行动。在强化学习中,智能体在环境中探索并执行动作,环境对每个动作做出响应并提供一个奖励或惩罚,智能体的目标是学习一个策略,以最大化长期累积奖励。 ### 知识点四:安装与支持的操作系统 **描述**提到TextWorld的安装需要Python 3,并且当前仅支持Linux和macOS系统。对于Windows用户,提供了使用Docker作为解决方案的信息。这里涉及几个IT知识点: - **Python 3**:一种广泛使用的高级编程语言,适用于快速开发,是进行机器学习研究和开发的常用语言。 - **Linux**和**macOS**:两种流行的操作系统,分别基于Unix系统和类Unix系统。 - **Windows**:另一种广泛使用的操作系统,具有不同的软件兼容性。 - **Docker**:一个开源的应用容器引擎,允许开发者打包应用及其依赖环境为一个轻量级、可移植的容器,使得在任何支持Docker的平台上一致地运行。 ### 知识点五:系统库和依赖 **描述**提到在基于Debian/Ubuntu的系统上,可以安装一些系统库来支持TextWorld的本机组件。这里涉及的知识点包括: - **Debian/Ubuntu**:基于Debian的Linux发行版,是目前最流行的Linux发行版之一。 - **系统库**:操作系统中包含的一系列预编译的软件包和库,供应用程序在运行时使用。 - **包管理工具**,如**apt**(Advanced Package Tool),它是一个在Debian及其衍生系统中用于安装、删除和管理软件包的命令行工具。 ### 知识点六:与创建者联系方式 **描述**提供了与TextWorld创建者的联系方式,包括电子邮件地址和一个Gitter频道。这说明了如何与开源项目的维护者进行沟通与反馈: - **电子邮件**是常见的沟通方式,允许用户与开发者直接交流。 - **Gitter**是一个基于GitHub的即时消息工具,通常用于开源项目中的实时协作和交流。 ### 结语 综合以上信息,我们可以了解到TextWorld是一个专为强化学习设计的学习环境沙箱,它通过创建基于文本的游戏环境,让研究者和开发者训练和测试RL代理。它主要针对Linux和macOS系统,不过也有适合Windows用户的替代方案。此外,了解如何安装和配置TextWorld,以及如何与创建者沟通,对于开发者来说是十分重要的基础技能。
recommend-type

Coze智能体工作流全攻略

# 1. Coze智能体工作流概述 在现代企业中,工作流管理不仅是提高效率的关键因素,而且