17.1 最优包围盒 (Optimal Bounding Box)

相关章节

17.1.0 类比解释:包裹不规则礼物

生活场景类比

想象你要邮寄一个形状不规则的礼物(如一个复杂的雕塑)给朋友。邮局提供各种标准尺寸的盒子,你希望找到能装下这个礼物的最小盒子,以节省运费。

轴对齐包围盒(AABB):就像把礼物直着放进盒子,不管礼物是什么方向,盒子的边都与货架对齐。这很简单,但可能浪费很多空间(比如长条形的礼物斜着放)。

最优包围盒(OBB):你可以旋转礼物,找到最紧凑的摆放方式。比如一根长长的拐杖,直着放需要大盒子,但斜着放可能只需要很小的盒子。

核心问题:如何找到最优的旋转角度,使得包围盒的体积最小?

为什么困难

  • 不能尝试所有可能的旋转(无限多种可能)
  • 局部最优不一定全局最优
  • 需要在精度和计算时间之间权衡

为什么需要最优包围盒?

场景1:碰撞检测优化

游戏开发中,需要快速判断两个物体是否碰撞。
- 使用AABB:简单快速,但紧密性差
- 使用OBB:更紧密,减少误报
- 使用最优OBB:最紧密,最佳性能

示例:一辆长长的卡车
- AABB体积:1000 m³
- PCA-based OBB体积:400 m³  
- 最优OBB体积:250 m³
- 碰撞检测效率提升:4倍

场景2:空间分割

在k-d树或八叉树中,紧密的包围盒可以减少节点重叠,
提高查询效率。

场景3:物理模拟

计算物体的转动惯量、质心等物理属性时,
紧密的包围盒提供更准确的近似。

17.1.1 理论基础

问题定义

最优包围盒问题旨在找到一个包含给定三维点集的最小体积长方体。与轴对齐包围盒(AABB)不同,最优包围盒可以任意旋转,因此能够更紧密地包裹物体。

数学表述:给定三维点集 ,寻找一个旋转矩阵 使得旋转后的点集 的轴对齐包围盒体积最小:

其中 是三维特殊正交群(所有行列式为1的正交矩阵)。

几何解释

场景:2D中的最优矩形包围盒

原始点集(不规则分布):
    Y
    5 |     *   *
    4 |   *   *
    3 | *   *
    2 |   *   *
    1 | *   *
    0 +-----------
      0 1 2 3 4 5 X

AABB(轴对齐):
    +-----------+
    |     *   *|
    |   *   *  |
    | *   *    |
    |   *   *  |
    | *   *    |
    +-----------+
    体积 = 5 × 5 = 25

最优OBB(旋转45度):
      +-----+
     /       \
    /  *   *  \
   / *   *     \
  /  *   *      \
 /   *   *       \
+-----+           +
 \\   *   *       /
  \\-----+       /
   
    体积 = 4 × 3 = 12(显著减小!)

算法原理

CGAL的oriented_bounding_box实现基于Chang、Gorissen和Melchior(2011)提出的混合优化算法,结合了以下技术:

  1. 遗传算法:全局搜索旋转空间
  2. Nelder-Mead单纯形法:局部优化
  3. 2D精确算法:沿当前最优轴进行精确优化

算法流程可视化

输入:点云(如一个兔子模型)

Step 1: 凸包预处理
- 计算凸包,减少点数
- 10000个点 → 500个凸包顶点

Step 2: 初始种群生成(遗传算法)
生成30个随机旋转矩阵(种群):
    R1, R2, ..., R30

Step 3: 评估适应度
对每个Ri,计算旋转后点的AABB体积:
    fitness(Ri) = volume(AABB(Ri × points))

Step 4: 进化迭代(100代)
For gen = 1 to 100:
    a. 选择:保留体积最小的15个矩阵
    b. 交叉:配对生成15个新矩阵
       R_new = QR(α×Ra + (1-α)×Rb)  // QR保持正交性
    c. 变异:添加小随机扰动
    d. 局部优化:对前5个用Nelder-Mead精化

Step 5: 返回最优解
最佳旋转矩阵 R_best
应用R_best得到最优包围盒

凸包预处理

算法首先计算输入点集的凸包,将问题规模从 减少到 ,其中 是凸包上的点数。这是基于以下观察:最优包围盒的顶点必然位于凸包上。

凸包示例

原始点集:1000个点(内部有大量冗余)

凸包计算后:
    只有表面上的50个点是关键的
    内部950个点不影响包围盒

计算加速:
- 凸包计算:O(n log n)
- 但后续优化:从O(1000)降到O(50)
- 总体加速:20倍

17.1.2 架构分析

类层次结构

CGAL::oriented_bounding_box() [入口函数]
    └── CGAL::Optimal_bounding_box::internal::
        └── construct_oriented_bounding_box()
            ├── extreme_points_3() [凸包计算]
            └── compute_best_transformation()
                └── Evolution::evolve() [主优化循环]

核心组件

1. Oriented_bounding_box_traits_3

template <typename K>
class Oriented_bounding_box_traits_3 : public K
{
public:
    typedef typename K::FT FT;
    typedef CGAL::Aff_transformation_3<K> Aff_transformation_3;
    typedef CGAL::Eigen_matrix<FT, 3, 3> Matrix;
    typedef CGAL::Eigen_vector<FT, 3> Vector;
    
    // QR分解获取正交矩阵
    static Matrix get_Q(const Matrix& m);
};

该traits类提供:

  • 基于Eigen的矩阵运算支持
  • QR分解确保旋转矩阵的正交性
  • 与CGAL几何内核的集成

2. Evolution类

遗传算法的主控制器,管理种群的进化过程:

template <typename PointRange, typename Traits>
class Evolution
{
    void evolve(std::size_t max_generations,
                std::size_t population_size,
                std::size_t nelder_mead_iterations);
    void genetic_algorithm();  // 遗传操作
};

3. Population和Vertex

template<typename Traits>
struct Vertex_with_fitness {
    Matrix m_mat;    // 旋转矩阵
    FT m_val;        // 适应度值(包围盒体积)
};
 
template<typename Traits>
class Population {
    typedef std::array<Vertex, 4> Simplex;  // 单纯形顶点
    std::vector<Simplex> m_simplices;       // 种群
};

文件组织

Optimal_bounding_box/
├── include/CGAL/
│   ├── optimal_bounding_box.h                    # 主入口
│   └── Optimal_bounding_box/
│       ├── oriented_bounding_box.h               # 核心算法
│       ├── Oriented_bounding_box_traits_3.h      # Traits定义
│       └── internal/
│           ├── evolution.h                       # 遗传算法
│           ├── population.h                      # 种群管理
│           ├── fitness_function.h                # 适应度计算
│           ├── nelder_mead_functions.h           # NM优化
│           ├── optimize_2.h                      # 2D精确优化
│           └── helper.h                          # 辅助函数
├── examples/
│   └── obb_example.cpp                           # 使用示例
└── test/                                         # 测试用例

17.1.3 实现细节

核心算法流程

// 来自 oriented_bounding_box.h
template <typename PointRange, typename Traits>
void compute_best_transformation(
    const PointRange& points,
    typename Traits::Aff_transformation_3& transformation,
    typename Traits::Aff_transformation_3& inverse_transformation,
    CGAL::Random& rng,
    const Traits& traits)
{
    const std::size_t max_generations = 100;
    const std::size_t population_size = 30;
    const std::size_t nelder_mead_iterations = 20;
 
    // 创建进化优化器
    Evolution<PointRange, Traits> search_solution(points, rng, traits);
    
    // 执行进化优化
    search_solution.evolve(max_generations, population_size, 
                          nelder_mead_iterations);
    
    // 获取最优旋转矩阵
    const Matrix& rot = search_solution.get_best_vertex().matrix();
    
    // 构造CGAL变换对象
    transformation = Aff_transformation_3(
        rot(0, 0), rot(0, 1), rot(0, 2),
        rot(1, 0), rot(1, 1), rot(1, 2),
        rot(2, 0), rot(2, 1), rot(2, 2));
    
    // 正交矩阵的逆等于转置
    inverse_transformation = Aff_transformation_3(
        rot(0, 0), rot(1, 0), rot(2, 0),
        rot(0, 1), rot(1, 1), rot(2, 1),
        rot(0, 2), rot(1, 2), rot(2, 2));
}

算法可视化

输入:点云(如斯坦福兔子模型)

Phase 1: 凸包计算
原始点:35,947个顶点
凸包顶点:约500个
时间:0.1秒

Phase 2: 遗传算法进化
初始种群(随机旋转):
    R1: 体积 = 1500
    R2: 体积 = 2300
    ...
    R30: 体积 = 1800

第10代:
    最佳体积:850
    平均体积:1200

第50代:
    最佳体积:620
    平均体积:750

第100代:
    最佳体积:598(收敛)

Phase 3: 局部精化(Nelder-Mead)
初始:598
迭代5次:585
迭代10次:582
迭代20次:581(收敛)

最终OBB体积:581(相比初始最佳提升61%)

遗传算法实现

// 来自 evolution.h
void genetic_algorithm()
{
    const std::size_t m = m_population.size();
    const std::size_t first_group_size = m / 2;
    const std::size_t second_group_size = m - first_group_size;
    
    // 随机分组
    std::vector<std::size_t> group1(first_group_size), group2(first_group_size);
    std::generate(group1.begin(), group1.end(), 
                  [&]{ return m_rng.get_int(0, im); });
    
    // 交叉操作I:选择较优父代
    const FT lweight = 0.4, uweight = 0.6;
    for(std::size_t i=0; i<first_group_size; ++i) {
        const FT fitnessA = m_population[group1[i]][j].fitness();
        const FT fitnessB = m_population[group2[i]][j].fitness();
        const FT threshold = (fitnessA < fitnessB) ? uweight : lweight;
        
        if(r < threshold)
            offspring[j] = m_population[group1[i]][j];
        else
            offspring[j] = m_population[group2[i]][j];
    }
    
    // 交叉操作II:矩阵插值
    for(std::size_t i=0; i<second_group_size; ++i) {
        const FT lambda = (fitnessA < fitnessB) ? uweight : lweight;
        const FT rambda = FT(1) - lambda;
        
        // 矩阵线性组合后QR正交化
        offspring[j] = Vertex{m_traits.get_Q(lambda*lm + rambda*rm), 
                              m_points, m_traits};
    }
}

适应度计算

// 来自 fitness_function.h
template <typename Traits, typename PointRange>
typename Traits::FT compute_fitness(
    const typename Traits::Matrix& R,
    const PointRange& points,
    const Traits&)
{
    FT xmin, ymin, zmin, xmax, ymax, zmax;
    xmin = ymin = zmin = FT((std::numeric_limits<double>::max)());
    xmax = ymax = zmax = FT(std::numeric_limits<double>::lowest());
 
    // 计算旋转后点的包围盒
    for(const Point& pt : points) {
        Vector pv(3);
        pv.set(0, pt.x());
        pv.set(1, pt.y());
        pv.set(2, pt.z());
        pv = R * pv;  // 应用旋转
 
        xmin = (std::min)(xmin, pv(0));
        ymin = (std::min)(ymin, pv(1));
        zmin = (std::min)(zmin, pv(2));
        xmax = (std::max)(xmax, pv(0));
        ymax = (std::max)(ymax, pv(1));
        zmax = (std::max)(zmax, pv(2));
    }
 
    // 返回体积作为适应度(越小越好)
    return ((xmax - xmin) * (ymax - ymin) * (zmax - zmin));
}

Nelder-Mead优化

// 来自 nelder_mead_functions.h
template <typename Simplex, typename PointRange, typename Traits>
void nelder_mead(Simplex& simplex,
                 const std::size_t nelder_mead_iterations,
                 const PointRange& points,
                 const Traits& traits)
{
    for(std::size_t t=0; t<nelder_mead_iterations; ++t) {
        // 按适应度排序
        std::sort(simplex.begin(), simplex.end(),
                  [](const Vertex& vi, const Vertex& vj) -> bool
                  { return vi.fitness() < vj.fitness(); });
 
        // 计算前三个最优点的质心
        const Matrix centroid_m = nm_centroid(simplex[0].matrix(),
                                              simplex[1].matrix(),
                                              simplex[2].matrix(), traits);
 
        // 反射操作
        const Matrix refl_m = reflection(centroid_m, simplex[3].matrix());
        
        // 根据反射结果选择:反射、扩展、收缩或缩减
        // ... 省略详细逻辑
    }
}

17.1.4 使用示例

基本用法

#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/optimal_bounding_box.h>
#include <CGAL/Polygon_mesh_processing/triangulate_faces.h>
#include <CGAL/Polygon_mesh_processing/IO/polygon_mesh_io.h>
 
namespace PMP = CGAL::Polygon_mesh_processing;
 
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_3 Point;
typedef CGAL::Surface_mesh<Point> Surface_mesh;
 
int main(int argc, char** argv)
{
    const std::string filename = (argc > 1) ? argv[1] 
        : CGAL::data_file_path("meshes/pig.off");
 
    Surface_mesh sm;
    if(\!PMP::IO::read_polygon_mesh(filename, sm) || sm.is_empty()) {
        std::cerr << "Invalid input file." << std::endl;
        return EXIT_FAILURE;
    }
 
    // 计算最优包围盒
    std::array<Point, 8> obb_points;
    CGAL::oriented_bounding_box(sm, obb_points,
        CGAL::parameters::use_convex_hull(true));
 
    // 从8个顶点构造网格
    Surface_mesh obb_sm;
    CGAL::make_hexahedron(
        obb_points[0], obb_points[1], obb_points[2], obb_points[3],
        obb_points[4], obb_points[5], obb_points[6], obb_points[7], 
        obb_sm);
 
    // 计算体积
    PMP::triangulate_faces(obb_sm);
    std::cout << "Volume: " << PMP::volume(obb_sm) << std::endl;
 
    return EXIT_SUCCESS;
}

获取变换矩阵

// 获取旋转矩阵而非顶点
CGAL::Aff_transformation_3<K> transformation;
CGAL::oriented_bounding_box(sm, transformation);
 
// 应用变换将物体对齐到坐标轴
CGAL::Aff_transformation_3<K> inverse = transformation.inverse();
for(auto& p : sm.points()) {
    p = inverse.transform(p);
}

点云输入

std::vector<Point> points;
// ... 填充点云
 
std::array<Point, 8> obb_points;
CGAL::oriented_bounding_box(points, obb_points);

17.1.5 复杂度分析

时间复杂度

阶段复杂度说明
凸包计算使用CGAL的convex_hull_3
遗传算法G=代数,P=种群大小,h=凸包点数
2D精确优化每代执行一次
总体主导项

其中:

  • :输入点数
  • :凸包点数(通常
  • :最大代数
  • :种群大小
  • :Nelder-Mead迭代次数

空间复杂度

  • 点存储:
  • 凸包:
  • 种群:(固定大小)
  • 总体

数值稳定性

  1. 正交矩阵保持:通过QR分解确保所有旋转矩阵保持正交性
  2. 体积计算:使用双精度浮点数,避免溢出
  3. 早期退出:在适应度计算中,当部分体积已超过当前最优值时提前终止

精度与性能权衡

  • use_convex_hull(true):推荐,显著加速但结果相同
  • 随机种子:可通过random_seed参数设置以获得可重复结果
  • 迭代次数:可通过模板参数调整,默认设置平衡了精度和速度

17.1.6 实用指导

调试技巧

技巧1:可视化包围盒计算过程

// 记录每代最佳体积,绘制收敛曲线
class EvolutionTracker {
    std::vector<double> best_volumes;
    std::vector<double> avg_volumes;
    
public:
    void record_generation(const auto& population) {
        double best = std::numeric_limits<double>::max();
        double sum = 0;
        
        for(const auto& individual : population) {
            double vol = individual.fitness();
            best = std::min(best, vol);
            sum += vol;
        }
        
        best_volumes.push_back(best);
        avg_volumes.push_back(sum / population.size());
    }
    
    void plot_convergence() const {
        std::cout << "Generation\\tBest\\tAverage" << std::endl;
        for(size_t i = 0; i < best_volumes.size(); ++i) {
            std::cout << i << "\\t"
                      << best_volumes[i] << "\\t"
                      << avg_volumes[i] << std::endl;
        }
    }
};

技巧2:验证结果正确性

// 验证包围盒确实包含所有点
bool validate_obb(const std::array<Point, 8>& obb, 
                  const std::vector<Point>& points)
{
    // 计算OBB的8个顶点的凸包
    // 检查每个点是否在凸包内
    
    for(const auto& p : points) {
        bool inside = CGAL::bounded_side_3(
            obb.begin(), obb.end(), p, K())
            \!= CGAL::ON_UNBOUNDED_SIDE;
        
        if(\!inside) {
            std::cerr << "Point outside OBB: " << p << std::endl;
            return false;
        }
    }
    return true;
}

技巧3:比较不同方法

// 比较AABB、PCA-OBB和最优OBB
void compare_bounding_boxes(const Surface_mesh& mesh) {
    // AABB
    auto aabb = CGAL::bounding_box(mesh.points().begin(), 
                                   mesh.points().end());
    double aabb_vol = (aabb.xmax() - aabb.xmin()) *
                      (aabb.ymax() - aabb.ymin()) *
                      (aabb.zmax() - aabb.zmin());
    
    // PCA-based OBB(简化实现)
    // ...
    
    // 最优OBB
    std::array<Point, 8> obb;
    CGAL::oriented_bounding_box(mesh, obb);
    // 计算体积...
    
    std::cout << "AABB volume: " << aabb_vol << std::endl;
    std::cout << "PCA-OBB volume: " << pca_vol << std::endl;
    std::cout << "Optimal OBB volume: " << optimal_vol << std::endl;
    std::cout << "Improvement: " << (1 - optimal_vol/aabb_vol) * 100 << "%" << std::endl;
}

常见错误FAQ

Q1: 计算结果不稳定,每次运行结果不同?

A: 遗传算法使用随机初始化。解决方案:

// 设置随机种子以获得可重复结果
CGAL::Random random(12345);  // 固定种子
CGAL::oriented_bounding_box(mesh, obb_points,
    CGAL::parameters::random_seed(12345));

Q2: 对于简单形状(如立方体),计算很慢?

A: 简单形状不需要复杂优化:

// 先检查是否已经是紧密包围盒
auto aabb = CGAL::bounding_box(points.begin(), points.end());
double aabb_vol = ...;
 
// 如果是立方体或球体,AABB已经是最优的
if(is_cubic_or_spherical(points)) {
    return aabb;  // 跳过OBB计算
}

Q3: 如何处理大规模数据(>100万点)?

A: 使用采样策略:

// 对大规模数据进行采样
std::vector<Point> sampled;
sampled.reserve(10000);
 
// 均匀采样
for(size_t i = 0; i < points.size(); i += points.size() / 10000) {
    sampled.push_back(points[i]);
}
 
// 在采样上计算OBB
CGAL::oriented_bounding_box(sampled, obb_points);

Q4: 如何保存和加载计算的OBB?

A: 保存变换矩阵:

// 保存
CGAL::Aff_transformation_3<K> transform;
CGAL::oriented_bounding_box(mesh, transform);
 
std::ofstream file("obb_transform.txt");
for(int i = 0; i < 3; ++i) {
    for(int j = 0; j < 4; ++j) {
        file << transform.m(i,j) << " ";
    }
    file << std::endl;
}
 
// 加载
// ... 读取矩阵并重建Aff_transformation_3

性能优化实战

场景:批量处理多个物体的OBB

// 并行计算多个物体的OBB
#include <CGAL/Parallel_tag.h>
#include <thread>
 
void compute_obbs_parallel(const std::vector<Surface_mesh>& meshes)
{
    std::vector<std::array<Point, 8>> obbs(meshes.size());
    
    // 使用OpenMP并行
    #pragma omp parallel for
    for(size_t i = 0; i < meshes.size(); ++i) {
        CGAL::oriented_bounding_box(meshes[i], obbs[i],
            CGAL::parameters::use_convex_hull(true));
    }
}

17.1.7 与其他包围盒的比较

类型计算复杂度紧致度适用场景
AABB快速碰撞检测
OBB (PCA)方向性强的物体
OBB (CGAL)需要最优包裹
包围球球形物体
k-DOP中-高固定方向集

CGAL的OBB实现通过混合优化策略,在可接受的时间内获得接近最优的包围盒,特别适用于:

  • 碰撞检测的预处理
  • 空间分割结构构建
  • 物体方向标准化
  • 体积计算和物理模拟