1 // Copyright (c) 2018 Liangliang Nan. All rights reserved. 2 // 3 // This file is part of CGAL (www.cgal.org) 4 // 5 // $URL: https://github.com/CGAL/cgal/blob/v5.3/Polygonal_surface_reconstruction/include/CGAL/Polygonal_surface_reconstruction.h $ 6 // $Id: Polygonal_surface_reconstruction.h 0779373 2020-03-26T13:31:46+01:00 Sébastien Loriot 7 // SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial 8 // 9 // Author(s) : Liangliang Nan 10 11 #ifndef CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H 12 #define CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H 13 14 #include <CGAL/license/Polygonal_surface_reconstruction.h> 15 16 #include <CGAL/bounding_box.h> 17 #include <CGAL/property_map.h> 18 #include <CGAL/internal/hypothesis.h> 19 #include <CGAL/internal/compute_confidences.h> 20 #include <CGAL/internal/point_set_with_planes.h> 21 22 #include <unordered_map> 23 24 /*! 25 \file Polygonal_surface_reconstruction.h 26 */ 27 28 namespace CGAL { 29 30 /*! 31 \ingroup PkgPolygonalSurfaceReconstruction 32 33 \brief 34 35 Implementation of the Polygonal Surface Reconstruction method. 36 37 Given a set of 3D points with unoriented normals sampled 38 from the outer boundary of a piecewise planar object, the Polygonal Surface 39 Reconstruction method \cgalCite{nan2017polyfit} outputs a simplified and 40 watertight surface mesh interpolating the input point set. 41 42 The method first generates a set of face candidates by intersecting the planar 43 primitives. Then an optimal subset of the candidate faces is selected through 44 optimization under hard constraints that enforce the final model to be manifold 45 and watertight. 46 47 The reconstruction assumes the planar segmentation of the point cloud is 48 provided in the input. 49 50 \tparam GeomTraits a geometric traits class, model of Kernel 51 */ 52 template <class GeomTraits> 53 class Polygonal_surface_reconstruction 54 { 55 public: 56 57 /// \name Types 58 59 typedef typename GeomTraits::FT FT; ///< number type. 60 typedef typename GeomTraits::Point_3 Point; ///< point type. 61 typedef typename GeomTraits::Vector_3 Vector; ///< vector type. 62 typedef typename GeomTraits::Plane_3 Plane; ///< plane type. 63 64 private: 65 66 // Polygon mesh storing the candidate faces 67 typedef CGAL::Surface_mesh<Point> Polygon_mesh; 68 69 typedef typename Polygon_mesh::Face_index Face_descriptor; 70 typedef typename Polygon_mesh::Vertex_index Vertex_descriptor; 71 typedef typename Polygon_mesh::Edge_index Edge_descriptor; 72 typedef typename Polygon_mesh::Halfedge_index Halfedge_descriptor; 73 74 // Public methods 75 public: 76 77 /// \name Creation 78 79 /*! 80 Creates a Polygonal Surface Reconstruction object. 81 After construction, candidate faces are generated and point/face confidence values are 82 computed, allowing to reuse them in the subsequent reconstruction step with different parameters. 83 84 \tparam PointRange is the range of input points, model of `ConstRange`. 85 \tparam PointMap is a model of `ReadablePropertyMap` with value type `GeomTraits::Point_3`. 86 \tparam NormalMap is a model of `ReadablePropertyMap` with value type `GeomTraits::Vector_3`. 87 \tparam IndexMap is a model of `ReadablePropertyMap` with value type `int`. 88 89 \param points range of input points. 90 \param point_map property map: value_type of `typename PointRange::const_iterator` -> `Point_3` 91 \param normal_map property map: value_type of `typename PointRange::const_iterator` -> `Vector_3` 92 \param index_map property map: value_type of `typename PointRange::const_iterator` -> `int`, 93 denoting the index of the plane it belongs to (-1 if the point is not assigned to a plane) 94 */ 95 template < 96 typename PointRange, 97 typename PointMap, 98 typename NormalMap, 99 typename IndexMap 100 > 101 Polygonal_surface_reconstruction( 102 const PointRange& points, 103 PointMap point_map, 104 NormalMap normal_map, 105 IndexMap index_map 106 ); 107 108 109 /// \name Operations 110 111 /** Reconstructs a watertight polygonal mesh model. 112 113 \tparam MixedIntegerProgramTraits a model of `MixedIntegerProgramTraits` 114 115 \tparam PolygonMesh a model of `MutableFaceGraph` 116 117 \return `true` if the reconstruction succeeded, `false` otherwise. 118 */ 119 template <typename MixedIntegerProgramTraits, typename PolygonMesh> 120 bool reconstruct( 121 PolygonMesh& output_mesh, ///< the final reconstruction result 122 double wt_fitting = 0.43, ///< weight for the data fitting term. 123 double wt_coverage = 0.27, ///< weight for the point coverage term. 124 double wt_complexity = 0.30 ///< weight for the model complexity term. 125 ); 126 127 /*! Gives the user the possibility to access the intermediate candidate faces 128 (i.e., the faces induced by the intersection of the supporting planes). 129 \tparam PolygonMesh a model of `MutableFaceGraph`. 130 */ 131 template <typename PolygonMesh> 132 void output_candidate_faces(PolygonMesh& candidate_faces) const; 133 134 /// Gets the error message (if reconstruction failed). error_message()135 const std::string& error_message() const { return error_message_; } 136 137 // Data members. 138 private: 139 internal::Hypothesis<GeomTraits> hypothesis_; 140 141 // The generated candidate faces stored as a polygon mesh 142 Polygon_mesh candidate_faces_; 143 144 std::string error_message_; 145 146 private: // Copying is not allowed 147 Polygonal_surface_reconstruction(const Polygonal_surface_reconstruction& psr); 148 149 }; // end of Polygonal_surface_reconstruction 150 151 152 //////////////////////////////////////////////////////////////////////////s 153 154 // implementations 155 156 template <class GeomTraits> 157 158 template < 159 typename PointRange, 160 typename PointMap, 161 typename NormalMap, 162 typename IndexMap 163 > Polygonal_surface_reconstruction(const PointRange & points,PointMap point_map,NormalMap normal_map,IndexMap index_map)164 Polygonal_surface_reconstruction<GeomTraits>::Polygonal_surface_reconstruction( 165 const PointRange& points, 166 PointMap point_map, 167 NormalMap normal_map, 168 IndexMap index_map 169 ) : error_message_("") 170 { 171 if (points.empty()) { 172 error_message_ = "empty input points"; 173 return; 174 } 175 176 typedef internal::Planar_segment<GeomTraits> Planar_segment; 177 typedef internal::Point_set_with_planes<GeomTraits> Point_set_with_planes; 178 179 Point_set_with_planes point_set(points, point_map, normal_map, index_map); 180 181 const std::vector< Planar_segment* >& planar_segments = point_set.planar_segments(); 182 if (planar_segments.size() < 4) { 183 error_message_ = "at least 4 planes required to reconstruct a closed surface mesh (only " 184 + std::to_string(planar_segments.size()) + " provided)"; 185 return; 186 } 187 188 hypothesis_.generate(point_set, candidate_faces_); 189 190 typedef internal::Candidate_confidences<GeomTraits> Candidate_confidences; 191 Candidate_confidences conf; 192 conf.compute(point_set, candidate_faces_); 193 } 194 195 196 template <class GeomTraits> 197 template <typename PolygonMesh> output_candidate_faces(PolygonMesh & candidate_faces)198 void Polygonal_surface_reconstruction<GeomTraits>::output_candidate_faces(PolygonMesh& candidate_faces) const { 199 candidate_faces.clear(); // make sure it is empty. 200 CGAL::copy_face_graph(candidate_faces_, candidate_faces); 201 } 202 203 204 template <class GeomTraits> 205 template <typename MixedIntegerProgramTraits, typename PolygonMesh> reconstruct(PolygonMesh & output_mesh,double wt_fitting,double wt_coverage,double wt_complexity)206 bool Polygonal_surface_reconstruction<GeomTraits>::reconstruct( 207 PolygonMesh& output_mesh, 208 double wt_fitting /* = 0.43 */, 209 double wt_coverage /* = 0.27 */, 210 double wt_complexity /* = 0.30 */) 211 { 212 if (!error_message_.empty()) { // an error has occurred in the constructor 213 return false; 214 } 215 216 if (candidate_faces_.num_faces() < 4) { 217 error_message_ = "at least 4 candidate faces required to reconstruct a closed surface mesh (only " 218 + std::to_string(candidate_faces_.num_faces()) + " computed)"; 219 return false; 220 } 221 222 typedef typename internal::Hypothesis<GeomTraits>::Adjacency Adjacency; 223 const Adjacency& adjacency = hypothesis_.extract_adjacency(candidate_faces_); 224 225 // Internal data structure 226 Polygon_mesh target_mesh = candidate_faces_; 227 228 // The number of supporting points of each face 229 typename Polygon_mesh::template Property_map<Face_descriptor, std::size_t> face_num_supporting_points = 230 target_mesh.template add_property_map<Face_descriptor, std::size_t>("f:num_supporting_points").first; 231 232 // The area of each face 233 typename Polygon_mesh::template Property_map<Face_descriptor, FT> face_areas = 234 target_mesh.template add_property_map<Face_descriptor, FT>("f:face_area").first; 235 236 // The point covered area of each face 237 typename Polygon_mesh::template Property_map<Face_descriptor, FT> face_covered_areas = 238 target_mesh.template add_property_map<Face_descriptor, FT>("f:covered_area").first; 239 240 // The supporting plane of each face 241 typename Polygon_mesh::template Property_map<Face_descriptor, const Plane*> face_supporting_planes = 242 target_mesh.template add_property_map<Face_descriptor, const Plane*>("f:supp_plane").first; 243 244 // Gives each face an index 245 typename Polygon_mesh::template Property_map<Face_descriptor, std::size_t> face_indices = 246 target_mesh.template add_property_map<Face_descriptor, std::size_t>("f:index").first; 247 248 double total_points = 0.0; 249 std::size_t idx = 0; 250 for(auto f : target_mesh.faces()) { 251 total_points += face_num_supporting_points[f]; 252 face_indices[f] = idx; 253 ++idx; 254 } 255 256 257 typedef MixedIntegerProgramTraits MIP_Solver; 258 typedef typename MixedIntegerProgramTraits::Variable Variable; 259 typedef typename MixedIntegerProgramTraits::Linear_objective Linear_objective; 260 typedef typename MixedIntegerProgramTraits::Linear_constraint Linear_constraint; 261 262 MIP_Solver solver; 263 264 // Adds variables 265 266 // Binary variables: 267 // x[0] ... x[num_faces - 1] : binary labels of all the input faces 268 // x[num_faces] ... x[num_faces + num_edges - 1] : binary labels of all the intersecting edges (remain or not) 269 // x[num_faces + num_edges] ... x[num_faces + num_edges + num_edges] : binary labels of corner edges (sharp edge of not) 270 271 std::size_t num_faces = target_mesh.number_of_faces(); 272 std::size_t num_edges(0); 273 274 typedef typename internal::Hypothesis<GeomTraits>::Intersection Intersection; 275 276 std::unordered_map<const Intersection*, std::size_t> edge_usage_status; // keep or remove an intersecting edges 277 for (std::size_t i = 0; i < adjacency.size(); ++i) { 278 const Intersection& fan = adjacency[i]; 279 if (fan.size() == 4) { 280 std::size_t var_idx = num_faces + num_edges; 281 edge_usage_status[&fan] = var_idx; 282 ++num_edges; 283 } 284 } 285 286 std::size_t total_variables = num_faces + num_edges + num_edges; 287 288 const std::vector<Variable*>& variables = solver.create_variables(total_variables); 289 for (std::size_t i = 0; i < total_variables; ++i) { 290 Variable* v = variables[i]; 291 v->set_variable_type(Variable::BINARY); 292 } 293 294 // Adds objective 295 296 const typename Polygon_mesh::template Property_map<Vertex_descriptor, Point>& coords = target_mesh.points(); 297 std::vector<Point> vertices(target_mesh.number_of_vertices()); 298 idx = 0; 299 for(auto v : target_mesh.vertices()) { 300 vertices[idx] = coords[v]; 301 ++idx; 302 } 303 304 typedef typename GeomTraits::Iso_cuboid_3 Box; 305 306 const Box& box = CGAL::bounding_box(vertices.begin(), vertices.end()); 307 FT dx = box.xmax() - box.xmin(); 308 FT dy = box.ymax() - box.ymin(); 309 FT dz = box.zmax() - box.zmin(); 310 FT box_area = FT(2.0) * (dx * dy + dy * dz + dz * dx); 311 312 // Chooses a better scale: all actual values multiplied by total number of points 313 double coeff_data_fitting = wt_fitting; 314 double coeff_coverage = total_points * wt_coverage / box_area; 315 double coeff_complexity = total_points * wt_complexity / double(adjacency.size()); 316 317 Linear_objective * objective = solver.create_objective(Linear_objective::MINIMIZE); 318 319 std::unordered_map<const Intersection*, std::size_t> edge_sharp_status; // the edge is sharp or not 320 std::size_t num_sharp_edges = 0; 321 for (std::size_t i = 0; i < adjacency.size(); ++i) { 322 const Intersection& fan = adjacency[i]; 323 if (fan.size() == 4) { 324 std::size_t var_idx = num_faces + num_edges + num_sharp_edges; 325 edge_sharp_status[&fan] = var_idx; 326 327 // Accumulates model complexity term 328 objective->add_coefficient(variables[var_idx], coeff_complexity); 329 ++num_sharp_edges; 330 } 331 } 332 CGAL_assertion(num_edges == num_sharp_edges); 333 334 for(auto f : target_mesh.faces()) { 335 std::size_t var_idx = face_indices[f]; 336 337 // Accumulates data fitting term 338 std::size_t num = face_num_supporting_points[f]; 339 objective->add_coefficient(variables[var_idx], -coeff_data_fitting * num); 340 341 // Accumulates model coverage term 342 double uncovered_area = (face_areas[f] - face_covered_areas[f]); 343 objective->add_coefficient(variables[var_idx], coeff_coverage * uncovered_area); 344 } 345 346 // Adds constraints: the number of faces associated with an edge must be either 2 or 0 347 std::size_t var_edge_used_idx = 0; 348 for (std::size_t i = 0; i < adjacency.size(); ++i) { 349 Linear_constraint* c = solver.create_constraint(0.0, 0.0); 350 const Intersection& fan = adjacency[i]; 351 for (std::size_t j = 0; j < fan.size(); ++j) { 352 Face_descriptor f = target_mesh.face(fan[j]); 353 std::size_t var_idx = face_indices[f]; 354 c->add_coefficient(variables[var_idx], 1.0); 355 } 356 357 if (fan.size() == 4) { 358 std::size_t var_idx = num_faces + var_edge_used_idx; 359 c->add_coefficient(variables[var_idx], -2.0); // 360 ++var_edge_used_idx; 361 } 362 else { // boundary edge 363 // will be set to 0 (i.e., we don't allow open surface) 364 } 365 } 366 367 // Adds constraints: for the sharp edges. The explanation of posing this constraint can be found here: 368 // https://user-images.githubusercontent.com/15526536/30185644-12085a9c-942b-11e7-831d-290dd2a4d50c.png 369 double M = 1.0; 370 for (std::size_t i = 0; i < adjacency.size(); ++i) { 371 const Intersection& fan = adjacency[i]; 372 if (fan.size() != 4) 373 continue; 374 375 // If an edge is sharp, the edge must be selected first: 376 // X[var_edge_usage_idx] >= X[var_edge_sharp_idx] 377 Linear_constraint* c = solver.create_constraint(0.0); 378 std::size_t var_edge_usage_idx = edge_usage_status[&fan]; 379 c->add_coefficient(variables[var_edge_usage_idx], 1.0); 380 std::size_t var_edge_sharp_idx = edge_sharp_status[&fan]; 381 c->add_coefficient(variables[var_edge_sharp_idx], -1.0); 382 383 for (std::size_t j = 0; j < fan.size(); ++j) { 384 Face_descriptor f1 = target_mesh.face(fan[j]); 385 const Plane* plane1 = face_supporting_planes[f1]; 386 std::size_t fid1 = face_indices[f1]; 387 for (std::size_t k = j + 1; k < fan.size(); ++k) { 388 Face_descriptor f2 = target_mesh.face(fan[k]); 389 const Plane* plane2 = face_supporting_planes[f2]; 390 std::size_t fid2 = face_indices[f2]; 391 392 if (plane1 != plane2) { 393 // The constraint is: 394 //X[var_edge_sharp_idx] + M * (3 - (X[fid1] + X[fid2] + X[var_edge_usage_idx])) >= 1 395 // which equals to 396 //X[var_edge_sharp_idx] - M * X[fid1] - M * X[fid2] - M * X[var_edge_usage_idx] >= 1 - 3M 397 c = solver.create_constraint(1.0 - 3.0 * M); 398 c->add_coefficient(variables[var_edge_sharp_idx], 1.0); 399 c->add_coefficient(variables[fid1], -M); 400 c->add_coefficient(variables[fid2], -M); 401 c->add_coefficient(variables[var_edge_usage_idx], -M); 402 } 403 } 404 } 405 } 406 407 // Optimization 408 409 if (solver.solve()) { 410 411 // Marks results 412 const std::vector<double>& X = solver.solution(); 413 414 std::vector<Face_descriptor> to_delete; 415 std::size_t f_idx(0); 416 for(auto f : target_mesh.faces()) { 417 if (static_cast<int>(std::round(X[f_idx])) == 0) 418 to_delete.push_back(f); 419 ++f_idx; 420 } 421 422 for (std::size_t i = 0; i < to_delete.size(); ++i) { 423 Face_descriptor f = to_delete[i]; 424 Halfedge_descriptor h = target_mesh.halfedge(f); 425 Euler::remove_face(h, target_mesh); 426 } 427 428 // Marks the sharp edges 429 typename Polygon_mesh::template Property_map<Edge_descriptor, bool> edge_is_sharp = 430 target_mesh.template add_property_map<Edge_descriptor, bool>("e:sharp_edges").first; 431 for (auto e : target_mesh.edges()) 432 edge_is_sharp[e] = false; 433 434 for (std::size_t i = 0; i < adjacency.size(); ++i) { 435 const Intersection& fan = adjacency[i]; 436 if (fan.size() != 4) 437 continue; 438 439 std::size_t idx_sharp_var = edge_sharp_status[&fan]; 440 if (static_cast<int>(X[idx_sharp_var]) == 1) { 441 for (std::size_t j = 0; j < fan.size(); ++j) { 442 Halfedge_descriptor h = fan[j]; 443 Face_descriptor f = target_mesh.face(h); 444 if (f != Polygon_mesh::null_face()) { // some faces may be deleted 445 std::size_t fid = face_indices[f]; 446 if (static_cast<int>(std::round(X[fid])) == 1) { 447 Edge_descriptor e = target_mesh.edge(h); 448 edge_is_sharp[e] = true; 449 break; 450 } 451 } 452 } 453 } 454 } 455 456 // Converts from internal data structure to the required `PolygonMesh`. 457 output_mesh.clear(); // make sure it is empty. 458 CGAL::copy_face_graph(target_mesh, output_mesh); 459 } 460 else { 461 error_message_ = "solving the binary program failed"; 462 return false; 463 } 464 465 return true; 466 } 467 468 } //namespace CGAL 469 470 #endif // CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H 471