【原创】CGAL使用心得
CGAL使用心得
作者:李浩
首先我说说我研究CGAL的背景,由于,早一阵子,有一个需求,需要求出在一堆二维线中(包括直线和弧线),找出所有的最小区域和最大外包。如下图所示。
在这两个图形中,要快速的找到每一个最小的封闭区域和一个由这些最小封闭区域组成的一个最大封装区域。这样的算法,有,像最常见的建构多边形TOPO,然后用雷达扫描法,可以求出来。但是,这么多的算法开源库,让我下了使用开源库来解决这个问题的决心,很快我就锁定了CGAL这个强大的图形算法库。
很快的我发现,CGAL里的ARRANGEMENT能够实现类似需求的功能,就这样,我开始慢慢的对ARRANGEMENT进行研究。但研究CGAL的应用,不可能不学习CGAL的基本结构。CGAL是一个基于C++模版的算法库(好像很多的C++开源算法库,都是基于模版的)。学习其基础对C++泛型编程的知识有一定的要求。如果你看过候捷的《STL源码剖析》一书,你会更容易的看懂CGAL的代码。
CGAL中的重要基本名词包括核心、域数据类型、TRAITS特性;
核心中的数据存储是用到了域数据类型,而核心中的数据与基本几何数据的提取是通过TRAITS来的。
ARRANGEMENT中定义了点、面、边的概念,每一个最小区域在它里面被描述成面,而组成最小区域的线被描述成边,所有线的交点被描述成点。这样,需求上的概念与ARRANGEMENT中的概念可以匹配上,就证明,可以用这个算法来实现需求。
在慢慢的学习过程中,我发现CGAL是一个对精度要求比效率要求更高的库(这一度让我想放弃CGAL,但后来,发现再差的效率,都比一般的算法求解出来的速度要快),而我们能够提供的数据,精度是达不到的。就拿MF_FLOAT域数据类型来说,它里面真正保存数据是一个std::vector<unsigned int>,这样的精度是DOUBLE型不能比的。到现在,我还没有能够非常完美的解决这个问题。精度不够,特别表现在构建弧线时,CGAL preCondition就会通不过,报异常。这样让我很头大。发现是,我们提供的数据,在构建CGAL的弧时,弧的终点不在其支持圆上。想了很多的办法。最后网上有一位牛人告诉我,要重写CGAL的一个DCEL也许能够解决这个问题。DCEL是doubly-connected edge list data-structure,这种数据结构的最基本的观点是,将被共边的边,看作是一对方向互逆的边(注意,在这个数据结构中,每条边都是有方向,组成的一个环就是一个面)。我又开始学习CGAL中这一部分的内容 Arr_default_dcel,并且学习着,里面对顶点、面、边、孤点等几何对象的定义。发现,如果真正需要解决精度问题,不仅仅是重写DCEL能够解决的。对核心的部分内容也需要重写,这个工作量太大,代价也大。目前公司肯定是不允许的。更要命的是,公司的需求是,能够在传进去的线段上附加信息,出来后,其线段上的信息要不丢失。一开始,我想,有这个需求,不重写DCEL是不行了。于是,我开始跟踪CGAL构建ARRANGEMENT的过程,发现CGAL中,大部分的赋值操作,都是直接的内存拷贝。并且,不管怎么样,CGAL中真正处理的线类型只可能是X_monotone_curve_2,所以,就算你在开始构建的线段上能够附加上信息,当CGAL内部通过这个线段构建X_monotone_curve_2时,你的信息也会丢失。慢慢的,我发现,出来之后的线段也是X_monotone_curve_2,也就是说,只要你能在构建X_monotone_curve_2时能够将你的信息附加上去,你的信息,就有可能在计算出来之后的结果上,还存在。CGAL将核心中定义的CURVE转换成X_monotone_curve_2的过程是由make_x_objects完成的,所以,我在这里进行了改动,在转换过程中,将附加的值的信息给考虑上了。最终完成了这个需求。现在,又回到了,精度的问题了,把附加信息的问题解决了之后,重写这条路更加是不允许走了,通过CURVE构建的过程,我决定,将原来的构造方法改变,改成三点构造法。然后,在外边,对输入的数据的精度,进行更进一步的处理。最终应该是能完成这个功能。具体的,现在同事还在测试中。
通过对CGAL这一段时间的学习,我发觉,CGAL确实是一个很强大的图形算法库,对数据精度要求相当高,所以处理出来的数据正确率也是相当高的,对于那种对算法处理速度要求特别高的,不推荐用CGAL,但可以用CGAL中的算法的思想。
目前CGAL也有商业化的产品GeometryFactory,客户还是挺多的,像国产CAD的龙头产品(吼得最大,动作最大的产品,哈)ZWCAD,也是其客户。
哈哈,就稍微总结在这里,对CGAL的学习,借用一句广告词——永不止步。
附加说明:
1. CGAL整体概述
CGAL是一个用C++描述的,包含三个主要部分的计算几何算法库.
第一部分是核心组件(Kernel),它包括基本的几何对象以及做用在这些对象上的各种操作.这些对象被实现成使用表现类参数化的独立的类,这样使得核心更具有灵活性和适应性.
第二部分是一系列的基础几何数据结构和算法.它们被特征类参数化.而特征类定义了数据结构或者算法和它们使用的原生类型(primitives)的接口.在很多情况下CGAL中的核心类可以作为这些数据结构或算法的特征类使用.
第三部分是由一些支持设施比如为方便调试设计的迭代器,随即数源,I/O支持以及一些可视化工具等等.这个部分主要介绍核心部分.核心由一些基础对象组成,比如点,向量,方向,直线,射线,线段,三角形,ISO型长方形和四面体.每个部分都有一些对这些对象进行操作的函数.一般有访问函数(比如一个点的坐标),测试点和这个对象的位置关系,得到对象的包围盒子的函数,长度,面积等等.核心中还包含一些基本超作,比如仿射变幻,相交的检测与计算,距离计算.
2. CGAL核心说明
我们学习的对象是d维欧几里德仿射空间.这里我们主要考虑2维和3维得情况.空间中的对象是有点集组成.表示点的一般方法是使用笛卡儿坐标.它假定了一个参照框架(一个原点和d个正交轴).这个框架中一个点是由一个d维的向量表示的(c0,c1,...,cd-1),相应的线性空间中的向量也是如此.每个点都有唯一的笛卡儿坐标与之对应.另一种表示点的方法是齐次坐标.在这个框架中一个点是有一个d+1(h0,h1...,hd)维向量表示的.根据公式ci=hi/hd,对应的点的笛卡儿坐)标(c0,c1,...,cd-1)可以计算出来.注意齐次坐标的点的表示是不唯一的.当λ≠0时,向量(h0,h1,…hd)和向量(λh0, λh1 …, λhd)表示同一个点.如果一个点的笛卡儿坐标是(c0,c1,…,cd-1),它可以表示成(c0,c1,…,cd-1,1)这个齐次坐标.齐次坐标系可实际上可以在一个更一般的空间的中表示对象.这个空间叫射影空间.在CGAL中我们不会进行射影几何的计算.我们使用齐次坐标是为了避免除法运算,而增加的这个坐标是作为公共分母.
2.1 通过参数化实现泛型
几乎所有的核心对象(已经对应的函数)都是由模板来实现的.而模板参数是用用户来选择从而决定核心对象的表现形式.
2.2 笛卡儿坐标系核心
通过Cartesian<FieldNumberType>你可以给笛卡儿坐标系选择表现形式.当你选择了坐标系的表现形式以后,你必须同时指定坐标的数据类型.这种数据类型必须具有域的性质.前面提到int不是一种域类型.但是在笛卡儿坐标系中不包括除法运算,所以环类型在这种情况下是可以使用的.当指定了Cartesian<FieldNumberType>以后Cartesian<FieldNumberType>::FT和Cartesian<FieldNumberType>::RT都被映射成域类型.
Cartesian<FieldNumberType>的内部使用了引用计数来节省拷贝的开支.CGAL同时也提供了不使用引用计数的Simple_cartesian<FieldNumberType>.调试的时候使用Simple_cartesian <FieldNumberType>会比较方便,这是因为坐标被保存在类的内部,因此可以直接访问坐标.在使用的不同算法的时候,这两种实现方式将有不同的效率.
2.3 齐次坐标系核心(由于ARRANGMENT的例子都是基于笛卡儿坐标系核心的,所以没有做过多的研究)
齐次坐标系中可以避免除法运算,因为引入的补充坐标可以坐标为公共分母.避免了除法运算对几何计算的精确性相当有利.通过Homogeneous<RingNumberType>可以指定齐次坐标的表现形式.而在笛卡儿坐标系中我们还必须指定坐标的数值类型.因为齐次坐标系不使用除法,作为齐次坐标表现的数值类型必须被建立成为相对较弱的环数据类型.然而齐次核心提供的一些操作中使用到了除法,例如计算笛卡儿坐标系下的平方距离.为了不改变齐次坐标的弱类型参数,我们使用Quotient<RingNumberType>来解决需要除法计算的情况.这种类型可以认为是一种把环类型转换成为域类型的配接器.它保存数为分子和分母的形式.使用Homegeneous <RingNumberType>后,Homogeneous<RingNumberType>::FT和Quotient<RingNumberType>等价,同时Homogeneous<RingNumberType>::RT和RingNumberType等价.
Homogeneous<RingNumberType>使用引用计数.CGAL同样也提供了没有引用计数技术的Simple_homogeneous<RingNumberType>.
2.4 命名约定
使用核心类不仅可以避免出问题,而且使得CGAL类具有一致性.
1. 以大写字母开头的名字表示几何对象,像Point,Segment,Triangle.
2. 下划线加上对象的维度,_2,_3或者_d
3. 核心类型加上参数类型比如Cartesian<double>,Homogeneous<leda_integer>.
2.5 作为特征类的核心组件
CGAL基本库中的算法与数据结构是由一些特征类来参数化的.这些特征类包含了一类对象和上面算法或数据结构的操作和函数的行为一致.基础库中的大部分的算法与数据结构都可以由核心组件作为特征类.一些算法可以根据传入的几何对象的类型进行自动的推导,从而不需要直接指定.有些类还需要更多的参数.有些则不能使用核心组件.
2.6 选择一种核心组件和预定义核心组件
如果你使用整形的笛卡儿坐标,大部分的几何计算将只使用整形数值.特别是在只使用断言计算的时候.例如点集三角化和凸包计算.这些情况下笛卡儿坐标系是第一选择,即使是使用RingNumberType.你可以使用精度受限的int和long,使用double来表示整形,或者任意精度的整形例如GMP整形的包装类GMPZ,lead_integer,或者MP_Float.要注意,除非使用任意精度的环类型,溢出将会产生错误.
如果出现新建点的情况,比如求两条直线的交点,笛卡儿坐标中的计算经常出现除法.因此使用笛卡儿坐标的时候需要FieldNumberType.相对的,转换到齐次坐标的时候也一样.double是一个不精确的FieldNumberType.你可以把任何RingNumberType传给Quotient配接器来得到一个FieldNumberType从而可以在笛卡儿坐标系中进行运算.一般来说使用RingNumberType进行齐次坐标运算是比较合适的.其他一些FieldNumberType有leda_rational和leda_real..
如果可靠性对你来说非常重要,使用经过认定的精确计算的数据类型是比较好的选择.Filtered_kernel提供了一种过滤机制使得核心具有既精确又有效率的断言.仍然还有许多人喜欢使用double,因为他们需要速度,而且可以接受近似的结果,甚至可以忍受时不时由于舍入误差而崩溃的算法.
预定义核心组件
为了使用方便,CGAL预定义3个类型
它们都是笛卡儿坐标系核心
它们都支持从double型来建立笛卡儿坐标系里的点
它们用不同的方式处理建立几何对象的问题
---Exact_predicates_exact_constructions:精确的生成几何对象,提供精确的几何断言
---Exact_predicates_exact_construtions_kernel_with_sqrt:和上面一样,但数值类型提供了精确的开方运算
---Exact_predictates_inexact_constructions_kernel:提供精确断言,但是生成几何对象可能存在舍入误差.这对于大部分的CGAL算法来说已经足够了.而且比前面两种情况要快很多.
这三种预定义核心组件都是基于笛卡儿坐标系核心的,只是其构造的域参数类型不同。
我个人建议,内核用基本的笛卡儿坐标系核心,域参数类型用MF_FLOAT或者DOUBLE类型,因为,这样的效率会比其它,对精度对准确度有要求的域参数类型和内核封装类型(其实也是从笛卡儿坐标系核心派生出来的)快。效率最高的Exact_predictates_inexact_constructions_kernel就是Filtered_kernel<Simple_cartesian <double>>