本章描述了CGAL的二维三角剖分。章节定义回顾了三角剖分的主要定义。Section Representation讨论了二维三角剖分在CGAL中的表示方式。软件设计部分介绍了二维三角测量软件包的总体软件设计。下一节介绍CGAL中可用的不同二维三角剖分类:基本三角剖分(基础三角剖分部分)、德劳内三角剖分(德劳内三角剖分部分)、常规三角剖分(常规三角剖分部分)、和
二维三角剖分可以大致描述为一组平面三角形 T T T ,使得:
更精确地说,三角网可以描述为单纯形的复合体。让我们首先记录一些定义。
单纯复形:是一个由单纯形组成的集合
纬度:单纯复形的维度 d d d是指单纯形的最大维数。
单纯:如果集合 T T T的任何单纯形包含在具有最大维数的 T T T的单纯形中,则单纯复形 T T T是单纯的。
相邻:如果 T T T中的两个最大维 d d d的单形子共享 d − 1 d-1 d−1维子面,则称它们相邻。如果邻接关系定义了一个连通图,该连通图位于 T T T的最大维数的简形集上,则称为连通的简形复数。
定义域: T T T中所有简式的并集 U T U_T UT称为 T T T的定义域。
奇异点:在 T T T定域内的点 p p p,如果它在 U T U_T UT中的周围既不是拓扑球也不是拓扑盘,则称它为奇异点。
二维三角剖分可以描述为一个单纯的、连通的、无奇点的二维单纯复合体。
三角剖分的每一个面都可以给定一个朝向,而该朝向又可以引出与该面相关联的边缘的朝向。如果两个相邻的面在它们的共同入射边上诱导出相反的方向,则它们的方向称为一致的。如果每个面的方向都可以选择,使所有入射面对都具有一致的方向,那么三角剖分就被称为可定向的。
CGAL三角剖分的数据结构允许用户表示任何可定向的无边界二维三角剖分的组合。在此数据结构之上,二维三角剖分类负责三角剖分的几何嵌入,并被设计用于处理平面三角剖分。三角剖分的平面可以嵌入在更高维度的空间中。
CGAL的三角剖分是完全三角剖分,这意味着它们的定域是它们顶点的凸包。因为任何平面三角网都可以完成,这不是一个真正的限制。例如,一个多边形区域的三角剖分可以被构造并表示为一个约束三角剖分的子集,其中区域边界边已被输入为约束边(参见Section约束三角剖分、约束Delaunay三角剖分和约束与子约束之间双向映射的约束三角剖分)。
严格地说,面这个术语应该用来设计任何维度的面,三角剖分的二维面应该被恰当地称为facet。然而,按照一种常见的用法,我们以后经常称面为二维三角剖分的面。
CGAL的二维三角剖分可以看作是一个平面剖分,其有界面为三角形并覆盖了顶点集的凸包。这个分区的单个无界面是凸包的补面。在许多应用中,如Kirkpatrick的层次结构或增量Delaunay结构,只处理三角形面很方便。因此,一个称为无限顶点的虚构顶点被添加到三角剖分中,同时还添加了无限条边*和与之相连的无限面。每条无限边都与无限顶点和凸包的一个顶点相关联。每个无限面都与无限顶点和凸包边相关联。因此,三角剖分的每条边恰好对应两个面,三角剖分的面集在拓扑上等价于一个二维球体。注意,无限顶点没有显著坐标,也不能对它或无限面应用几何谓词。
这扩展到在退化情况下产生的低维三角剖分,或当三角剖分少于三个顶点时。包括无限面的一维三角剖分是一个由边和顶点组成的环,在拓扑上相当于一个11 - 1球体。零维三角剖分,其域简化为一个点,用两个顶点表示,在拓扑上等价于一个0- 0球。图39.2说明了这一点,示例Triangulation_2/low_dimension .cpp
显示了如何遍历低维三角剖分。
因为三角剖分是一组具有恒定尺寸复杂度的三角形面,所以三角剖分不是作为平面地图上的一层来实现的。CGAL使用了基于面和顶点而不是边缘的三角剖分的适当内部表示。这样的表示可以节省存储空间,并产生更快的算法[1]。表示的基本元素是顶点和面。每个三角形面都可以访问它的三个事件顶点和它的三个相邻面。每个顶点都可以访问它的一个入射面,并通过该面访问它的入射面的圆形列表。一个面的三个顶点按逆时针顺序用0、1和2索引。一个面的邻居也用0,1,2索引,以这样一种方式,用i索引的邻居与具有相同索引的顶点相反。如图39.3所示,图中显示的函数ccw(i)和cw(i)分别计算 i+1和 i-1对3的模。边不是显式表示的,它们只是通过两个面的邻接关系隐式表示的。每条边都有两种隐式表示:与顶点索引i相对的面f的边可以表示为f的邻居(i)的边。
CGAL的三角网类提供了高级的几何功能,例如三角网中,点的位置、插入、移除或位移点。它们作为一层构建在称为三角测量数据结构的数据结构之上。三角测量数据结构可以被认为是三角测量的面和顶点的容器。此数据结构还负责三角剖分的所有组合方面。
这种几何方面和组合部分之间的分离反映在软件设计中,因为三角测量类有两个模板参数:
Triangulation_data_structure_2
作为三角测量数据结构的默认模型。类Triangulation_data_structure_2
有两个模板参数,分别代表一个顶点类和一个面类。CGAL为这些模板参数定义概念,并为这些概念提供默认模型。顶点类和基类由几何特征类模板化,这使它们能够获得三角剖分的几何原语的一些知识这些默认的顶点和面基类可以被用户定制的基类替换,例如,为了处理附加到三角剖分的顶点或面的附加属性。TriangulationDataStructure_2
描述了任何三角测量类的三角测量数据结构需求,但对几何特征类的需求实际上依赖于三角测量类。通常,顶点和面基类的需求由TriangulationVertexBase_2
和TriangulationFaceBase_2
这两个基本概念描述。然而,一些三角测量类要求基类实现对基本概念的改进。 类Triangulation_2
作为其他2D三角剖分类的基类,并实现了三角剖分的用户界面。三角剖分的顶点和面通过handles
, iterators
和 circulators
访问。handles
是概念句柄的模型,它提供了两个解引用操作符*
和->
。circulator
是一种专门用于访问循环序列的类型。每当被访问的元素不是序列的一部分时,就使用句柄。迭代器和循环器用于访问三角剖分的全部或部分。
迭代器和循环器都是双向且不可变的。循环器和迭代器可转换为具有相同值类型的句柄,因此在调用成员函数时,任何句柄类型参数都可以被具有相同值类型的迭代器或循环器替换。
三角剖分类提供了一个函数,以顺时针或逆时针顺序访问一个面的顶点和邻居。有循环器访问与给定顶点或与其相邻的顶点相关的边或面。另一种循环器类型允许访问给定直线所遍历的所有面。
三角剖分类提供了一些迭代器来访问所有的面、边或顶点,也提供了一些迭代器来选择性地访问有限的面、边或顶点。
三角剖分类提供了测试任何特征的无限特性的方法,以及测试给定顶点的特定特征(边或面)在三角剖分中的存在性的方法。
三角剖分类提供了一种针对三角剖分定位给定点的方法。特别地,该方法报告点是否与三角剖分的顶点重合,是否位于边缘上,是否在面内或凸壳外。在低维三角剖分的情况下,查询点也可以位于三角剖分仿射外壳之外。
三角剖分类还提供了相对于三角剖分的给定有限面或相对于其周圆定位点的方法。三角网的面及其圆周都是逆时针方向的。
三角剖分可以通过几个函数进行修改:插入点、移除顶点、位移顶点、翻转边缘。当两个入射面的结合形成一个凸四边形时,边的翻转是可能的(见图39.6)。
三角剖分定义了Triangulation_3::All_vertices_iterator
等迭代器类型。它们的行为类似于句柄,因为不需要解引用迭代器来获得句柄。只要API需要句柄,迭代器也可以被传递。为了编写c++ 11 for循环,三角测量调用还提供了范围类型Triangulation_2::All_vertex_handles
,它有一个嵌套类型Triangulation_2::All_vertex_handles::iterator
。这个迭代器的值类型是Triangulation_2::Vertex_handle
。对于顶点和单元格的各种迭代器也是类似的。
对于范围Triangulation_2::All_edges
,它包含Triangulation_2::All_edges::iterator == Triangulation_2::All_edges_iterator
。对于用于边和点的各种迭代器也是类似的。
注意,如果您希望将pre c++ 11的for-loops与range类结合使用,则只需要迭代器类型。
#include
#include
#include // 三角网
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Triangulation_2<K> Triangulation;
typedef Triangulation::Vertex_handle Vertex_handle;
typedef Triangulation::Point Point;
typedef Triangulation::Finite_vertex_handles Finite_vertex_handles;
// The following types are different
// Its value type is Triangulation_2::Vertex
typedef Triangulation::Finite_vertices_iterator Finite_vertices_iterator;
// Its value type is Triangulation_2::Vertex_handle
typedef Finite_vertex_handles::iterator Finite_vertex_handles_iterator;
int main()
{
std::vector<Point> points = { Point(0,0), Point(1,0), Point(0,1) };
Triangulation T;
T.insert(points.begin(), points.end());
// 遍历三角网的顶点
// 方法一
std::cout << "Triangulation_2::Finite_vertices_iterator is like a Triangulation_2::Vertex_handle\n";
for (Finite_vertices_iterator it = T.finite_vertices_begin();it != T.finite_vertices_end();++it)
{
std::cout << it->point() << std::endl;
}
// 方法二
std::cout << "Triangulation_2::Finite_vertex_handles::iterator dereferences to Triangulation_2::Vertex_handle\n";
Finite_vertex_handles::iterator b, e;
std::tie(b, e) = T.finite_vertex_handles();
for (; b != e; ++b)
{
// you must dereference the iterator to get a handle
Vertex_handle vh = *b;
std::cout << vh->point() << std::endl;
}
// 方法三
std::cout << "and you can use a C++11 for loop\n";
for (Vertex_handle vh : T.finite_vertex_handles())
{
std::cout << vh->point() << std::endl;
}
return 0;
}
定位由一个随机游走实现。行走始于作为可选参数给出的面的顶点,或在没有可选参数给出的三角剖分的任意顶点。Delaunay三角剖分在最坏情况下需要时间 O ( n ) O(n) O(n) ,但如果顶点均匀随机分布,平均只需 O ( n ) O(\sqrt{n}) O(n)。类Triangulation_hierarchy_2
,在triangulationhierarchy
部分中描述,实现了一个数据结构,旨在提供一个可选的更有效的点定位算法。
点的插入是通过定位包含该点的面,并将该面分割为三个新面来完成的。如果该点落在凸包外,则通过翻转恢复三角剖分。除了位置之外,插入需要时间 O ( 1 ) O(1) O(1)。这个界只是位于凸包外的点的平摊界。移除一个顶点是通过移除所有的相邻三角形,并重新三角化孔来完成的。移除所花费的时间最多与 d 2 d^2 d2成正比,其中 d d d是被移除顶点的度数,对于随机顶点,该度数为 O ( 1 ) O(1) O(1)。对顶点进行位移的方法是首先验证位移后三角网嵌入是否保持平面;如果是,顶点将直接放置在新位置;否则,将在新位置插入一个点,并删除原始位置上的顶点。
有限特征上的面、边和顶点迭代器是由访问所有(有限和无限)特征的对应迭代器派生的,这些特征本身是由三角剖分数据结构的相应迭代器派生的。
三角剖分的几何特征类需要提供几何对象(点、段和三角形)以及这些对象上的几何谓词来构建三角剖分。所需的谓词是:
概念TriangulationTraits_2
描述了三角剖分的几何特征类的要求。CGAL内核类是这个概念的模型。CGAL库还使用内核几何对象和谓词提供了专门的TriangulationTraits_2
模型。这些类本身是用CGAL内核模板化的,并从内核中提取所需的类型和谓词。Projection_traits_xy_3
类是一个几何特征类,用于构建地形的三角剖分。这种三角剖分是嵌入在三维空间中的二维三角剖分,数据点是三维点。三角剖分是根据这些点在 x y xy xy平面上的投影建立的,然后提升到原始的三维数据点。这对于处理GIS地形尤其有用。traits类并没有真正地投影三维点,并维护每个点及其投影之间的映射(这需要占用空间并且容易出错),而是提供了忽略点的 z z z坐标的几何谓词。参见Delaunay三角测量部分的示例。CGAL还提供几何性状类Projection_traits_yz_3
和Projection_traits_xz_3
,分别处理 y z yz yz平面和 x z xz xz平面上的投影,以及几何性状类Projection_traits_3
处理由其正交向量定义的任意平面上的投影。
下面的程序使用默认内核Exact_predicate_inexact_constructions_kernel
作为几何特征类和默认的三角测量数据结构创建二维点的三角测量。输入点从文件中读取并插入到三角测量中。最后将凸包上的点写入cout。
#include
#include
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Triangulation_2<K> Triangulation;
typedef Triangulation::Vertex_circulator Vertex_circulator;
typedef Triangulation::Point Point;
int main()
{
std::ifstream in("triangulation_prog1.cin");
std::istream_iterator<Point> begin(in);
std::istream_iterator<Point> end;
Triangulation t;
t.insert(begin, end);
Vertex_circulator vc = t.incident_vertices(t.infinite_vertex()),
done(vc);
if (vc != nullptr)
{
do
{
std::cout << vc->point() << std::endl;
} while (++vc != done);
}
return 0;
}
可以通过调用CGAL::draw
函数来可视化二维三角剖分,如下例所示。该函数打开一个新窗口,显示给定的2D三角剖分。对这个函数的调用是阻塞的,也就是说,只要用户关闭窗口,程序就会继续。
#include
#include
#include
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Triangulation_2<K> Triangulation;
typedef Triangulation::Point Point;
int main(int argc, char* argv[]) {
std::ifstream in((argc>1)?argv[1]:"data/triangulation_prog1.cin");
std::istream_iterator<Point> begin(in);
std::istream_iterator<Point> end;
Triangulation t;
t.insert(begin, end);
CGAL::draw(t);
return EXIT_SUCCESS;
}
此函数需要CGAL_Qt5
,并且仅在定义了宏CGAL_USE_BASIC_VIEWER
时可用。链接到cmake目标CGAL::CGAL_Basic_viewer
将链接到CGAL_Qt5
并添加定义CGAL_USE_BASIC_VIEWER
。
类Delaunay_triangulation_2
用来表示平面上一组数据点的Delaunay三角剖分。Delaunay三角剖分满足以下空圆性质(也称为Delaunay性质):三角剖分的任何面的围合圆内部不包含任何数据点。对于没有四个共圆点子集的点集,Delaunay三角剖分是唯一的,它与点集的Voronoi图是对偶的。类Delaunay_triangulation_2
源自类Triangulation_2
。类Delaunay_triangulation_2
继承由基本类Triangulation_2
定义的类型。traits
类提供的其他类型被定义为表示双重Voronoi图。类Delaunay_triangulation_2
覆盖在三角剖分中插入、移动或移除点的成员函数,以维护Delaunay属性。它还有一个成员函数(Vertex_handle Delaunay_triangulation_2::nearest_vertex(const Point& p)
)来进行最近邻查询,还有一个成员函数来构造双Voronoi图的元素(顶点和边)。
几何特征类必须是概念DelaunayTriangulationTraits_2
的模型,它改进了概念TriangulationTraits_2
。特别地,这个概念提供了side_of_oriented_circle
谓词,给定四个点 p , q , r , s p,q,r,s p,q,r,s,决定了点 s s s相对于经过 p p p, q q q和 r r r的圆的位置。side_of_oriented_circle
谓词实际上定义了Delaunay三角剖分。更改这个谓词允许用户为不同的度量构建Delaunay三角剖分的变体,例如 L 1 L_1 L1或 L ∞ L_{\infty} L∞度量或由凸对象定义的任何度量。然而,使用奇异度规的人必须小心,构造的三角剖分必须是凸包的三角剖分这意味着凸包的边必须是Delaunay边。这对于任何光滑凸度量(如 L 2 L_2 L2)都是允许的,对于其他度量(如 L ∞ L_{\infty} L∞)也可以通过将精心选择的哨点添加到点集中来保证。CGAL内核类是欧氏度规的DelaunayTriangulationTraits_2
概念的模型。地形的traits
类,Projection_traits_xy_3
, Projection_traits_yz_3
和Projection_traits_xz_3
也是DelaunayTriangulationTraits_2
的模型,除非它们不满足对偶函数和最近顶点查询的要求。
在Delaunay三角剖分中插入一个新点,首先使用基本三角剖分的插入成员函数,然后执行一系列翻转以恢复Delaunay属性。如果新顶点在更新的Delaunay三角剖分中度为 d d d,则必须执行的翻转次数为 O ( d ) O(d) O(d)。对于均匀随机分布的点,每次插入平均花费时间 O ( 1 ) O(1) O(1),一旦点被定位在三角剖分中。
移除调用三角剖分中的移除,然后以满足Delaunay准则的方式重新三角剖分创建的孔。移除度为 d d d的顶点需要时间 O ( d 2 ) O(d^2) O(d2)对于三角剖分中的一个随机顶点,度 d d d为 O ( 1 ) O(1) O(1)。当被移除顶点的程度很小( ≤ 7 \leq7 ≤7)时,使用一种特殊的程序,允许将随机点的全局移除时间减少2倍。
将点 p p p的顶点 v v v 移动到新位置 p ′ p' p′,首先检查将 v v v移动到 p ′ p' p′ 后三角嵌入是否保持平面。如果是,它将 v v v移动到 p ′ p' p′,并简单地执行一系列翻转来恢复Delaunay属性,时间复杂度为 O ( d ) O(d) O(d),其中 d d d是位移后顶点的度数。否则,置换是通过在新位置插入一个顶点,并删除原始的顶点来完成的。在最坏的情况下,复杂度为 O ( n ) O(n) O(n),但对于单位正方形中均匀分布的顶点,复杂度仅为 O ( 1 + δ √ n ) O(1+δ√n) O(1+δ√n),其中 δ δ δ为新位置和旧位置之间的欧氏距离。在完成点定位之后,在最坏的情况下,找到点的最近邻居时间复杂度为 O ( n ) O(n) O(n),但对于均匀随机分布的顶点和任何查询点,时间复杂度为 O ( 1 ) O(1) O(1)。
下面的代码为地形模型的垂直投影使用常用的欧氏度规创建Delaunay三角剖分。这些点有高程,也就是说它们是3D点,但用于构建Delaunay三角剖分的谓词只使用这些点的 x x x和 y y y坐标计算。
类Projection_traits_xy_3
是2D和3D线性几何核的一部分。
#include // 内核
#include // xy投影面
#include // Delaunay三角剖分
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Projection_traits_xy_3<K> Gt;
typedef CGAL::Delaunay_triangulation_2<Gt> Delaunay;
typedef K::Point_3 Point;
int main()
{
std::ifstream in("terrain.cin");
std::istream_iterator<Point> begin(in);
std::istream_iterator<Point> end;
Delaunay dt(begin, end);
std::cout << dt.number_of_vertices() << std::endl;
return 0;
}
下面的代码计算一组数据点的Voronoi图的边,并计算该图的有限边数和射线数
#include
#include
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef CGAL::Delaunay_triangulation_2<K> Triangulation;
typedef Triangulation::Edge_iterator Edge_iterator;
typedef Triangulation::Point Point;
int main()
{
std::ifstream in("voronoi.cin");
std::istream_iterator<Point> begin(in);
std::istream_iterator<Point> end;
Triangulation T;
T.insert(begin, end);
int ns = 0;
int nr = 0;
Edge_iterator eit = T.edges_begin();
for (; eit != T.edges_end(); ++eit)
{
CGAL::Object o = T.dual(eit);
if (CGAL::object_cast<K::Segment_2>(&o))
{
++ns;
}
else if (CGAL::object_cast<K::Ray_2>(&o))
{
++nr;
}
}
std::cout << "The Voronoi diagram has " << ns << " finite edges "<< " and " << nr << " rays" << std::endl;
return 0;
}
下面的代码计算一组点的Delaunay三角剖分,并打印受给定矩形限制的Voronoi边。
#include
#include
#include
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point_2;
typedef K::Iso_rectangle_2 Iso_rectangle_2;
typedef K::Segment_2 Segment_2;
typedef K::Ray_2 Ray_2;
typedef K::Line_2 Line_2;
typedef CGAL::Delaunay_triangulation_2<K> Delaunay_triangulation_2;
//A class to recover Voronoi diagram from stream.
//Rays, lines and segments are cropped to a rectangle
//so that only segments are stored
struct Cropped_voronoi_from_delaunay {
std::list<Segment_2> m_cropped_vd;
Iso_rectangle_2 m_bbox;
Cropped_voronoi_from_delaunay(const Iso_rectangle_2& bbox) :m_bbox(bbox) {}
template <class RSL>
void crop_and_extract_segment(const RSL& rsl) {
CGAL::Object obj = CGAL::intersection(rsl, m_bbox);
const Segment_2* s = CGAL::object_cast<Segment_2>(&obj);
if (s) m_cropped_vd.push_back(*s);
}
void operator<<(const Ray_2& ray) { crop_and_extract_segment(ray); }
void operator<<(const Line_2& line) { crop_and_extract_segment(line); }
void operator<<(const Segment_2& seg) { crop_and_extract_segment(seg); }
};
int main() {
//consider some points
std::vector<Point_2> points;
points.push_back(Point_2(0, 0));
points.push_back(Point_2(1, 1));
points.push_back(Point_2(0, 1));
Delaunay_triangulation_2 dt2;
//insert points into the triangulation
dt2.insert(points.begin(), points.end());
//construct a rectangle
Iso_rectangle_2 bbox(-1, -1, 2, 2);
Cropped_voronoi_from_delaunay vor(bbox);
//extract the cropped Voronoi diagram
dt2.draw_dual(vor);
//print the cropped Voronoi diagram as segments
std::copy(vor.m_cropped_vd.begin(), vor.m_cropped_vd.end(),
std::ostream_iterator<Segment_2>(std::cout, "\n"));
}
未完待续……