PCL统计点云Volume

发布于:2025-08-05 ⋅ 阅读:(11) ⋅ 点赞:(0)

1. 指定参考平面;

2. 将原始点云变换到已参考平面作为xOy平面的坐标系下;

3. 对投影到参考平面的2D点云进行三角化,高度为三角形各顶点到平面距离的平均值;

4. 累加所有三棱柱体积作为点云Volume

Coord2DTransformer.h

#pragma once
#include<vector>
#include<glm/glm.hpp>
#include<ransac_plane.h>
#include<Eigen/Dense>
#include"common.h"
#include<pcl/point_cloud.h>
#include <pcl/common/common.h>


// 返回值为角度值
double angle_between_vectors(const glm::dvec3& v1, const glm::dvec3& v2);

float angleBetweenVectors(const Eigen::Vector3f& u, const Eigen::Vector3f& v, bool in_degrees = false);
class Coord2DTransformer
{
public:
	pcl::PointCloud<pcl::PointXYZ>::Ptr rectify(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, Eigen::Vector4f plane, const Eigen::Vector3f& centroid);


	void SetData(std::vector<Point3d>& input, const std::vector<int>& indices = std::vector<int>())
	{
		std::vector<Point3d> srcPoints;
		if (indices.empty()) {
			srcPoints = input;
		}
		else {
			srcPoints.resize(indices.size());
			for (int i = 0; i < indices.size(); i++) {
				srcPoints[i] = input[indices[i]];
			}
		}

		// fit_plane参见博客《CGAL散乱点拟合最小二乘平面(3D平面拟合,基于Eigen)》
		// https://blog.csdn.net/xmyzqs1212/article/details/142742841
		int minInliers = std::floor(srcPoints.size() * 0.66666);
		Plane3D plane = parallelRansacFitPlane(srcPoints, 10000, 0.1, minInliers, 4, 0.8).param;
		centroid = calculateCenter(srcPoints);
		// 使用并行RANSAC拟合平面
		
		//PlaneModel plane = parallelRansacFitPlane(points, 10000, 0.1, 300, 4, 0.8);
		Vec3d norm = plane.normal();
		Vec3d arbitrary(1, 0, 0);
		auto angle = angle_between_vectors(norm, arbitrary);
		// 避免任意选取的向量与平面法向量方向一致(方向一致没办法通过向量积计算正交向量)
		if (angle < 1.0 || angle > 179) {
			arbitrary = Vec3d(0, 1, 0);
		}

		basis1 = glm::normalize(glm::cross(norm, arbitrary));
		basis2 = glm::normalize(glm::cross(norm, basis1));

		points2d.resize(srcPoints.size());
		for (int i = 0; i < srcPoints.size(); i++)
		{
			Vec3d v = srcPoints[i] - centroid;
			points2d[i][0] = glm::dot(v, basis1);
			points2d[i][1] = glm::dot(v, basis2);
		}
	}

	Point3d calcCoord3D(double x, double y)
	{
		auto v = x * basis1 + y * basis2;
		return Point3d(centroid.x + v.x, centroid.y + v.y, centroid.z + v.z);
	}

	Point3d calcCoord3D(const Point2d& pt)
	{
		return calcCoord3D(pt.x, pt.y);
	}

	void calcCoord3D(const std::vector<Vec2d>& pts2d, std::vector<Point3d>& result)
	{
		result.resize(pts2d.size());
		for (int i = 0; i < pts2d.size(); i++)
		{
			result[i] = calcCoord3D(pts2d[i][0], pts2d[i][1]);
		}
	}

	std::vector<Point2d> points2d;
	Vec3d basis1;
	Vec3d basis2;
	Point3d centroid;
};

Coord2DTransformer.cpp

#include "Coord2DTransformer.h"
#include<glm/ext.hpp>

double angle_between_vectors(const glm::dvec3& v1, const glm::dvec3& v2)
{
	double dot_product = glm::dot(glm::normalize(v1), glm::normalize(v2));
	double magnitude_v1 = glm::length(v1);
	double magnitude_v2 = glm::length(v2);

	double cos_angle = dot_product / (magnitude_v1 * magnitude_v2);
	// 确保cos_angle在[-1, 1]范围内,避免浮点数误差
	cos_angle = std::max(-1.0, std::min(1.0, cos_angle));

	return std::acos(cos_angle) / glm::pi<double>() * 180.0; // 返回角度
}

float angleBetweenVectors(const Eigen::Vector3f& u, const Eigen::Vector3f& v, bool in_degrees/* = false*/) {
  // 计算点积
  float dot_product = u.dot(v);

  // 计算向量长度
  float norm_u = u.norm();
  float norm_v = v.norm();

  // 避免除以零(如果任一向量是零向量,返回 NaN)
  if (norm_u == 0 || norm_v == 0) {
    return std::numeric_limits<float>::quiet_NaN();
  }

  // 计算夹角的余弦值(限制在 [-1, 1] 范围内,避免数值误差)
  float cos_theta = dot_product / (norm_u * norm_v);
  cos_theta = std::clamp(cos_theta, -1.0f, 1.0f);

  // 计算弧度角
  float angle_rad = std::acos(cos_theta);

  // 转换为度数(可选)
  if (in_degrees) {
    return angle_rad * (180.0f / static_cast<float>(M_PI));
  }
  return angle_rad;
}

pcl::PointCloud<pcl::PointXYZ>::Ptr Coord2DTransformer::rectify(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, Eigen::Vector4f plane, const Eigen::Vector3f& centroid)
{
	//PlaneModel plane = parallelRansacFitPlane(points, 10000, 0.1, 300, 4, 0.8);
	Eigen::Vector3f norm_eigen = plane.head<3>();
	norm_eigen.normalize();
	glm::dvec3 norm(norm_eigen[0], norm_eigen[1], norm_eigen[2]);
	glm::dvec3 arbitrary(1, 0, 0);
	auto angle = angle_between_vectors(norm, arbitrary);
	// 避免任意选取的向量与平面法向量方向一致(方向一致没办法通过向量积计算正交向量)
	if (angle < 1.0 || angle > 179) {
		arbitrary = glm::dvec3(0, 1, 0);
	}

	this->centroid = Point3d(centroid[0], centroid[1], centroid[2]);

	basis1 = glm::normalize(glm::cross(norm, arbitrary));
	basis2 = glm::normalize(glm::cross(norm, basis1));

	pcl::PointCloud<pcl::PointXYZ>::Ptr result(new pcl::PointCloud<pcl::PointXYZ>);
	result->width = cloud->size();
	result->height = 1;
	result->is_dense = true;
	result->points.resize(cloud->size());

	//points2d.resize(cloud->points.size());
#pragma omp parallel for
	for (int i = 0; i < cloud->size(); i++)
	{
		glm::dvec3 tmpPt(cloud->points[i].x, cloud->points[i].y, cloud->points[i].z);
		Vec3d v = tmpPt - this->centroid;
		result->points[i].x = glm::dot(v, basis1);
		result->points[i].y = glm::dot(v, basis2);
		result->points[i].z = glm::dot(v, norm);
	}
	return result;
}

main.cpp

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/io/pcd_io.h> // 用于保存点云(可选)
#include<pcl/io/ply_io.h>
#include <pcl/console/print.h>  // 用于调试输出
//#include <pcl/gpu/containers/initialization.h>
//#include <pcl/gpu/features/features.hpp>
#include <pcl/common/centroid.h>
#include <pcl/common/common.h>
#include <pcl/sample_consensus/ransac.h>
#include <pcl/sample_consensus/sac_model_sphere.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>
#include <pcl/segmentation/conditional_euclidean_clustering.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/filters/statistical_outlier_removal.h> // 包含StatisticalOutlierRemoval头文件
#include <pcl/filters/random_sample.h> // 包含RandomSample头文件
#include"Coord2DTransformer.h"
#include <pcl/visualization/pcl_visualizer.h>

#include<opencv2/opencv.hpp>

#include <chrono>
#include <iomanip>  // 用于格式化输出
#include <ctime>    // 用于将时间转换为可读格式
#include<thread>

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Delaunay_triangulation_2.h>

typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Delaunay_triangulation_2<K> Delaunay;
typedef K::Point_2 Point_2;


// 读取.xyz文件并存储到pcl::PointCloud<pcl::PointXYZ>::Ptr
pcl::PointCloud<pcl::PointXYZ>::Ptr readXYZFile(const std::string& filename) {
    // 创建点云对象
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

    // 打开文件
    std::ifstream file(filename);
    if (!file.is_open()) {
        std::cerr << "Error: Could not open file " << filename << std::endl;
        return cloud;
    }

    std::string line;
    while (std::getline(file, line)) {
        std::istringstream iss(line);
        std::string token;
        pcl::PointXYZ point;

        // 读取X坐标
        if (std::getline(iss, token, ';')) {
            point.x = std::stof(token);
        }
        else {
            std::cerr << "Error: Invalid X coordinate in line: " << line << std::endl;
            continue;
        }

        // 读取Y坐标
        if (std::getline(iss, token, ';')) {
            point.y = std::stof(token);
        }
        else {
            std::cerr << "Error: Invalid Y coordinate in line: " << line << std::endl;
            continue;
        }

        // 读取Z坐标
        if (std::getline(iss, token, ';')) {
            point.z = std::stof(token);
        }
        else {
            std::cerr << "Error: Invalid Z coordinate in line: " << line << std::endl;
            continue;
        }

        // 将点添加到点云中
        cloud->push_back(point);
    }

    file.close();
    return cloud;
}

void print_time(const std::string& str)
{
    // 获取当前时间点
    auto now = std::chrono::system_clock::now();

    // 转换为时间戳(精确到毫秒)
    auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();

    // 获取当前时间的秒级表示
    auto now_time_t = std::chrono::system_clock::to_time_t(now);
    std::tm* now_tm = std::localtime(&now_time_t);

    // 打印时间(精确到毫秒)
    std::cout << std::put_time(now_tm, "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) << std::setfill('0') << (milliseconds % 1000);
    printf("   %s\n", str.c_str());
}

// 自定义函数:生成随机颜色
pcl::RGB getRandomColor()
{
    pcl::RGB color;
    color.r = rand() % 256;
    color.g = rand() % 256;
    color.b = rand() % 256;
    return color;
}

Eigen::Vector3f computeCentroidPCL(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
  Eigen::Vector4f centroid_homogeneous; // 四维齐次坐标 (x, y, z, 1)
  pcl::compute3DCentroid(*cloud, centroid_homogeneous);
  return centroid_homogeneous.head<3>(); // 返回前三维 (x, y, z)
}

// 哈希函数用于 Point_2
struct Point2Hash {
  size_t operator()(const Point_2& p) const {
    size_t h1 = std::hash<double>()(CGAL::to_double(p.x()));
    size_t h2 = std::hash<double>()(CGAL::to_double(p.y()));
    return h1 ^ (h2 << 1);
  }
};

// 判断两个 Point_2 是否相等
struct Point2Equal {
  bool operator()(const Point_2& p1, const Point_2& p2) const {
    return CGAL::to_double(p1.x()) == CGAL::to_double(p2.x()) &&
      CGAL::to_double(p1.y()) == CGAL::to_double(p2.y());
  }
};

double computeVolumeFromHeightMapOptimized(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
  if (cloud->empty()) return 0.0;

  // 1. 构建哈希表(XY坐标到Z值的映射)
  std::unordered_map<Point_2, double, Point2Hash, Point2Equal> xy_to_z;
#pragma omp parallel for
  for (size_t i = 0; i < cloud->size(); ++i) {
    const auto& point = (*cloud)[i];
    Point_2 p2(point.x, point.y);
#pragma omp critical
    xy_to_z[p2] = point.z;  // 假设每个XY坐标唯一
  }

  // 2. Delaunay 三角剖分(单线程,CGAL不支持并行)
  std::vector<Point_2> points_2d;
  for (const auto& point : *cloud) {
    points_2d.push_back(Point_2(point.x, point.y));
  }
	print_time("开始Delaunay三角剖分");
  Delaunay dt(points_2d.begin(), points_2d.end());
  print_time("Delaunay三角剖分 done");

  // 3. 并行计算每个三角面片的体积
  double total_volume = 0.0;
//#pragma omp parallel for reduction(+:total_volume)
  for (auto face = dt.finite_faces_begin(); face != dt.finite_faces_end(); ++face) {
    // 获取三角面片的三个顶点
    auto p1 = face->vertex(0)->point();
    auto p2 = face->vertex(1)->point();
    auto p3 = face->vertex(2)->point();

    // 从哈希表快速查找Z值(无需遍历点云)
    double z1 = xy_to_z[p1];
    double z2 = xy_to_z[p2];
    double z3 = xy_to_z[p3];

    // 计算三角面片的面积(2D)
    double area = CGAL::area(p1, p2, p3);

    // 体积 = 面积 × 平均高度
    total_volume += area * ((z1 + z2 + z3) / 3.0);
  }

  return total_volume;
}

bool extractPlane(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, Eigen::Vector4f& plane_coeff, Eigen::Vector3f& centroid, int sampleCnt = 100000)
{
    //pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
    //// 数量降到足够少,然后RANSAC提取底部平面
    //pcl::RandomSample<pcl::PointXYZ> rs;
    //rs.setInputCloud(cloud);
    //rs.setSample(sampleCnt); // 设置采样点数
    //rs.filter(*cloud_filtered);

    centroid = computeCentroidPCL(cloud);

    //// 创建统计离群值去除滤波器对象
    //pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
    //sor.setInputCloud(cloud);  // 设置输入点云
    //sor.setMeanK(20);          // 设置用于计算平均距离的最近邻点数
    //sor.setStddevMulThresh(1.0); // 设置标准差倍数阈值,这里设置为1.0
    //sor.filter(*cloud); // 执行滤波操作

    // 创建分割对象
    pcl::SACSegmentation<pcl::PointXYZ> seg;
    pcl::PointIndices::Ptr inliers(new pcl::PointIndices); // 平面内点索引
    pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients); // 平面模型系数

    // 设置分割参数
    seg.setOptimizeCoefficients(true); // 优化模型系数
    seg.setModelType(pcl::SACMODEL_PLANE); // 模型类型为平面
    seg.setMethodType(pcl::SAC_RANSAC); // 使用 RANSAC 方法
    seg.setMaxIterations(1000); // 最大迭代次数
    seg.setDistanceThreshold(0.2); // 距离阈值,用于判断点是否为内点

    // 设置输入点云
    seg.setInputCloud(cloud);

    // 执行分割
    seg.segment(*inliers, *coefficients);

    if (inliers->indices.empty()) {
        std::cerr << "Error: Could not estimate a planar model for the given dataset." << std::endl;
        return false;
    }

    // 提取平面点云
    pcl::PointCloud<pcl::PointXYZ>::Ptr plane_cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::ExtractIndices<pcl::PointXYZ> extract;
    extract.setInputCloud(cloud);
    extract.setIndices(inliers);
    extract.setNegative(false); // 提取内点
    extract.filter(*plane_cloud);

    //// 保存提取的平面点云
    //pcl::io::savePCDFile("plane_cloud.pcd", *plane_cloud);
    //std::cout << "Plane point cloud saved to plane_cloud.pcd" << std::endl;

    plane_coeff = Eigen::Map<const Eigen::Vector4f>(coefficients->values.data());
    print_time("extractPlane done");
    return true;
}


// 保存Mat到二值文件
void saveMatInBinaryFloat(const cv::Mat& mat, std::string filename)
{
  FILE* saveF;
  saveF = fopen(filename.c_str(), "wb");
  if (saveF == NULL)
  {
    std::printf("保存Mat到二值文件出错!\n");
    return;
  }
  int width = mat.cols;
  int height = mat.rows;
  int nChannels = mat.channels();
  printf("%s  -- %d %d %d\n", filename.c_str(), width, height, nChannels);
  fwrite(&width, sizeof(int), 1, saveF);
  fwrite(&height, sizeof(int), 1, saveF);
  fwrite(&nChannels, sizeof(int), 1, saveF);

  int cnt = width * height * nChannels;
  float* data = (float*)mat.data;
  for (int i = 0; i < cnt; i++)
    fwrite(&data[i], sizeof(float), 1, saveF);
  fclose(saveF);
}

/**
 * @brief 读取 ASCII 格式的 .xyz 文件(包含 XYZ 和 RGB 值),并提取 XYZ 坐标
 * @param filename 输入文件名(.xyz)
 * @param cloud 输出的点云(仅 XYZ 坐标)
 * @return 是否成功读取
 */
bool readXYZFile(const std::string& filename, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud)
{
  // 打开文件
  std::ifstream file(filename);
  if (!file.is_open())
  {
    std::cerr << "Error: Could not open file " << filename << std::endl;
    return false;
  }

  // 清空并初始化点云
  cloud->clear();
  cloud->width = 0;
  cloud->height = 1;  // 无序点云
  cloud->is_dense = true;  // 假设没有 NaN 或 Inf 值

  std::string line;
  while (std::getline(file, line))
  {
    // 跳过空行和注释行(可选)
    if (line.empty() || line[0] == '#')
      continue;

    std::istringstream iss(line);
    pcl::PointXYZ point;

    // 读取 XYZ 坐标(前 3 个值)
    if (!(iss >> point.x >> point.y >> point.z))
    {
      std::cerr << "Warning: Invalid line format: " << line << std::endl;
      continue;
    }

    // 忽略 RGB 值(后 3 个值)
    float r, g, b;
    iss >> r >> g >> b;  // 读取但不存储

    // 添加到点云
    cloud->points.push_back(point);
  }

  // 更新点云宽度(点数)
  cloud->width = cloud->points.size();

  file.close();
  return true;
}


int main() {
  //  // 读取.xyz文件
    std::string filename = "q.xyz";
    const std::string file_name = "q.ply";
  ////std::string filename = "q.xyz";
		//pcl::PointCloud<pcl::PointXYZ>::Ptr cloud2 = std::make_shared<pcl::PointCloud<pcl::PointXYZ>>();
  //  readXYZFile(filename, cloud2);
  //  print_time("load data done");

  //  if (pcl::io::savePLYFileBinary(file_name, *cloud2) != 0)
  //  {
  //    PCL_ERROR("Failed to save ply!\n");
  //    return -1;
  //  }
  //  //std::cout << "Saved " << cloud->size() << " points to " << file_name << std::endl;
  //  print_time("save done");

    //------------------------------------------------------
    // 3. 重新读入
    //------------------------------------------------------
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PLYReader reader;
    //reader.setVerbosity(true);  // 启用详细日志

    if (reader.read(file_name, *cloud) == -1) {
      pcl::console::print_error("Failed to load PLY file!\n");
      return -1;
    }

    //std::cout << "Loaded " << cloud->size() << " points from " << file_name << std::endl;
    print_time("load data done");

    Eigen::Vector4f plane_coeff;
    Eigen::Vector3f centroid;
    if (!extractPlane(cloud, plane_coeff, centroid))
    {
        printf("Plane extract failed!\n");
        return -1;
    }

    //erasePtsByDistToPlane(cloud, plane_coeff);

    Coord2DTransformer transformer;
		auto transformed_cloud = transformer.rectify(cloud, plane_coeff, centroid);
    print_time("rectify done");

    auto volume = computeVolumeFromHeightMapOptimized(transformed_cloud);
		printf("Volume of the point cloud: %.2f cubic meters\n", volume);
		print_time("compute volume done");

    return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到