文章目录
- 1.概述
- 2.点和线段
- 3.点序列的凸包
- 3.1.内置数组中点的凸包
- 3.2.向量中点的凸包
- 4.关于kernel和Traits类
- 5.概念和模型
1.概述
本教程是为CGAL新手,大概知道c++和几何算法的基本知识。第一部分展示了如何定义点和段类,以及如何在它们上应用几何谓词。本节进一步提高了对使用浮点数作为坐标时存在严重问题的认识。在第二部分中,您将遇到一个典型的CGAL函数,它计算一个2D凸包。第三部分展示了trait类的含义,第四部分解释了概念和模型的概念。
2.点和线段
在第一个例子中,我们演示了如何构造一些点和段,并对它们执行一些基本操作。所有的CGAL头文件都在子目录include/CGAL中。所有的CGAL类和函数都在名称空间CGAL中。类以大写字母开头,全局函数以小写字母开头,常量都是大写字母。对象的维度用后缀表示。几何原语,如点类型,是在核中定义的。我们为第一个示例选择的内核使用双精度浮点数表示点的笛卡尔坐标。除了这些类型,我们还看到了像三个点的方向测试这样的谓词,以及像距离和中点计算这样的结构。谓词有一组离散的可能结果,而构造要么产生一个数字,要么产生另一个几何实体。
#include <iostream>
#include <CGAL/Simple_cartesian.h>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_2 Point_2;
typedef Kernel::Segment_2 Segment_2;
int main()
{
Point_2 p(1, 1), q(10, 10);
std::cout << "p = " << p << std::endl;//点对象可直接输出
std::cout << "q = " << q.x() << " " << q.y() << std::endl;//也可以分开输出
std::cout << "sqdist(p,q) = "
<< CGAL::squared_distance(p, q) << std::endl;//两个几何对象相距最近点的平方和距离
Segment_2 s(p, q);//构造线段
Point_2 m(5, 9);
std::cout << "m = " << m << std::endl;
std::cout << "sqdist(Segment_2(p,q), m) = "
<< CGAL::squared_distance(s, m) << std::endl;//两个几何对象相距最近点的平方和距离
std::cout << "p, q, and m ";
switch (CGAL::orientation(p, q, m)) {//
case CGAL::COLLINEAR:
std::cout << "are collinear\n";
break;
case CGAL::LEFT_TURN:
std::cout << "make a left turn\n";
break;
case CGAL::RIGHT_TURN:
std::cout << "make a right turn\n";
break;
}
std::cout << " midpoint(p,q) = " << CGAL::midpoint(p, q) << std::endl;//中点
return 0;
}
/*
squared_distance:计算两个几何物体之间欧几里得距离的平方。对于任意几何对象obj1和obj2,距离的平方定义为最小的squared_distance(p1, p2),其中p1是obj1的一个点,p2是obj2的一个点。请注意,对于具有内部(有界区域)的对象,该内部是对象的一部分。因此,到内部点的距离的平方是零,而不是到边界上最近点的距离的平方。
*/
用浮点数进行几何运算可能令人惊讶,如下面的示例所示。
#include <iostream>
#include <CGAL/Simple_cartesian.h>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_2 Point_2;
int main()
{
{
Point_2 p(0, 0.3), q(1, 0.6), r(2, 0.9);
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
{
Point_2 p(0, 1.0 / 3.0), q(1, 2.0 / 3.0), r(2, 1);
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
{
Point_2 p(0, 0), q(1, 1), r(2, 2);
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
return 0;
}
阅读代码,我们可以假设它会打印三次“共线”。然而,实际输出如下:
not collinear
not collinear
collinear
这是因为这些分数不能表示为双精度数,而共线性检验将在内部计算一个3x3矩阵的行列式,该行列式接近但不等于零,因此前两个检验的非共线性。类似的情况也可能发生在执行左边的点上,但由于行列式计算期间的舍入误差,这些点似乎是共线的,或者执行右边。如果您必须确保您的数字得到完全精确的解释,您可以使用执行精确谓词和提取结构的CGAL内核。
#include <iostream>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <sstream>
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;//使用精确内核
typedef Kernel::Point_2 Point_2;
int main()
{
Point_2 p(0, 0.3), q, r(2, 0.9);
{
q = Point_2(1, 0.6);//使用双精度也不一定共线
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
{
std::istringstream input("0 0.3 1 0.6 2 0.9");//使用字符串完整的描述数字
input >> p >> q >> r;
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
{
q = CGAL::midpoint(p, r);
std::cout << (CGAL::collinear(p, q, r) ? "collinear\n" : "not collinear\n");
}
return 0;
}
下面是输出,您可能仍然会感到惊讶。
not collinear
collinear
collinear
在第一个块中,这些点仍然不是共线的,原因很简单,您看到的文本坐标被转换为浮点数。当它们被转换成任意精度理数时,它们精确地表示浮点数,而不是文本!这在第二个块中是不同的,它对应于从文件中读取数字。然后直接从字符串构造任意精度的理数,以便它们精确地表示文本。在第三块代码中,可以看到作为中点结构的结构是精确的,正如内核类型的名称所暗示的那样。在许多情况下,您将使用“精确”的浮点数,因为它们是由某些应用程序计算或从传感器获得的。它们不是字符串“0.1”或动态计算为“1.0/10.0”,而是一个全精度浮点数。如果它们被输入到没有构造的算法中,则可以使用提供精确谓词但不精确构造的内核。一个这样的例子是凸包算法,我们将在下一节看到。输出是输入的子集,算法只比较坐标并执行方向测试。乍一看,内核执行精确的谓词和构造似乎是完美的选择,但性能要求或有限的内存资源使得它不是。此外,对于许多算法来说,精确构造是无关紧要的。例如,一种表面网格简化算法,通过将其折叠到边缘的中点来迭代地收缩边缘。大多数CGAL包都会解释它们应该使用或支持哪种内核。
3.点序列的凸包
本节中的所有示例都计算一组点的二维凸包。我们展示了算法将其输入作为表示一系列点的begin/end迭代器对,并将结果(在本例中是凸包上的点)写入输出迭代器。
3.1.内置数组中点的凸包
在第一个例子中,我们有一个包含五个点的数组作为输入。由于这些点的凸包是输入的子集,因此提供一个具有相同大小的数组来存储结果是安全的。
#include <iostream>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/convex_hull_2.h>
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point_2;
int main()
{
Point_2 points[5] = { Point_2(0,0), Point_2(10,0), Point_2(10,10), Point_2(6,5), Point_2(4,1) };
Point_2 result[5];
Point_2 *ptr = CGAL::convex_hull_2(points, points + 5, result);
std::cout << ptr - result << " points on the convex hull:" << std::endl;
for (int i = 0; i < ptr - result; i++) {
std::cout << result[i] << std::endl;
}
return 0;
}
在前一节中,我们已经看到CGAL附带了几个内核。由于凸包算法只比较输入点的坐标和方向测试,我们可以选择提供精确谓词但不提供精确几何结构的核。凸包函数接受三个参数,作为输入的开始和结束指针,以及作为结果的数组的开始指针。该函数返回指向最后写入的凸包点后面的结果数组的指针,因此指针差告诉我们凸包上有多少个点。
3.2.向量中点的凸包
在第二个示例中,将内置数组替换为标准模板库的std::vector。back_inserter用于在末尾插入元素。实现方法是构造一个迭代器,这个迭代器可以在容器末尾添加元素。
#include <iostream>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/convex_hull_2.h>
#include <vector>
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point_2;
typedef std::vector<Point_2> Points;
int main()
{
Points points, result;
points.push_back(Point_2(0, 0));
points.push_back(Point_2(10, 0));
points.push_back(Point_2(10, 10));
points.push_back(Point_2(6, 5));
points.push_back(Point_2(4, 1));
CGAL::convex_hull_2(points.begin(), points.end(), std::back_inserter(result));//尾部迭代器
std::cout << result.size() << " points on the convex hull" << std::endl;
return 0;
}
通过调用std::vector类的push_back()方法,将一些点放入vector中。
然后调用凸包函数。前两个参数,point .begin()和points.end()是迭代器,它们是指针的泛化:它们可以解引用和自增。从某种意义上说,凸包函数是通用的,它接受任何可以解引用和递增的输入。
第三个参数是将结果写入的位置。在前面的例子中,我们提供了一个指向已分配内存的指针。这种指针的泛化是输出迭代器,它允许对解引用迭代器进行自增和赋值。在这个例子中,我们从一个空向量开始,它会根据需要增长。因此,不能简单地传递result.begin(),而是传递一个由辅助函数std::back_inserter(result)生成的输出迭代器。此输出迭代器在递增时不做任何操作,并对赋值操作调用result.push_back(…)。
如果你知道STL,标准模板库,上面的内容就很有意义了,因为这就是STL从容器中解耦算法的方式。如果您不了解STL,那么最好先熟悉一下它的基本思想。
4.关于kernel和Traits类
在本节中,我们将展示如何表达必须满足的需求,以便像convex_hull_2()这样的函数可以与任意点类型一起使用。
如果您查看函数convex_hull_2()和其他2D凸包算法的手册页,您会发现它们有两个版本。在我们目前看到的示例中,该函数使用两个迭代器来获取输入点的范围,并使用一个输出迭代器来将结果写入。第二个版本有一个额外的模板参数Traits,以及这个类型的额外参数。
C++ 的 traits 技术,是一种约定俗称的技术方案,用来为同一类数据(包括自定义数据类型和内置数据类型)提供统一的操作函数,例如 advance(), swap(), encode()/decode() 等。
template<class InputIterator , class OutputIterator , class Traits >
OutputIterator
convex_hull_2(InputIterator first,
InputIterator beyond,
OutputIterator result,
const Traits & ch_traits)
典型凸包算法使用的几何基元是什么?当然,这取决于算法,所以让我们考虑一下可能是最简单有效的算法,即所谓的“Graham/Andrew Scan”。该算法首先从左到右对点进行排序,然后通过从排序列表中一个接一个地添加点来增量地构建凸包。要做到这一点,它至少必须知道一些点的类型,它应该知道如何对这些点进行排序,它必须能够计算出三个点的方向。
这就是模板参数Traits的用武之地。对于ch_graham_andrew(),它必须提供以下嵌套类型:
- Traits::Point_2
- Traits::Less_xy_2
- Traits::Left_turn_2
- Traits::Equal_2
可以猜到,Left_turn_2负责方向测试,而Less_xy_2用于对点进行排序。这些类型必须满足的需求在ConvexHullTraits_2概念中被完整地记录下来。这些类型重新分组的原因很简单。另一种选择是使用相当长的函数模板,以及更长的函数调用。
template <class InputIterator, class OutputIterator, class Point_2, class Less_xy_2, class Left_turn_2, class Equal_2>
OutputIterator
ch_graham_andrew( InputIterator first,
InputIterator beyond,
OutputIterator result);
有两个明显的问题:什么可以用作这个模板形参的实参?我们为什么要有模板参数呢?
要回答第一个问题,CGAL概念内核的任何模型都提供了ConvexHullTraits_2概念所需的内容。
至于第二个问题,考虑一个应用程序,我们想要计算投影到yz平面上的3D点的凸壳。使用Projection_traits_yz_3类,这是对前面示例的一个小修改。
#include <iostream>
#include <iterator>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Projection_traits_yz_3.h>
#include <CGAL/convex_hull_2.h>
typedef CGAL::Exact_predicates_inexact_constructions_kernel K3;
typedef CGAL::Projection_traits_yz_3<K3> K;
typedef K::Point_2 Point_2;
int main()
{
std::istream_iterator< Point_2 > input_begin(std::cin);
std::istream_iterator< Point_2 > input_end;
std::ostream_iterator< Point_2 > output(std::cout, "\n");
CGAL::convex_hull_2(input_begin, input_end, output, K());
return 0;
}
另一个例子是关于用户定义的点类型,或者来自CGAL以外的第三方库的点类型。在类的作用域中,将点类型与此点类型所需的谓词放在一起,就可以使用这些点运行convex_hull_2()。
最后,让我们解释一下为什么性状对象被传递给凸包函数?它将允许使用更一般的投影特征对象来存储状态,例如,如果投影平面是由一个方向给出的,这是硬连接在类Projection_traits_yz_3中。
5.概念和模型
在上一节中,我们写道:CGAL概念内核的任何模型都提供了ConvexHullTraits_2概念所需的内容。概念是对类型的一组要求,也就是说,它具有某些嵌套类型、某些成员函数,或者带有以该类型为对象的某些自由函数。概念的模型是满足概念需求的类。让我们看一下下面的函数。
template <typename T>
T duplicate(T t)
{
return t;
}
如果你想用类C实例化这个函数,这个类必须至少提供一个复制构造函数,我们说类C必须是CopyConstructible的一个模型。单例类不满足这个要求。
另一个例子是函数:
template <typename T>
T& std::min(const T& a, const T& b)
{
return (a<b)?a:b;
}
只有为用作T的类型定义了操作符<(…)时,该函数才会编译,并且我们说该类型必须是LessThanComparable的模型。
具有所需自由函数的概念的一个例子是CGAL包CGAL和Boost Graph Library中的HalfedgeListGraph。为了成为类G的HalfedgeListGraph的模型,必须有一个全局函数halfedges(const G&),等等。
一个带有必需特征类的概念的例子是InputIterator。对于InputIterator的模型,必须存在类std::iterator_traits的特化(或者必须适用泛型模板)。