18.1 点云处理与分析

相关章节

点云是由大量三维坐标点组成的数据集合,通常由激光扫描仪、深度相机或摄影测量技术获取。与网格数据不同,点云不包含显式的连接关系,这使得处理点云数据具有独特的挑战。CGAL提供了完整的点云处理工具链,从基础的数据管理到高级的形状检测和分类。

18.1.1 点云数据简介

点云数据在现代三维应用中无处不在:

  • 工业测量:激光扫描获取零件几何
  • 建筑测绘:建筑物和遗址的三维数字化
  • 自动驾驶:LiDAR传感器的环境感知
  • 医学影像:CT/MRI数据的三维重建
  • 文化遗产:文物和艺术品的数字化保护

点云处理面临的主要挑战包括:

  1. 数据量大:扫描数据通常包含数百万甚至上亿个点
  2. 噪声干扰:传感器误差导致点位置存在偏差
  3. 不均匀采样:扫描密度随距离和角度变化
  4. 缺失数据:遮挡区域导致数据不完整
  5. 无拓扑信息:需要重建表面连接关系

CGAL的点云处理框架提供了解决这些挑战的完整工具集。

18.1.2 Point_set_3数据结构

CGAL::Point_set_3是CGAL中专门设计用于存储和处理点云数据的数据结构。与简单的点向量不同,Point_set_3提供了以下核心特性:

  • 动态属性系统:支持添加任意类型的属性(法线、颜色、强度等)
  • 高效内存管理:使用属性容器避免内存碎片
  • 惰性删除机制:标记删除点后延迟回收内存
  • 自动参数绑定:与CGAL算法无缝集成

基本使用

以下代码展示了Point_set_3的基本使用方法:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 添加点
  point_set.insert(Point(0., 0., 0.));
  point_set.insert(Point(1., 0., 0.));
  point_set.insert(Point(0., 1., 0.));
  point_set.insert(Point(1., 1., 0.));
 
  std::cout << "点云包含 " << point_set.size() << " 个点" << std::endl;
 
  // 遍历点云
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end(); ++it)
  {
    const Point& p = point_set.point(*it);
    std::cout << "点 " << *it << ": ("
              << p.x() << ", " << p.y() << ", " << p.z() << ")" << std::endl;
  }
 
  return 0;
}

法线属性管理

法线是点云处理中最重要的属性之一,用于描述表面朝向。Point_set_3提供了便捷的法线管理功能:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 添加点
  point_set.insert(Point(0., 0., 0.));
  point_set.insert(Point(1., 0., 0.));
  point_set.insert(Point(0., 1., 0.));
 
  // 添加法线属性
  point_set.add_normal_map();
 
  // 设置法线值
  Point_set::iterator it = point_set.begin();
  point_set.normal(*(it++)) = Vector(0., 0., 1.);
  point_set.normal(*(it++)) = Vector(0., 0., 1.);
  point_set.normal(*(it++)) = Vector(0., 0., 1.);
 
  // 同时添加点和法线
  point_set.insert(Point(1., 1., 0.), Vector(0., 0., 1.));
 
  // 遍历点和法线
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end(); ++it)
  {
    std::cout << "点: " << point_set.point(*it)
              << " 法线: " << point_set.normal(*it) << std::endl;
  }
 
  return 0;
}

自定义属性

Point_set_3的强大之处在于支持任意类型的自定义属性。以下示例展示了如何添加颜色、强度等属性:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 颜色结构
struct Color
{
  unsigned char r, g, b;
  Color() : r(0), g(0), b(0) {}
  Color(unsigned char _r, unsigned char _g, unsigned char _b)
    : r(_r), g(_g), b(_b) {}
};
 
int main(int, char**)
{
  Point_set point_set;
 
  // 添加自定义属性
  auto [color_map, color_added] = point_set.add_property_map<Color>("color", Color(255, 255, 255));
  auto [intensity_map, intensity_added] = point_set.add_property_map<float>("intensity", 0.0f);
  auto [label_map, label_added] = point_set.add_property_map<int>("label", -1);
 
  // 添加带属性的点
  for (int i = 0; i < 10; ++i)
  {
    Point_set::iterator it = point_set.insert(Point(i * 0.1, 0., 0.));
    color_map[*it] = Color(i * 25, 255 - i * 25, 128);
    intensity_map[*it] = static_cast<float>(i) / 10.0f;
    label_map[*it] = i % 3;  // 0, 1, 2 三类标签
  }
 
  // 查询属性
  std::cout << "属性列表:" << std::endl;
  for (const std::string& prop : point_set.properties())
    std::cout << " - " << prop << std::endl;
 
  // 访问属性
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end(); ++it)
  {
    const Color& c = color_map[*it];
    std::cout << "点 " << point_set.point(*it)
              << " RGB(" << (int)c.r << "," << (int)c.g << "," << (int)c.b << ")"
              << " 强度=" << intensity_map[*it]
              << " 标签=" << label_map[*it] << std::endl;
  }
 
  // 删除属性
  point_set.remove_property_map(intensity_map);
 
  return 0;
}

空间索引集成

Point_set_3内部使用KD树实现高效的空间查询。当调用点云处理算法时,CGAL会自动构建和使用这些空间索引结构:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Point_set_3/IO/XYZ.h>
 
#include <iostream>
#include <fstream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int argc, char** argv)
{
  Point_set point_set;
 
  // 从文件读取点云
  std::ifstream in((argc > 1) ? argv[1] : "data/points.xyz");
  if (!in)
  {
    std::cerr << "无法打开输入文件" << std::endl;
    return 1;
  }
 
  in >> point_set;
  std::cout << "读取了 " << point_set.size() << " 个点" << std::endl;
 
  // 显示点云信息
  std::cout << point_set.info();
 
  // 写入文件
  std::ofstream out("output.xyz");
  out << point_set;
 
  return 0;
}

点集合并

Point_set_3支持高效的点集合并操作:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set_1, point_set_2;
 
  // 填充第一个点云
  for (int i = 0; i < 5; ++i)
    point_set_1.insert(Point(i * 1.0, 0., 0.));
 
  // 填充第二个点云
  for (int i = 0; i < 5; ++i)
    point_set_2.insert(Point(i * 1.0, 1., 0.));
 
  std::cout << "合并前: 点云1=" << point_set_1.size()
            << " 点云2=" << point_set_2.size() << std::endl;
 
  // 合并点云
  point_set_1.join(point_set_2);
 
  std::cout << "合并后: 点云1=" << point_set_1.size()
            << " 点云2=" << point_set_2.size() << std::endl;
 
  // 或者使用运算符+=
  Point_set point_set_3;
  point_set_3.insert(Point(0., 2., 0.));
  point_set_1 += point_set_3;
 
  std::cout << "最终点云大小: " << point_set_1.size() << std::endl;
 
  return 0;
}

18.1.3 法线估计

法线估计是点云处理的基础步骤,它为每个点计算表面法线方向。CGAL提供了多种法线估计算法。

PCA局部平面拟合

主成分分析(PCA)是最常用的法线估计方法。它通过拟合局部邻域的最小二乘平面来计算法线:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/mst_orient_normals.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 生成示例点云(平面)
  for (int i = 0; i < 100; ++i)
    for (int j = 0; j < 100; ++j)
      point_set.insert(Point(i * 0.01, j * 0.01, 0.0));
 
  std::cout << "原始点云: " << point_set.size() << " 个点" << std::endl;
 
  // 添加法线属性
  point_set.add_normal_map();
 
  // 使用PCA估计法线
  // 参数:
  // - Sequential_tag: 串行执行
  // - 12: 每个点使用12个最近邻
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set,
     12,  // 邻域大小
     point_set.parameters().
     neighbor_radius(0.0));  // 0表示不使用半径限制
 
  std::cout << "法线估计完成" << std::endl;
 
  // 显示部分法线
  int count = 0;
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end() && count < 5; ++it, ++count)
  {
    std::cout << "点: " << point_set.point(*it)
              << " 法线: " << point_set.normal(*it) << std::endl;
  }
 
  return 0;
}

法线方向一致性

PCA估计的法线方向是随机的(可能指向表面内部或外部)。为了获得一致朝向的法线,需要使用方向传播算法:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/mst_orient_normals.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 生成球面上的点
  const double radius = 1.0;
  const int n_theta = 50;
  const int n_phi = 50;
 
  for (int i = 0; i < n_theta; ++i)
  {
    double theta = 2.0 * CGAL_PI * i / n_theta;
    for (int j = 0; j < n_phi; ++j)
    {
      double phi = CGAL_PI * j / n_phi;
      double x = radius * sin(phi) * cos(theta);
      double y = radius * sin(phi) * sin(theta);
      double z = radius * cos(phi);
      point_set.insert(Point(x, y, z));
    }
  }
 
  std::cout << "生成球面点云: " << point_set.size() << " 个点" << std::endl;
 
  // 添加法线属性
  point_set.add_normal_map();
 
  // 第一步:估计法线(方向随机)
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  std::cout << "PCA法线估计完成" << std::endl;
 
  // 第二步:使用最小生成树(MST)定向法线
  // 算法从Z坐标最高的点开始,向外传播法线方向
  Point_set::iterator unoriented_begin =
    CGAL::mst_orient_normals(point_set,
                              12,  // 邻域大小
                              point_set.parameters());
 
  // 移除未能定向的点(如果有)
  point_set.remove(unoriented_begin, point_set.end());
  point_set.collect_garbage();
 
  std::cout << "法线定向完成,剩余 " << point_set.size() << " 个点" << std::endl;
 
  // 验证法线方向(球面法线应指向外侧)
  int count = 0;
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end() && count < 5; ++it, ++count)
  {
    Point p = point_set.point(*it);
    Vector n = point_set.normal(*it);
    // 对于球面,法线应与位置向量同向
    Vector expected = Vector(p.x(), p.y(), p.z()) / radius;
    double dot = n * expected;
    std::cout << "点: (" << p.x() << "," << p.y() << "," << p.z() << ")"
              << " 法线: (" << n.x() << "," << n.y() << "," << n.z() << ")"
              << " 一致性: " << (dot > 0 ? "正确" : "错误") << std::endl;
  }
 
  return 0;
}

不同邻域大小的影响

邻域大小的选择对法线估计质量有重要影响:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
 
#include <iostream>
#include <cmath>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 计算法线误差(与真实法线的角度差)
double normal_error(const Vector& estimated, const Vector& ground_truth)
{
  double dot = estimated * ground_truth;
  // 处理方向不确定性
  if (dot < 0) dot = -dot;
  return std::acos(std::min(1.0, std::max(-1.0, dot))) * 180.0 / CGAL_PI;
}
 
int main(int, char**)
{
  // 创建带噪声的平面点云
  Point_set point_set;
  const Vector ground_truth_normal(0., 0., 1.);
 
  CGAL::Random random;
  for (int i = 0; i < 1000; ++i)
  {
    double x = random.get_double(0., 1.);
    double y = random.get_double(0., 1.);
    double z = random.get_double(-0.01, 0.01);  // 添加噪声
    point_set.insert(Point(x, y, z));
  }
 
  // 测试不同邻域大小
  std::vector<unsigned int> k_values = {6, 12, 24, 48};
 
  for (unsigned int k : k_values)
  {
    Point_set test_set = point_set;
    test_set.add_normal_map();
 
    CGAL::pca_estimate_normals<CGAL::Sequential_tag>
      (test_set, k, test_set.parameters());
 
    // 计算平均误差
    double total_error = 0.0;
    for (Point_set::const_iterator it = test_set.begin();
         it != test_set.end(); ++it)
    {
      total_error += normal_error(test_set.normal(*it), ground_truth_normal);
    }
    double avg_error = total_error / test_set.size();
 
    std::cout << "k=" << k << ": 平均法线误差 = " << avg_error << "度" << std::endl;
  }
 
  return 0;
}

邻域大小选择指南:

邻域大小适用场景优缺点
6-8高分辨率扫描、锐利特征细节保留好,但对噪声敏感
12-18一般用途平衡噪声鲁棒性和细节保留
24-36噪声较大的数据平滑效果好,可能丢失细节
48+严重噪声数据高度平滑,特征模糊

Jet拟合估计法线

对于曲面特征明显的点云,可以使用基于Jet拟合的法线估计:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/jet_estimate_normals.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 生成带曲面的点云
  for (int i = 0; i < 100; ++i)
  {
    double x = i / 100.0;
    for (int j = 0; j < 100; ++j)
    {
      double y = j / 100.0;
      double z = 0.1 * sin(2 * CGAL_PI * x) * sin(2 * CGAL_PI * y);
      point_set.insert(Point(x, y, z));
    }
  }
 
  point_set.add_normal_map();
 
  // 使用Jet拟合估计法线
  // degree_fitting: 拟合多项式的阶数(默认2)
  // degree_monge: Monge形式的阶数(默认2)
  CGAL::jet_estimate_normals<CGAL::Sequential_tag>
    (point_set,
     18,  // 邻域大小
     point_set.parameters().
     degree_fitting(2).
     degree_monge(2));
 
  std::cout << "Jet法线估计完成,共 " << point_set.size() << " 个点" << std::endl;
 
  return 0;
}

18.1.4 移动最小二乘平滑

移动最小二乘(MLS)是一种强大的点云平滑和重采样技术。它通过将点投影到局部拟合的曲面上来实现平滑。

曲面重建原理

MLS方法的核心思想是:对于每个点,在其邻域内拟合一个隐式曲面,然后将该点投影到曲面上。这种方法可以有效去除噪声同时保留几何特征。

投影与平滑

CGAL提供了基于Jet拟合的MLS平滑实现:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/jet_smooth_point_set.h>
#include <CGAL/pca_estimate_normals.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成带噪声的曲面点云
  for (int i = 0; i < 100; ++i)
  {
    double x = i / 100.0;
    for (int j = 0; j < 100; ++j)
    {
      double y = j / 100.0;
      // 原始曲面:z = sin(2*pi*x) * sin(2*pi*y)
      double z = sin(2 * CGAL_PI * x) * sin(2 * CGAL_PI * y);
      // 添加噪声
      z += random.get_double(-0.05, 0.05);
      point_set.insert(Point(x, y, z));
    }
  }
 
  std::cout << "原始点云: " << point_set.size() << " 个点(带噪声)" << std::endl;
 
  // 估计法线(平滑前)
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  // MLS平滑
  // 参数:
  // - k: 邻域大小
  // - degree_fitting: 拟合多项式阶数
  // - degree_monge: Monge形式阶数
  CGAL::jet_smooth_point_set<CGAL::Sequential_tag>
    (point_set,
     24,  // 邻域大小(平滑需要较大邻域)
     point_set.parameters().
     degree_fitting(2).
     degree_monge(2));
 
  std::cout << "MLS平滑完成" << std::endl;
 
  // 重新估计法线(平滑后)
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  return 0;
}

参数选择指南

MLS平滑的效果高度依赖于参数选择:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/jet_smooth_point_set.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 计算点云到原始曲面的平均距离(用于评估平滑质量)
double compute_error(const Point_set& point_set)
{
  double total_error = 0.0;
  for (Point_set::const_iterator it = point_set.begin();
       it != point_set.end(); ++it)
  {
    Point p = point_set.point(*it);
    // 原始曲面: z = sin(2*pi*x) * sin(2*pi*y)
    double z_true = sin(2 * CGAL_PI * p.x()) * sin(2 * CGAL_PI * p.y());
    double error = std::abs(p.z() - z_true);
    total_error += error;
  }
  return total_error / point_set.size();
}
 
int main(int, char**)
{
  CGAL::Random random;
 
  // 测试不同参数组合
  std::vector<unsigned int> k_values = {12, 18, 24};
  std::vector<unsigned int> degree_values = {2, 3, 4};
 
  for (unsigned int k : k_values)
  {
    for (unsigned int degree : degree_values)
    {
      // 生成带噪声的点云
      Point_set point_set;
      for (int i = 0; i < 100; ++i)
      {
        double x = i / 100.0;
        for (int j = 0; j < 100; ++j)
        {
          double y = j / 100.0;
          double z = sin(2 * CGAL_PI * x) * sin(2 * CGAL_PI * y);
          z += random.get_double(-0.05, 0.05);
          point_set.insert(Point(x, y, z));
        }
      }
 
      // 应用MLS平滑
      CGAL::jet_smooth_point_set<CGAL::Sequential_tag>
        (point_set, k,
         point_set.parameters().
         degree_fitting(degree).
         degree_monge(degree));
 
      double error = compute_error(point_set);
      std::cout << "k=" << k << ", degree=" << degree
                << ": 平均误差 = " << error << std::endl;
    }
  }
 
  return 0;
}

MLS参数选择建议:

参数说明推荐值
k邻域大小数据点数的平方根或18-30
degree_fitting拟合多项式阶数2(一般),3-4(复杂曲面)
degree_mongeMonge形式阶数与degree_fitting相同

注意事项:

  • 邻域过小:无法充分平滑噪声
  • 邻域过大:过度平滑,丢失细节
  • 阶数过高:计算开销大,可能过拟合
  • 阶数过低:无法捕捉复杂曲面特征

18.1.5 RANSAC形状检测

RANSAC(随机采样一致性)是检测点云中几何形状的有效方法。CGAL实现了高效的RANSAC算法,可以检测平面、球体、圆柱、圆锥和圆环等形状。

平面、圆柱、球体检测

以下示例展示了如何使用RANSAC检测点云中的基本几何形状:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Shape_detection/Efficient_RANSAC.h>
#include <CGAL/pca_estimate_normals.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// RANSAC类型定义
typedef CGAL::Shape_detection::Efficient_RANSAC_traits
  <Kernel, Point_set, Point_set::Point_map, Point_set::Vector_map> Traits;
typedef CGAL::Shape_detection::Efficient_RANSAC<Traits>          Efficient_ransac;
 
// 形状类型
typedef CGAL::Shape_detection::Plane<Traits>                     Plane;
typedef CGAL::Shape_detection::Sphere<Traits>                    Sphere;
typedef CGAL::Shape_detection::Cylinder<Traits>                  Cylinder;
typedef CGAL::Shape_detection::Cone<Traits>                      Cone;
typedef CGAL::Shape_detection::Torus<Traits>                     Torus;
 
int main(int, char**)
{
  Point_set point_set;
 
  // 生成测试数据:平面 + 球体
  CGAL::Random random;
 
  // 添加平面点(带少量噪声)
  for (int i = 0; i < 500; ++i)
  {
    double x = random.get_double(-1., 1.);
    double y = random.get_double(-1., 1.);
    double z = random.get_double(-0.01, 0.01);  // 接近z=0平面
    point_set.insert(Point(x, y, z));
  }
 
  // 添加球面点
  double radius = 0.5;
  Point center(0., 0., 1.);
  for (int i = 0; i < 500; ++i)
  {
    // 球坐标随机采样
    double theta = random.get_double(0., 2. * CGAL_PI);
    double phi = random.get_double(0., CGAL_PI);
    double x = center.x() + radius * sin(phi) * cos(theta);
    double y = center.y() + radius * sin(phi) * sin(theta);
    double z = center.z() + radius * cos(phi);
    point_set.insert(Point(x, y, z));
  }
 
  std::cout << "输入点云: " << point_set.size() << " 个点" << std::endl;
 
  // 估计法线(RANSAC需要法线信息)
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  // 创建RANSAC检测器
  Efficient_ransac ransac;
  ransac.set_input(point_set,
                   point_set.point_map(),
                   point_set.normal_map());
 
  // 注册要检测的形状类型
  ransac.add_shape_factory<Plane>();
  ransac.add_shape_factory<Sphere>();
  ransac.add_shape_factory<Cylinder>();
 
  // 设置检测参数
  Efficient_ransac::Parameters parameters;
  parameters.probability = 0.05;        // 检测概率(越低越严格)
  parameters.min_points = 100;          // 最小点数阈值
  parameters.epsilon = 0.02;            // 点到形状的最大距离
  parameters.cluster_epsilon = 0.1;     // 聚类距离阈值
  parameters.normal_threshold = 0.9;    // 法线一致性阈值(cos角度)
 
  // 执行检测
  ransac.detect(parameters);
 
  // 输出检测结果
  std::cout << "\n检测到 " << ransac.shapes().size() << " 个形状:" << std::endl;
 
  int shape_index = 0;
  for (const auto& shape : ransac.shapes())
  {
    std::cout << "\n形状 " << (++shape_index) << ":";
 
    // 获取形状信息
    if (Plane* plane = dynamic_cast<Plane*>(shape.get()))
    {
      std::cout << " 类型: 平面" << std::endl;
      std::cout << "  平面方程: " << plane->plane() << std::endl;
      std::cout << "  点数: " << shape->indices_of_assigned_points().size() << std::endl;
    }
    else if (Sphere* sphere = dynamic_cast<Sphere*>(shape.get()))
    {
      std::cout << " 类型: 球体" << std::endl;
      std::cout << "  中心: " << sphere->center() << std::endl;
      std::cout << "  半径: " << sphere->radius() << std::endl;
      std::cout << "  点数: " << shape->indices_of_assigned_points().size() << std::endl;
    }
    else if (Cylinder* cylinder = dynamic_cast<Cylinder*>(shape.get()))
    {
      std::cout << " 类型: 圆柱" << std::endl;
      std::cout << "  轴点: " << cylinder->axis_point() << std::endl;
      std::cout << "  轴方向: " << cylinder->axis_direction() << std::endl;
      std::cout << "  半径: " << cylinder->radius() << std::endl;
      std::cout << "  点数: " << shape->indices_of_assigned_points().size() << std::endl;
    }
  }
 
  std::cout << "\n未分配点数: " << ransac.number_of_unassigned_points() << std::endl;
 
  return 0;
}

参数设置策略

RANSAC算法的性能高度依赖于参数选择。以下是参数调优的详细指南:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Shape_detection/Efficient_RANSAC.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/compute_average_spacing.h>
 
#include <iostream>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
 
typedef CGAL::Point_set_3<Point> Point_set;
typedef CGAL::Shape_detection::Efficient_RANSAC_traits
  <Kernel, Point_set, Point_set::Point_map, Point_set::Vector_map> Traits;
typedef CGAL::Shape_detection::Efficient_RANSAC<Traits> Efficient_ransac;
typedef CGAL::Shape_detection::Plane<Traits> Plane;
 
// 自动计算RANSAC参数
Efficient_ransac::Parameters compute_auto_parameters(
  const Point_set& point_set,
  double noise_level = 0.01)  // 噪声水平估计(相对于包围盒对角线)
{
  Efficient_ransac::Parameters params;
 
  // 计算平均点间距
  double avg_spacing = CGAL::compute_average_spacing<CGAL::Sequential_tag>
    (point_set, 6, point_set.parameters());
 
  std::cout << "平均点间距: " << avg_spacing << std::endl;
 
  // 根据点间距设置参数
  params.epsilon = avg_spacing * 2.0;        // 点到形状距离
  params.cluster_epsilon = avg_spacing * 5.0; // 聚类距离
  params.normal_threshold = 0.85;             // 法线阈值(约30度)
  params.min_points = point_set.size() / 50;  // 至少2%的点
  params.probability = 0.01;                  // 高置信度
 
  return params;
}
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成包含多个平面的点云
  // 平面1: z = 0
  for (int i = 0; i < 300; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-1., 1.),
                           random.get_double(-0.01, 0.01)));
 
  // 平面2: x = 0
  for (int i = 0; i < 300; ++i)
    point_set.insert(Point(random.get_double(-0.01, 0.01),
                           random.get_double(-1., 1.),
                           random.get_double(-1., 1.)));
 
  // 平面3: y = 0
  for (int i = 0; i < 300; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-0.01, 0.01),
                           random.get_double(-1., 1.)));
 
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  // 使用自动参数
  std::cout << "=== 自动参数 ===" << std::endl;
  auto params = compute_auto_parameters(point_set);
 
  Efficient_ransac ransac;
  ransac.set_input(point_set,
                   point_set.point_map(),
                   point_set.normal_map());
  ransac.add_shape_factory<Plane>();
 
  ransac.detect(params);
 
  std::cout << "检测到 " << ransac.shapes().size() << " 个平面" << std::endl;
 
  return 0;
}

RANSAC参数详解:

参数说明设置建议
probability检测失败概率上限0.01-0.05(值越小越严格)
min_points形状最小点数根据数据规模,通常为1-5%
epsilon点到形状最大距离2-3倍平均点间距
cluster_epsilon空间聚类距离5-10倍平均点间距
normal_threshold法线一致性阈值0.8-0.9(cos值,对应25-36度)

结果过滤与后处理

检测完成后,通常需要对结果进行过滤和优化:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Shape_detection/Efficient_RANSAC.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/structure_point_set.h>
 
#include <iostream>
#include <vector>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
typedef Kernel::Plane_3 Plane_3;
 
typedef CGAL::Point_set_3<Point> Point_set;
typedef CGAL::Shape_detection::Efficient_RANSAC_traits
  <Kernel, Point_set, Point_set::Point_map, Point_set::Vector_map> Traits;
typedef CGAL::Shape_detection::Efficient_RANSAC<Traits> Efficient_ransac;
typedef CGAL::Shape_detection::Plane<Traits> Plane_shape;
 
// 过滤和优化检测结果
void filter_and_refine_shapes(
  Efficient_ransac& ransac,
  double min_coverage = 0.05,      // 最小覆盖率
  double max_overlap = 0.8)        // 最大重叠率
{
  auto shapes = ransac.shapes();
  std::vector<std::shared_ptr<Efficient_ransac::Shape>> filtered_shapes;
 
  std::size_t total_points = 0;
  for (const auto& shape : shapes)
    total_points += shape->indices_of_assigned_points().size();
 
  std::cout << "原始形状数: " << shapes.size() << std::endl;
 
  for (const auto& shape : shapes)
  {
    std::size_t shape_points = shape->indices_of_assigned_points().size();
    double coverage = double(shape_points) / total_points;
 
    // 过滤小形状
    if (coverage < min_coverage)
    {
      std::cout << "过滤小形状(覆盖率: " << coverage * 100 << "%)" << std::endl;
      continue;
    }
 
    filtered_shapes.push_back(shape);
  }
 
  std::cout << "过滤后形状数: " << filtered_shapes.size() << std::endl;
}
 
// 提取平面并构建结构化点集
void extract_structured_point_set(
  const Point_set& input_points,
  Efficient_ransac& ransac)
{
  // 提取检测到的平面
  std::vector<Plane_3> planes;
  std::vector<int> point_plane_indices(input_points.size(), -1);
 
  int plane_idx = 0;
  for (const auto& shape : ransac.shapes())
  {
    if (Plane_shape* plane = dynamic_cast<Plane_shape*>(shape.get()))
    {
      planes.push_back(plane->plane());
 
      // 标记属于该平面的点
      for (std::size_t idx : shape->indices_of_assigned_points())
        point_plane_indices[idx] = plane_idx;
 
      ++plane_idx;
    }
  }
 
  std::cout << "提取了 " << planes.size() << " 个平面" << std::endl;
 
  // 创建平面索引属性映射
  auto [plane_index_map, added] = const_cast<Point_set&>(input_points)
    .add_property_map<int>("plane_index", -1);
 
  std::size_t idx = 0;
  for (Point_set::const_iterator it = input_points.begin();
       it != input_points.end(); ++it, ++idx)
  {
    plane_index_map[*it] = point_plane_indices[idx];
  }
}
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成建筑结构点云(多个相交平面)
  // 地面
  for (int i = 0; i < 400; ++i)
    point_set.insert(Point(random.get_double(-2., 2.),
                           random.get_double(-2., 2.),
                           random.get_double(-0.01, 0.01)));
 
  // 墙面1
  for (int i = 0; i < 300; ++i)
    point_set.insert(Point(random.get_double(-0.01, 0.01),
                           random.get_double(-2., 2.),
                           random.get_double(0., 2.)));
 
  // 墙面2
  for (int i = 0; i < 300; ++i)
    point_set.insert(Point(random.get_double(-2., 2.),
                           random.get_double(-0.01, 0.01),
                           random.get_double(0., 2.)));
 
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  // 检测形状
  Efficient_ransac ransac;
  ransac.set_input(point_set,
                   point_set.point_map(),
                   point_set.normal_map());
  ransac.add_shape_factory<Plane_shape>();
 
  Efficient_ransac::Parameters params;
  params.probability = 0.01;
  params.min_points = 100;
  params.epsilon = 0.05;
  params.cluster_epsilon = 0.2;
  params.normal_threshold = 0.9;
 
  ransac.detect(params);
 
  // 过滤结果
  filter_and_refine_shapes(ransac);
 
  // 提取结构化信息
  extract_structured_point_set(point_set, ransac);
 
  return 0;
}

18.1.6 点云分类基础

点云分类是为每个点分配语义标签的过程,如地面、植被、建筑等。CGAL提供了特征提取和简单分类的工具。

特征提取

特征提取是分类的基础。常用的点云特征包括:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/compute_average_spacing.h>
 
#include <iostream>
#include <vector>
#include <cmath>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
typedef Kernel::FT FT;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 点特征结构
struct PointFeatures
{
  double linearity;      // 线性度(1D特征)
  double planarity;      // 平面度(2D特征)
  double scattering;     // 散射度(3D特征)
  double verticality;    // 垂直度
  double height;         // 高度
  double density;        // 局部密度
};
 
// 基于PCA的特征提取
PointFeatures compute_pca_features(
  const std::vector<Point>& neighbors,
  const Point& query_point)
{
  PointFeatures features = {0, 0, 0, 0, 0, 0};
 
  if (neighbors.size() < 3)
    return features;
 
  // 计算质心
  Point centroid(0, 0, 0);
  for (const Point& p : neighbors)
    centroid = Point(centroid.x() + p.x(),
                     centroid.y() + p.y(),
                     centroid.z() + p.z());
  centroid = Point(centroid.x() / neighbors.size(),
                   centroid.y() / neighbors.size(),
                   centroid.z() / neighbors.size());
 
  // 构建协方差矩阵
  double cov[3][3] = {{0}};
  for (const Point& p : neighbors)
  {
    double dx = p.x() - centroid.x();
    double dy = p.y() - centroid.y();
    double dz = p.z() - centroid.z();
 
    cov[0][0] += dx * dx;
    cov[0][1] += dx * dy;
    cov[0][2] += dx * dz;
    cov[1][1] += dy * dy;
    cov[1][2] += dy * dz;
    cov[2][2] += dz * dz;
  }
 
  for (int i = 0; i < 3; ++i)
    for (int j = 0; j < 3; ++j)
      cov[i][j] /= neighbors.size();
 
  cov[1][0] = cov[0][1];
  cov[2][0] = cov[0][2];
  cov[2][1] = cov[1][2];
 
  // 计算特征值(简化计算,实际应使用特征值分解)
  // 这里使用迹和行列式近似
  double trace = cov[0][0] + cov[1][1] + cov[2][2];
 
  // 简化的特征值估计
  double lambda1 = std::max({cov[0][0], cov[1][1], cov[2][2]});
  double lambda3 = std::min({cov[0][0], cov[1][1], cov[2][2]});
  double lambda2 = trace - lambda1 - lambda3;
 
  // 归一化
  if (trace > 0)
  {
    lambda1 /= trace;
    lambda2 /= trace;
    lambda3 /= trace;
  }
 
  // 计算几何特征
  features.linearity = (lambda1 - lambda2) / lambda1;
  features.planarity = (lambda2 - lambda3) / lambda1;
  features.scattering = lambda3 / lambda1;
 
  // 垂直度(基于主方向)
  Vector vertical(0, 0, 1);
  // 简化:使用z分量近似
  features.verticality = std::abs(centroid.z() - query_point.z());
 
  // 高度
  features.height = query_point.z();
 
  // 密度
  features.density = neighbors.size();
 
  return features;
}
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成混合点云
  // 地面(平面)
  for (int i = 0; i < 200; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-1., 1.),
                           random.get_double(-0.01, 0.01)));
 
  // 垂直结构(线性)
  for (int i = 0; i < 100; ++i)
    point_set.insert(Point(random.get_double(-0.02, 0.02),
                           random.get_double(-0.02, 0.02),
                           random.get_double(0., 1.)));
 
  // 散乱点
  for (int i = 0; i < 100; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-1., 1.),
                           random.get_double(0.5, 1.5)));
 
  std::cout << "点云大小: " << point_set.size() << std::endl;
 
  // 为每个点计算特征
  std::vector<PointFeatures> features;
 
  // 这里简化处理,实际应使用KD树查找邻域
  std::cout << "特征提取完成" << std::endl;
 
  return 0;
}

简单分类器实现

基于提取的特征,可以实现简单的规则分类器:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
#include <vector>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 语义标签
enum class SemanticLabel
{
  UNKNOWN = 0,
  GROUND = 1,      // 地面
  VEGETATION = 2,  // 植被
  BUILDING = 3,    // 建筑
  POLE = 4         // 杆状物
};
 
// 简单规则分类器
class RuleBasedClassifier
{
public:
  struct Features
  {
    double planarity;
    double linearity;
    double scattering;
    double height;
    double verticality;
  };
 
  SemanticLabel classify(const Features& f) const
  {
    // 地面:低高度 + 高平面度
    if (f.height < 0.2 && f.planarity > 0.7)
      return SemanticLabel::GROUND;
 
    // 杆状物:高线性度 + 高垂直度
    if (f.linearity > 0.8 && f.verticality > 0.9)
      return SemanticLabel::POLE;
 
    // 建筑:中等高度 + 高平面度
    if (f.height > 0.5 && f.planarity > 0.6)
      return SemanticLabel::BUILDING;
 
    // 植被:高散射度
    if (f.scattering > 0.3)
      return SemanticLabel::VEGETATION;
 
    return SemanticLabel::UNKNOWN;
  }
};
 
// 应用分类
void classify_point_set(Point_set& point_set,
                        const std::vector<SemanticLabel>& labels)
{
  // 添加标签属性
  auto [label_map, added] = point_set.add_property_map<int>("label", 0);
 
  std::size_t idx = 0;
  for (Point_set::iterator it = point_set.begin();
       it != point_set.end(); ++it, ++idx)
  {
    label_map[*it] = static_cast<int>(labels[idx]);
  }
 
  // 统计各类别数量
  std::vector<int> counts(5, 0);
  for (SemanticLabel label : labels)
    counts[static_cast<int>(label)]++;
 
  std::cout << "分类统计:" << std::endl;
  std::cout << "  地面: " << counts[1] << std::endl;
  std::cout << "  植被: " << counts[2] << std::endl;
  std::cout << "  建筑: " << counts[3] << std::endl;
  std::cout << "  杆状: " << counts[4] << std::endl;
  std::cout << "  未知: " << counts[0] << std::endl;
}
 
int main(int, char**)
{
  // 示例:使用规则分类器
  RuleBasedClassifier classifier;
 
  // 测试特征
  std::vector<RuleBasedClassifier::Features> test_features = {
    {0.9, 0.1, 0.0, 0.05, 0.1},   // 地面
    {0.1, 0.1, 0.8, 1.2, 0.5},    // 植被
    {0.8, 0.2, 0.0, 2.5, 0.9},    // 建筑墙面
    {0.1, 0.9, 0.0, 1.5, 0.95},   // 杆状
  };
 
  for (const auto& f : test_features)
  {
    SemanticLabel label = classifier.classify(f);
    std::cout << "特征 (平面度=" << f.planarity
              << ", 线性度=" << f.linearity
              << ", 高度=" << f.height << ") -> ";
 
    switch (label)
    {
      case SemanticLabel::GROUND:
        std::cout << "地面" << std::endl;
        break;
      case SemanticLabel::VEGETATION:
        std::cout << "植被" << std::endl;
        break;
      case SemanticLabel::BUILDING:
        std::cout << "建筑" << std::endl;
        break;
      case SemanticLabel::POLE:
        std::cout << "杆状" << std::endl;
        break;
      default:
        std::cout << "未知" << std::endl;
    }
  }
 
  return 0;
}

形状直径函数(SDF)

形状直径函数(Shape Diameter Function, SDF)是一种用于形状分析和分割的特征。它通过测量形状内部到表面的距离来捕捉局部形状特征。

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
 
#include <iostream>
#include <vector>
#include <cmath>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
typedef Kernel::FT FT;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 简化的SDF计算(基于法线反方向的局部穿透深度估计)
double compute_sdf_approximation(
  const Point& query_point,
  const Vector& normal,
  const std::vector<Point>& neighbors,
  double max_distance = 1.0)
{
  if (neighbors.empty())
    return 0.0;
 
  // SDF估计:沿法线反方向搜索最近点
  Vector inward = -normal;
  inward = inward / std::sqrt(inward * inward);  // 归一化
 
  double min_distance = max_distance;
 
  for (const Point& neighbor : neighbors)
  {
    Vector diff = Vector(query_point, neighbor);
    double projection = diff * inward;
 
    // 只考虑法线反方向的点
    if (projection > 0)
    {
      double distance = std::sqrt(diff * diff);
      if (distance < min_distance)
        min_distance = distance;
    }
  }
 
  // 归一化SDF值
  return 1.0 - (min_distance / max_distance);
}
 
// 使用SDF进行形状分割
std::vector<int> sdf_segmentation(
  const Point_set& point_set,
  const std::vector<double>& sdf_values,
  double threshold = 0.5)
{
  std::vector<int> segments(point_set.size(), -1);
  int current_segment = 0;
 
  // 简化:基于SDF阈值分割
  for (std::size_t i = 0; i < sdf_values.size(); ++i)
  {
    if (sdf_values[i] > threshold)
      segments[i] = current_segment++;
  }
 
  return segments;
}
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成简单形状(立方体)
  // 前面
  for (int i = 0; i < 100; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-1., 1.),
                           1.0 + random.get_double(-0.01, 0.01)));
 
  // 后面
  for (int i = 0; i < 100; ++i)
    point_set.insert(Point(random.get_double(-1., 1.),
                           random.get_double(-1., 1.),
                           -1.0 + random.get_double(-0.01, 0.01)));
 
  // 估计法线
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  std::cout << "SDF计算示例" << std::endl;
  std::cout << "点云大小: " << point_set.size() << std::endl;
 
  // 实际应用中,SDF计算需要:
  // 1. 构建KD树进行高效邻域查询
  // 2. 沿法线方向发射射线
  // 3. 计算穿透深度
 
  return 0;
}

18.1.7 完整处理流程示例

以下示例展示了从原始点云到结构化输出的完整处理流程:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Point_set_3/IO/XYZ.h>
 
// 点云处理算法
#include <CGAL/grid_simplify_point_set.h>
#include <CGAL/remove_outliers.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/mst_orient_normals.h>
#include <CGAL/jet_smooth_point_set.h>
#include <CGAL/compute_average_spacing.h>
 
// 形状检测
#include <CGAL/Shape_detection/Efficient_RANSAC.h>
 
#include <iostream>
#include <fstream>
#include <vector>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
typedef Kernel::Vector_3 Vector;
typedef Kernel::Plane_3 Plane_3;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 形状检测类型
typedef CGAL::Shape_detection::Efficient_RANSAC_traits
  <Kernel, Point_set, Point_set::Point_map, Point_set::Vector_map> Traits;
typedef CGAL::Shape_detection::Efficient_RANSAC<Traits> Efficient_ransac;
typedef CGAL::Shape_detection::Plane<Traits> Plane_shape;
typedef CGAL::Shape_detection::Sphere<Traits> Sphere_shape;
typedef CGAL::Shape_detection::Cylinder<Traits> Cylinder_shape;
 
// 处理统计
struct ProcessingStats
{
  std::size_t original_size;
  std::size_t after_simplification;
  std::size_t after_outlier_removal;
  std::size_t final_size;
  double average_spacing;
  std::size_t num_shapes;
  double processing_time;
};
 
// 完整处理流程
ProcessingStats process_point_cloud(Point_set& point_set)
{
  ProcessingStats stats;
  stats.original_size = point_set.size();
 
  std::cout << "=== 点云处理流程 ===" << std::endl;
  std::cout << "1. 原始点云: " << stats.original_size << " 个点" << std::endl;
 
  // 步骤1:计算平均点间距
  stats.average_spacing = CGAL::compute_average_spacing<CGAL::Sequential_tag>
    (point_set, 6, point_set.parameters());
  std::cout << "   平均点间距: " << stats.average_spacing << std::endl;
 
  // 步骤2:网格简化
  double cell_size = stats.average_spacing * 2.0;
  Point_set::iterator first_to_remove =
    CGAL::grid_simplify_point_set(point_set, cell_size,
                                   point_set.parameters());
  point_set.remove(first_to_remove, point_set.end());
  point_set.collect_garbage();
  stats.after_simplification = point_set.size();
  std::cout << "2. 网格简化后: " << stats.after_simplification << " 个点" << std::endl;
 
  // 步骤3:离群点移除
  Point_set::iterator first_outlier =
    CGAL::remove_outliers<CGAL::Sequential_tag>
    (point_set,
     24,  // 邻域大小
     point_set.parameters().
     threshold_percent(5.0).  // 最多移除5%
     threshold_distance(stats.average_spacing * 3.0));
 
  point_set.remove(first_outlier, point_set.end());
  point_set.collect_garbage();
  stats.after_outlier_removal = point_set.size();
  std::cout << "3. 离群点移除后: " << stats.after_outlier_removal << " 个点" << std::endl;
 
  // 步骤4:法线估计
  point_set.add_normal_map();
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
  std::cout << "4. 法线估计完成" << std::endl;
 
  // 步骤5:法线定向
  Point_set::iterator unoriented_begin =
    CGAL::mst_orient_normals(point_set, 12, point_set.parameters());
  point_set.remove(unoriented_begin, point_set.end());
  point_set.collect_garbage();
  std::cout << "5. 法线定向完成" << std::endl;
 
  // 步骤6:MLS平滑(可选)
  CGAL::jet_smooth_point_set<CGAL::Sequential_tag>
    (point_set, 18,
     point_set.parameters().
     degree_fitting(2).
     degree_monge(2));
  std::cout << "6. MLS平滑完成" << std::endl;
 
  // 步骤7:重新估计法线
  CGAL::pca_estimate_normals<CGAL::Sequential_tag>
    (point_set, 12, point_set.parameters());
 
  Point_set::iterator unoriented_begin2 =
    CGAL::mst_orient_normals(point_set, 12, point_set.parameters());
  point_set.remove(unoriented_begin2, point_set.end());
  point_set.collect_garbage();
  stats.final_size = point_set.size();
  std::cout << "7. 最终点云: " << stats.final_size << " 个点" << std::endl;
 
  return stats;
}
 
// 形状检测
void detect_shapes(Point_set& point_set, ProcessingStats& stats)
{
  std::cout << "\n=== 形状检测 ===" << std::endl;
 
  Efficient_ransac ransac;
  ransac.set_input(point_set,
                   point_set.point_map(),
                   point_set.normal_map());
 
  // 注册形状类型
  ransac.add_shape_factory<Plane_shape>();
  ransac.add_shape_factory<Sphere_shape>();
  ransac.add_shape_factory<Cylinder_shape>();
 
  // 设置参数
  Efficient_ransac::Parameters params;
  params.probability = 0.01;
  params.min_points = static_cast<std::size_t>(point_set.size() * 0.02);  // 2%
  params.epsilon = stats.average_spacing * 2.0;
  params.cluster_epsilon = stats.average_spacing * 5.0;
  params.normal_threshold = 0.9;
 
  // 检测
  ransac.detect(params);
 
  stats.num_shapes = ransac.shapes().size();
  std::cout << "检测到 " << stats.num_shapes << " 个形状" << std::endl;
 
  // 输出形状信息
  int plane_count = 0, sphere_count = 0, cylinder_count = 0;
  for (const auto& shape : ransac.shapes())
  {
    if (dynamic_cast<Plane_shape*>(shape.get()))
      plane_count++;
    else if (dynamic_cast<Sphere_shape*>(shape.get()))
      sphere_count++;
    else if (dynamic_cast<Cylinder_shape*>(shape.get()))
      cylinder_count++;
  }
 
  std::cout << "  平面: " << plane_count << std::endl;
  std::cout << "  球体: " << sphere_count << std::endl;
  std::cout << "  圆柱: " << cylinder_count << std::endl;
  std::cout << "  未分配: " << ransac.number_of_unassigned_points() << " 个点" << std::endl;
}
 
int main(int argc, char** argv)
{
  Point_set point_set;
 
  // 读取输入
  if (argc > 1)
  {
    std::ifstream in(argv[1]);
    if (!in)
    {
      std::cerr << "无法打开文件: " << argv[1] << std::endl;
      return 1;
    }
    in >> point_set;
    std::cout << "从文件读取点云: " << point_set.size() << " 个点" << std::endl;
  }
  else
  {
    // 生成测试数据
    CGAL::Random random;
 
    // 地面平面
    for (int i = 0; i < 500; ++i)
      point_set.insert(Point(random.get_double(-2., 2.),
                             random.get_double(-2., 2.),
                             random.get_double(-0.01, 0.01)));
 
    // 墙面
    for (int i = 0; i < 300; ++i)
      point_set.insert(Point(random.get_double(-0.02, 0.02),
                             random.get_double(-2., 2.),
                             random.get_double(0., 2.)));
 
    // 圆柱
    double cyl_radius = 0.3;
    Point cyl_center(1., 1., 1.);
    for (int i = 0; i < 200; ++i)
    {
      double theta = random.get_double(0., 2. * CGAL_PI);
      double z = random.get_double(0., 2.);
      double x = cyl_center.x() + cyl_radius * cos(theta);
      double y = cyl_center.y() + cyl_radius * sin(theta);
      point_set.insert(Point(x, y, z));
    }
 
    std::cout << "生成测试点云: " << point_set.size() << " 个点" << std::endl;
  }
 
  // 执行处理流程
  ProcessingStats stats = process_point_cloud(point_set);
 
  // 形状检测
  detect_shapes(point_set, stats);
 
  // 保存结果
  std::ofstream out("processed_point_set.xyz");
  out << point_set;
  std::cout << "\n结果已保存到 processed_point_set.xyz" << std::endl;
 
  // 输出统计摘要
  std::cout << "\n=== 处理统计摘要 ===" << std::endl;
  std::cout << "原始点数: " << stats.original_size << std::endl;
  std::cout << "最终点数: " << stats.final_size << std::endl;
  std::cout << "压缩率: " << (100.0 * stats.final_size / stats.original_size) << "%" << std::endl;
  std::cout << "检测形状: " << stats.num_shapes << std::endl;
 
  return 0;
}

18.1.8 性能优化

处理大规模点云时需要考虑性能优化策略。

并行处理

CGAL支持多线程并行处理:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/pca_estimate_normals.h>
#include <CGAL/jet_smooth_point_set.h>
 
#include <iostream>
#include <chrono>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
int main(int, char**)
{
  Point_set point_set;
  CGAL::Random random;
 
  // 生成大规模点云
  for (int i = 0; i < 1000; ++i)
    for (int j = 0; j < 1000; ++j)
      point_set.insert(Point(i * 0.01, j * 0.01, random.get_double(-0.01, 0.01)));
 
  std::cout << "点云大小: " << point_set.size() << std::endl;
 
  point_set.add_normal_map();
 
  // 串行处理
  {
    Point_set test_set = point_set;
    auto start = std::chrono::high_resolution_clock::now();
 
    CGAL::pca_estimate_normals<CGAL::Sequential_tag>
      (test_set, 12, test_set.parameters());
 
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "串行法线估计: " << duration.count() << " ms" << std::endl;
  }
 
  // 并行处理(如果CGAL支持)
#ifdef CGAL_LINKED_WITH_TBB
  {
    Point_set test_set = point_set;
    auto start = std::chrono::high_resolution_clock::now();
 
    CGAL::pca_estimate_normals<CGAL::Parallel_tag>
      (test_set, 12, test_set.parameters());
 
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "并行法线估计: " << duration.count() << " ms" << std::endl;
  }
#else
  std::cout << "并行支持未启用" << std::endl;
#endif
 
  return 0;
}

内存优化

对于超大规模点云,可以考虑以下策略:

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Point_set_3.h>
 
#include <iostream>
#include <vector>
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
typedef Kernel::Point_3 Point;
 
typedef CGAL::Point_set_3<Point> Point_set;
 
// 分块处理策略
void process_in_chunks(const std::string& filename,
                       std::size_t chunk_size = 100000)
{
  std::cout << "分块处理: " << filename << std::endl;
  std::cout << "每块大小: " << chunk_size << " 点" << std::endl;
 
  // 实际实现需要:
  // 1. 流式读取点云文件
  // 2. 分块处理每个子集
  // 3. 合并结果
}
 
// 使用32位索引减少内存占用(对于小于2^32点的点云)
void use_compact_indices()
{
  // 定义 CGAL_POINT_SET_3_USE_STD_SIZE_T_AS_SIZE_TYPE
  // 可以使用32位索引,每个索引节省4字节
  std::cout << "使用紧凑索引模式" << std::endl;
}
 
int main(int, char**)
{
  Point_set point_set;
 
  // 预分配内存
  point_set.reserve(1000000);
 
  // 添加点...
 
  // 及时回收垃圾
  point_set.collect_garbage();
 
  // 清理不需要的属性
  // point_set.clear_properties();
 
  return 0;
}

18.1.9 本章小结

本章介绍了CGAL中点云处理与分析的核心功能:

Point_set_3数据结构

  • 提供动态属性系统,支持法线、颜色、标签等任意属性
  • 高效的内存管理和惰性删除机制
  • 与CGAL算法无缝集成

法线估计

  • PCA方法适用于一般情况
  • Jet拟合适用于曲面特征明显的数据
  • MST定向确保法线一致性

MLS平滑

  • 基于局部曲面拟合的平滑技术
  • 参数选择影响平滑效果和特征保留

RANSAC形状检测

  • 检测平面、球体、圆柱、圆锥、圆环等形状
  • 参数调优对检测质量至关重要
  • 支持结构化点集生成

点云分类

  • 基于PCA的特征提取
  • 规则分类器实现
  • SDF用于形状分析

性能优化

  • 并行处理加速计算
  • 分块处理超大规模数据
  • 内存管理策略

CGAL的点云处理框架提供了从原始数据到结构化输出的完整工具链,适用于各种应用场景。

18.1.10 练习

  1. 法线估计实验:使用不同邻域大小(k=6, 12, 24, 48)对同一组带噪声的点云进行法线估计,比较结果差异并分析原因。

  2. MLS平滑参数调优:对包含锐利边缘的点云应用MLS平滑,调整邻域大小和多项式阶数,观察对边缘保留的影响。

  3. 形状检测实践:创建一个包含多个几何形状(平面、球体、圆柱)的合成点云,使用RANSAC检测所有形状并计算检测精度。

  4. 点云分类器:实现一个基于几何特征的简单分类器,能够区分地面、墙面和植被点。

  5. 完整处理流程:编写一个完整的点云处理程序,包含读取、简化、去噪、平滑、法线估计、形状检测和保存结果的全流程。

  6. 性能测试:比较串行和并行算法在不同规模点云上的性能差异,绘制加速比曲线。

  7. 自定义属性:扩展Point_set_3,添加时间戳、反射强度等自定义属性,并实现基于这些属性的过滤功能。