GIS 应用软件可以兼容众多的传感器(如 LIDAR),生成密集点云。这些GIS 应用软件使用了比较高级的数据结构,如:不规则三角格网 (TIN)是生成数字高程模型 (DEM) 的基础,也可以利用TIN生成数字地形模型 (DTM)。对点云数据进行分类,提取地面、植被和建筑点(或其他用户定义的标签)等分类数据,从而使得获取的信息更加丰富。
注:因空间数据源获取方式不同,数据结构的定义也不尽一致。我们将在本教程中使用以下术语定义这些数据结构:
TIN:
Triangulated Irregular Network
,不规则三角格网,一种 2D 三角剖分结构,根据 3D 点在水平面上的投影关系连接它们,使用众多三角形构造地物表面。
DSM:
Digital Surface Model,数据表面模型,
包括建筑物和植被在内的整个被扫描表面模型。我们使用 TIN 来存储 DSM。
DTM:
Digital Terrain Model
, 数字地形模型,一种没有建筑物或植被等物体的裸露地表模型。我们使用 TIN 和栅格来存储 DTM。
DEM:
Digital Elevation Model
,
数字高程模型,
一个更通用的术语,包括 DSM 和 DTM。
本教程实现了以下功能:
(1)读取点云数据,计算 DSM存储在TIN中;(2)稠密的点云数据相对于现实物体而言是冗余的,是噪声。对点云数据进行过滤或抽稀,清洗掉建筑物立面和植被过多的数据,保留地面上较大的地物,随后将DEM中的孔洞进行填充,重建DEM网格,利用DEM网格生成栅格DEM模型或矢量等高线;
(3)使用3-label 监督分类算法,对点云数据进行分类,提取植被、建筑物等分组数据点。
2 不规则三角形网数据表示:TIN
CGAL 提供了用于几种用于计算三角形的数据结构和算法。可以通过将 二维德劳内(2D Delaunay)三角剖分模型和投影二维平面来生成 TIN。三角剖分结构是使用选定的二维平面(x,y)计算三角形顶角的二维位置,使用直线连接三角形坐标顶点,保存三角形顶点的三维信息,用于可视化或测量。
因此,可以通过CGAL::Delaunay_triangulation_2类简单地定义 TIN 数据结构:
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using Projection_traits = CGAL::Projection_traits_xy_3<Kernel>;
using Point_2 = Kernel::Point_2;
using Point_3 = Kernel::Point_3;
using Segment_3 = Kernel::Segment_3;
// Triangulated Irregular Network
using TIN = CGAL::Delaunay_triangulation_2<Projection_traits>;
3 数字表面模型表示:DSM
CGAL::Point_set_3类可以存储多种格式(XYZ、OFF、PLY、LAS)的点云数据,使用流计算保存到CGAL::Point_set_3对象中,存储在 TIN 中的DSM表示如下:
// Read points
std::ifstream ifile (fname, std::ios_base::binary);
CGAL::Point_set_3<Point_3> points;
ifile >> points;
std::cerr << points.size() << " point(s) read" << std::endl;
// Create DSM
TIN dsm (points.points().begin(), points.points().end());
由于 CGAL 的 Delaunay 三角形不规则格网使用的是图形网格模型,因此可以将生成的 TIN 转换为图形网格数据,例如CGAL::Surface_mesh
可以保存TIN支持的任何格式:
using Mesh = CGAL::Surface_mesh<Point_3>;
Mesh dsm_mesh;
CGAL::copy_face_graph (dsm, dsm_mesh);
std::ofstream dsm_ofile ("dsm.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dsm_ofile);
CGAL::IO::write_PLY (dsm_ofile, dsm_mesh);
dsm_ofile.close();
下图给出了在San Fransisco市 数据集上以这种方式计算DSM 示例(参见参考资料) 。
4 数字地形模型表示: DTM
DSM 是 计算DTM的基础,将DSM上的非地形点进行数据清洗后,通过计算可以生成DTM,即另一个用TIN表示的DTM。
作为一个例子,我们可以通过以下步骤计算得到一个 DTM :
- 设置高度阈值消除高度的剧烈变化
- 设置周长阈值聚类周边的地物作为连接的组件
- 过滤所有小于定义阈值的组件
该算法依赖于 2 个参数:高度阈值对应于建筑物的最小高度,以及周长阈值对应于 2D 投影上建筑物的最大尺寸。
CGAL Delaunay 提供了一组三角测量的 API,用于计算顶点、三角面的空间相互关系,可以获取比TIN本身更多的信息。在我们的例子中,计算每个顶点跟踪的输入点云中对应点索引,并且每个面都被赋予其连接组件的索引。
auto idx_to_point_with_info
= [&](const Point_set::Index& idx) -> std::pair<Point_3, Point_set::Index>
return std::make_pair (points.point(idx), idx);
TIN_with_info tin_with_info
(boost::make_transform_iterator (points.begin(), idx_to_point_with_info),
boost::make_transform_iterator (points.end(), idx_to_point_with_info));
使用泛洪算法计算不规则三角网构成的连接组件,以种子点进行泛洪,在未超过设定的阈值时,将附近的顶点坐标信息加入到同一个连接组件TIN中。
double spacing = CGAL::compute_average_spacing<Concurrency_tag>(points, 6);
spacing *= 2;
auto face_height
= [&](const TIN_with_info::Face_handle fh) -> double
double out = 0.;
for (int i = 0; i < 3; ++ i)
out = (std::max) (out, CGAL::abs(fh->vertex(i)->point().z() - fh->vertex((i+1)%3)->point().z()));
return out;
// Initialize faces info
for (TIN_with_info::Face_handle fh : tin_with_info.all_face_handles())
if (tin_with_info.is_infinite(fh) || face_height(fh) > spacing) // Filtered faces are given info() = -2
fh->info() = -2;
else // Pending faces are given info() = -1;
fh->info() = -1;
// Flooding algorithm
std::vector<int> component_size;
for (TIN_with_info::Face_handle fh : tin_with_info.finite_face_handles())
if (fh->info() != -1)
continue;
std::queue<TIN_with_info::Face_handle> todo;
todo.push(fh);
int size = 0;
while (!todo.empty())
TIN_with_info::Face_handle current = todo.front();
todo.pop();
if (current->info() != -1)
continue;
current->info() = int(component_size.size());
++ size;
for (int i = 0; i < 3; ++ i)
todo.push (current->neighbor(i));
component_size.push_back (size);
std::cerr << component_size.size() << " connected component(s) found" << std::endl;
这个带有连同组件的TIN可以保存为渲染着色的图形网格数据。
Mesh tin_colored_mesh;
Mesh::Property_map<Mesh::Face_index, CGAL::IO::Color>
color_map = tin_colored_mesh.add_property_map<Mesh::Face_index, CGAL::IO::Color>("f:color").first;
CGAL::copy_face_graph (tin_with_info, tin_colored_mesh,
CGAL::parameters::face_to_face_output_iterator
(boost::make_function_output_iterator
([&](const std::pair<TIN_with_info::Face_handle, Mesh::Face_index>& ff)
// Color unassigned faces gray
if (ff.first->info() < 0)
color_map[ff.second] = CGAL::IO::Color(128, 128, 128);
// Random color seeded by the component ID
CGAL::Random r (ff.first->info());
color_map[ff.second] = CGAL::IO::Color (r.get_int(64, 192),
r.get_int(64, 192),
r.get_int(64, 192));
})));
std::ofstream tin_colored_ofile ("colored_tin.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (tin_colored_ofile);
CGAL::IO::write_PLY (tin_colored_ofile, tin_colored_mesh);
tin_colored_ofile.close();
图 0.2给出了一个由连通分量着色的 TIN 示例。
图 0.2带有连通分量着色的 TIN 高于高度阈值的面不会分配给任何组件,并以灰色显示。
使用以下方式移除小于最大建筑物的组件:
int min_size = int(points.size() / 2);
std::vector<TIN_with_info::Vertex_handle> to_remove;
for (TIN_with_info::Vertex_handle vh : tin_with_info.finite_vertex_handles())
TIN_with_info::Face_circulator circ = tin_with_info.incident_faces (vh),
start = circ;
// Remove a vertex if it's only adjacent to components smaller than threshold
bool keep = false;
if (circ->info() >= 0 && component_size[std::size_t(circ->info())] > min_size)
keep = true;
break;
while (++ circ != start);
if (!keep)
to_remove.push_back (vh);
std::cerr << to_remove.size() << " vertices(s) will be removed after filtering" << std::endl;
for (TIN_with_info::Vertex_handle vh : to_remove)
tin_with_info.remove (vh);
由于简单地移除大型建筑物中间留有孔洞,对于这些孔洞,使用孔填充算法填充对孔进行三角测量、细化和平整,以生成形状更好的网格数据模型。
以下代码段将 TIN 复制到网格中,同时过滤掉过大的面,然后识别孔并填充所有孔,除了最大的孔(即外壳)。
// Copy and keep track of overly large faces
Mesh dtm_mesh;
std::vector<Mesh::Face_index> face_selection;
Mesh::Property_map<Mesh::Face_index, bool> face_selection_map
= dtm_mesh.add_property_map<Mesh::Face_index, bool>("is_selected", false).first;
double limit = CGAL::square (5 * spacing);
CGAL::copy_face_graph (tin_with_info, dtm_mesh,
CGAL::parameters::face_to_face_output_iterator
(boost::make_function_output_iterator
([&](const std::pair<TIN_with_info::Face_handle, Mesh::Face_index>& ff)
double longest_edge = 0.;
bool border = false;
for (int i = 0; i < 3; ++ i)
longest_edge = (std::max)(longest_edge, CGAL::squared_distance
(ff.first->vertex((i+1)%3)->point(),
ff.first->vertex((i+2)%3)->point()));
TIN_with_info::Face_circulator circ
= tin_with_info.incident_faces (ff.first->vertex(i)),
start = circ;
if (tin_with_info.is_infinite (circ))
border = true;
break;
while (++ circ != start);
if (border)
break;
// Select if face is too big AND it's not
// on the border (to have closed holes)
if (!border && longest_edge > limit)
face_selection_map[ff.second] = true;
face_selection.push_back (ff.second);
})));
// Save original DTM
std::ofstream dtm_ofile ("dtm.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_ofile);
CGAL::IO::write_PLY (dtm_ofile, dtm_mesh);
dtm_ofile.close();
std::cerr << face_selection.size() << " face(s) are selected for removal" << std::endl;
// Expand face selection to keep a well formed 2-manifold mesh after removal
CGAL::expand_face_selection_for_removal (face_selection, dtm_mesh, face_selection_map);
face_selection.clear();
for (Mesh::Face_index fi : faces(dtm_mesh))
if (face_selection_map[fi])
face_selection.push_back(fi);
std::cerr << face_selection.size() << " face(s) are selected for removal after expansion" << std::endl;
for (Mesh::Face_index fi : face_selection)
CGAL::Euler::remove_face (halfedge(fi, dtm_mesh), dtm_mesh);
dtm_mesh.collect_garbage();
if (!dtm_mesh.is_valid())
std::cerr << "Invalid mesh!" << std::endl;
// Save filtered DTM
std::ofstream dtm_holes_ofile ("dtm_with_holes.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_holes_ofile);
CGAL::IO::write_PLY (dtm_holes_ofile, dtm_mesh);
dtm_holes_ofile.close();
// Get all holes
std::vector<Mesh::Halfedge_index> holes;
CGAL::Polygon_mesh_processing::extract_boundary_cycles (dtm_mesh, std::back_inserter (holes));
std::cerr << holes.size() << " hole(s) identified" << std::endl;
// Identify outer hull (hole with maximum size)
double max_size = 0.;
Mesh::Halfedge_index outer_hull;
for (Mesh::Halfedge_index hi : holes)
CGAL::Bbox_3 hole_bbox;
for (Mesh::Halfedge_index haf : CGAL::halfedges_around_face(hi, dtm_mesh))
const Point_3& p = dtm_mesh.point(target(haf, dtm_mesh));
hole_bbox += p.bbox();
double size = CGAL::squared_distance (Point_2(hole_bbox.xmin(), hole_bbox.ymin()),
Point_2(hole_bbox.xmax(), hole_bbox.ymax()));
if (size > max_size)
max_size = size;
outer_hull = hi;
// Fill all holes except the bigest (which is the outer hull of the mesh)
for (Mesh::Halfedge_index hi : holes)
if (hi != outer_hull)
CGAL::Polygon_mesh_processing::triangulate_refine_and_fair_hole
(dtm_mesh, hi, CGAL::Emptyset_iterator(), CGAL::Emptyset_iterator());
// Save DTM with holes filled
std::ofstream dtm_filled_ofile ("dtm_filled.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_filled_ofile);
CGAL::IO::write_PLY (dtm_filled_ofile, dtm_mesh);
dtm_filled_ofile.close();
各向同性网格重建也可以在最后一步才计算,从而生成不受二维 Delaunay 面形状约束的、更加规则的网格。
CGAL::Polygon_mesh_processing::isotropic_remeshing (faces(dtm_mesh), spacing, dtm_mesh);
std::ofstream dtm_remeshed_ofile ("dtm_remeshed.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_remeshed_ofile);
CGAL::IO::write_PLY (dtm_remeshed_ofile, dtm_mesh);
dtm_remeshed_ofile.close();
图 0.3显示了这些不同的步骤如何影响输出网格,图 0.4显示了 DTM 各向同性网格。
图 0.3原始 DTM 和填充后处理的DTM
图 0.4最终生成的 DTM
TIN 数据结构的每个三角形都含有重心,利用三角形重心、顶点信息进行空间插值和光栅化处理,可以生成栅格DEM。
因为孔填充、网格重建的步骤是在 3D 网格上执行的,所以用2.5维表示的DTM 是不再有效。因此,我们首先使用各向同性 DTM 网格的顶点重建 TIN。
以下使用代码段使用简单位图格式(PPM)生成栅格DEM。
CGAL::Bbox_3 bbox = CGAL::bbox_3 (points.points().begin(), points.points().end());
// Generate raster image 1920-pixels large
std::size_t width = 1920;
std::size_t height = std::size_t((bbox.ymax() - bbox.ymin()) * 1920 / (bbox.xmax() - bbox.xmin()));
std::cerr << "Rastering with resolution " << width << "x" << height << std::endl;
// Use PPM format (Portable PixMap) for simplicity
std::ofstream raster_ofile ("raster.ppm", std::ios_base::binary);
// PPM header
raster_ofile << "P6" << std::endl // magic number
<< width << " " << height << std::endl // dimensions of the image
<< 255 << std::endl; // maximum color value
// Use rainbow color ramp output
Color_ramp color_ramp;
// Keeping track of location from one point to its neighbor allows
// for fast locate in DT
TIN::Face_handle location;
// Query each pixel of the image
for (std::size_t y = 0; y < height; ++ y)
for (std::size_t x = 0; x < width; ++ x)
Point_3 query (bbox.xmin() + x * (bbox.xmax() - bbox.xmin()) / double(width),
bbox.ymin() + (height-y) * (bbox.ymax() - bbox.ymin()) / double(height),
0); // not relevant for location in 2D
location = dtm_clean.locate (query, location);
// Points outside the convex hull will be colored black
std::array<unsigned char, 3> colors { 0, 0, 0 };
if (!dtm_clean.is_infinite(location))
std::array<double, 3> barycentric_coordinates
= CGAL::Polygon_mesh_processing::barycentric_coordinates
(Point_2 (location->vertex(0)->point().x(), location->vertex(0)->point().y()),
Point_2 (location->vertex(1)->point().x(), location->vertex(1)->point().y()),
Point_2 (location->vertex(2)->point().x(), location->vertex(2)->point().y()),
Point_2 (query.x(), query.y()),
Kernel());
double height_at_query
= (barycentric_coordinates[0] * location->vertex(0)->point().z()
+ barycentric_coordinates[1] * location->vertex(1)->point().z()
+ barycentric_coordinates[2] * location->vertex(2)->point().z());
// Color ramp generates a color depending on a value from 0 to 1
double height_ratio = (height_at_query - bbox.zmin()) / (bbox.zmax() - bbox.zmin());
colors = color_ramp.get(height_ratio);
raster_ofile.write (reinterpret_cast<char*>(&colors), 3);
raster_ofile.close();
图 0.5给出了彩色栅格DEM表示示例。
在 TIN 上提取等高线可以使用 CGAL 完成的另一个应用程序。我们在这里演示如何提取等高线来构建DEM。
boost::adjacency_list结构并跟踪从端点位置到图顶点的映射。
以下代码计算最小和最大高度差的50 个等值,并创建一个包含所有等值的图形:
std::array<double, 50> isovalues; // Contour 50 isovalues
for (std::size_t i = 0; i < isovalues.size(); ++ i)
isovalues[i] = bbox.zmin() + ((i+1) * (bbox.zmax() - bbox.zmin()) / (isovalues.size() - 2));
// First find on each face if they are crossed by some isovalues and
// extract segments in a graph
using Segment_graph = boost::adjacency_list<boost::listS, boost::vecS, boost::undirectedS, Point_3>;
Segment_graph graph;
using Map_p2v = std::map<Point_3, Segment_graph::vertex_descriptor>;
Map_p2v map_p2v;
for (TIN::Face_handle vh : dtm_clean.finite_face_handles())
for (double iv : isovalues)
if (face_has_isovalue (vh, iv))
Segment_3 segment = isocontour_in_face (vh, iv);
for (const Point_3& p : { segment.source(), segment.target() })
// Only insert end points of segments once to get a well connected graph
Map_p2v::iterator iter;
bool inserted;
std::tie (iter, inserted) = map_p2v.insert (std::make_pair (p, Segment_graph::vertex_descriptor()));
if (inserted)
iter->second = boost::add_vertex (graph);
graph[iter->second] = p;
boost::add_edge (map_p2v[segment.source()], map_p2v[segment.target()], graph);
创建图形后,可以使用CGAL::split_graph_into_polylines() 函数轻松地将其拆分为折线。
// Split segments into polylines
std::vector<std::vector<Point_3> > polylines;
Polylines_visitor<Segment_graph> visitor (graph, polylines);
CGAL::split_graph_into_polylines (graph, visitor);
std::cerr << polylines.size() << " polylines computed, with "
<< map_p2v.size() << " vertices in total" << std::endl;
// Output to WKT file
std::ofstream contour_ofile ("contour.wkt");
contour_ofile.precision(18);
CGAL::IO::write_multi_linestring_WKT (contour_ofile, polylines);
contour_ofile.close();
该函数需要提供一个可供访问的对象,以便在开始折线、向其添加点以及结束它时调用。我们可以使用定义一个简单的类使用它。
template <typename Graph>
class Polylines_visitor
private:
std::vector<std::vector<Point_3> >& polylines;
Graph& graph;
public:
Polylines_visitor (Graph& graph, std::vector<std::vector<Point_3> >& polylines)
: polylines (polylines), graph(graph) { }
void start_new_polyline()
polylines.push_back (std::vector<Point_3>());
void add_node (typename Graph::vertex_descriptor vd)
polylines.back().push_back (graph[vd]);
void end_polyline()
// filter small polylines
if (polylines.back().size() < 50)
polylines.pop_back();
6.3 等高线简化
由于输出噪声很大,用户可能希望简化折线。CGAL提供了折线简化算法,保证简化后两条折线不会相交。该算法利用CGAL::Constrained_triangulation_plus_2,将折线嵌入为一组约束:
namespace PS = CGAL::Polyline_simplification_2;
using CDT_vertex_base = PS::Vertex_base_2<Projection_traits>;
using CDT_face_base = CGAL::Constrained_triangulation_face_base_2<Projection_traits>;
using CDT_TDS = CGAL::Triangulation_data_structure_2<CDT_vertex_base, CDT_face_base>;
using CDT = CGAL::Constrained_Delaunay_triangulation_2<Projection_traits, CDT_TDS>;
using CTP = CGAL::Constrained_triangulation_plus_2<CDT>;
以下代码使用原始多段线的平方距离简化了多段线集。算法停止条件为:当没有更多顶点可以被移除且不超过平均间距4 倍的时候停止。
// Construct constrained Delaunay triangulation with polylines as constraints
CTP ctp;
for (const std::vector<Point_3>& poly : polylines)
ctp.insert_constraint (poly.begin(), poly.end());
// Simplification algorithm with limit on distance
PS::simplify (ctp, PS::Squared_distance_cost(), PS::Stop_above_cost_threshold (16 * spacing * spacing));
polylines.clear();
for (CTP::Constraint_id cid : ctp.constraints())
polylines.push_back (std::vector<Point_3>());
polylines.back().reserve (ctp.vertices_in_constraint (cid).size());
for (CTP::Vertex_handle vh : ctp.vertices_in_constraint(cid))
polylines.back().push_back (vh->point());
std::size_t nb_vertices
= std::accumulate (polylines.begin(), polylines.end(), 0u,
[](std::size_t size, const std::vector<Point_3>& poly) -> std::size_t
{ return size + poly.size(); });
std::cerr << nb_vertices
<< " vertices remaining after simplification ("
<< 100. * (nb_vertices / double(map_p2v.size())) << "%)" << std::endl;
// Output to WKT file
std::ofstream simplified_ofile ("simplified.wkt");
simplified_ofile.precision(18);
CGAL::IO::write_multi_linestring_WKT (simplified_ofile, polylines);
simplified_ofile.close();
图 0.6给出了等高线和简化的例子。
图 0.6使用 50 个等值均匀分布的轮廓。顶部:使用 148k 顶点和简化的原始轮廓,其公差等于输入点云的平均间距(剩余原始折线顶点的 3.4%)。底部:公差为平均间距的 4 倍(剩余顶点的 1.3%)和平均间距的 10 倍(剩余顶点的 0.9%)的简化。折线在所有情况下都没有交叉点。
7 点云数据分类
CGAL 提供了一个包分类算法,可用于将点云分割成用户定义的标签集。目前在 CGAL 中可用的最先进的分类器是来自 ETHZ 的随机森林。由于它是一个有监督的分类器,因此需要一个训练集。
以下代码段显示了如何使用一些手动选择的训练集来训练随机森林分类器并计算由图切割算法正则化的分类:
// Get training from input
Point_set::Property_map<int> training_map;
bool training_found;
std::tie (training_map, training_found) = points.property_map<int>("training");
if (training_found)
std::cerr << "Classifying ground/vegetation/building" << std::endl;
// Create labels
Classification::Label_set labels ({ "ground", "vegetation", "building" });
// Generate features
Classification::Feature_set features;
Classification::Point_set_feature_generator<Kernel, Point_set, Point_set::Point_map>
generator (points, points.point_map(), 5); // 5 scales
#ifdef CGAL_LINKED_WITH_TBB
// If TBB is used, features can be computed in parallel
features.begin_parallel_additions();
generator.generate_point_based_features (features);
features.end_parallel_additions();
#else
generator.generate_point_based_features (features);
#endif
// Train a random forest classifier
Classification::ETHZ::Random_forest_classifier classifier (labels, features);
classifier.train (points.range(training_map));
// Classify with graphcut regularization
Point_set::Property_map<int> label_map = points.add_property_map<int>("labels").first;
Classification::classify_with_graphcut<Concurrency_tag>
(points, points.point_map(), labels, classifier,
generator.neighborhood().k_neighbor_query(12), // regularize on 12-neighbors graph
0.5f, // graphcut weight
12, // Subdivide to speed-up process
label_map);
// Evaluate
std::cerr << "Mean IoU on training data = "
<< Classification::Evaluation(labels,
points.range(training_map),
points.range(label_map)).mean_intersection_over_union() << std::endl;
// Save the classified point set
std::ofstream classified_ofile ("classified.ply");
CGAL::IO::set_binary_mode (classified_ofile);
classified_ofile << points;
classified_ofile.close();
图 0.7给出了训练集和结果分类的示例。
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Projection_traits_xy_3.h>
#include <CGAL/Delaunay_triangulation_2.h>
#include <CGAL/Triangulation_vertex_base_with_info_2.h>
#include <CGAL/Triangulation_face_base_with_info_2.h>
#include <CGAL/boost/graph/graph_traits_Delaunay_triangulation_2.h>
#include <CGAL/boost/graph/copy_face_graph.h>
#include <CGAL/Point_set_3.h>
#include <CGAL/Point_set_3/IO.h>
#include <CGAL/compute_average_spacing.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Polygon_mesh_processing/locate.h>
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/remesh.h>
#include <boost/graph/adjacency_list.hpp>
#include <CGAL/boost/graph/split_graph_into_polylines.h>
#include <CGAL/IO/WKT.h>
#include <CGAL/Constrained_Delaunay_triangulation_2.h>
#include <CGAL/Constrained_triangulation_plus_2.h>
#include <CGAL/Polyline_simplification_2/simplify.h>
#include <CGAL/Polyline_simplification_2/Squared_distance_cost.h>
#include <CGAL/Classification.h>
#include <CGAL/Random.h>
#include <fstream>
#include <queue>
#include "include/Color_ramp.h"
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using Projection_traits = CGAL::Projection_traits_xy_3<Kernel>;
using Point_2 = Kernel::Point_2;
using Point_3 = Kernel::Point_3;
using Segment_3 = Kernel::Segment_3;
// Triangulated Irregular Network
using TIN = CGAL::Delaunay_triangulation_2<Projection_traits>;
// Triangulated Irregular Network (with info)
using Point_set = CGAL::Point_set_3<Point_3>;
using Vbi = CGAL::Triangulation_vertex_base_with_info_2 <Point_set::Index, Projection_traits>;
using Fbi = CGAL::Triangulation_face_base_with_info_2<int, Projection_traits>;
using TDS = CGAL::Triangulation_data_structure_2<Vbi, Fbi>;
using TIN_with_info = CGAL::Delaunay_triangulation_2<Projection_traits, TDS>;
namespace Classification = CGAL::Classification;
#ifdef CGAL_LINKED_WITH_TBB
using Concurrency_tag = CGAL::Parallel_tag;
#else
using Concurrency_tag = CGAL::Sequential_tag;
#endif
bool face_has_isovalue (TIN::Face_handle fh, double isovalue)
bool above = false, below = false;
for (int i = 0; i < 3; ++ i)
// Face has isovalue if one of its vertices is above and another
// one below
if (fh->vertex(i)->point().z() > isovalue)
above = true;
if (fh->vertex(i)->point().z() < isovalue)
below = true;
return (above && below);
Segment_3 isocontour_in_face (TIN::Face_handle fh, double isovalue)
Point_3 source;
Point_3 target;
bool source_found = false;
for (int i = 0; i < 3; ++ i)
Point_3 p0 = fh->vertex((i+1) % 3)->point();
Point_3 p1 = fh->vertex((i+2) % 3)->point();
// Check if the isovalue crosses segment (p0,p1)
if ((p0.z() - isovalue) * (p1.z() - isovalue) > 0)
continue;
double zbottom = p0.z();
double ztop = p1.z();
if (zbottom > ztop)
std::swap (zbottom, ztop);
std::swap (p0, p1);
// Compute position of segment vertex
double ratio = (isovalue - zbottom) / (ztop - zbottom);
Point_3 p = CGAL::barycenter (p0, (1 - ratio), p1,ratio);
if (source_found)
target = p;
source = p;
source_found = true;
return Segment_3 (source, target);
template <typename Graph>
class Polylines_visitor
private:
std::vector<std::vector<Point_3> >& polylines;
Graph& graph;
public:
Polylines_visitor (Graph& graph, std::vector<std::vector<Point_3> >& polylines)
: polylines (polylines), graph(graph) { }
void start_new_polyline()
polylines.push_back (std::vector<Point_3>());
void add_node (typename Graph::vertex_descriptor vd)
polylines.back().push_back (graph[vd]);
void end_polyline()
// filter small polylines
if (polylines.back().size() < 50)
polylines.pop_back();
namespace PS = CGAL::Polyline_simplification_2;
using CDT_vertex_base = PS::Vertex_base_2<Projection_traits>;
using CDT_face_base = CGAL::Constrained_triangulation_face_base_2<Projection_traits>;
using CDT_TDS = CGAL::Triangulation_data_structure_2<CDT_vertex_base, CDT_face_base>;
using CDT = CGAL::Constrained_Delaunay_triangulation_2<Projection_traits, CDT_TDS>;
using CTP = CGAL::Constrained_triangulation_plus_2<CDT>;
int main (int argc, char** argv)
const std::string fname = argc != 2 ? CGAL::data_file_path("points_3/b9_training.ply") : argv[1];
if (argc != 2)
std::cerr << "Usage: " << argv[0] << " points.ply" << std::endl;
std::cerr << "Running with default value " << fname << "\n";
// Read points
std::ifstream ifile (fname, std::ios_base::binary);
CGAL::Point_set_3<Point_3> points;
ifile >> points;
std::cerr << points.size() << " point(s) read" << std::endl;
// Create DSM
TIN dsm (points.points().begin(), points.points().end());
using Mesh = CGAL::Surface_mesh<Point_3>;
Mesh dsm_mesh;
CGAL::copy_face_graph (dsm, dsm_mesh);
std::ofstream dsm_ofile ("dsm.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dsm_ofile);
CGAL::IO::write_PLY (dsm_ofile, dsm_mesh);
dsm_ofile.close();
auto idx_to_point_with_info
= [&](const Point_set::Index& idx) -> std::pair<Point_3, Point_set::Index>
return std::make_pair (points.point(idx), idx);
TIN_with_info tin_with_info
(boost::make_transform_iterator (points.begin(), idx_to_point_with_info),
boost::make_transform_iterator (points.end(), idx_to_point_with_info));
double spacing = CGAL::compute_average_spacing<Concurrency_tag>(points, 6);
spacing *= 2;
auto face_height
= [&](const TIN_with_info::Face_handle fh) -> double
double out = 0.;
for (int i = 0; i < 3; ++ i)
out = (std::max) (out, CGAL::abs(fh->vertex(i)->point().z() - fh->vertex((i+1)%3)->point().z()));
return out;
// Initialize faces info
for (TIN_with_info::Face_handle fh : tin_with_info.all_face_handles())
if (tin_with_info.is_infinite(fh) || face_height(fh) > spacing) // Filtered faces are given info() = -2
fh->info() = -2;
else // Pending faces are given info() = -1;
fh->info() = -1;
// Flooding algorithm
std::vector<int> component_size;
for (TIN_with_info::Face_handle fh : tin_with_info.finite_face_handles())
if (fh->info() != -1)
continue;
std::queue<TIN_with_info::Face_handle> todo;
todo.push(fh);
int size = 0;
while (!todo.empty())
TIN_with_info::Face_handle current = todo.front();
todo.pop();
if (current->info() != -1)
continue;
current->info() = int(component_size.size());
++ size;
for (int i = 0; i < 3; ++ i)
todo.push (current->neighbor(i));
component_size.push_back (size);
std::cerr << component_size.size() << " connected component(s) found" << std::endl;
Mesh tin_colored_mesh;
Mesh::Property_map<Mesh::Face_index, CGAL::IO::Color>
color_map = tin_colored_mesh.add_property_map<Mesh::Face_index, CGAL::IO::Color>("f:color").first;
CGAL::copy_face_graph (tin_with_info, tin_colored_mesh,
CGAL::parameters::face_to_face_output_iterator
(boost::make_function_output_iterator
([&](const std::pair<TIN_with_info::Face_handle, Mesh::Face_index>& ff)
// Color unassigned faces gray
if (ff.first->info() < 0)
color_map[ff.second] = CGAL::IO::Color(128, 128, 128);
// Random color seeded by the component ID
CGAL::Random r (ff.first->info());
color_map[ff.second] = CGAL::IO::Color (r.get_int(64, 192),
r.get_int(64, 192),
r.get_int(64, 192));
})));
std::ofstream tin_colored_ofile ("colored_tin.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (tin_colored_ofile);
CGAL::IO::write_PLY (tin_colored_ofile, tin_colored_mesh);
tin_colored_ofile.close();
int min_size = int(points.size() / 2);
std::vector<TIN_with_info::Vertex_handle> to_remove;
for (TIN_with_info::Vertex_handle vh : tin_with_info.finite_vertex_handles())
TIN_with_info::Face_circulator circ = tin_with_info.incident_faces (vh),
start = circ;
// Remove a vertex if it's only adjacent to components smaller than threshold
bool keep = false;
if (circ->info() >= 0 && component_size[std::size_t(circ->info())] > min_size)
keep = true;
break;
while (++ circ != start);
if (!keep)
to_remove.push_back (vh);
std::cerr << to_remove.size() << " vertices(s) will be removed after filtering" << std::endl;
for (TIN_with_info::Vertex_handle vh : to_remove)
tin_with_info.remove (vh);
// Copy and keep track of overly large faces
Mesh dtm_mesh;
std::vector<Mesh::Face_index> face_selection;
Mesh::Property_map<Mesh::Face_index, bool> face_selection_map
= dtm_mesh.add_property_map<Mesh::Face_index, bool>("is_selected", false).first;
double limit = CGAL::square (5 * spacing);
CGAL::copy_face_graph (tin_with_info, dtm_mesh,
CGAL::parameters::face_to_face_output_iterator
(boost::make_function_output_iterator
([&](const std::pair<TIN_with_info::Face_handle, Mesh::Face_index>& ff)
double longest_edge = 0.;
bool border = false;
for (int i = 0; i < 3; ++ i)
longest_edge = (std::max)(longest_edge, CGAL::squared_distance
(ff.first->vertex((i+1)%3)->point(),
ff.first->vertex((i+2)%3)->point()));
TIN_with_info::Face_circulator circ
= tin_with_info.incident_faces (ff.first->vertex(i)),
start = circ;
if (tin_with_info.is_infinite (circ))
border = true;
break;
while (++ circ != start);
if (border)
break;
// Select if face is too big AND it's not
// on the border (to have closed holes)
if (!border && longest_edge > limit)
face_selection_map[ff.second] = true;
face_selection.push_back (ff.second);
})));
// Save original DTM
std::ofstream dtm_ofile ("dtm.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_ofile);
CGAL::IO::write_PLY (dtm_ofile, dtm_mesh);
dtm_ofile.close();
std::cerr << face_selection.size() << " face(s) are selected for removal" << std::endl;
// Expand face selection to keep a well formed 2-manifold mesh after removal
CGAL::expand_face_selection_for_removal (face_selection, dtm_mesh, face_selection_map);
face_selection.clear();
for (Mesh::Face_index fi : faces(dtm_mesh))
if (face_selection_map[fi])
face_selection.push_back(fi);
std::cerr << face_selection.size() << " face(s) are selected for removal after expansion" << std::endl;
for (Mesh::Face_index fi : face_selection)
CGAL::Euler::remove_face (halfedge(fi, dtm_mesh), dtm_mesh);
dtm_mesh.collect_garbage();
if (!dtm_mesh.is_valid())
std::cerr << "Invalid mesh!" << std::endl;
// Save filtered DTM
std::ofstream dtm_holes_ofile ("dtm_with_holes.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_holes_ofile);
CGAL::IO::write_PLY (dtm_holes_ofile, dtm_mesh);
dtm_holes_ofile.close();
// Get all holes
std::vector<Mesh::Halfedge_index> holes;
CGAL::Polygon_mesh_processing::extract_boundary_cycles (dtm_mesh, std::back_inserter (holes));
std::cerr << holes.size() << " hole(s) identified" << std::endl;
// Identify outer hull (hole with maximum size)
double max_size = 0.;
Mesh::Halfedge_index outer_hull;
for (Mesh::Halfedge_index hi : holes)
CGAL::Bbox_3 hole_bbox;
for (Mesh::Halfedge_index haf : CGAL::halfedges_around_face(hi, dtm_mesh))
const Point_3& p = dtm_mesh.point(target(haf, dtm_mesh));
hole_bbox += p.bbox();
double size = CGAL::squared_distance (Point_2(hole_bbox.xmin(), hole_bbox.ymin()),
Point_2(hole_bbox.xmax(), hole_bbox.ymax()));
if (size > max_size)
max_size = size;
outer_hull = hi;
// Fill all holes except the bigest (which is the outer hull of the mesh)
for (Mesh::Halfedge_index hi : holes)
if (hi != outer_hull)
CGAL::Polygon_mesh_processing::triangulate_refine_and_fair_hole
(dtm_mesh, hi, CGAL::Emptyset_iterator(), CGAL::Emptyset_iterator());
// Save DTM with holes filled
std::ofstream dtm_filled_ofile ("dtm_filled.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_filled_ofile);
CGAL::IO::write_PLY (dtm_filled_ofile, dtm_mesh);
dtm_filled_ofile.close();
CGAL::Polygon_mesh_processing::isotropic_remeshing (faces(dtm_mesh), spacing, dtm_mesh);
std::ofstream dtm_remeshed_ofile ("dtm_remeshed.ply", std::ios_base::binary);
CGAL::IO::set_binary_mode (dtm_remeshed_ofile);
CGAL::IO::write_PLY (dtm_remeshed_ofile, dtm_mesh);
dtm_remeshed_ofile.close();
TIN dtm_clean (dtm_mesh.points().begin(), dtm_mesh.points().end());
CGAL::Bbox_3 bbox = CGAL::bbox_3 (points.points().begin(), points.points().end());
// Generate raster image 1920-pixels large
std::size_t width = 1920;
std::size_t height = std::size_t((bbox.ymax() - bbox.ymin()) * 1920 / (bbox.xmax() - bbox.xmin()));
std::cerr << "Rastering with resolution " << width << "x" << height << std::endl;
// Use PPM format (Portable PixMap) for simplicity
std::ofstream raster_ofile ("raster.ppm", std::ios_base::binary);
// PPM header
raster_ofile << "P6" << std::endl // magic number
<< width << " " << height << std::endl // dimensions of the image
<< 255 << std::endl; // maximum color value
// Use rainbow color ramp output
Color_ramp color_ramp;
// Keeping track of location from one point to its neighbor allows
// for fast locate in DT
TIN::Face_handle location;
// Query each pixel of the image
for (std::size_t y = 0; y < height; ++ y)
for (std::size_t x = 0; x < width; ++ x)
Point_3 query (bbox.xmin() + x * (bbox.xmax() - bbox.xmin()) / double(width),
bbox.ymin() + (height-y) * (bbox.ymax() - bbox.ymin()) / double(height),
0); // not relevant for location in 2D
location = dtm_clean.locate (query, location);
// Points outside the convex hull will be colored black
std::array<unsigned char, 3> colors { 0, 0, 0 };
if (!dtm_clean.is_infinite(location))
std::array<double, 3> barycentric_coordinates
= CGAL::Polygon_mesh_processing::barycentric_coordinates
(Point_2 (location->vertex(0)->point().x(), location->vertex(0)->point().y()),
Point_2 (location->vertex(1)->point().x(), location->vertex(1)->point().y()),
Point_2 (location->vertex(2)->point().x(), location->vertex(2)->point().y()),
Point_2 (query.x(), query.y()),
Kernel());
double height_at_query
= (barycentric_coordinates[0] * location->vertex(0)->point().z()
+ barycentric_coordinates[1] * location->vertex(1)->point().z()
+ barycentric_coordinates[2] * location->vertex(2)->point().z());
// Color ramp generates a color depending on a value from 0 to 1
double height_ratio = (height_at_query - bbox.zmin()) / (bbox.zmax() - bbox.zmin());
colors = color_ramp.get(height_ratio);
raster_ofile.write (reinterpret_cast<char*>(&colors), 3);
raster_ofile.close();
// Smooth heights with 5 successive Gaussian filters
double gaussian_variance = 4 * spacing * spacing;
for (TIN::Vertex_handle vh : dtm_clean.finite_vertex_handles())
double z = vh->point().z();
double total_weight = 1;
TIN::Vertex_circulator circ = dtm_clean.incident_vertices (vh),
start = circ;
if (!dtm_clean.is_infinite(circ))
double sq_dist = CGAL::squared_distance (vh->point(), circ->point());
double weight = std::exp(- sq_dist / gaussian_variance);
z += weight * circ->point().z();
total_weight += weight;
while (++ circ != start);
z /= total_weight;
vh->point() = Point_3 (vh->point().x(), vh->point().y(), z);
std::array<double, 50> isovalues; // Contour 50 isovalues
for (std::size_t i = 0; i < isovalues.size(); ++ i)
isovalues[i] = bbox.zmin() + ((i+1) * (bbox.zmax() - bbox.zmin()) / (isovalues.size() - 2));
// First find on each face if they are crossed by some isovalues and
// extract segments in a graph
using Segment_graph = boost::adjacency_list<boost::listS, boost::vecS, boost::undirectedS, Point_3>;
Segment_graph graph;
using Map_p2v = std::map<Point_3, Segment_graph::vertex_descriptor>;
Map_p2v map_p2v;
for (TIN::Face_handle vh : dtm_clean.finite_face_handles())
for (double iv : isovalues)
if (face_has_isovalue (vh, iv))
Segment_3 segment = isocontour_in_face (vh, iv);
for (const Point_3& p : { segment.source(), segment.target() })
// Only insert end points of segments once to get a well connected graph
Map_p2v::iterator iter;
bool inserted;
std::tie (iter, inserted) = map_p2v.insert (std::make_pair (p, Segment_graph::vertex_descriptor()));
if (inserted)
iter->second = boost::add_vertex (graph);
graph[iter->second] = p;
boost::add_edge (map_p2v[segment.source()], map_p2v[segment.target()], graph);
// Split segments into polylines
std::vector<std::vector<Point_3> > polylines;
Polylines_visitor<Segment_graph> visitor (graph, polylines);
CGAL::split_graph_into_polylines (graph, visitor);
std::cerr << polylines.size() << " polylines computed, with "
<< map_p2v.size() << " vertices in total" << std::endl;
// Output to WKT file
std::ofstream contour_ofile ("contour.wkt");
contour_ofile.precision(18);
CGAL::IO::write_multi_linestring_WKT (contour_ofile, polylines);
contour_ofile.close();
// Construct constrained Delaunay triangulation with polylines as constraints
CTP ctp;
for (const std::vector<Point_3>& poly : polylines)
ctp.insert_constraint (poly.begin(), poly.end());
// Simplification algorithm with limit on distance
PS::simplify (ctp, PS::Squared_distance_cost(), PS::Stop_above_cost_threshold (16 * spacing * spacing));
polylines.clear();
for (CTP::Constraint_id cid : ctp.constraints())
polylines.push_back (std::vector<Point_3>());
polylines.back().reserve (ctp.vertices_in_constraint (cid).size());
for (CTP::Vertex_handle vh : ctp.vertices_in_constraint(cid))
polylines.back().push_back (vh->point());
std::size_t nb_vertices
= std::accumulate (polylines.begin(), polylines.end(), 0u,
[](std::size_t size, const std::vector<Point_3>& poly) -> std::size_t
{ return size + poly.size(); });
std::cerr << nb_vertices
<< " vertices remaining after simplification ("
<< 100. * (nb_vertices / double(map_p2v.size())) << "%)" << std::endl;
// Output to WKT file
std::ofstream simplified_ofile ("simplified.wkt");
simplified_ofile.precision(18);
CGAL::IO::write_multi_linestring_WKT (simplified_ofile, polylines);
simplified_ofile.close();
// Get training from input
Point_set::Property_map<int> training_map;
bool training_found;
std::tie (training_map, training_found) = points.property_map<int>("training");
if (training_found)
std::cerr << "Classifying ground/vegetation/building" << std::endl;
// Create labels
Classification::Label_set labels ({ "ground", "vegetation", "building" });
// Generate features
Classification::Feature_set features;
Classification::Point_set_feature_generator<Kernel, Point_set, Point_set::Point_map>
generator (points, points.point_map(), 5); // 5 scales
#ifdef CGAL_LINKED_WITH_TBB
// If TBB is used, features can be computed in parallel
features.begin_parallel_additions();
generator.generate_point_based_features (features);
features.end_parallel_additions();
#else
generator.generate_point_based_features (features);
#endif
// Train a random forest classifier
Classification::ETHZ::Random_forest_classifier classifier (labels, features);
classifier.train (points.range(training_map));
// Classify with graphcut regularization
Point_set::Property_map<int> label_map = points.add_property_map<int>("labels").first;
Classification::classify_with_graphcut<Concurrency_tag>
(points, points.point_map(), labels, classifier,
generator.neighborhood().k_neighbor_query(12), // regularize on 12-neighbors graph
0.5f, // graphcut weight
12, // Subdivide to speed-up process
label_map);
// Evaluate
std::cerr << "Mean IoU on training data = "
<< Classification::Evaluation(labels,
points.range(training_map),
points.range(label_map)).mean_intersection_over_union() << std::endl;
// Save the classified point set
std::ofstream classified_ofile ("classified.ply");
CGAL::IO::set_binary_mode (classified_ofile);
classified_ofile << points;
classified_ofile.close();
return EXIT_SUCCESS;
Pix4dmapper是一款集全自动、快速、专业精度为一体的无人机
数据和航空影像
数据处理软件。无需专业知识,无需人工干预,即可将数千张影像快速制作成专业的、精确的二维地图和三维模型。
本文讲解采用Pix4dmapper摄影测量内业
数据处理完整流程,主要步骤包括:新建项目、添加影像、编辑相机文件、输出坐标系、自由空三、导入像控点、刺像控点、高精度平差处理、
生成点云纹理、
生成DSM和DOM等。
一、无人机航空摄影测量内业
数据准备
二、Pix4dmapper摄影测量内业
数据处理完整流程
CGAL或计算几何算法库是一个C ++库,可让您轻松访问各种高效而可靠的几何算法。这些算法可用于广泛的应用程序,包括计算机辅助设计,机器人技术,分子生物学,医学成像,地理信息系统等。
CGAL具有广泛的数据结构和算法,包括Voronoi图,单元格复合体和多面体,三角剖分,曲线排列,表面和体积网格生成,空间搜索,alpha形状,几何处理等等。这些的使用导致美观,视觉复杂和准确的表示。
算术和代数
组合算法
凸包算法
细胞复合物和多面体
```c++
#include <
CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <
CGAL/intersections.h>
#include <vector>
#include <iostream>
typedef
CGAL::Exact_predicates_exact_constructions_kernel K;
typedef K::Point_3 Point;
typedef K::Triangle_3 Triangle;
int main()
// 输入两组
点云数据
std::vector<Point> points1 = {Point(, , ), Point(1, , ), Point(, 1, )};
std::vector<Point> points2 = {Point(.5, .5, ), Point(1.5, .5, ), Point(.5, 1.5, )};
// 构造三角形
Triangle tri1(points1[], points1[1], points1[2]);
Triangle tri2(points2[], points2[1], points2[2]);
// 计算交集
auto result =
CGAL::intersection(tri1, tri2);
if (result) {
// 如果有交集,输出交集面积
std::cout << "交集面积为:" << result->area() << std::endl;
} else {
std::cout << "两者没有交集" << std::endl;
return ;
这个代码使用了
CGAL 的精确计算库,可以保证计算结果的精度。