关键词
- 核
Kernel
- 几何图元:点
Point
、分段Segment
- 谓词:方向
orientation
- 构造:距离
squared_distance
、中点midpoint
- 特征
Traits
- 概念
concept
- 模型
model
概念:类型要求,例如:
InputIterator
模型:满足某一概念的一个类型,例如Point_2
特征类:表征一组模型,例如Kernel
,ConvexHullTraits_2
谓词:具有一组离散的可能结果
构造:产生一个新的实体(数/几何实体)
这个教程适合CGAL新手(知道C++和一些几何算法基本知识)。
第一部分展示如何定义点和线段的类型,如何应用一些几何谓词。本节也可以让我们认识到使用浮点数作为坐标存在严重问题。
第二部分说明一个经典的 CGAL 函数:计算 2D 凸包。
第三部分说明Traits
是啥意思。
第四部分解释concept
和model
的概念。
1. 三个点、一个线段
CGAL头文件都在include/CGAL目录,类型和函数都定义在CGAL
命名空间下。类型以大写字母开头,全局函数用小写字母,常量全大写,维度作为后缀,如:
Point
squared_distance
COLLINEAR
Point_2
几何图元定义在核Kernel
中,如:
CGAL::Simple_cartesian<double>
表示使用双精度double
浮点数的笛卡尔坐标
谓词(具有一组离散的可能结果),如:
orientation
获得三个点的方向
构造(产生一个数字或另一个几何实体),如:
squared_distance
图元距离
midpoint
中点
Kernel_23/points_and_segment.cpp
#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;
}
用浮点数表示几何图形可能不太准确:
Kernel_23/surprising.cpp
#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矩阵行列式来判断三点是否共线并不可行,因为它们的行列式逼近但不等于0。
有时候不共线的点可能也因为舍入误差被判断为共线了。
如果你想要精度完整,你可以选择一个使用精准谓词和构造的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包都会说明他们应该使用或支持哪些内核。
2. 点序列凸包
3. 关于核和特征类型
本节我们展示如何表达必要要求,以便convex_hull_2()
函数可以应用到任意点类型。
如果你翻阅手册,你会发现conver_hull_2
等凸包算法有两个版本。我们刚才看到的示例中,它采用两个输入迭代器表示输入,一个输出迭代器表示输出。第二个版本有一个附加模板参数Traits
,和该类型的形参。
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);
这里有两问题:啥可以作为模板参数?我们为啥要模板参数?
为了回答这俩问题,概念Kernel
的任何模型都提供了概念ConvexHullTraits_2
。
对于第二个问题,我们想象一个场景:我们想要计算3D点云投影到YZ平面的凸包。我们搞个Projection_traits_yz_3
类型,从前面示例改的:
Convex_hull_2/convex_hull_yz.cpp
#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;
}
另一个示例说明用户自定义的点类型和第三方库来的点类型。将点类型和点类型所需的谓词放在类的范围内,我们就可以使用这些点来运行convex_hull_2()
。
额~ 简单了,是吧~
最后,我们解释下为什么要将一个Traits
对象传递个凸包函数?它可以让我们使用一个更通用的投影特征对象来存储状态,例如:如果投影面由一个方向表示,它可以硬编码到Projection_traits_yz_3
中。
4. 概念和模型
在上一节中,我们写到:概念Kernel
的任何模型都提供了概念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
的一个模型。
HalfedgeListGraph是另一个例子。如果类型T想要成为HalfedgeListGraph
的一个模型,则要求类型T拥有全局函数halfedges(const T&)
。
InputIterator
是另一个例子。要成为InputIterator
的一个模型,必须存在std::iterator_traits
的特化(或通用模板可以被应用)。