From 362f4fecf6979348b0353c77e211cf256a75c4c8 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 6 Apr 2017 17:33:36 -0400 Subject: [PATCH] Removed odm_texturing, cleaned up 2.5d generation algorithm, added params in ODM pipeline Former-commit-id: f1571ceb7e020b7d4c6994256d76d4befa9af752 --- modules/odm_25dmeshing/src/Odm25dMeshing.cpp | 186 ++++++------------ modules/odm_25dmeshing/src/Odm25dMeshing.hpp | 11 +- modules/odm_25dmeshing/src/PlyInterpreter.cpp | 13 +- modules/odm_25dmeshing/src/PlyInterpreter.hpp | 14 +- opendm/config.py | 18 +- scripts/odm_app.py | 2 + scripts/odm_meshing.py | 17 +- scripts/odm_texturing.py | 119 ----------- 8 files changed, 107 insertions(+), 273 deletions(-) delete mode 100644 scripts/odm_texturing.py diff --git a/modules/odm_25dmeshing/src/Odm25dMeshing.cpp b/modules/odm_25dmeshing/src/Odm25dMeshing.cpp index bb7fa4d5..197a6a6c 100644 --- a/modules/odm_25dmeshing/src/Odm25dMeshing.cpp +++ b/modules/odm_25dmeshing/src/Odm25dMeshing.cpp @@ -64,18 +64,31 @@ void Odm25dMeshing::parseArguments(int argc, char **argv) { } else if (argument == "-verbose") { log.setIsPrintingInCout(true); } else if (argument == "-maxVertexCount" && argIndex < argc) { -// ++argIndex; -// if (argIndex >= argc) -// { -// throw OdmMeshingException("Argument '" + argument + "' expects 1 more input following it, but no more inputs were provided."); -// } -// std::stringstream ss(argv[argIndex]); -// ss >> maxVertexCount_; -// if (ss.bad()) -// { -// throw OdmMeshingException("Argument '" + argument + "' has a bad value (wrong type)."); -// } -// log << "Vertex count was manually set to: " << maxVertexCount_ << "\n"; + ++argIndex; + if (argIndex >= argc) throw Odm25dMeshingException("Argument '" + argument + "' expects 1 more input following it, but no more inputs were provided."); + std::stringstream ss(argv[argIndex]); + ss >> maxVertexCount; + if (ss.bad()) throw Odm25dMeshingException("Argument '" + argument + "' has a bad value (wrong type)."); + maxVertexCount = std::max(maxVertexCount, 0); + log << "Vertex count was manually set to: " << maxVertexCount << "\n"; + } else if (argument == "-outliersRemovalPercentage" && argIndex < argc) { + ++argIndex; + if (argIndex >= argc) throw Odm25dMeshingException("Argument '" + argument + "' expects 1 more input following it, but no more inputs were provided."); + std::stringstream ss(argv[argIndex]); + ss >> outliersRemovalPercentage; + if (ss.bad()) throw Odm25dMeshingException("Argument '" + argument + "' has a bad value (wrong type)."); + + outliersRemovalPercentage = std::min(99.99, std::max(outliersRemovalPercentage, 0)); + log << "Outliers removal was manually set to: " << outliersRemovalPercentage << "\n"; + } else if (argument == "-wlopIterations" && argIndex < argc) { + ++argIndex; + if (argIndex >= argc) throw Odm25dMeshingException("Argument '" + argument + "' expects 1 more input following it, but no more inputs were provided."); + std::stringstream ss(argv[argIndex]); + ss >> wlopIterations; + if (ss.bad()) throw Odm25dMeshingException("Argument '" + argument + "' has a bad value (wrong type)."); + + wlopIterations = std::min(1000, std::max(wlopIterations, 1)); + log << "WLOP iterations was manually set to: " << wlopIterations << "\n"; } else if (argument == "-inputFile" && argIndex < argc) { ++argIndex; if (argIndex >= argc) { @@ -141,55 +154,41 @@ void Odm25dMeshing::loadPointCloud(){ void Odm25dMeshing::buildMesh(){ size_t pointCountBeforeOutRemoval = points.size(); - + if (outliersRemovalPercentage > 0) log << "Removing outliers\n"; const unsigned int NEIGHBORS = 24; - const double REMOVED_PERCENTAGE = 5; - points.erase(CGAL::remove_outliers(points.begin(), points.end(), - CGAL::Nth_of_tuple_property_map<0, PointNormalColor>(), NEIGHBORS, REMOVED_PERCENTAGE), + CGAL::First_of_pair_property_map(), NEIGHBORS, outliersRemovalPercentage), points.end()); - std::vector(points).swap(points); + std::vector(points).swap(points); size_t pointCount = points.size(); log << "Removed " << (pointCountBeforeOutRemoval - pointCount) << " points\n"; - size_t pointCountBeforeSimplify = pointCount; + const double RETAIN_PERCENTAGE = std::min(((100. * (double)maxVertexCount) / (double)pointCount), 10.); // percentage of points to retain. + std::vector simplifiedPoints; - const double CELL_SIZE = 0.01; - points.erase(CGAL::grid_simplify_point_set(points.begin(), points.end(), - CGAL::Nth_of_tuple_property_map<0, PointNormalColor>(), - CELL_SIZE), - points.end()); - std::vector(points).swap(points); + log << "Performing weighted locally optimal projection simplification and regularization (retain: " << RETAIN_PERCENTAGE << "%, iterate: " << wlopIterations << ")" << "\n"; - pointCount = points.size(); + CGAL::wlop_simplify_and_regularize_point_set( + points.begin(), + points.end(), + std::back_inserter(simplifiedPoints), + CGAL::First_of_pair_property_map(), + RETAIN_PERCENTAGE, + -1.0, // auto radius = 8 times the average spacing of the point set. + wlopIterations, + false); // require_uniform_sampling - log << "Removed " << (pointCountBeforeSimplify - pointCount) << " points\n"; + pointCount = simplifiedPoints.size(); if (pointCount < 3){ throw Odm25dMeshingException("Not enough points"); } - log << "Smoothing point set\n"; - - const double SHARPNESS_ANGLE = 89; // control sharpness of the result. The bigger the smoother the result will be - const int SMOOTH_PASSES = 3; - - for (int i = 0; i < SMOOTH_PASSES; ++i) - { - log << "Pass " << (i + 1) << " of " << SMOOTH_PASSES << "\n"; - - CGAL::bilateral_smooth_point_set ( - points.begin(), - points.end(), - CGAL::Nth_of_tuple_property_map<0, PointNormalColor>(), - CGAL::Nth_of_tuple_property_map<1, PointNormalColor>(), - NEIGHBORS, - SHARPNESS_ANGLE); - } + log << "Final vertex count is " << pointCount << "\n"; std::vector< std::pair > pts; try{ @@ -199,7 +198,7 @@ void Odm25dMeshing::buildMesh(){ } for (size_t i = 0; i < pointCount; ++i){ - pts.push_back(std::make_pair(cgalPoint(points[i].get<0>().x(), points[i].get<0>().y()), i)); + pts.push_back(std::make_pair(cgalPoint(simplifiedPoints[i].x(), simplifiedPoints[i].y()), i)); } log << "Computing delaunay triangulation\n"; @@ -216,75 +215,26 @@ void Odm25dMeshing::buildMesh(){ // Convert to tinyply format std::vector vertices; - std::vector colors; std::vector vertexIndicies; try{ vertices.reserve(pointCount); - colors.reserve(pointCount); vertexIndicies.reserve(triIndexes); } catch (const std::bad_alloc&){ throw Odm25dMeshingException("Not enough memory"); } - for (size_t i = 0; i < pointCount; ++i){ - vertices.push_back(points[i].get<0>().x()); - vertices.push_back(points[i].get<0>().y()); - vertices.push_back(points[i].get<0>().z()); - colors.push_back(points[i].get<2>()[0]); - colors.push_back(points[i].get<2>()[1]); - colors.push_back(points[i].get<2>()[2]); + for (size_t i = 0; i < pointCount; ++i){ + vertices.push_back(simplifiedPoints[i].x()); + vertices.push_back(simplifiedPoints[i].y()); + vertices.push_back(simplifiedPoints[i].z()); } for (DT::Face_iterator face = dt.faces_begin(); face != dt.faces_end(); ++face) { - vertexIndicies.push_back(static_cast(face->vertex(0)->info())); - vertexIndicies.push_back(static_cast(face->vertex(1)->info())); - vertexIndicies.push_back(static_cast(face->vertex(2)->info())); - } - - log << "Removing spikes\n"; - - const float THRESHOLD = 0.2; - const int PASSES = 5; - - std::vector heights; - float current_threshold = THRESHOLD; - - for (int i = 1; i <= PASSES; i++){ - log << "Pass " << i << " of " << PASSES << "\n"; - - for (DT::Vertex_iterator vertex = dt.vertices_begin(); vertex != dt.vertices_end(); ++vertex){ - // Check if the height between this vertex and its - // incident vertices is greater than THRESHOLD - Vertex_circulator vc = dt.incident_vertices(vertex), done(vc); - - if (vc != 0){ - float height = vertices[vertex->info() * 3 + 2]; - bool spike = false; - - do{ - if (dt.is_infinite(vc)) continue; - - float ivHeight = vertices[vc->info() * 3 + 2]; - if (fabs(height - ivHeight) > current_threshold) spike = true; - - heights.push_back(ivHeight); - }while(++vc != done); - - if (spike){ - // Replace the height of the vertex by the median height - // of its incident vertices - std::sort(heights.begin(), heights.end()); - - vertices[vertex->info() * 3 + 2] = heights[heights.size() / 2]; - } - - heights.clear(); - } - } - - current_threshold /= 2; + vertexIndicies.push_back(face->vertex(0)->info()); + vertexIndicies.push_back(face->vertex(1)->info()); + vertexIndicies.push_back(face->vertex(2)->info()); } log << "Saving mesh to file.\n"; @@ -295,7 +245,6 @@ void Odm25dMeshing::buildMesh(){ tinyply::PlyFile plyFile; plyFile.add_properties_to_element("vertex", {"x", "y", "z"}, vertices); - plyFile.add_properties_to_element("vertex", { "diffuse_red", "diffuse_green", "diffuse_blue"}, colors); plyFile.add_properties_to_element("face", { "vertex_index" }, vertexIndicies, 3, tinyply::PlyProperty::Type::INT8); plyFile.write(outputStream, false); @@ -308,32 +257,19 @@ void Odm25dMeshing::printHelp() { bool printInCoutPop = log.isPrintingInCout(); log.setIsPrintingInCout(true); - log << "odm_25dmeshing\n\n"; + log << "Usage: odm_25dmeshing -inputFile [plyFile] [optional-parameters]\n"; + log << "Create a 2.5D mesh from an oriented point cloud (points with normals) using a constrained delaunay triangulation. " + << "The program requires a path to an input PLY point cloud file, all other input parameters are optional.\n\n"; - log << "Purpose:" << "\n"; - log - << "Create a 2.5D mesh from an oriented point cloud (points with normals) using a constrained delaunay triangulation" - << "\n"; + log << " -inputFile to PLY point cloud\n" + << " -outputFile where the output PLY 2.5D mesh should be saved (default: " << outputFile << ")\n" + << " -logFile log file path (default: " << logFilePath << ")\n" + << " -verbose whether to print verbose output (default: " << (printInCoutPop ? "true" : "false") << ")\n" + << " -outliersRemovalPercentage <0 - 99.99> percentage of outliers to remove. Set to 0 to disable. (default: " << outliersRemovalPercentage << ")\n" + << " -maxVertexCount <0 - N> Maximum number of vertices in the output mesh. The mesh might have fewer vertices, but will not exceed this limit. (default: " << maxVertexCount << ")\n" + << " -wlopIterations <1 - 1000> Iterations of the Weighted Locally Optimal Projection (WLOP) simplification algorithm. Higher values take longer but produce a smoother mesh. (default: " << wlopIterations << ")\n" - log << "Usage:" << "\n"; - log << "The program requires a path to an input PLY point cloud file, all other input parameters are optional." - << "\n\n"; - - log << "The following flags are available\n"; - log << "Call the program with flag \"-help\", or without parameters to print this message, or check any generated log file.\n"; - log - << "Call the program with flag \"-verbose\", to print log messages in the standard output stream as well as in the log file.\n\n"; - - log << "Parameters are specified as: \"- \", (without <>), and the following parameters are configureable: " - << "\n"; - log << "\"-inputFile \" (mandatory)" << "\n"; - log << "\"Input ply file that must contain a point cloud with normals.\n\n"; - - log << "\"-outputFile \" (optional, default: odm_mesh.ply)" << "\n"; - log << "\"Target file in which the mesh is saved.\n\n"; - - log << "\"-logFile \" (optional, default: odm_25dmeshing_log.txt)" - << "\n"; + << "\n"; log.setIsPrintingInCout(printInCoutPop); } diff --git a/modules/odm_25dmeshing/src/Odm25dMeshing.hpp b/modules/odm_25dmeshing/src/Odm25dMeshing.hpp index d557fb09..9c349cc1 100644 --- a/modules/odm_25dmeshing/src/Odm25dMeshing.hpp +++ b/modules/odm_25dmeshing/src/Odm25dMeshing.hpp @@ -10,8 +10,7 @@ #include #include #include -#include -#include +#include #include "Logger.hpp" #include "PlyInterpreter.hpp" @@ -60,10 +59,12 @@ private: Logger log; std::string inputFile = ""; - std::string outputFile = "odm_mesh.ply"; + std::string outputFile = "odm_25dmesh.ply"; std::string logFilePath = "odm_25dmeshing_log.txt"; - - std::vector points; + unsigned int maxVertexCount = 100000; + double outliersRemovalPercentage = 2; + unsigned int wlopIterations = 35; + std::vector points; }; class Odm25dMeshingException: public std::exception { diff --git a/modules/odm_25dmeshing/src/PlyInterpreter.cpp b/modules/odm_25dmeshing/src/PlyInterpreter.cpp index 6bd5d3f4..42f54bbc 100644 --- a/modules/odm_25dmeshing/src/PlyInterpreter.cpp +++ b/modules/odm_25dmeshing/src/PlyInterpreter.cpp @@ -7,10 +7,7 @@ bool PlyInterpreter::is_applicable(CGAL::Ply_reader& reader) { && reader.does_tag_exist ("z") && reader.does_tag_exist ("nx") && reader.does_tag_exist ("ny") - && reader.does_tag_exist ("nz") - && reader.does_tag_exist ("diffuse_red") - && reader.does_tag_exist ("diffuse_green") - && reader.does_tag_exist ("diffuse_blue"); + && reader.does_tag_exist ("nz"); } // Describes how to process one line (= one point object) @@ -18,21 +15,15 @@ void PlyInterpreter::process_line(CGAL::Ply_reader& reader) { FT x = (FT)0., y = (FT)0., z = (FT)0., nx = (FT)0., ny = (FT)0., nz = (FT)0.; - Color c = {{ 0, 0, 0 }}; - reader.assign (x, "x"); reader.assign (y, "y"); reader.assign (z, "z"); reader.assign (nx, "nx"); reader.assign (ny, "ny"); reader.assign (nz, "nz"); - reader.assign (c[0], "diffuse_red"); - reader.assign (c[1], "diffuse_green"); - reader.assign (c[2], "diffuse_blue"); Point3 p(x, y, z); Vector3 n(nx, ny, nz); - PointNormalColor pnc(p, n, c); - points.push_back(pnc); + points.push_back(std::make_pair(p, n)); } diff --git a/modules/odm_25dmeshing/src/PlyInterpreter.hpp b/modules/odm_25dmeshing/src/PlyInterpreter.hpp index fe8ae535..b2270756 100644 --- a/modules/odm_25dmeshing/src/PlyInterpreter.hpp +++ b/modules/odm_25dmeshing/src/PlyInterpreter.hpp @@ -7,7 +7,6 @@ #include #include #include -#include // types typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel; @@ -15,19 +14,14 @@ typedef Kernel::FT FT; typedef Kernel::Point_3 Point3; typedef Kernel::Vector_3 Vector3; - -// Point with normal vector stored as a std::pair. -// Color is red/green/blue array -typedef CGAL::cpp11::array Color; - -// points, normals and colors -typedef boost::tuple PointNormalColor; +// points, normals +typedef std::pair Pwn; class PlyInterpreter { - std::vector& points; + std::vector& points; public: - PlyInterpreter (std::vector& points) + PlyInterpreter (std::vector& points) : points (points) { } bool is_applicable (CGAL::Ply_reader& reader); diff --git a/opendm/config.py b/opendm/config.py index 14b92e8b..05384e18 100644 --- a/opendm/config.py +++ b/opendm/config.py @@ -158,7 +158,6 @@ def config(): default=False, help='Use 2.5D mesh to compute the orthophoto') - parser.add_argument('--use-pmvs', action='store_true', default=False, @@ -253,6 +252,23 @@ def config(): 'times slightly but helps reduce memory usage. ' 'Default: %(default)s')) + parser.add_argument('--mesh-remove-outliers', + metavar='', + default=2, + type=float, + help=('Percentage of outliers to remove from the point set. Set to 0 to disable. ' + 'Applies to 2.5D mesh only. ' + 'Default: %(default)s')) + + parser.add_argument('--mesh-wlop-iterations', + metavar='', + default=35, + type=int, + help=('Iterations of the Weighted Locally Optimal Projection (WLOP) simplification algorithm. ' + 'Higher values take longer but produce a smoother mesh. ' + 'Applies to 2.5D mesh only. ' + 'Default: %(default)s')) + parser.add_argument('--texturing-data-term', metavar='', default='gmi', diff --git a/scripts/odm_app.py b/scripts/odm_app.py index 57ccbf3c..30c2d6cb 100644 --- a/scripts/odm_app.py +++ b/scripts/odm_app.py @@ -59,6 +59,8 @@ class ODMApp(ecto.BlackBox): oct_tree=p.args.mesh_octree_depth, samples=p.args.mesh_samples, solver=p.args.mesh_solver_divide, + remove_outliers=p.args.mesh_remove_outliers, + wlop_iterations=p.args.mesh_wlop_iterations, verbose=p.args.verbose), 'texturing': ODMMvsTexCell(data_term=p.args.texturing_data_term, outlier_rem_type=p.args.texturing_outlier_removal_type, diff --git a/scripts/odm_meshing.py b/scripts/odm_meshing.py index b8fc686d..fa4346c0 100644 --- a/scripts/odm_meshing.py +++ b/scripts/odm_meshing.py @@ -19,6 +19,14 @@ class ODMeshingCell(ecto.Cell): 'is solved in the surface reconstruction step. ' 'Increasing this value increases computation ' 'times slightly but helps reduce memory usage.', 9) + + params.declare("remove_outliers", 'Percentage of outliers to remove from the point set. Set to 0 to disable. ' + 'Applies to 2.5D mesh only.', 2) + + params.declare("wlop_iterations", 'Iterations of the Weighted Locally Optimal Projection (WLOP) simplification algorithm. ' + 'Higher values take longer but produce a smoother mesh. ' + 'Applies to 2.5D mesh only. ', 35) + params.declare("verbose", 'print additional messages to console', False) def declare_io(self, params, inputs, outputs): @@ -87,12 +95,17 @@ class ODMeshingCell(ecto.Cell): 'outfile': tree.odm_25dmesh, 'infile': infile, 'log': tree.odm_25dmeshing_log, - 'verbose': verbose + 'verbose': verbose, + 'max_vertex': self.params.max_vertex, + 'remove_outliers': self.params.remove_outliers, + 'wlop_iterations': self.params.wlop_iterations } # run 2.5D meshing binary system.run('{bin}/odm_25dmeshing -inputFile {infile} ' - '-outputFile {outfile} -logFile {log} {verbose} '.format(**kwargs)) + '-outputFile {outfile} -logFile {log} ' + '-maxVertexCount {max_vertex} -outliersRemovalPercentage {remove_outliers} ' + '-wlopIterations {wlop_iterations} {verbose}'.format(**kwargs)) else: log.ODM_WARNING('Found a valid ODM 2.5D Mesh file in: %s' % tree.odm_25dmesh) diff --git a/scripts/odm_texturing.py b/scripts/odm_texturing.py deleted file mode 100644 index af3cc872..00000000 --- a/scripts/odm_texturing.py +++ /dev/null @@ -1,119 +0,0 @@ -import os - -import ecto - -from opendm import log -from opendm import io -from opendm import system -from opendm import context - - -class ODMTexturingCell(ecto.Cell): - def declare_params(self, params): - params.declare("resize", 'resizes images by the largest side', 2400) - params.declare("resolution", 'The resolution of the output textures. Must be ' - 'greater than textureWithSize.', 4096) - params.declare("size", 'The resolution to rescale the images performing ' - 'the texturing.', 3600) - params.declare("verbose", 'print additional messages to console', False) - - def declare_io(self, params, inputs, outputs): - inputs.declare("tree", "Struct with paths", []) - inputs.declare("args", "The application arguments.", {}) - inputs.declare("reconstruction", "Clusters output. list of ODMReconstructions", []) - outputs.declare("reconstruction", "Clusters output. list of ODMReconstructions", []) - - def process(self, inputs, outputs): - - # Benchmarking - start_time = system.now_raw() - - log.ODM_INFO('Running ODM Texturing Cell') - - # get inputs - args = self.inputs.args - tree = self.inputs.tree - verbose = '-verbose' if self.params.verbose else '' - - # define paths and create working directories - system.mkdir_p(tree.odm_texturing) - if args.use_25dmesh: system.mkdir_p(tree.odm_25dtexturing) - - # check if we rerun cell or not - rerun_cell = (args.rerun is not None and - args.rerun == 'odm_texturing') or \ - (args.rerun_all) or \ - (args.rerun_from is not None and - 'odm_texturing' in args.rerun_from) - - # Undistort radial distortion - if not os.path.isdir(tree.odm_texturing_undistorted_image_path) or rerun_cell: - system.run(' '.join([ - 'cd {} &&'.format(tree.opensfm), - 'PYTHONPATH={}:{}'.format(context.pyopencv_path, - context.opensfm_path), - 'python', - os.path.join(context.odm_modules_src_path, - 'odm_slam/src/undistort_radial.py'), - '--output', - tree.odm_texturing_undistorted_image_path, - tree.opensfm, - ])) - - system.run( - 'PYTHONPATH=%s %s/bin/export_bundler %s' % - (context.pyopencv_path, context.opensfm_path, tree.opensfm)) - else: - log.ODM_WARNING( - 'Found a valid Bundler file in: %s' % - (tree.opensfm_reconstruction)) - - - runs = [{ - 'out_dir': tree.odm_texturing, - 'model': tree.odm_mesh - }] - - if args.use_25dmesh: - runs += [{ - 'out_dir': tree.odm_25dtexturing, - 'model': tree.odm_25dmesh - }] - - for r in runs: - odm_textured_model_obj = os.path.join(r['out_dir'], tree.odm_textured_model_obj) - - if not io.file_exists(odm_textured_model_obj) or rerun_cell: - log.ODM_DEBUG('Writing ODM Textured file in: %s' - % odm_textured_model_obj) - - # odm_texturing definitions - kwargs = { - 'bin': context.odm_modules_path, - 'out_dir': r['out_dir'], - 'bundle': tree.opensfm_bundle, - 'imgs_path': tree.odm_texturing_undistorted_image_path, - 'imgs_list': tree.opensfm_bundle_list, - 'model': r['model'], - 'log': os.path.join(r['out_dir'], tree.odm_texuring_log), - 'resize': self.params.resize, - 'resolution': self.params.resolution, - 'size': self.params.size, - 'verbose': verbose - } - - # run texturing binary - system.run('{bin}/odm_texturing -bundleFile {bundle} ' - '-imagesPath {imgs_path} -imagesListPath {imgs_list} ' - '-inputModelPath {model} -outputFolder {out_dir}/ ' - '-textureResolution {resolution} -bundleResizedTo {resize} {verbose} ' - '-textureWithSize {size} -logFile {log}'.format(**kwargs)) - else: - log.ODM_WARNING('Found a valid ODM Texture file in: %s' - % odm_textured_model_obj) - - if args.time: - system.benchmark(start_time, tree.benchmarking, 'Texturing') - - log.ODM_INFO('Running ODM Texturing Cell - Finished') - return ecto.OK if args.end_with != 'odm_texturing' else ecto.QUIT