// Copyright (c) 2018 Liangliang Nan. All rights reserved. // // This file is part of CGAL (www.cgal.org) // // $URL: https://github.com/CGAL/cgal/blob/v5.1/Polygonal_surface_reconstruction/include/CGAL/Polygonal_surface_reconstruction.h $ // $Id: Polygonal_surface_reconstruction.h 0779373 2020-03-26T13:31:46+01:00 Sébastien Loriot // SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial // // Author(s) : Liangliang Nan #ifndef CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H #define CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H #include #include #include #include #include #include #include /*! \file Polygonal_surface_reconstruction.h */ namespace CGAL { /*! \ingroup PkgPolygonalSurfaceReconstruction \brief Implementation of the Polygonal Surface Reconstruction method. Given a set of 3D points with unoriented normals sampled from the outer boundary of a piecewise planar object, the Polygonal Surface Reconstruction method \cgalCite{nan2017polyfit} outputs a simplified and watertight surface mesh interpolating the input point set. The method first generates a set of face candidates by intersecting the planar primitives. Then an optimal subset of the candidate faces is selected through optimization under hard constraints that enforce the final model to be manifold and watertight. The reconstruction assumes the planar segmentation of the point cloud is provided in the input. \tparam GeomTraits a geometric traits class, model of Kernel */ template class Polygonal_surface_reconstruction { public: /// \name Types typedef typename GeomTraits::FT FT; ///< number type. typedef typename GeomTraits::Point_3 Point; ///< point type. typedef typename GeomTraits::Vector_3 Vector; ///< vector type. typedef typename GeomTraits::Plane_3 Plane; ///< plane type. private: // Polygon mesh storing the candidate faces typedef CGAL::Surface_mesh Polygon_mesh; typedef typename Polygon_mesh::Face_index Face_descriptor; typedef typename Polygon_mesh::Vertex_index Vertex_descriptor; typedef typename Polygon_mesh::Edge_index Edge_descriptor; typedef typename Polygon_mesh::Halfedge_index Halfedge_descriptor; // Public methods public: /// \name Creation /*! Creates a Polygonal Surface Reconstruction object. After construction, candidate faces are generated and point/face confidence values are computed, allowing to reuse them in the subsequent reconstruction step with different parameters. \tparam PointRange is the range of input points, model of `ConstRange`. \tparam PointMap is a model of `ReadablePropertyMap` with value type `GeomTraits::Point_3`. \tparam NormalMap is a model of `ReadablePropertyMap` with value type `GeomTraits::Vector_3`. \tparam IndexMap is a model of `ReadablePropertyMap` with value type `int`. \param points range of input points. \param point_map property map: value_type of `typename PointRange::const_iterator` -> `Point_3` \param normal_map property map: value_type of `typename PointRange::const_iterator` -> `Vector_3` \param index_map property map: value_type of `typename PointRange::const_iterator` -> `int`, denoting the index of the plane it belongs to (-1 if the point is not assigned to a plane) */ template < typename PointRange, typename PointMap, typename NormalMap, typename IndexMap > Polygonal_surface_reconstruction( const PointRange& points, PointMap point_map, NormalMap normal_map, IndexMap index_map ); /// \name Operations /** Reconstructs a watertight polygonal mesh model. \tparam MixedIntegerProgramTraits a model of `MixedIntegerProgramTraits` \tparam PolygonMesh a model of `MutableFaceGraph` \return `true` if the reconstruction succeeded, `false` otherwise. */ template bool reconstruct( PolygonMesh& output_mesh, ///< the final reconstruction result double wt_fitting = 0.43, ///< weight for the data fitting term. double wt_coverage = 0.27, ///< weight for the point coverage term. double wt_complexity = 0.30 ///< weight for the model complexity term. ); /*! Gives the user the possibility to access the intermediate candidate faces (i.e., the faces induced by the intersection of the supporting planes). \tparam PolygonMesh a model of `MutableFaceGraph`. */ template void output_candidate_faces(PolygonMesh& candidate_faces) const; /// Gets the error message (if reconstruction failed). const std::string& error_message() const { return error_message_; } // Data members. private: internal::Hypothesis hypothesis_; // The generated candidate faces stored as a polygon mesh Polygon_mesh candidate_faces_; std::string error_message_; private: // Copying is not allowed Polygonal_surface_reconstruction(const Polygonal_surface_reconstruction& psr); }; // end of Polygonal_surface_reconstruction //////////////////////////////////////////////////////////////////////////s // implementations template template < typename PointRange, typename PointMap, typename NormalMap, typename IndexMap > Polygonal_surface_reconstruction::Polygonal_surface_reconstruction( const PointRange& points, PointMap point_map, NormalMap normal_map, IndexMap index_map ) : error_message_("") { if (points.empty()) { error_message_ = "empty input points"; return; } typedef internal::Planar_segment Planar_segment; typedef internal::Point_set_with_planes Point_set_with_planes; Point_set_with_planes point_set(points, point_map, normal_map, index_map); const std::vector< Planar_segment* >& planar_segments = point_set.planar_segments(); if (planar_segments.size() < 4) { error_message_ = "at least 4 planes required to reconstruct a closed surface mesh (only " + std::to_string(planar_segments.size()) + " provided)"; return; } hypothesis_.generate(point_set, candidate_faces_); typedef internal::Candidate_confidences Candidate_confidences; Candidate_confidences conf; conf.compute(point_set, candidate_faces_); } template template void Polygonal_surface_reconstruction::output_candidate_faces(PolygonMesh& candidate_faces) const { candidate_faces.clear(); // make sure it is empty. CGAL::copy_face_graph(candidate_faces_, candidate_faces); } template template bool Polygonal_surface_reconstruction::reconstruct( PolygonMesh& output_mesh, double wt_fitting /* = 0.43 */, double wt_coverage /* = 0.27 */, double wt_complexity /* = 0.30 */) { if (!error_message_.empty()) { // an error has occurred in the constructor return false; } if (candidate_faces_.num_faces() < 4) { error_message_ = "at least 4 candidate faces required to reconstruct a closed surface mesh (only " + std::to_string(candidate_faces_.num_faces()) + " computed)"; return false; } typedef typename internal::Hypothesis::Adjacency Adjacency; const Adjacency& adjacency = hypothesis_.extract_adjacency(candidate_faces_); // Internal data structure Polygon_mesh target_mesh = candidate_faces_; // The number of supporting points of each face typename Polygon_mesh::template Property_map face_num_supporting_points = target_mesh.template add_property_map("f:num_supporting_points").first; // The area of each face typename Polygon_mesh::template Property_map face_areas = target_mesh.template add_property_map("f:face_area").first; // The point covered area of each face typename Polygon_mesh::template Property_map face_covered_areas = target_mesh.template add_property_map("f:covered_area").first; // The supporting plane of each face typename Polygon_mesh::template Property_map face_supporting_planes = target_mesh.template add_property_map("f:supp_plane").first; // Gives each face an index typename Polygon_mesh::template Property_map face_indices = target_mesh.template add_property_map("f:index").first; double total_points = 0.0; std::size_t idx = 0; for(auto f : target_mesh.faces()) { total_points += face_num_supporting_points[f]; face_indices[f] = idx; ++idx; } typedef MixedIntegerProgramTraits MIP_Solver; typedef typename MixedIntegerProgramTraits::Variable Variable; typedef typename MixedIntegerProgramTraits::Linear_objective Linear_objective; typedef typename MixedIntegerProgramTraits::Linear_constraint Linear_constraint; MIP_Solver solver; // Adds variables // Binary variables: // x[0] ... x[num_faces - 1] : binary labels of all the input faces // x[num_faces] ... x[num_faces + num_edges - 1] : binary labels of all the intersecting edges (remain or not) // x[num_faces + num_edges] ... x[num_faces + num_edges + num_edges] : binary labels of corner edges (sharp edge of not) std::size_t num_faces = target_mesh.number_of_faces(); std::size_t num_edges(0); typedef typename internal::Hypothesis::Intersection Intersection; std::unordered_map edge_usage_status; // keep or remove an intersecting edges for (std::size_t i = 0; i < adjacency.size(); ++i) { const Intersection& fan = adjacency[i]; if (fan.size() == 4) { std::size_t var_idx = num_faces + num_edges; edge_usage_status[&fan] = var_idx; ++num_edges; } } std::size_t total_variables = num_faces + num_edges + num_edges; const std::vector& variables = solver.create_variables(total_variables); for (std::size_t i = 0; i < total_variables; ++i) { Variable* v = variables[i]; v->set_variable_type(Variable::BINARY); } // Adds objective const typename Polygon_mesh::template Property_map& coords = target_mesh.points(); std::vector vertices(target_mesh.number_of_vertices()); idx = 0; for(auto v : target_mesh.vertices()) { vertices[idx] = coords[v]; ++idx; } typedef typename GeomTraits::Iso_cuboid_3 Box; const Box& box = CGAL::bounding_box(vertices.begin(), vertices.end()); FT dx = box.xmax() - box.xmin(); FT dy = box.ymax() - box.ymin(); FT dz = box.zmax() - box.zmin(); FT box_area = FT(2.0) * (dx * dy + dy * dz + dz * dx); // Chooses a better scale: all actual values multiplied by total number of points double coeff_data_fitting = wt_fitting; double coeff_coverage = total_points * wt_coverage / box_area; double coeff_complexity = total_points * wt_complexity / double(adjacency.size()); Linear_objective * objective = solver.create_objective(Linear_objective::MINIMIZE); std::unordered_map edge_sharp_status; // the edge is sharp or not std::size_t num_sharp_edges = 0; for (std::size_t i = 0; i < adjacency.size(); ++i) { const Intersection& fan = adjacency[i]; if (fan.size() == 4) { std::size_t var_idx = num_faces + num_edges + num_sharp_edges; edge_sharp_status[&fan] = var_idx; // Accumulates model complexity term objective->add_coefficient(variables[var_idx], coeff_complexity); ++num_sharp_edges; } } CGAL_assertion(num_edges == num_sharp_edges); for(auto f : target_mesh.faces()) { std::size_t var_idx = face_indices[f]; // Accumulates data fitting term std::size_t num = face_num_supporting_points[f]; objective->add_coefficient(variables[var_idx], -coeff_data_fitting * num); // Accumulates model coverage term double uncovered_area = (face_areas[f] - face_covered_areas[f]); objective->add_coefficient(variables[var_idx], coeff_coverage * uncovered_area); } // Adds constraints: the number of faces associated with an edge must be either 2 or 0 std::size_t var_edge_used_idx = 0; for (std::size_t i = 0; i < adjacency.size(); ++i) { Linear_constraint* c = solver.create_constraint(0.0, 0.0); const Intersection& fan = adjacency[i]; for (std::size_t j = 0; j < fan.size(); ++j) { Face_descriptor f = target_mesh.face(fan[j]); std::size_t var_idx = face_indices[f]; c->add_coefficient(variables[var_idx], 1.0); } if (fan.size() == 4) { std::size_t var_idx = num_faces + var_edge_used_idx; c->add_coefficient(variables[var_idx], -2.0); // ++var_edge_used_idx; } else { // boundary edge // will be set to 0 (i.e., we don't allow open surface) } } // Adds constraints: for the sharp edges. The explanation of posing this constraint can be found here: // https://user-images.githubusercontent.com/15526536/30185644-12085a9c-942b-11e7-831d-290dd2a4d50c.png double M = 1.0; for (std::size_t i = 0; i < adjacency.size(); ++i) { const Intersection& fan = adjacency[i]; if (fan.size() != 4) continue; // If an edge is sharp, the edge must be selected first: // X[var_edge_usage_idx] >= X[var_edge_sharp_idx] Linear_constraint* c = solver.create_constraint(0.0); std::size_t var_edge_usage_idx = edge_usage_status[&fan]; c->add_coefficient(variables[var_edge_usage_idx], 1.0); std::size_t var_edge_sharp_idx = edge_sharp_status[&fan]; c->add_coefficient(variables[var_edge_sharp_idx], -1.0); for (std::size_t j = 0; j < fan.size(); ++j) { Face_descriptor f1 = target_mesh.face(fan[j]); const Plane* plane1 = face_supporting_planes[f1]; std::size_t fid1 = face_indices[f1]; for (std::size_t k = j + 1; k < fan.size(); ++k) { Face_descriptor f2 = target_mesh.face(fan[k]); const Plane* plane2 = face_supporting_planes[f2]; std::size_t fid2 = face_indices[f2]; if (plane1 != plane2) { // The constraint is: //X[var_edge_sharp_idx] + M * (3 - (X[fid1] + X[fid2] + X[var_edge_usage_idx])) >= 1 // which equals to //X[var_edge_sharp_idx] - M * X[fid1] - M * X[fid2] - M * X[var_edge_usage_idx] >= 1 - 3M c = solver.create_constraint(1.0 - 3.0 * M); c->add_coefficient(variables[var_edge_sharp_idx], 1.0); c->add_coefficient(variables[fid1], -M); c->add_coefficient(variables[fid2], -M); c->add_coefficient(variables[var_edge_usage_idx], -M); } } } } // Optimization if (solver.solve()) { // Marks results const std::vector& X = solver.solution(); std::vector to_delete; std::size_t f_idx(0); for(auto f : target_mesh.faces()) { if (static_cast(std::round(X[f_idx])) == 0) to_delete.push_back(f); ++f_idx; } for (std::size_t i = 0; i < to_delete.size(); ++i) { Face_descriptor f = to_delete[i]; Halfedge_descriptor h = target_mesh.halfedge(f); Euler::remove_face(h, target_mesh); } // Marks the sharp edges typename Polygon_mesh::template Property_map edge_is_sharp = target_mesh.template add_property_map("e:sharp_edges").first; for (auto e : target_mesh.edges()) edge_is_sharp[e] = false; for (std::size_t i = 0; i < adjacency.size(); ++i) { const Intersection& fan = adjacency[i]; if (fan.size() != 4) continue; std::size_t idx_sharp_var = edge_sharp_status[&fan]; if (static_cast(X[idx_sharp_var]) == 1) { for (std::size_t j = 0; j < fan.size(); ++j) { Halfedge_descriptor h = fan[j]; Face_descriptor f = target_mesh.face(h); if (f != Polygon_mesh::null_face()) { // some faces may be deleted std::size_t fid = face_indices[f]; if (static_cast(std::round(X[fid])) == 1) { Edge_descriptor e = target_mesh.edge(h); edge_is_sharp[e] = true; break; } } } } } // Converts from internal data structure to the required `PolygonMesh`. output_mesh.clear(); // make sure it is empty. CGAL::copy_face_graph(target_mesh, output_mesh); } else { error_message_ = "solving the binary program failed"; return false; } return true; } } //namespace CGAL #endif // CGAL_POLYGONAL_SURFACE_RECONSTRUCTION_H