文章目录
- Triangulated Surface Mesh Simplification 三角表面网格简化
- 1 介绍
- 2 简化过程概述
- 3 成本策略
- 3.1 Lindstrom-Turk 成本和布局策略
- 3.2 Garland-Heckbert 成本和布局策略
- 3.3 成本策略政策
- 4 应用程序编程接口
- 4.1 API概述
- 4.2 强制参数
- 4.3 可选的命名参数
- 4.4 调用示例
- 5 例子
- 5.1 使用 Surface_mesh 的示例
- 5.2 使用默认多面体的示例
- 5.3 使用富集多面体的示例
- 5.4 OpenMesh 简化示例
- 5.5 边缘标记为不可移除的示例
- 5.6 面法线有界变化的示例
- 5.7 多面体包络示例
- 5.8 访客示例
- 5.9 使用 Garland-Heckbert 策略的示例
- 6 设计和实现历史
Triangulated Surface Mesh Simplification 三角表面网格简化
原文地址: https://doc.cgal.org/latest/Surface_mesh_simplification/index.html#Chapter_Triangulated_Surface_Mesh_Simplification
该包提供了一种通过边缘折叠来简化三角表面网格的算法。 用户可以定义成本、约束和放置策略来决定何时以及如何折叠边缘。 默认情况下提供了一些策略,例如 Turk/Lindstrom 和 Garland-Heckbert 无记忆方法。
1 介绍
表面网格简化是减少表面网格中使用的面数,同时尽可能保留整体形状、体积和边界的过程。 它与细分相反。
这里提出的算法可以使用一种称为边缘折叠的方法来简化任何有方向的 2 流形表面,具有任意数量的连接组件,有或没有边界(边界或孔)和手柄(任意属)。 粗略地说,该方法包括迭代地用单个顶点替换边,每次折叠删除 2 个三角形。
根据用户提供的成本函数给出的优先级折叠边,并且替换顶点的坐标由另一个用户提供的放置函数确定。 当满足用户提供的停止谓词时,例如达到所需的边数,算法终止。
这里实现的算法是通用的,因为它不需要表面网格是特定类型,而是 MutableFaceGraph 和 HalfedgeListGraph 概念的模型。 我们给出了 Surface_mesh、Polyhedron_3 和 OpenMesh 的示例。
该设计是基于策略的 (https://en.wikipedia.org/wiki/Policy-based_design),这意味着您可以通过传递一组策略对象来自定义流程的某些方面。 每个策略对象指定算法的一个特定方面,例如如何选择边以及放置替换顶点的位置。 所有策略都有一个合理的默认值。 此外,该 API 使用所谓的命名参数技术,该技术允许您以任意顺序仅传递相关参数,而忽略那些默认值合适的参数。
2 简化过程概述
实现简化算法的自由函数不仅采用表面网格和所需的停止谓词,还采用许多控制和监视简化过程的附加参数。 本节简要描述该过程,以便为算法参数的讨论奠定背景。
有两种略有不同的“边缘”折叠操作。 一种称为边缘折叠,另一种称为半边缘折叠。 给定一条连接顶点 w 和 v 的边 e,边折叠操作将 e、w 和 v 替换为新的顶点 r,而半边折叠操作将 v 拉入 w,消除 e 并保留 w 。 在这两种情况下,操作都会删除边 e 以及与其相邻的 2 个三角形。
该包使用 halfedge-collapse 操作,该操作是通过另外删除 1 个顶点 (v) 和 2 条边(每个相邻三角形一条)来实现的。 它可以选择将剩余的顶点 (w) 移动到新位置,称为放置,在这种情况下,净效果与边折叠操作中的相同。
当然,边缘折叠产生的表面网格会与初始表面网格有一定程度的偏差,并且由于简化的目标是减少三角形的数量,同时尽可能保留表面网格的整体外观,因此 有必要测量这种偏差。 一些方法尝试测量从初始表面网格到完全简化的表面网格的总偏差,例如,通过跟踪累积误差同时保留简化变化的历史记录。 其他方法(例如在此包中实现的方法)尝试仅测量每个单独边缘折叠的成本(单个简化步骤引入的局部偏差),并将整个过程规划为一系列增加成本的步骤。
全局误差跟踪方法可产生高度准确的简化,但会占用大量额外空间。 成本驱动的方法(如本包中的方法)产生的简化精度稍差,但占用的额外空间要少得多,甚至在某些情况下不占用任何空间。
该软件包提供了两种成本驱动的方法。 该软件包中实现的第一个成本驱动方法,即Lindstrom&Turk,主要基于[4]、[5],并有[3]、[2]和[1]的贡献。 该软件包中实现的第二种成本驱动方法,即 Garland&Heckbert,主要基于 [2],并由 Trettner 和 Kobbelt [7] 进行增强。
该算法分两个阶段进行。 在称为收集阶段的第一阶段中,初始折叠成本被分配给表面网格中的每条边。 然后在第二阶段(称为折叠阶段)中,按照成本增加的顺序处理边缘。 一些处理过的边缘被折叠,而一些则被丢弃。 折叠的边被顶点替换,并且现在入射到替换顶点的所有边的折叠成本被重新计算,从而影响剩余未处理的边的顺序。
并非所有选择进行处理的边都会折叠。 如果处理后的边不满足某些拓扑和几何条件,则可以立即将其丢弃,而不会折叠。
[2] 中提出的算法通过将某些顶点对视为形成伪边并继续以与 [4] 中相同的方式折叠边和伪边,收缩(折叠)任意顶点对,而不仅仅是边。 5]。 然而,收缩任意顶点对可能会导致非流形表面网格,但该包的当前状态只能处理流形表面网格,因此,它只能折叠边缘。 也就是说,这个包不能用作顶点收缩的框架。 因此,我们的 [2] 实现仅折叠边缘。
3 成本策略
计算折叠成本和顶点放置的具体方式称为成本策略。 用户可以以策略和相关参数的形式选择不同的策略,传递给算法。
该包的当前版本提供了一组实现三种策略的策略:Lindstrom-Turk 策略(默认)、Garland-Heckbert 系列策略以及由边长成本和可选中点放置组成的策略( 更快但不太准确)。
3.1 Lindstrom-Turk 成本和布局策略
[4]、[5]中提出的策略的主要特点是,简化的表面网格在每一步都不会与原始表面网格(或上一步的表面网格)进行比较,因此不需要保留额外的数据。 信息,例如原始表面网格或局部变化的历史记录。 因此称为无记忆简化。
在每一步中,所有剩余的边都是潜在的折叠候选边,并选择成本最低的边。 折叠边的成本由为替换它的顶点选择的位置给出。
替换顶点位置被计算为 3 个线性无关线性等式约束系统的解。 每个约束都是通过最小化受先前计算的约束影响的二次目标函数来获得的。 有几种可能的候选约束,并且按重要性顺序考虑每个约束。 候选约束可能与先前接受的约束不兼容,在这种情况下,它会被拒绝并考虑下一个约束。 一旦接受了 3 个约束,系统就会求解出顶点位置。 第一个考虑的约束保留表面网格边界的形状(如果边缘轮廓具有边界边缘)。 接下来的约束保留表面网格的总体积。 如果需要,接下来的约束将优化体积和边界形状的局部变化。 最后,如果仍然需要约束(因为之前计算的约束不兼容),则添加第三个(也是最后一个)约束以支持等边三角形而不是细长三角形。
然后,成本是形状、体积和边界优化项的加权和,其中用户指定每个项的单位加权单位因子。
当边缘即将折叠时,即在所有先前折叠之后,仅使用当前与其相邻的三角形独立计算每条边缘的局部变化。 因此,最小局部变化的传递路径最终产生相当接近绝对最小值的全局变化。
3.2 Garland-Heckbert 成本和布局策略
与 Lindstrom-Turk 策略的情况一样,[2] 中引入的 Garland-Heckbert 策略不会将生成的网格与原始网格进行比较,也不依赖于局部变化的历史。 相反,它通过使用分配给每个顶点的二次矩阵来编码到原始网格的近似距离。
在其经典版本中,二次矩阵 Q 被分配给每个顶点 v,并对任何点 p 到 v 的相邻面的总平方距离进行编码,该距离由矩阵乘积 p′Qp 给出。 在每个步骤中,都会选择使折叠成本最小化的边进行折叠操作。 折叠边的成本是通过最小化误差函数 p′Qp 来计算的,其中 Q 是边端点的组合二次矩阵,p 是最小化成本的点(即决策变量)。 选择最小化目标函数的点 p 作为新的放置点。 由于误差函数是二次的,因此可以通过简单地计算梯度并将其等于零来找到其最小值。 如果由于奇点而失败,则在边缘上找到最佳点和成本。 放置新顶点后,通过简单地对已折叠边的两个末端的二次矩阵求和来为其分配一个新的二次矩阵。 垂直于相邻面的附加伪面被添加到边界顶点的二次矩阵中,以便尽可能保留网格的清晰边界。
Trettner 和 Kobbelt [7] 提出了 Garland-Heckbert 的扩展,他们提出了概率二次曲线的概念。 在这种新方法中,不再像经典版本那样使用到输入平面或多边形的距离来执行能量最小化; 相反,通过在输入(顶点位置和面法线)中引入高斯噪声来使输入几何体变得不确定。 这种方差自然会恶化结果的紧密度,但另一方面,它可以创建更均匀的三角测量,并且该方法对噪声的容忍度更高,同时仍然保持特征敏感性。
图 71.1 萨福的头部模型(最左边,34882 个顶点)。 从左到右,四个 Garland-Heckbert 变体的简化输出(1745 个顶点)和对称 Hausdorff 距离:平面 (0.217912)、概率平面 (0.256801)、三角形 (0.268872) 和概率三角形 (0.490846)。
3.3 成本策略政策
算法使用的成本策略是通过三个策略来选择的:GetPlacement、GetCost 和 Filter。
调用 GetPlacement 策略来计算 halfedge-collapse 后剩余顶点的新位置。 它返回一个可选值,如果边缘不应折叠,则可以不存在该值。
调用 GetCost 策略来计算折叠边的成本。 该策略使用放置来计算成本(这是一种误差度量)并确定边的顺序。
该算法维护一个内部数据结构(可变优先级队列),该结构允许按成本递增顺序处理每条边。 这种数据结构需要一些每条边的附加信息,例如边的成本。 如果每条边的附加信息的记录占用N字节的存储空间,则简化100万条边(正常大小)的表面网格需要100万倍的N字节的额外存储空间。 因此,为了最大限度地减少简化表面网格所需的额外内存量,仅将成本附加到每条边,而不附加任何其他内容。
但这是一个权衡:折叠的成本是放置(为剩余顶点选择的新位置)的函数,因此在为每条边调用 GetCost 之前,还必须调用 GetPlacement 来获取放置参数 到成本函数。 但该放置(即 3D 点)并未附加到每条边,因为这很容易使额外的存储需求增加两倍。 一方面,这极大地节省了内存,但另一方面也是一种处理浪费,因为当一条边有效折叠时,必须再次调用 GetPlacement 才能知道是否要移动剩余的顶点。
早期的原型表明,将放置附加到边缘,从而避免边缘折叠后对放置函数的一次冗余调用,对总运行时间影响不大。 这是因为每条边的成本不仅计算一次,而且在此过程中多次更改,因此放置函数也必须调用多次。 缓存放置只能避免边缘折叠时的最后一次调用,但不能避免由于放置(和成本)发生变化而需要的所有先前调用。
最后,我们解释一下 PlacementFilter 策略。 虽然成本是优先级队列中使用的标量,但可能会有其他标准来决定是否应执行边缘折叠。 虽然这样的标准可以很容易地集成到成本函数中,即将成本设置为无穷大,以便不被视为崩溃的候选者,但我们仅在一条边是下一个要崩溃的边时测试该边的标准。 如果标准的计算成本很高,例如当我们检查简化的网格是否位于输入网格的公差包络内时,这使得网格简化速度更快。
4 应用程序编程接口
4.1 API概述
由于该算法不存在鲁棒性问题,因此不需要精确的谓词或构造,并且可以安全地使用 Simple_cartesian 。 [1]
简化算法作为免费模板函数 Surface_mesh_simplification::edge_collapse() 实现。 该函数有两个强制参数和几个可选参数。
4.2 强制参数
该算法有两个主要参数:要简化(就地)的表面网格和停止谓词。
要简化的表面网格必须是 MutableFaceGraph 和 HalfedgeListGraph 概念的模型。
在选择每条边进行处理之后、在将其分类为可折叠或不可折叠之前(即在它折叠之前),将调用停止谓词。 如果停止谓词返回 true,则算法终止。
4.3 可选的命名参数
BGL 中还引入了命名参数的概念。 您可以在 [6] 或以下站点中阅读相关内容:https://www.boost.org/libs/graph/doc/bgl_named_params.html。 命名参数允许用户通过名称仅指定真正需要的参数,从而使参数顺序变得不重要。
假设有一个函数 f(),它有 3 个参数,分别为姓名、年龄和性别,并且有变量 n、a 和 g 作为参数传递给该函数。 如果没有命名参数,您可以这样调用它:f(n,a,g),但是如果使用命名参数,您可以这样调用它:f(name(n).age(a).gender(g))。
也就是说,通过将每个参数包装到一个名称与该参数的名称相匹配的函数中,为每个参数指定一个名称。 整个命名参数列表实际上是由点 (.) 分隔的函数调用的组合。 因此,如果函数混合使用强制参数和命名参数,则可以使用逗号将最后一个非命名参数与第一个命名参数分隔开,如下所示:
f(non_named_par0, non_named_pa1, name(n).age(a).gender(g))
当您使用命名参数时,顺序无关紧要,因此: f(name(n).age(a).gender(g)) 相当于: f(age(a).gender(g).name( n)),并且您可以省略任何具有默认值的命名参数。
4.4 调用示例
/*
surface_mesh : the surface_mesh to simplify
stop_predicate : policy indicating when the simplification must finish
vertex_index_map(vimap) : property-map giving each vertex a unique integer index
edge_index_map(eimap) : property-map giving each edge a unique integer index
edge_is_constrained_map(ebmap): property-map specifying whether an edge is a constrained edge or not
get_cost(cf) : function object computing the cost of a collapse
get_placement(pf) : function object computing the placement for the remaining vertex
filter(filter) : function object to reject a candidate chosen for the next edge collapse
visitor(vis) : function object tracking the simplification process
*/
int r = edge_collapse(surface_mesh, stop_predicate,CGAL::parameters::vertex_index_map(vimap).edge_index_map(eimap).edge_is_border_map(ebmap).get_cost(cf).get_placement(pf).filter(filter).visitor(vis));
5 例子
5.1 使用 Surface_mesh 的示例
以下示例说明了 Surface_mesh 的简化。 未指定的成本策略默认为 Lindstrom-Turk。
文件 Surface_mesh_simplification/edge_collapse_surface_mesh.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_ratio_stop_predicate.h>
#include <chrono>
#include <fstream>
#include <iostream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Surface_mesh;
namespace SMS = CGAL::Surface_mesh_simplification;
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube-meshed.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now();// In this example, the simplification stops when the number of undirected edges// drops below 10% of the initial countdouble stop_ratio = (argc > 2) ? std::stod(argv[2]) : 0.1;SMS::Edge_count_ratio_stop_predicate<Surface_mesh> stop(stop_ratio);int r = SMS::edge_collapse(surface_mesh, stop);std::chrono::steady_clock::time_point end_time = std::chrono::steady_clock::now();std::cout << "\nFinished!\n" << r << " edges removed.\n" << surface_mesh.number_of_edges() << " final edges.\n";std::cout << "Time elapsed: " << std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count() << "ms" << std::endl;CGAL::IO::write_polygon_mesh((argc > 3) ? argv[3] : "out.off", surface_mesh, CGAL::parameters::stream_precision(17));return EXIT_SUCCESS;
}
5.2 使用默认多面体的示例
以下示例说明了使用默认顶点、半边和面的 Polyhedron_3 的简化。 未指定的成本策略默认为 Lindstrom-Turk。
文件 Surface_mesh_simplification/edge_collapse_polyhedron.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Polyhedron_3.h>
// Simplification function
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
// Stop-condition policy
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <iostream>
#include <fstream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef CGAL::Polyhedron_3<Kernel> Surface_mesh;
namespace SMS = CGAL::Surface_mesh_simplification;
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/small_cube.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}// This is a stop predicate (defines when the algorithm terminates).// In this example, the simplification stops when the number of undirected edges// left in the surface mesh drops below the specified number (1000)const std::size_t edge_count_treshold = (argc > 2) ? std::stoi(argv[2]) : 1000;SMS::Edge_count_stop_predicate<Surface_mesh> stop(edge_count_treshold);// This the actual call to the simplification algorithm.// The surface mesh and stop conditions are mandatory arguments.// The index maps are needed because the vertices and edges// of this surface mesh lack an "id()" field.std::cout << "Collapsing edges of Polyhedron: " << filename << ", aiming for " << edge_count_treshold << " final edges..." << std::endl;int r = SMS::edge_collapse(surface_mesh, stop,CGAL::parameters::vertex_index_map(get(CGAL::vertex_external_index, surface_mesh)).halfedge_index_map(get(CGAL::halfedge_external_index, surface_mesh)));std::cout << "\nFinished!\n" << r << " edges removed.\n"<< (surface_mesh.size_of_halfedges()/2) << " final edges.\n";std::ofstream os(argc > 3 ? argv[3] : "out.off");os.precision(17);os << surface_mesh;return EXIT_SUCCESS;
}
5.3 使用富集多面体的示例
以下示例与前面的示例等效,但使用丰富的多面体,其半边支持 id 字段来存储算法所需的边索引。
文件 Surface_mesh_simplification/edge_collapse_enriched_polyhedron.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Polyhedron_3.h>
// Extended polyhedron items which include an id() field
#include <CGAL/Polyhedron_items_with_id_3.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_ratio_stop_predicate.h>
#include <iostream>
#include <fstream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point;
// Setup an enriched polyhedron type which stores an id() field in the items
typedef CGAL::Polyhedron_3<Kernel,CGAL::Polyhedron_items_with_id_3> Surface_mesh;
typedef boost::graph_traits<Surface_mesh>::vertex_descriptor vertex_descriptor;
typedef boost::graph_traits<Surface_mesh>::halfedge_descriptor halfedge_descriptor;
namespace SMS = CGAL::Surface_mesh_simplification;
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/small_cube.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}// The items in this polyhedron have an "id()" field// which the default index maps used in the algorithm// need to get the index of a vertex/edge.// However, the Polyhedron_3 class doesn't assign any value to// this id(), so we must do it here:int index = 0;for(halfedge_descriptor hd : halfedges(surface_mesh))hd->id() = index++;index = 0;for(vertex_descriptor vd : vertices(surface_mesh))vd->id() = index++;// In this example, the simplification stops when the number of undirected edges// drops below xx% of the initial countconst double ratio = (argc > 2) ? std::stod(argv[2]) : 0.1;SMS::Edge_count_ratio_stop_predicate<Surface_mesh> stop(ratio);// The index maps are not explicitelty passed as in the previous// example because the surface mesh items have a proper id() field.// On the other hand, we pass here explicit cost and placement// function which differ from the default policies, omitted in// the previous example.std::cout << "Collapsing edges of mesh: " << filename << ", aiming for " << 100 * ratio << "% of the input edges..." << std::endl;int r = SMS::edge_collapse(surface_mesh, stop);std::cout << "\nFinished!\n" << r << " edges removed.\n"<< (surface_mesh.size_of_halfedges()/2) << " final edges.\n";std::ofstream os((argc > 3) ? argv[3] : "out.off");os.precision(17);os << surface_mesh;return EXIT_SUCCESS;
}
5.4 OpenMesh 简化示例
以下示例展示了如何将网格简化包应用于网格数据结构,该数据结构不是 CGAL 的一部分,而是 FaceGraph 的模型。
此示例中的特殊之处在于允许将 3D CGAL 点与顶点关联的属性映射。
文件 Surface_mesh_simplification/edge_collapse_OpenMesh.cpp
#include <OpenMesh/Core/IO/MeshIO.hh>
#include <OpenMesh/Core/Mesh/PolyMesh_ArrayKernelT.hh>
#include <CGAL/boost/graph/graph_traits_PolyMesh_ArrayKernelT.h>
// Simplification function
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_length_cost.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Midpoint_placement.h>
#include <iostream>
#include <fstream>
typedef OpenMesh::PolyMesh_ArrayKernelT</* default traits*/> Surface_mesh;
typedef boost::graph_traits<Surface_mesh>::edge_descriptor edge_descriptor;
class Constrained_edge_map
{
public:typedef boost::read_write_property_map_tag category;typedef bool value_type;typedef bool reference;typedef edge_descriptor key_type;Constrained_edge_map(Surface_mesh& sm): sm_(sm){sm_.add_property(constraint);}inline friend value_type get(const Constrained_edge_map& em, key_type e){bool b = em.sm_.property(em.constraint,em.sm_.edge_handle(e.idx()));return b;}inline friend void put(const Constrained_edge_map& em, key_type e, value_type b){em.sm_.property(em.constraint,em.sm_.edge_handle(e.idx())) = b;}
private:Surface_mesh& sm_;OpenMesh::EPropHandleT<bool> constraint;
};
namespace SMS = CGAL::Surface_mesh_simplification;
int main(int argc, char** argv)
{Surface_mesh surface_mesh;Constrained_edge_map constraints_map(surface_mesh);const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube-meshed.off");OpenMesh::IO::read_mesh(surface_mesh, filename);if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}// For the purpose of the example we mark 100 edges as constrained edgesint count = 0;for(edge_descriptor e : edges(surface_mesh))put(constraints_map, e, (count++ < 100));// This is a stop predicate (defines when the algorithm terminates).// In this example, the simplification stops when the number of undirected edges// left in the surface mesh drops below the specified number (1000)const std::size_t stop_n = (argc > 2) ? std::stoi(argv[2]) : 1000;SMS::Edge_count_stop_predicate<Surface_mesh> stop(stop_n);// This the actual call to the simplification algorithm.// The surface mesh and stop conditions are mandatory arguments.std::cout << "Collapsing edges of mesh: " << filename << ", aiming for " << stop_n << " final edges..." << std::endl;int r = SMS::edge_collapse(surface_mesh, stop,CGAL::parameters::halfedge_index_map(get(CGAL::halfedge_index,surface_mesh)).vertex_point_map(get(boost::vertex_point, surface_mesh)).edge_is_constrained_map(constraints_map));surface_mesh.garbage_collection();std::cout << "\nFinished!\n" << r << " edges removed.\n"<< num_edges(surface_mesh) << " final edges.\n";OpenMesh::IO::write_mesh(surface_mesh, "out.off");return EXIT_SUCCESS;
}
5.5 边缘标记为不可移除的示例
以下示例演示如何使用可选命名参数edge_is_constrained_map 来防止删除边。 标记为受约束的边保证位于最终的表面网格中。 然而,受约束边的顶点可能会改变,并且放置可能会改变点。 包装器 CGAL::Surface_mesh_simplification::Constrained_placement 保证这些点不会改变。
文件 Surface_mesh_simplification/edge_collapse_constrained_border_surface_mesh.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
// Simplification function
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
// Midpoint placement policy
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Midpoint_placement.h>
//Placement wrapper
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Constrained_placement.h>
// Stop-condition policy
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <iostream>
#include <fstream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Surface_mesh;
typedef boost::graph_traits<Surface_mesh>::halfedge_descriptor halfedge_descriptor;
typedef boost::graph_traits<Surface_mesh>::edge_descriptor edge_descriptor;
namespace SMS = CGAL::Surface_mesh_simplification;
// BGL property map which indicates whether an edge is marked as non-removable
struct Border_is_constrained_edge_map
{const Surface_mesh* sm_ptr;typedef edge_descriptor key_type;typedef bool value_type;typedef value_type reference;typedef boost::readable_property_map_tag category;Border_is_constrained_edge_map(const Surface_mesh& sm) : sm_ptr(&sm) {}friend value_type get(const Border_is_constrained_edge_map& m, const key_type& edge) {return CGAL::is_border(edge, *m.sm_ptr);}
};
// Placement class
typedef SMS::Constrained_placement<SMS::Midpoint_placement<Surface_mesh>,Border_is_constrained_edge_map > Placement;
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/mesh_with_border.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}Surface_mesh::Property_map<halfedge_descriptor, std::pair<Point_3, Point_3> > constrained_halfedges;constrained_halfedges = surface_mesh.add_property_map<halfedge_descriptor,std::pair<Point_3, Point_3> >("h:vertices").first;std::size_t nb_border_edges=0;for(halfedge_descriptor hd : halfedges(surface_mesh)){if(CGAL::is_border(hd, surface_mesh)){constrained_halfedges[hd] = std::make_pair(surface_mesh.point(source(hd, surface_mesh)),surface_mesh.point(target(hd, surface_mesh)));++nb_border_edges;}}// Contract the surface mesh as much as possibleSMS::Edge_count_stop_predicate<Surface_mesh> stop(0);Border_is_constrained_edge_map bem(surface_mesh);// This the actual call to the simplification algorithm.// The surface mesh and stop conditions are mandatory arguments.std::cout << "Collapsing as many edges of mesh: " << filename << " as possible..." << std::endl;int r = SMS::edge_collapse(surface_mesh, stop,CGAL::parameters::edge_is_constrained_map(bem).get_placement(Placement(bem)));std::cout << "\nFinished!\n" << r << " edges removed.\n"<< surface_mesh.number_of_edges() << " final edges.\n";CGAL::IO::write_polygon_mesh((argc > 2) ? argv[2] : "out.off", surface_mesh, CGAL::parameters::stream_precision(17));// now check!for(halfedge_descriptor hd : halfedges(surface_mesh)){if(CGAL::is_border(hd,surface_mesh)){--nb_border_edges;if(constrained_halfedges[hd] != std::make_pair(surface_mesh.point(source(hd, surface_mesh)),surface_mesh.point(target(hd, surface_mesh)))){std::cerr << "oops. send us a bug report\n";}}}assert(nb_border_edges==0);return EXIT_SUCCESS;
}
5.6 面法线有界变化的示例
表面网格简化并不能保证生成的表面没有自相交。 当使用 Lindstrom-Turk 方法折叠一条边时,即使是图 71.2 中所示的相当简单的网格也会导致自相交。
Surface_mesh_simplification::Bounded_normal_change_filter 类检查放置是否会反转作为边折叠候选的边的两个顶点的星形周围的面的法线。 然后它通过返回 boost::none 来拒绝这个放置。
-
笔记
该过滤器类取代了 Surface_mesh_simplification::Bounded_normal_change_placement 类的用法。 使用过滤器速度更快,因为它仅在接下来要折叠的边上执行,而不是在更新与作为边折叠结果的顶点相关的所有边时执行。
文件 Surface_mesh_simplification/edge_collapse_bounded_normal_change.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Timer.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_cost.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_placement.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Bounded_normal_change_filter.h>
#include <iostream>
#include <fstream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef CGAL::Surface_mesh<Kernel::Point_3> Surface_mesh;
namespace SMS = CGAL::Surface_mesh_simplification;
struct Dummy_placement {template <typename Profile>boost::optional<typename Profile::Point> operator()(const Profile&) const{return boost::none;}template <typename Profile>boost::optional<typename Profile::Point> operator()(const Profile&, const boost::optional<typename Profile::Point>& op) const{return op;}
};
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/fold.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}// This is a stop predicate (defines when the algorithm terminates).// In this example, the simplification stops when the number of undirected edges// left in the surface mesh drops below the specified numberconst std::size_t stop_n = (argc > 2) ? std::stoi(argv[2]) : num_edges(surface_mesh)/2 - 1;SMS::Edge_count_stop_predicate<Surface_mesh> stop(stop_n);typedef SMS::LindstromTurk_placement<Surface_mesh> Placement;CGAL::Timer t;t.start();// This the actual call to the simplification algorithm.// The surface mesh and stop conditions are mandatory arguments.// The index maps are needed because the vertices and edges// of this surface mesh lack an "id()" field.std::cout << "Collapsing edges of mesh: " << filename << ", aiming for " << stop_n << " final edges..." << std::endl;SMS::Bounded_normal_change_filter<> filter;SMS::edge_collapse(surface_mesh, stop,CGAL::parameters::get_cost(SMS::LindstromTurk_cost<Surface_mesh>()).filter(filter).get_placement(Placement()));std::cout << t.time() << " sec" << std::endl;CGAL::IO::write_polygon_mesh((argc > 3) ? argv[3] : "out.off", surface_mesh, CGAL::parameters::stream_precision(17));return EXIT_SUCCESS;
}
5.7 多面体包络示例
表面网格简化可以通过将简化网格保留在输入网格的包络内的方式来完成。 这利用了 Polyhedral_envelope 类,它可以检查查询点、线段或三角形是否位于多面体包络内,多面体包络由膨胀三角形的并集组成。 虽然用户给出了容差 ϵ,但检查是保守的,即可能存在位于作为半径为 ϵ 的球体的 Minkowski 和包络线获得的表面内的三角形,但这些三角形位于多面体包络线之外。
文件 Surface_mesh_simplification/edge_collapse_envelope.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_cost.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/LindstromTurk_placement.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Bounded_normal_change_filter.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Polyhedral_envelope_filter.h>
//bbox
#include <CGAL/Polygon_mesh_processing/bbox.h>
#include <iostream>
#include <fstream>
namespace SMS = CGAL::Surface_mesh_simplification;
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Surface;
typedef SMS::LindstromTurk_cost<Surface> Cost;
typedef SMS::LindstromTurk_placement<Surface> Placement;
typedef SMS::Polyhedral_envelope_filter<Kernel,SMS::Bounded_normal_change_filter<> > Filter;
int main(int argc, char** argv)
{Surface mesh;std::ifstream is(argc > 1 ? argv[1] : CGAL::data_file_path("meshes/helmet.off"));is >> mesh;SMS::Edge_count_stop_predicate<Surface> stop(0); // go as far as you can while in the envelopeCGAL::Iso_cuboid_3<Kernel> bbox(CGAL::Polygon_mesh_processing::bbox(mesh));Point_3 cmin = (bbox.min)();Point_3 cmax = (bbox.max)();const double diag = CGAL::approximate_sqrt(CGAL::squared_distance(cmin, cmax));std::cout << "eps = " << 0.01*diag << std::endl;Placement placement;Filter filter(0.01*diag);SMS::edge_collapse(mesh, stop, CGAL::parameters::get_cost(Cost()).filter(filter).get_placement(placement));std::ofstream out("out.off");out << mesh << std::endl;out.close();return EXIT_SUCCESS;
}
5.8 访客示例
最后一个示例展示了如何使用带有回调的访问者,这些回调在简化算法的不同步骤中调用。
文件 Surface_mesh_simplification/edge_collapse_visitor_surface_mesh.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
// Simplification function
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
// Visitor base
#include <CGAL/Surface_mesh_simplification/Edge_collapse_visitor_base.h>
// Stop-condition policy
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_ratio_stop_predicate.h>
#include <iostream>
#include <fstream>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Surface_mesh;
typedef boost::graph_traits<Surface_mesh>::halfedge_descriptor halfedge_descriptor;
typedef boost::graph_traits<Surface_mesh>::vertex_descriptor vertex_descriptor;
namespace SMS = CGAL::Surface_mesh_simplification;
typedef SMS::Edge_profile<Surface_mesh> Profile;
// The following is a Visitor that keeps track of the simplification process.
// In this example the progress is printed real-time and a few statistics are
// recorded (and printed in the end).
//
struct Stats
{std::size_t collected = 0;std::size_t processed = 0;std::size_t collapsed = 0;std::size_t non_collapsable = 0;std::size_t cost_uncomputable = 0;std::size_t placement_uncomputable = 0;
};
struct My_visitor : SMS::Edge_collapse_visitor_base<Surface_mesh>
{My_visitor(Stats* s) : stats(s) {}// Called during the collecting phase for each edge collected.void OnCollected(const Profile&, const boost::optional<double>&){++(stats->collected);std::cerr << "\rEdges collected: " << stats->collected << std::flush;}// Called during the processing phase for each edge selected.// If cost is absent the edge won't be collapsed.void OnSelected(const Profile&,boost::optional<double> cost,std::size_t initial,std::size_t current){++(stats->processed);if(!cost)++(stats->cost_uncomputable);if(current == initial)std::cerr << "\n" << std::flush;std::cerr << "\r" << current << std::flush;}// Called during the processing phase for each edge being collapsed.// If placement is absent the edge is left uncollapsed.void OnCollapsing(const Profile&,boost::optional<Point> placement){if(!placement)++(stats->placement_uncomputable);}// Called for each edge which failed the so called link-condition,// that is, which cannot be collapsed because doing so would// turn the surface mesh into a non-manifold.void OnNonCollapsable(const Profile&){++(stats->non_collapsable);}// Called after each edge has been collapsedvoid OnCollapsed(const Profile&, vertex_descriptor){++(stats->collapsed);}Stats* stats;
};
int main(int argc, char** argv)
{Surface_mesh surface_mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/small_cube.off");std::ifstream is(filename);if(!is || !(is >> surface_mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(surface_mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}// In this example, the simplification stops when the number of undirected edges// drops below xx% of the initial countconst double ratio = (argc > 2) ? std::stod(argv[2]) : 0.1;SMS::Edge_count_ratio_stop_predicate<Surface_mesh> stop(ratio);Stats stats;My_visitor vis(&stats);// The index maps are not explicitelty passed as in the previous// example because the surface mesh items have a proper id() field.// On the other hand, we pass here explicit cost and placement// function which differ from the default policies, omitted in// the previous example.int r = SMS::edge_collapse(surface_mesh, stop, CGAL::parameters::visitor(vis));std::cout << "\nEdges collected: " << stats.collected<< "\nEdges processed: " << stats.processed<< "\nEdges collapsed: " << stats.collapsed<< std::endl<< "\nEdges not collapsed due to topological constraints: " << stats.non_collapsable<< "\nEdge not collapsed due to cost computation constraints: " << stats.cost_uncomputable<< "\nEdge not collapsed due to placement computation constraints: " << stats.placement_uncomputable<< std::endl;std::cout << "\nFinished!\n" << r << " edges removed.\n"<< surface_mesh.number_of_edges() << " final edges.\n";CGAL::IO::write_polygon_mesh((argc > 3) ? argv[3] : "out.off", surface_mesh, CGAL::parameters::stream_precision(17));return EXIT_SUCCESS;
}
5.9 使用 Garland-Heckbert 策略的示例
每个 Garland-Heckbert 简化策略都是通过一个类来实现的,该类重新组合成本和布局策略,因为它们共享顶点二次数据,所以必须一起使用。 使用基于平面的二次误差度量的经典策略是通过类 Surface_mesh_simplification::GarlandHeckbert_plane_policies 实现的。 尽管这两个策略必须一起使用,但仍然可以使用行为修饰符(例如 Surface_mesh_simplification::Bounded_normal_change_placement)来包装任一策略。
文件 Surface_mesh_simplification/edge_collapse_garland_heckbert.cpp
#include <CGAL/Simple_cartesian.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Edge_count_ratio_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Bounded_normal_change_placement.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/GarlandHeckbert_policies.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/boost/graph/IO/polygon_mesh_io.h>
#include <chrono>
#include <fstream>
#include <iostream>
#include <vector>
typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::FT FT;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Surface_mesh;
namespace SMS = CGAL::Surface_mesh_simplification;
typedef SMS::GarlandHeckbert_plane_policies<Surface_mesh, Kernel> Classic_plane;
typedef SMS::GarlandHeckbert_probabilistic_plane_policies<Surface_mesh, Kernel> Prob_plane;
typedef SMS::GarlandHeckbert_triangle_policies<Surface_mesh, Kernel> Classic_tri;
typedef SMS::GarlandHeckbert_probabilistic_triangle_policies<Surface_mesh, Kernel> Prob_tri;
template <typename GHPolicies>
void collapse_gh(Surface_mesh& mesh,const double ratio)
{std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now();SMS::Edge_count_ratio_stop_predicate<Surface_mesh> stop(ratio);// Garland&Heckbert simplification policiestypedef typename GHPolicies::Get_cost GH_cost;typedef typename GHPolicies::Get_placement GH_placement;typedef SMS::Bounded_normal_change_placement<GH_placement> Bounded_GH_placement;GHPolicies gh_policies(mesh);const GH_cost& gh_cost = gh_policies.get_cost();const GH_placement& gh_placement = gh_policies.get_placement();Bounded_GH_placement placement(gh_placement);int r = SMS::edge_collapse(mesh, stop,CGAL::parameters::get_cost(gh_cost).get_placement(placement));std::chrono::steady_clock::time_point end_time = std::chrono::steady_clock::now();std::cout << "Time elapsed: "<< std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count()<< "ms" << std::endl;std::cout << "\nFinished!\n" << r << " edges removed.\n" << edges(mesh).size() << " final edges.\n";
}
// Usage:
// ./command [input] [ratio] [policy] [output]
// policy can be "cp" (classic plane), "ct" (classic triangle), "pp" (probabilistic plane), "pt" (probabilistic triangle)
int main(int argc, char** argv)
{Surface_mesh mesh;const std::string filename = (argc > 1) ? argv[1] : CGAL::data_file_path("meshes/cube-meshed.off");if(!CGAL::IO::read_polygon_mesh(filename, mesh)){std::cerr << "Failed to read input mesh: " << filename << std::endl;return EXIT_FAILURE;}if(!CGAL::is_triangle_mesh(mesh)){std::cerr << "Input geometry is not triangulated." << std::endl;return EXIT_FAILURE;}std::cout << "Input mesh has " << num_vertices(mesh) << " nv "<< num_edges(mesh) << " ne "<< num_faces(mesh) << " nf" << std::endl;const double ratio = (argc > 2) ? std::stod(argv[2]) : 0.2;std::cout << "Collapsing edges of mesh: " << filename << ", aiming for " << 100 * ratio << "% of the input edges..." << std::endl;const std::string policy = (argc > 3) ? argv[3] : "cp";if(policy == "cp")collapse_gh<Classic_plane>(mesh, ratio);else if(policy == "ct")collapse_gh<Classic_tri>(mesh, ratio);else if(policy == "pp")collapse_gh<Prob_plane>(mesh, ratio);elsecollapse_gh<Prob_tri>(mesh, ratio);CGAL::IO::write_polygon_mesh((argc > 4) ? argv[4] : "out.off", mesh, CGAL::parameters::stream_precision(17));return EXIT_SUCCESS;
}
请注意,这些策略取决于第三方Eigen库。
6 设计和实现历史
该软件包的核心以及大部分简化策略是 Fernando Cacciola 在 2006 年至 2009 年间的工作。
Andreas Fabri 在 CGAL 4.11 中添加了 Surface_mesh_simplification::Bounded_normal_change_placement 功能。
Garland-Heckbert 简化政策的实施是 Baskın Şenbaşlar(Google Summer of Code 2019)和 Julian Komaromy(Google Summer of Code 2021)工作的结果,他们都受到 Mael Rouxel-Labbé 的指导,他还为 代码和文档。