如何运用快速凸包算法高效处理散点云数据?
摘要: 凸包问题是计算几何中的一个经典问题,目标是找到包含给定点集的最小凸多边形(二维情形)或凸多面体(三维情形)。它解决的问题在于找到一个凸多边形或凸多边形,这个多边形包含了给定的 n 个点,使得这个多边形是包含这些点的最小凸集合。本笔记首先
散点云处理笔记(三):快速凸包算法(Quick Hull Algorithm)
凸包问题是计算几何中的一个经典问题,目标是找到包含给定点集的最小凸多边形(二维情形)或凸多面体(三维情形)。它解决的问题在于找到一个凸多边形或凸多边形,这个多边形包含了给定的 n 个点,使得这个多边形是包含这些点的最小凸集合。本笔记首先给出三维散点凸包算法的数学推导,可以从“凸包”这个基本概念出发,一步步深入到算法背后的数学原理,最后给出快速凸包算法的。
1. 最小凸包的数学定义
最小凸包问题(Convex Hull Problem)的数学表达可以从集合论、凸分析和计算几何三个角度进行形式化定义。核心思想是:在包含给定点集的所有凸集中,寻找体积(或表面积、维度)最小的那个。下面从凸组合、交集、对偶优化表达的几个角度给出最小凸包问题的定义。
Definition 1.1 基于凸组合的定义(构造性表达)
设点集\(\mathbf{P}=\{\mathbf{p}_1,\mathbf{p}_2,\cdots,\mathbf{p}_n\}\subset\mathbb{R}^d\),其中凸包\(\mathbf{conv}(\mathbf{P})\) 是\(\mathbf{P}\) 中所有点的凸组合构成的集合:
\[\mathbf{conv}(\mathbf{P})=\left\{\sum_{i=1}^{n}\lambda_i\mathbf{p}_i\bigg|\lambda_i\geq0,\sum_{i=1}^n\lambda_i=1\right\} \tag{1}
\]
这个定义直接给出了凸包作为点集的构造方式,但不易直接用于算法设计。
Definition 1.2 基于交集的定义(几何表达)
凸包也可以表示为所有包含 \(\mathbf{P}\)的凸集的交集:
\[\mathbf{conv}(\mathbf{P})=\bigcap\{\mathbf{C}\subseteq\mathbb{R}^d\big|\mathbf{C}\text{是凸包且}\mathbf{P}\subseteq{\mathbf{C}}\} \tag{2}
\]
这是“最小凸集”的精确数学刻画:它唯一且必然存在。
Definition 1.3 基于支撑函数的对偶表达(优化视角)
在凸分析中,凸集可以用其支撑函数 \(h_c(\mathbf{u})=\sup_{\mathbf{x}\in{\mathbf{c}}}\mathbf{u}^{T}\mathbf{x}\) 描述。凸包问题等于求一个凸包多面体\(H\),使得其支撑函数等于点集支撑函数的上确界:
\[h_{H}(\mathbf{u})=\max_{\mathbf{p}\in{P}}\mathbf{u}^T\mathbf{p},\forall{\mathbf{u}\in{\mathbb{R}^d}} \tag{3}
\]
此时\(H=\mathbf{conv}(\mathbf{P})\)。这个表达与版空间表示紧密相关: 凸包是所有满足\(\mathbf{u}^T\mathbf{x}\leq{\max_{\mathbf{p}\in{P}}\mathbf{u}^T\mathbf{p}}\)的半空间的交集。
Definition 1.4 凸包问题离散形式(计算几何常用)
在实际算法中,我们通常要求输出凸包的顶点集(极点)及其面/边的邻接关系。因此,凸包问题可表述为:
给定有限点集\(P\subset\mathbb{\mathbb{R}^d}\),找出最小的子集\(V\subseteq{P}\),使得\(\mathbf{conv}(V)=\mathbf{conv}(P)\),并给出\(\mathbf{conv}(V)\)的\(\mathbf{facet}(d-1\text{维度})\) 的几何与拓朴信息。
其中,\(\mathbf{p}\in{P}\)是极点(凸包顶点)当且仅当它不能表示为\(P\{p\}\)中点的凸组合。
Definition 1.5 优化形式的等价表述(用于数学规划)
凸包问题还可以写成线性规划的对偶形式:寻找一个凸多面体 \(Q\),使得 \(P\subseteq{Q}\)且\(Q\)的某种度量(如体积)最小。但在离散情况下,这通常转化为枚举所有可能的外包多面体并比较。
最小凸包问题的核心数学表达是 “包含给定点集的最小凸集”,它可以用凸组合、凸集交、支撑函数等方式等价定义。在计算几何中,我们通常寻求输出该凸集的顶点和面结构。
2.有符号体积 (Signed Volume),有符号距离(Signed Distance)与几何判定
有符号体积(Signed Volume)是三维凸包算法中最核心的几何判定工具。它本质上是一个行列式,能够告诉我们四个点构成的四面体是“正向”还是“反向”,从而判断点与平面的相对位置、面的可见性以及三角形的定向。
Definition 2.1 有符号体积的定义
给定三维空间中的四个点\(A,B,C,D\),它们构成一个四面体。有效符号体积由以下行列式给出:
\[Volume(A,B,C,D)=\frac{1}{6}\left|\begin{matrix}A_x,A_y,A_z, 1\\
B_x,B_y,B_z,1\\
C_x,C_y,C_z,1\\
D_x,D_y,D_z,1
\end{matrix}\right| \tag{4}
\]
这个行列式的几何意义:从点\(\mathbf{A}\)到\(\mathbf{B},\mathbf{C},\mathbf{D}\) 的三个向量的混合积(Scalar Triple Product) 的\(\frac{1}{6}\) 倍:
\[Volume(A,B,C,D)=\frac{1}{6}\cdot{\left((B-A)\times(C-A)\right)}\cdot(D-A) \tag{5}
\]
其中\(\times\)是向量叉乘,\(\cdot\)是点乘。
符号体积的符号含义可以揭示了四个点的空间关系:
正体积:\(D\)位于平面\(ABC\) 的正侧(即按照右侧法则,\(A\rightarrow{B}\rightarrow{C}\) 的朝向,\(D\)在法向量指向的一侧)。
负体积:D位于平面 \(ABC\)的负侧 (另一侧)。
零体积: 四点共面(退化四面体)。
注意:符号的具体正负依赖于顶点\(A,B,C\)的排列顺序。通常约定 \(A,B,C\) 按逆时针方向(从外部看)排列时,外法向指向外,此时正体积表示点在外部。其在凸包构建的作用如下:
2.1 点与平面的定向测试(Orientation Test)
在构建凸包时,经常需要判断一个点 \(P\) 是在某个平面 \(F\)的内侧还是外侧。
取平面上的三个非共线点(例如一个三角形的三个顶点 A,B,C),计算有符号体积 Volume(A,B,C,P):
如果 \(volume>0\):点 PP 在平面的正侧(通常定义为外部)。
如果 \(volume<0\):点 PP 在平面的负侧(内部)。
如果 \(volume=0\):点 PP 在平面上(共面)。
这个测试是增量法和 Quick hull 算法中剔除内部点和判断可见性的基础。
2.2 面的可见性测试(Visibility Test)
当向现有凸包添加一个新点 \(P\)时,必须找出凸包上哪些面“可见”——即从点 \(P\)看过去,这些面朝外的一面能被看到。
对于凸包上的每个面 \(F\),我们已知它的外法向(由顶点顺序约定)。计算有符号体积\(Volume(A,B,C,P)\),其中 \(A,B,C\)是 \(F\)的顶点且顺序与法向一致:
如果$ Volume>0$,表示 P位于面的外侧(可见)。
如果 \(Volume<0\),表示 P 位于面的内侧(不可见,被遮挡)。
所有可见面的边界构成一个闭合的“地平线”(horizon),然后需要删除这些可见面,并将 \(P\)与地平线上的每条边连接,形成新面。
2.3 三角形朝向一致性(Orientation Consistency)
在输出凸包或进行三角剖分时,需要确保所有三角形的顶点排列顺序一致(例如全部为逆时针,从外部看)。通过计算有符号体积可以检查和修正朝向:
计算三角形 \(ABC\)与一个已知在凸包外部的点 \(P\)的体积,若为负则说明三角形朝向相反,需要翻转顶点顺序。
有符号体积是连接代数(行列式)与几何(定向、可见性)的桥梁。在三维凸包算法中,它几乎出现在每一个关键决策点:判断点是否在凸包内部、确定新点能“看到”哪些面、维持凸包所有面法向一致。理解并正确使用有符号体积,就等于掌握了实现三维凸包算法的数学钥匙。
Definition 2.2 有符号距离的定义
对于由法向量 n(单位向量)和一点 \(\mathbf{p}_0\)确定的平面,点 x 到该平面的有符号距离为:
\[d=\mathbf{n}\cdot(\mathbf{x}-\mathbf{p}_0) \tag{6}
\]
其中:
\(d>0\):点位于平面正侧(法向量指向的一侧)。
\(d<0\):点位于平面正侧(法向量指向的一侧)。
\(d=0\):点在平面上。
3.最小凸包算法的算法总结
基于上述数学定义,主要的凸包算法遵循不同的构造思路。
增量法 (Incremental Algorithm):归纳法的算法体现
它本质上是数学归纳法在算法中的直接体现。其核心思想是:
归纳基础:从一个小的初始凸包(如一个四面体)开始。
归纳步骤:假设已经为前 k 个点构造好了凸包 CH(P_k),现在加入一个新的点 p。
更新凸包:如果 p 在当前凸包内部,则凸包不变;如果 p 在外部,则需要删除所有能被 p 看到的“可见面”,这些面的边界会形成一个闭合的“地平线”。最后,将 p 与这条地平线上的每条边相连,形成新的三角面,从而更新凸包。
分治法 (Divide and Conquer):归并思想的挑战
该算法的思路是将点集递归地分成两半,分别计算左右两部分的凸包,最后将两个凸包“缝合”成一个完整的凸包。
难点:缝合过程远比二维复杂,需要计算两个分离凸多面体的“外壳”(即同时与两个凸包相切的支撑面),这在三维空间中的计算相当复杂。
地位:尽管分治法在理论上可以达到最优的 O(n log n) 时间复杂度,但由于实现难度高,在实际应用中不如随机化的增量算法或Quick hull普及。
Quick hull算法:分治思想与几何选择
正如其名,它借鉴了快速排序 (Quick Sort) 的“分治”哲学。与增量法每次处理一个随机点不同,Quick hull每次都选择距离当前面最远的点作为新的顶点。这个策略能让算法快速地逼近凸包形状,从而在平均情况下达到很高的效率。
算法
平均/期望复杂度
最坏复杂度
主要特点
增量法 (随机化)
O(n log n)
O(n²)
实现相对简单,思想直观,通过随机化保证平均性能。
Quick hull
O(n log n)
O(n²)
效率高,是Qhull等知名库的实现基础。
分治法
O(n log n)
O(n log n)
理论上最优,但实现复杂,常数较大。
4. Quick Hull 算法原理
快速凸包算法也可以看成是增量算法的一种变种,与随机增量算法相比,它们的不同就在于每次迭代从面的外部点集中选择的点不同。随机增量算法从外部点集中随机的选择一个点,但是快速凸包算法是选择距离最远的一个点,著名的开源代码Qhull、 CGAL都有快速凸包算法的C++实现。本篇文章介绍三维的快速凸包算法的原理和实现。其基本思想:
在介绍快速三维凸包算法前,先介绍算法中会频繁使用到的两个基本的几何操作:
选取散点集上的3个散点\(\{v_1,v_2,v_3\}\),确定一个平面。
平面可以用方程\(\mathbf{n}\cdot\mathbf{P}+d=0\)表示,其平面的法向量\(\mathbf{n}=(v_2-v_1)\times(v_3-v_1),d=-\mathbf{n}\cdot{v_1}\),叉积的顺序影响法向量的方向,若\(\mathbf{n}\) 是零向量,说明\(v_1,v_2,v_3\)三点共线。
计算三维空间上点到平面的有符号距离。
设三维点为\(P\),平面为\(\Gamma:\mathbf{n}\cdot{P}+d=0\),那么点\(P\)到平面\(\Gamma\)的符号距离表示围为\(dist(P)=(\mathbf{n}\cdot{P}+d)/||\mathbf{n}||\)。:
若\(dist(P)>0\),则称点P在平面\(\Gamma\) 的上方(Above the Plane).
若\(dist(P)<0\),则称点P在平面\(\Gamma\)的下方(Below the Plane).
若\(dist(P)=0\),则称点P在平面\(\Gamma\) 上(On the Plane)。
快速凸包算法是基于Beneath Beyond理论,增量算法也同样基于该理论,该理论如下所示,这里只考虑三维空间下的凸包:
设\(H\)是一个\(R^3\) 空间上的凸包,\(p\) 是\(H\)上的一个点。\(F\)是凸包\(conv(p\bigcup{H})\) 上的面,当且仅当:
\(F\)是凸包\(H\) 的一个面且点\(p\)在面\(F\)的下方;
\(F\) 不是凸包\(H\)内的一个面,\(F\)是由凸包\(H\)的边和点\(p\)构成,点\(p\)在该边相邻的一个面的上方,在该边相邻的另一个面的下方。
若点\(p\)在\(H\)内,则\(conv(p\bigcup{H})\)与\(H\) 重合,显然,结论成立。若点\(p\)在\(H\)外,分为两种情况,\(H\)是由极点\(\{p_0,p_1,p_3,p_3,p_4,p_5\}\) 构成的凸包,\(conv(p\bigcup{H})\) 是由极点\(\{p,p_0,p_1,p_2,p_3,p_4,p_5\}\) 构成的凸包。对于凸包\(H\)来说,点\(p\)在三角形面片\(\triangle{p_0p_3p_4}\) ,\(\triangle{p_0 p_5 p_1}\) ,\(\triangle{p_2 p_3 p_4}\) ,\(\triangle{p_2 p_5 p_3}\) 的上方,点\(p\)在三角形面片\(\triangle{p_0p_1p_4}\),\(\triangle{p_0p_5p_1}\),\(\triangle{p_2p_4p_1}\),\(\triangle{p_2p_1p_5}\) 的下方,因此在边\(\overline{p_2 p_4}\), \(\overline{p_4p_0}\),\(\overline{p_0p_5}\),\(\overline{p_5p_2}\) 一侧的面片是在点\(p\) 的上方,另一侧的面片在点\(p\)的下方,这样的边为临界边。当一个面\(F\)在\(conv(p\bigcup{H})\) 上时,若点p在面\(F\)的下方,则\(F\)是\(H\)的一个面,否则,它是由点p与临界边构成的面片。
5. Quick Hull算法步骤
输入: 散点集\(\mathbf{P}=\{\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3,\cdots,\mathbf{p}_n\}\subset \mathbb{R}^3\)
步骤1:构建初始凸包(四面体)
找到沿 x,y,z 方向的极值点(最大 / 最小坐标)。
从中选取四个不共面点\(v_0,v_1,v_2,v_3\),构建初始四面体。
对 4 个三角面分别计算平面方程\(\Gamma_i\)与外法向量\(\mathbf{n}_i\)。
利用符号体积进行四点不共面判断:
\[V=\frac{1}{6}|(\mathbf{v}_1-\mathbf{v}_0)\cdot[(\mathbf{v}_2-\mathbf{v}_0)\times(\mathbf{v}_3-\mathbf{v}_0)]|\neq0
\]
步骤2:为每个面建立外侧点集
对每个面\(\Gamma_i\),将所有满足
\[dist(\Gamma_i,\mathbf{p})>0
\]
的点归入该面的外侧点集\(\mathbf{S}_{\Gamma_i}\) 。
步骤3: 递归扩展凸包(主循环)
循环直到所有面的外侧点集为空:
(1)选择全局最远外侧点:遍历所有非空\(S_{\Gamma_i}\)找到最远点\(\mathbf{p}^*\)
\[\mathbf{p}^*=argmax_{\mathbf{p}\in{S_{\Gamma_i}}}|dist(\mathbf{p},\Gamma_i)|
\]
\(\mathbf{p}^*\) 点必为新凸包顶点。
(2)标记所有可见面,一个面\(\Gamma_{i}\) 对\(\mathbf{p}_*\)可见,当且仅当:
\[dist(\mathbf{p}^*,\Gamma_i)>\epsilon
\]
所有可见面将被删除。
(3) 提取地平线(Horizon):地平线是满足以下条件的边:边属于一个可见面、一个不可见面这些边构成闭合多边形环。
(4) 对地平线每条边 (a,b),构造新三角面:\(\Gamma_{new}=(\mathbf{p}^*,\mathbf{a},\mathbf{b})\) 计算其外法向量与平面方程,保证法向量向外,保证凸性。
(5)外侧点重分配数学依据,原属于\(F_{vis}\)的外侧点,必须重新测试到新面\(\Gamma_{new}\) 的有向距离:
\[dist(\Gamma_new,\mathbf{p})>0
\]
满足则归入新面外侧集,否则视为内部点。
步骤4:清理与合并共面点
删除重复面、退化面;将落在凸包面上的点归入对应面,不影响凸包结构。
6. C++其代码实现
基础库源码链接,参见这里,下面是快速凸包算法的源码实现。
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h> // C/C++标准库
#include <crtdbg.h> // CRT调试库,用于内存泄漏检测
#include <time.h> // 用于计时和随机数种子
#include "./include/SrGeometricTools.h" // 自定义几何工具头文件:提供点/向量的基本运算(加减乘除、点乘、叉乘等)
#include "./include/SrDataType.h" // 自定义数据类型头文件:定义SrPoint3D/SrVector3等基础几何类型
#include <list> //双向链表,存储顶点、面片集合
#include <assert.h> //断言,用于调试时检查程序逻辑合法性
#include <map> //映射表,用于边界边管理、顶点/面片索引映射
//-----------------------------宏定义----------------------------------
//面片标记:无状态
#define FACET_NULL 0x00
//面片标记:已访问(用于搜索可见面片)
#define FACET_VISITED 0x01
//面片标记:边界面片(用于构建新凸包面)
#define FACET_BORDER 0x02
//----------------------------类型重定义-------------------------------
//三维点类型
typedef SrPoint3D cPoint;
//三维向量类型
typedef SrVector3 cVector;
//---------------------------类前置声明---------------------------------
class cFacet;
class cEdge;
class cOutsideSet;
class cVertex;
//顶点列表 & 迭代器
typedef std::list<cVertex*> VertexList;
typedef VertexList::iterator VertexIterator;
//面片列表 & 迭代器
typedef std::list<cFacet*> FacetList;
typedef FacetList::iterator FacetIterator;
// 边界边映射:<顶点, 对应的边界边>
typedef std::map<cVertex*, cEdge*> BoundaryEdgeMap;
typedef BoundaryEdgeMap::iterator BoundaryEdgeIterator;
//-----------------------------顶点类-------------------------------------
class cVertex
{
public:
cVertex()
{
mOnHull = false; // 默认不是凸包顶点
mPoint.x = mPoint.y = mPoint.z = 0.0f; // 坐标初始化为0
}
public:
bool mOnHull; //Indicate whether or not the point is the extreme point of the convex polyhedron.
cPoint mPoint; //The location information of this vertex.
};
//---------------------------外部点集合类-------------------------
//存储某个面片“外侧”的所有点(这些点会用于扩展凸包)
class cOutsideSet
{
public:
cOutsideSet() {}
~cOutsideSet()
{
mVertexList.clear(); // 清空点集,释放链表资源
}
public:
VertexList mVertexList; //Container of outside point set.
};
//---------------------------平面类:三维空间中的平面----------------
class cPlane
{
public:
cPlane() {}
~cPlane() {}
// 通过三个不共线的点初始化一个平面
bool initPlane(const cPoint& p0, const cPoint& p1, const cPoint& p2)
{
mNormal = (p1 - p0).cross(p2 - p0);
if (mNormal.isZero())
return false;
//It's not necessary to normalize the vector here.
//normal.normalize();
mD = -mNormal.dot(p0);
return true;
}
// 判断平面是否有效(法向量非零)
bool isValid() const
{
return !mNormal.isZero();
}
// 计算点到平面的有向距离
SrReal distance(const cPoint& point) const
{
return mNormal.dot(point) + mD;
}
// 判断点是否在平面的正方向一侧(凸包可见性判断核心)
bool isOnPositiveSide(const cPoint& p) const
{
return GREATER(distance(p), 0);
}
public:
cVector mNormal; // 平面法向量
SrReal mD; // 平面方程参数
};
// --------------------------- 面片类:凸包的三角面片 ---------------------------
class cFacet
{
public:
//构造函数
cFacet()
{
// 邻面初始化为空
mNeighbor[0] = NULL;
mNeighbor[1] = NULL;
mNeighbor[2] = NULL;
// 顶点初始化为空
mVertex[0] = NULL;
mVertex[1] = NULL;
mVertex[2] = NULL;
mOutsideSet = NULL; //外部点集为空
mVisitFlag = FACET_NULL; //访问标记初始化
mIterator = NULL; //迭代器指针为空
}
// 清空面片数据
void clear()
{
mNeighbor[0] = NULL;
mNeighbor[1] = NULL;
mNeighbor[2] = NULL;
mVertex[0] = NULL;
mVertex[1] = NULL;
mVertex[2] = NULL;
mOutsideSet = NULL;
mVisitFlag = FACET_NULL;
}
//析构函数
~cFacet()
{
// 释放迭代器内存
if (mIterator)
{
delete mIterator;
mIterator = NULL;
}
// 释放外部点集
if (mOutsideSet)
{
delete mOutsideSet;
mOutsideSet = NULL;
}
}
// 用三个顶点初始化三角面片
void initFace(cVertex* p0, cVertex* p1, cVertex* p2)
{
mVertex[0] = p0;
mVertex[1] = p1;
mVertex[2] = p2;
}
// 设置面片的三个相邻面片
void setNeighbors(cFacet* f0, cFacet* f1, cFacet* f2)
{
mNeighbor[0] = f0;
mNeighbor[1] = f1;
mNeighbor[2] = f2;
}
//找到此外部点集中距离当前面片最远的顶点
const VertexIterator furthestVertex()const
{
ASSERT(mOutsideSet != NULL);
cPlane plane;
ASSERT(plane.initPlane(mVertex[0]->mPoint, mVertex[1]->mPoint, mVertex[2]->mPoint));
VertexIterator iter = mOutsideSet->mVertexList.begin();
VertexIterator iterVertex;
SrReal maxDist = 0, dist;
// 遍历所有外部点,找距离最大的点
for (; iter != mOutsideSet->mVertexList.end(); iter++)
{
dist = plane.mNormal.dot((*iter)->mPoint) + plane.mD;
if (LESS(maxDist, dist))
{
maxDist = dist;
iterVertex = iter;
}
}
return iterVertex;
}
public:
cOutsideSet* mOutsideSet; // 该面片外侧的点集:The outside point set.
cVertex* mVertex[3]; // 面片的三个顶点:The point information of this facet.
cFacet* mNeighbor[3]; // 面片的三个相邻面片:Indicate the neighbor facets of this one.
unsigned char mVisitFlag; // 访问标记(搜索可见面时使用):Indicate the flag in the process of determining the visible face set.
FacetIterator* mIterator; // 指向面片在列表中的迭代器(用于快速删除):Indicate the location in the pending face list.
};
//----------------------边缘类-----------------------------------------
//用于构建凸包扩展时的边界环
class cEdge
{
public:
cEdge()
{
mPoint[0] = NULL;
mPoint[1] = NULL;
mNeighbor[0] = NULL;
mNeighbor[1] = NULL;
}
//设置边的两个端点
void setEndpoint(cVertex* p0, cVertex* p1)
{
mPoint[0] = p0;
mPoint[1] = p1;
}
//设置边相邻的两个面片
void setNeighbor(cFacet* f0, cFacet* f1)
{
mNeighbor[0] = f0;
mNeighbor[1] = f1;
}
public:
cVertex* mPoint[2]; // 边的两个顶点: The point information of this edge.
cFacet* mNeighbor[2]; // 边相邻的两个面片: The neighbor facets of this edge.
};
//-----------------------凸包输出结构----------------------------------------------
//面片索引结构
typedef struct _tFacet
{
int mFInx[3]; // 相邻面片索引
int mVInx[3]; // 顶点索引
}tFacet;
//最终凸包数据结构(供外部使用)
typedef struct
{
SrPoint3D* mVertes; // 凸包顶点数组
tFacet* mFacet; // 凸包面片数组
int mNumVertes; // 顶点数量
int mNumFacet; // 面片数量
}tHull;
// 导出凸包到 TXT 文件,给 MATLAB 可视化
void exportHullToFile(tHull* hull, const char* filename)
{
FILE* f = fopen(filename, "w");
if (!f) return;
// 第一行:顶点数 面片数
fprintf(f, "%d %d\n", hull->mNumVertes, hull->mNumFacet);
// 输出所有顶点 x y z
for (int i = 0; i < hull->mNumVertes; i++)
{
fprintf(f, "%.6f %.6f %.6f\n",
hull->mVertes[i].x,
hull->mVertes[i].y,
hull->mVertes[i].z);
}
// 输出所有面片:三个顶点索引(MATLAB从1开始,所以+1)
for (int i = 0; i < hull->mNumFacet; i++)
{
fprintf(f, "%d %d %d\n",
hull->mFacet[i].mVInx[0] + 1,
hull->mFacet[i].mVInx[1] + 1,
hull->mFacet[i].mVInx[2] + 1);
}
fclose(f);
printf("凸包数据已导出到: %s\n", filename);
}
//导出散点序列
bool exportPoints3dset(SrPoint3D *point3dset,int nPooints,const char* filename_output){
FILE* f = fopen(filename_output, "w");
if(f==nullptr){
return false;
}
for(int ipoint = 0; ipoint < nPooints; ipoint++){
fprintf(f,"%.6f %.6f %.6f\n",point3dset[ipoint].x,point3dset[ipoint].y,point3dset[ipoint].z);
}
fclose(f);
return true;
}
//-----------------------------------------------------
class QuickHull
{
public:
// 对外接口:输入点集,输出凸包
bool quickHull(SrPoint3D* points, int numPoint, tHull* resultHull);
private:
// 判断三点是否共线
bool collinear(const cPoint& p0, const cPoint& p1, const cPoint& p2);
// 判断四点是否共面
bool coplanar(const cPoint& p0, const cPoint& p1, const cPoint& p2, const cPoint& p3);
// 释放面片列表内存
void deallocate(FacetList& ftList);
//从指定点出发,找到所有“可见”的面片(会被删除的面)
void findVisibleFacet(cVertex* furPoint, cFacet* f, FacetList& visibleSet, BoundaryEdgeMap& boudaryMap);
// 用边界环和新顶点构建新面片
void constructNewFacets(cVertex* point, FacetList& newFacetList, BoundaryEdgeMap& boundary);
// 为每个面片分配外部点
void determineOutsideSet(FacetList& facetList, VertexList& allVertex);
// 更新待处理面片列表
void updateFacetPendList(FacetList& facetPendList, FacetList& newFacetList, cFacet*& head);
// 划分点到对应面片的外部集合
void partitionOutsideSet(FacetList& facetPendList, FacetList& newFacetList, VertexList& allVertex, cFacet*& head);
// 收集所有可见面的外部点(合并后重新分配)
void gatherOutsideSet(FacetList& facetPendList, FacetList& visFacetList, VertexList& visOutsideSet);
// QuickHull 核心递归迭代过程
void quickHullScan(FacetList& facetPendList, cFacet*& head);
// 初始化初始四面体(凸包初始种子)
bool initTetrahedron(VertexList& vertexes, FacetList& tetrahedron);
// 递归遍历凸包,导出顶点和面片索引
void recursiveExport(cFacet*, std::map<cVertex*, int>&, std::map<cFacet*, int>&, int&, int&, std::list<cFacet*>&);
// 将凸包整理成输出结构
void exportHull(cFacet* head, tHull* hull);
};
bool QuickHull::quickHull(SrPoint3D* points, int numPoint, tHull* resultHull)
{
// If the first and last point are equal the collinearity test some lines below will always be true.
int i, sizePoint;
for (i = numPoint - 1; i > 0; i--)
if (points[i].x != points[0].x ||
points[i].y != points[0].y ||
points[i].z != points[0].z)
break;
sizePoint = i + 1;
if (sizePoint <= 3)
return false;
//Copy all the vertexes into the vertex list.
VertexList vertexList;
cVertex* newVertex;
VertexIterator vertexIter;
cVertex** buffer = new cVertex * [sizePoint];
for (i = 0; i < sizePoint; i++)
{
newVertex = new cVertex;
newVertex->mPoint.x = points[i].x;
newVertex->mPoint.y = points[i].y;
newVertex->mPoint.z = points[i].z;
vertexList.push_back(newVertex);
buffer[i] = newVertex;
}
cFacet* head;
FacetList facetPendList, newFacetList;
//Initialize the first tetrahedron.
if (!initTetrahedron(vertexList, newFacetList))
{//If it fails, free the storage allocated to the vertex structure.
for (i = 0; i < sizePoint; i++)
delete buffer[i];
delete[]buffer;
return false;
}
partitionOutsideSet(facetPendList, newFacetList, vertexList, head);
// If there exist no vertexes outside the hull, the tetrahedron is the hull.Or else, go into the if-exp.
if (!facetPendList.empty())
{
quickHullScan(facetPendList, head);
}
//Export the facets and vertexes to the tHull structure.
//Free the storage of the facets.
exportHull(head, resultHull);
//Free the storage of the vertexes.
for (i = 0; i < sizePoint; i++)
delete buffer[i];
delete[]buffer;
return true;
}
bool QuickHull::collinear(const cPoint& p0, const cPoint& p1, const cPoint& p2)
{
cVector normal = (p1 - p0).cross(p2 - p0);
if (EQUAL(normal.magnitudeSquared(), 0))
return true;
return false;
}
bool QuickHull::coplanar(const cPoint& p0, const cPoint& p1, const cPoint& p2, const cPoint& p3)
{
SrVector3 normal = (p1 - p0).cross(p2 - p0);
if (EQUAL(normal.dot(p3 - p0), 0))
return true;
return false;
}
void QuickHull::deallocate(FacetList& ftList)
{
FacetIterator fcIter;
for (fcIter = ftList.begin(); fcIter != ftList.end(); fcIter++)
{
cFacet* vlue = *fcIter;
delete vlue;
}
ftList.clear();
}
void QuickHull::findVisibleFacet(cVertex* furPoint, cFacet* f, FacetList& visibleSet, BoundaryEdgeMap& boudaryMap)
{
f->mVisitFlag = FACET_VISITED;
visibleSet.push_back(f);
FacetIterator facetIter = visibleSet.begin();
cEdge* boundaryEdge = NULL;
int i;
cPlane plane;
for (; facetIter != visibleSet.end(); facetIter++)
{
for (i = 0; i < 3; i++)
{
cFacet* neighbor = (*facetIter)->mNeighbor[i];
if (neighbor->mVisitFlag == FACET_NULL)
{
neighbor->mVisitFlag = FACET_VISITED;
ASSERT(plane.initPlane(neighbor->mVertex[0]->mPoint, neighbor->mVertex[1]->mPoint, neighbor->mVertex[2]->mPoint));
if (plane.isOnPositiveSide(furPoint->mPoint))
{
visibleSet.push_back(neighbor);
}
else
{
neighbor->mVisitFlag = FACET_BORDER;
boundaryEdge = new cEdge;
boundaryEdge->setNeighbor(*facetIter, neighbor);
boundaryEdge->setEndpoint((*facetIter)->mVertex[i], (*facetIter)->mVertex[(i + 1) % 3]);
boudaryMap.insert(std::make_pair((*facetIter)->mVertex[i], boundaryEdge));
}
}
else if (neighbor->mVisitFlag == FACET_BORDER)
{
boundaryEdge = new cEdge();
boundaryEdge->setNeighbor(*facetIter, neighbor);
boundaryEdge->setEndpoint((*facetIter)->mVertex[i], (*facetIter)->mVertex[(i + 1) % 3]);
boudaryMap.insert(std::make_pair((*facetIter)->mVertex[i], boundaryEdge));
}
}
}
}
void QuickHull::constructNewFacets(cVertex* point, FacetList& newFacetList, BoundaryEdgeMap& boundary)
{
ASSERT(boundary.size() >= 3);
BoundaryEdgeIterator current = boundary.begin();
//The boundary edges are closed.
current = boundary.begin();
cEdge* edge = current->second;
int i;
while (!boundary.empty())
{
edge = current->second;
cFacet* facet = new cFacet();
//Clear the mVisitFlag. Because it's set FACET_BORDER in findVisibleFacet().
edge->mNeighbor[1]->mVisitFlag = FACET_NULL;
//Update neighbor facet of the invisible facet , at least one edge of which belong to the boundary.
for (i = 0; i < 3; i++)
{
if (edge->mNeighbor[1]->mNeighbor[i] == edge->mNeighbor[0])
{
ASSERT(edge->mNeighbor[1]->mNeighbor[i]->mVisitFlag == FACET_VISITED);
edge->mNeighbor[1]->mNeighbor[i] = facet;
break;
}
}
facet->mVertex[0] = point;
facet->mVertex[1] = edge->mPoint[0];
facet->mVertex[2] = edge->mPoint[1];
facet->mNeighbor[1] = edge->mNeighbor[1];
newFacetList.push_back(facet);
boundary.erase(current);
current = boundary.find(edge->mPoint[1]);
delete edge;
}
//Update the neighbor facets of new facets.
FacetIterator curFacet = newFacetList.begin();
FacetIterator lastFacet = newFacetList.end();
lastFacet--;
for (; curFacet != newFacetList.end(); lastFacet = curFacet, curFacet++)
{
(*curFacet)->mNeighbor[0] = *lastFacet;
(*lastFacet)->mNeighbor[2] = *curFacet;
}
}
void QuickHull::determineOutsideSet(FacetList& facetList, VertexList& allVertex)
{
FacetIterator facetIter;
VertexIterator vertexIter, tempVertexIter;
cPlane plane;
for (facetIter = facetList.begin(); facetIter != facetList.end(); facetIter++)
{
ASSERT(plane.initPlane((*facetIter)->mVertex[0]->mPoint, (*facetIter)->mVertex[1]->mPoint, (*facetIter)->mVertex[2]->mPoint));
for (vertexIter = allVertex.begin(); vertexIter != allVertex.end(); )
{
if (plane.isOnPositiveSide((*vertexIter)->mPoint))
{//Update the outside vertex set of every new facet.
tempVertexIter = vertexIter;
tempVertexIter++;
if (!(*facetIter)->mOutsideSet)
{
(*facetIter)->mOutsideSet = new cOutsideSet();
}
(*facetIter)->mOutsideSet->mVertexList.splice((*facetIter)->mOutsideSet->mVertexList.end(), allVertex, vertexIter);
vertexIter = tempVertexIter;
}
else
{
vertexIter++;
}
}
}
}
void QuickHull::updateFacetPendList(FacetList& facetPendList, FacetList& newFacetList, cFacet*& head)
{
//If there exist new facets with nonempty outside vertex set, push them back into the pending facet list.
FacetIterator facetIter;
FacetIterator tempFacetIter;
for (facetIter = newFacetList.begin(); facetIter != newFacetList.end(); )
{
if ((*facetIter)->mOutsideSet)
{
tempFacetIter = facetIter;
tempFacetIter++;
facetPendList.splice(facetPendList.end(), newFacetList, facetIter);
facetIter = facetPendList.end();
facetIter--;
if (!(*facetIter)->mIterator)
{
(*facetIter)->mIterator = new FacetIterator;
}
*(*facetIter)->mIterator = facetIter;
facetIter = tempFacetIter;
}
else
{
head = (*facetIter);
facetIter++;
}
}
}
void QuickHull::partitionOutsideSet(FacetList& facetPendList, FacetList& newFacetList, VertexList& allVertex, cFacet*& head)
{
// For each facet, look at each unassigned point and decide if it belongs to the outside set of this facet.
determineOutsideSet(newFacetList, allVertex);
// Add all the facets with non-empty outside sets to the set of facets for further consideration
updateFacetPendList(facetPendList, newFacetList, head);
}
void QuickHull::gatherOutsideSet(FacetList& facetPendList, FacetList& visFacetList, VertexList& visOutsideSet)
{
FacetIterator facetIter;
FacetIterator tempFacetIter;
for (facetIter = visFacetList.begin(); facetIter != visFacetList.end(); )
{
//Copy the outside set of the visible facet into the visOutsideSet.
cOutsideSet*& outsideSet = (*facetIter)->mOutsideSet;
tempFacetIter = facetIter;
tempFacetIter++;
if (outsideSet)
{
visOutsideSet.splice(visOutsideSet.end(), outsideSet->mVertexList, outsideSet->mVertexList.begin(), outsideSet->mVertexList.end());
}
//If some facets in the visible set exist in the pending list, remove them.
if ((*facetIter)->mIterator /*&& *(*facetIter)->mIterator!=facetPendList.end()*/)
{
facetPendList.erase(*(*facetIter)->mIterator);
}
//The visible triangles are unuseful.So deallocate them.
facetIter = tempFacetIter;
}
}
void QuickHull::quickHullScan(FacetList& facetPendList, cFacet*& head)
{
FacetList visFacetList;
VertexList visOutsideSet;
FacetList newFaceList;
FacetIterator facetIter;
BoundaryEdgeMap boundary;
cPlane plane;
while (!facetPendList.empty())
{
ASSERT(visOutsideSet.empty());
ASSERT(visFacetList.empty());
ASSERT(boundary.empty());
ASSERT(newFaceList.empty());
FacetIterator f = facetPendList.begin();
cFacet* facet = *f;
//There must be at least one vertex.
VertexIterator furVertexIter = facet->furthestVertex();
cVertex* furVertex = *furVertexIter;
facet->mOutsideSet->mVertexList.erase(furVertexIter);
//Find the visible facet set by the furthest vertex .
findVisibleFacet(furVertex, facet, visFacetList, boundary);
ASSERT(!visFacetList.empty());
ASSERT(!boundary.empty());
gatherOutsideSet(facetPendList, visFacetList, visOutsideSet);
constructNewFacets(furVertex, newFaceList, boundary);
ASSERT(!newFaceList.empty());
partitionOutsideSet(facetPendList, newFaceList, visOutsideSet, head);
//Free the storage of the visible facet set.
deallocate(visFacetList);
visOutsideSet.clear();
newFaceList.clear();
}
}
bool QuickHull::initTetrahedron(VertexList& vertexes, FacetList& tetrahedron)
{
//Check weather or not all the vertexes are collinear.
VertexIterator ptIndex1 = vertexes.begin();
VertexIterator ptIndex2 = ptIndex1;
VertexIterator ptIndex3 = vertexes.end();
ptIndex2++;
ptIndex3--;
while (ptIndex2 != ptIndex3 && collinear((*ptIndex1)->mPoint, (*ptIndex2)->mPoint, (*ptIndex3)->mPoint))
ptIndex2++;
//All the vertexes are collinear.
if (ptIndex2 == ptIndex3)
return false;
// Find the vertexes that have minimum value, maximum value in the x-dimension and minimum value
// in the y-dimension.
VertexIterator minx = vertexes.begin(), maxx = vertexes.begin(), miny = vertexes.begin();
VertexIterator vertexIter = vertexes.begin();
for (; vertexIter != vertexes.end(); vertexIter++)
{
if ((*vertexIter)->mPoint.x < (*minx)->mPoint.x)
minx = vertexIter;
else if ((*vertexIter)->mPoint.x > (*maxx)->mPoint.x)
maxx = vertexIter;
else if ((*vertexIter)->mPoint.y < (*miny)->mPoint.y)
miny = vertexIter;
}
//If the three maximum and maximum vertexes found above aren't collinear, initialize the first tetrahedron by using them.
if (!collinear((*minx)->mPoint, (*maxx)->mPoint, (*miny)->mPoint))
{
ptIndex1 = minx;
ptIndex2 = maxx;
ptIndex3 = miny;
}
cPlane plane;
ASSERT(plane.initPlane((*ptIndex1)->mPoint, (*ptIndex2)->mPoint, (*ptIndex3)->mPoint));
//Find the vertexes that have minimum and maximum distance from the plane.
SrReal minDist = plane.distance((*vertexes.begin())->mPoint);
SrReal maxDist = minDist;
vertexIter = vertexes.begin();
VertexIterator minPtIndex = vertexIter, maxPtIndex = vertexIter;
vertexIter++;
for (; vertexIter != vertexes.end(); vertexIter++)
{
SrReal dist = plane.distance((*vertexIter)->mPoint);
if (dist < minDist)
{
minDist = dist;
minPtIndex = vertexIter;
}
if (dist > maxDist)
{
maxDist = dist;
maxPtIndex = vertexIter;
}
}
VertexIterator extIndex = maxPtIndex;
if (coplanar((*ptIndex1)->mPoint, (*ptIndex2)->mPoint, (*ptIndex3)->mPoint, (*extIndex)->mPoint))
{
//swap initP0 and initP2. It's important for the direction of normal of the constructed plane.
cVertex tmp = *(*ptIndex1);
*(*ptIndex1) = *(*ptIndex3);
*(*ptIndex3) = tmp;
extIndex = minPtIndex;
if (coplanar((*ptIndex1)->mPoint, (*ptIndex2)->mPoint, (*ptIndex3)->mPoint, (*extIndex)->mPoint))
{// All of the vertexes are on the same plane.
return false;
}
}
//Initialize the first tetrahedron.
cFacet* f0 = new cFacet();
cFacet* f1 = new cFacet();
cFacet* f2 = new cFacet();
cFacet* f3 = new cFacet();
f0->initFace(*ptIndex1, *ptIndex3, *ptIndex2);
f1->initFace(*ptIndex1, *ptIndex2, *extIndex);
f2->initFace(*ptIndex1, *extIndex, *ptIndex3);
f3->initFace(*ptIndex2, *ptIndex3, *extIndex);
f0->setNeighbors(f2, f3, f1);
f1->setNeighbors(f0, f3, f2);
f2->setNeighbors(f1, f3, f0);
f3->setNeighbors(f0, f2, f1);
vertexes.erase(ptIndex1);
vertexes.erase(ptIndex2);
vertexes.erase(ptIndex3);
vertexes.erase(extIndex);
tetrahedron.push_back(f0);
tetrahedron.push_back(f1);
tetrahedron.push_back(f2);
tetrahedron.push_back(f3);
return true;
}
void QuickHull::recursiveExport(cFacet* head, std::map<cVertex*, int>& vMap, std::map<cFacet*, int>& fMap, int& vId, int& fId, std::list<cFacet*>& fList)
{
head->mVisitFlag = FACET_VISITED;
fList.push_back(head);
fMap.insert(std::make_pair(head, fId++));
int i;
for (i = 0; i < 3; i++)
{
if (vMap.find(head->mVertex[i]) == vMap.end())
{
vMap.insert(std::make_pair(head->mVertex[i], vId++));
}
}
for (i = 0; i < 3; i++)
{
if (head->mNeighbor[i]->mVisitFlag != FACET_VISITED)
{
recursiveExport(head->mNeighbor[i], vMap, fMap, vId, fId, fList);
}
}
}
void QuickHull::exportHull(cFacet* head, tHull* hull)
{
std::map<cVertex*, int> vMap;
std::map<cFacet*, int> fMap;
std::list<cFacet*> fList;
int vId = 0, fId = 0;
recursiveExport(head, vMap, fMap, vId, fId, fList);
hull->mNumVertes = vMap.size();
hull->mVertes = new SrPoint3D[hull->mNumVertes];
std::map<cVertex*, int>::iterator mapIter;
for (mapIter = vMap.begin(); mapIter != vMap.end(); mapIter++)
{
ASSERT(mapIter->second < hull->mNumVertes);
hull->mVertes[mapIter->second].x = mapIter->first->mPoint.x;
hull->mVertes[mapIter->second].y = mapIter->first->mPoint.y;
hull->mVertes[mapIter->second].z = mapIter->first->mPoint.z;
}
hull->mNumFacet = fList.size();
hull->mFacet = new tFacet[hull->mNumFacet];
std::list<cFacet*>::iterator fIter;
int v0, v1, v2, fIndx = 0;
cFacet* tempFacet;
for (fIter = fList.begin(); fIter != fList.end(); )
{
v0 = vMap[(*fIter)->mVertex[0]];
v1 = vMap[(*fIter)->mVertex[1]];
v2 = vMap[(*fIter)->mVertex[2]];
ASSERT(v0 < hull->mNumVertes);
ASSERT(v1 < hull->mNumVertes);
ASSERT(v2 < hull->mNumVertes);
hull->mFacet[fIndx].mVInx[0] = v0;
hull->mFacet[fIndx].mVInx[1] = v1;
hull->mFacet[fIndx].mVInx[2] = v2;
hull->mFacet[fIndx].mFInx[0] = fMap[(*fIter)->mNeighbor[0]];
hull->mFacet[fIndx].mFInx[1] = fMap[(*fIter)->mNeighbor[1]];
hull->mFacet[fIndx].mFInx[2] = fMap[(*fIter)->mNeighbor[2]];
fIndx++;
tempFacet = *fIter;
fIter++;
delete tempFacet;
}
}
bool isConvex(tHull* hull)
{
int i, j;
SrVector3D normal;
SrReal d;
for (i = 0; i < hull->mNumFacet; i++)
{
SrPoint3D v0 = hull->mVertes[hull->mFacet[i].mVInx[0]];
SrPoint3D v1 = hull->mVertes[hull->mFacet[i].mVInx[1]];
SrPoint3D v2 = hull->mVertes[hull->mFacet[i].mVInx[2]];
normal = (v1 - v0).cross(v2 - v0);
d = -normal.dot(v0);
for (j = 0; j < hull->mNumVertes; j++)
{
SrReal result = normal.dot(hull->mVertes[0]) + d;
if (GREATER(result, 0))
return false;
}
}
return true;
}
void testQuickHull3D()
{
int numPoint = 1000, i;
SrPoint3D* point = new SrPoint3D[numPoint];
for (i = 0; i < numPoint; i++)
{
point[i].x = (float)rand();
point[i].y = (float)rand();
point[i].z = (float)rand();
}
tHull hull;
QuickHull hull3d;
double timeCount = clock();
exportPoints3dset(point,numPoint,"point3d.txt");
if (hull3d.quickHull(point, numPoint, &hull))
{
timeCount = (clock() - timeCount) / CLOCKS_PER_SEC;
printf("time:%.4f\n", timeCount);
printf("Number of Facets:%d, Number of vertexes:%d\n", hull.mNumFacet, hull.mNumVertes);
ASSERT(isConvex(&hull));
exportHullToFile(&hull, "convexhull_result.txt");
delete[] hull.mVertes;
delete[] hull.mFacet;
}
delete[]point;
}
int main(void)
{
testQuickHull3D();
_CrtDumpMemoryLeaks();
return 0;
}
