diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 7f26197a..b6538902 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -8,6 +8,8 @@ add_subdirectory(odm_extract_utm) add_subdirectory(odm_georef) add_subdirectory(odm_orthophoto) add_subdirectory(odm_cleanmesh) +add_subdirectory(odm_filterpoints) + if (ODM_BUILD_SLAM) add_subdirectory(odm_slam) endif () diff --git a/modules/odm_filterpoints/CMakeLists.txt b/modules/odm_filterpoints/CMakeLists.txt new file mode 100644 index 00000000..97ed317b --- /dev/null +++ b/modules/odm_filterpoints/CMakeLists.txt @@ -0,0 +1,21 @@ +project(odm_filterpoints) +cmake_minimum_required(VERSION 2.8) + +# Add compiler options. +add_definitions(-Wall -Wextra -Wconversion -pedantic -std=c++11) + +# PDAL and jsoncpp +find_package(PDAL REQUIRED CONFIG) +include_directories(${PDAL_INCLUDE_DIRS}) +include_directories("${PROJECT_SOURCE_DIR}/../../SuperBuild/src/pdal/vendor/jsoncpp/dist") +link_directories(${PDAL_LIBRARY_DIRS}) +add_definitions(${PDAL_DEFINITIONS}) + +# Add source directory +aux_source_directory("./src" SRC_LIST) + +# Add exectuteable +add_executable(${PROJECT_NAME} ${SRC_LIST}) + +# Link +target_link_libraries(${PROJECT_NAME} jsoncpp ${PDAL_LIBRARIES}) diff --git a/modules/odm_filterpoints/CMakeLists.txt.user b/modules/odm_filterpoints/CMakeLists.txt.user new file mode 100644 index 00000000..b22e5815 --- /dev/null +++ b/modules/odm_filterpoints/CMakeLists.txt.user @@ -0,0 +1,375 @@ + + + + + + EnvironmentId + {e8bffd0b-56b8-4848-bdbf-de67a1ef6b0a} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + true + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + {be5adc2c-1277-4808-ab17-0dcac16347d0} + 0 + 0 + 0 + + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Default + + + + + all + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + + clean + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Default + Default + CMakeProjectManager.CMakeBuildConfiguration + + + + CMAKE_BUILD_TYPE:STRING=Debug + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Debug + + + + + all + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + + clean + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Debug + CMakeProjectManager.CMakeBuildConfiguration + + + + CMAKE_BUILD_TYPE:STRING=Release + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Release + + + + + all + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + + clean + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Release + CMakeProjectManager.CMakeBuildConfiguration + + + + CMAKE_BUILD_TYPE:STRING=RelWithDebInfo + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Release with Debug Information + + + + + all + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + + clean + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release with Debug Information + Release with Debug Information + CMakeProjectManager.CMakeBuildConfiguration + + + + CMAKE_BUILD_TYPE:STRING=MinSizeRel + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Minimum Size Release + + + + + all + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Build + + ProjectExplorer.BuildSteps.Build + + + + + + clean + + true + CMake Build + + CMakeProjectManager.MakeStep + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Minimum Size Release + Minimum Size Release + CMakeProjectManager.CMakeBuildConfiguration + + 5 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy Configuration + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + odm_filterpoints + + CMakeProjectManager.CMakeRunConfiguration.odm_filterpoints +/data/OpenDroneMap/modules/odm_filterpoints/ + -inputFile /data/drone/brighton/smvs/orig.ply -outputFile /data/drone/brighton/smvs/out.ply -verbose + 3768 + false + true + false + false + true + + /data/OpenDroneMap/modules/build-odm_filterpoints-Desktop-Default + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 20 + + + Version + 20 + + diff --git a/opendm/config.py b/opendm/config.py index c2e306d9..c4726f41 100644 --- a/opendm/config.py +++ b/opendm/config.py @@ -314,6 +314,14 @@ def config(): default=False, help='Export the georeferenced point cloud in LAS format. Default: %(default)s') + parser.add_argument('--pc-maxsd', + metavar='', + type=float, + default=2.5, + help='Filters the point cloud by removing points that deviate more than maxsd standard deviations from the local mean. Set to 0 to disable filtering.' + '\nDefault: ' + '%(default)s') + parser.add_argument('--texturing-data-term', metavar='', default='gmi', @@ -420,14 +428,6 @@ def config(): help='DSM/DTM resolution in cm / pixel.' '\nDefault: %(default)s') - parser.add_argument('--dem-maxsd', - metavar='', - type=float, - default=2.5, - help='Points that deviate more than maxsd standard deviations from the local mean ' - 'are discarded. \nDefault: ' - '%(default)s') - parser.add_argument('--dem-decimation', metavar='', default=1, diff --git a/opendm/dem/commands.py b/opendm/dem/commands.py index 36234146..22949e10 100644 --- a/opendm/dem/commands.py +++ b/opendm/dem/commands.py @@ -56,7 +56,6 @@ def create_dems(filenames, demtype, radius=['0.56'], gapfill=False, def create_dem(filenames, demtype, radius, decimation=None, - maxsd=None, maxz=None, products=['idw'], outdir='', suffix='', verbose=False, resolution=0.1): """ Create DEM from collection of LAS files """ start = datetime.now() @@ -71,10 +70,6 @@ def create_dem(filenames, demtype, radius, decimation=None, # JSON pipeline json = pdal.json_gdal_base(bname, products, radius, resolution) - # A DSM for meshing does not use additional filters - if demtype != 'mesh_dsm': - json = pdal.json_add_filters(json, maxsd, maxz) - if demtype == 'dsm': json = pdal.json_add_classification_filter(json, 2, equality='max') elif demtype == 'dtm': diff --git a/opendm/dem/pdal.py b/opendm/dem/pdal.py index 4d949d4e..d50179a9 100644 --- a/opendm/dem/pdal.py +++ b/opendm/dem/pdal.py @@ -102,75 +102,6 @@ def json_add_classification_filter(json, classification, equality="equals"): return json -def json_add_maxsd_filter(json, meank=20, thresh=3.0): - """ Add outlier Filter element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.outlier', - 'method': 'statistical', - 'mean_k': meank, - 'multiplier': thresh - }) - return json - - -def json_add_maxz_filter(json, maxz): - """ Add max elevation Filter element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.range', - 'limits': 'Z[:{0}]'.format(maxz) - }) - - return json - - -def json_add_maxangle_filter(json, maxabsangle): - """ Add scan angle Filter element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.range', - 'limits': 'ScanAngleRank[{0}:{1}]'.format(str(-float(maxabsangle)), maxabsangle) - }) - return json - - -def json_add_scanedge_filter(json, value): - """ Add EdgeOfFlightLine Filter element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.range', - 'limits': 'EdgeOfFlightLine[{0}:{0}]'.format(value) - }) - return json - - -def json_add_returnnum_filter(json, value): - """ Add ReturnNum Filter element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.range', - 'limits': 'ReturnNum[{0}:{0}]'.format(value) - }) - return json - - -def json_add_filters(json, maxsd=None, maxz=None, maxangle=None, returnnum=None): - if maxsd is not None: - json = json_add_maxsd_filter(json, thresh=maxsd) - if maxz is not None: - json = json_add_maxz_filter(json, maxz) - if maxangle is not None: - json = json_add_maxangle_filter(json, maxangle) - if returnnum is not None: - json = json_add_returnnum_filter(json, returnnum) - return json - - -def json_add_crop_filter(json, wkt): - """ Add cropping polygon as Filter Element and return """ - json['pipeline'].insert(0, { - 'type': 'filters.crop', - 'polygon': wkt - }) - return json - - def is_ply_file(filename): _, ext = os.path.splitext(filename) return ext.lower() == '.ply' diff --git a/opendm/point_cloud.py b/opendm/point_cloud.py new file mode 100644 index 00000000..1fd7727e --- /dev/null +++ b/opendm/point_cloud.py @@ -0,0 +1,54 @@ +import os, sys +from opendm import system +from opendm import log +from opendm import context + +def filter(pointCloudPath, standard_deviation=2.5, meank=16, verbose=False): + """ + Filters a point cloud in place (it will replace the input file with the filtered result). + """ + if standard_deviation <= 0 or meank <= 0: + log.ODM_INFO("Skipping point cloud filtering") + return + + log.ODM_INFO("Filtering point cloud (statistical, meanK {}, standard deviation {})".format(meank, standard_deviation)) + + if not os.path.exists(pointCloudPath): + log.ODM_ERROR("{} does not exist, cannot filter point cloud. The program will now exit.".format(pointCloudPath)) + sys.exit(1) + + filter_program = os.path.join(context.odm_modules_path, 'odm_filterpoints') + if not os.path.exists(filter_program): + log.ODM_WARNING("{} program not found. Will skip filtering, but this installation should be fixed.") + return + + pc_path, pc_filename = os.path.split(pointCloudPath) + # pc_path = path/to + # pc_filename = pointcloud.ply + + basename, ext = os.path.splitext(pc_filename) + # basename = pointcloud + # ext = .ply + + tmpPointCloud = os.path.join(pc_path, "{}.tmp{}".format(basename, ext)) + + filterArgs = { + 'bin': filter_program, + 'inputFile': pointCloudPath, + 'outputFile': tmpPointCloud, + 'sd': standard_deviation, + 'meank': meank, + 'verbose': '--verbose' if verbose else '', + } + + system.run('{bin} -inputFile {inputFile} ' + '-outputFile {outputFile} ' + '-sd {sd} ' + '-meank {meank} {verbose} '.format(**filterArgs)) + + # Remove input file, swap temp file + if os.path.exists(tmpPointCloud): + os.remove(pointCloudPath) + os.rename(tmpPointCloud, pointCloudPath) + else: + log.ODM_WARNING("{} not found, filtering has failed.".format(tmpPointCloud)) diff --git a/scripts/odm_dem.py b/scripts/odm_dem.py index 7e6f87ef..f35a1c51 100644 --- a/scripts/odm_dem.py +++ b/scripts/odm_dem.py @@ -93,7 +93,6 @@ class ODMDEMCell(ecto.Cell): gapfill=True, outdir=odm_dem_root, resolution=resolution / 100.0, - maxsd=args.dem_maxsd, decimation=args.dem_decimation, verbose=args.verbose, max_workers=get_max_concurrency_for_dem(args.max_concurrency,tree.odm_georeferencing_model_laz) diff --git a/scripts/odm_georeferencing.py b/scripts/odm_georeferencing.py index 59ffa09c..134e1ed0 100644 --- a/scripts/odm_georeferencing.py +++ b/scripts/odm_georeferencing.py @@ -9,6 +9,7 @@ from opendm import types from opendm import system from opendm import context from opendm.cropper import Cropper +from opendm import point_cloud class ODMGeoreferencingCell(ecto.Cell): diff --git a/scripts/run_opensfm.py b/scripts/run_opensfm.py index d12c4baa..d9f54726 100644 --- a/scripts/run_opensfm.py +++ b/scripts/run_opensfm.py @@ -170,6 +170,9 @@ class ODMOpenSfMCell(ecto.Cell): if args.fast_orthophoto: system.run('PYTHONPATH=%s %s/bin/opensfm export_ply --no-cameras %s' % (context.pyopencv_path, context.opensfm_path, tree.opensfm)) + + # Filter + point_cloud.filter(os.path.join(tree.opensfm, 'reconstruction.ply'), standard_deviation=args.pc_maxsd, verbose=args.verbose) elif args.use_opensfm_dense: # Undistort images at full scale in JPG # (TODO: we could compare the size of the PNGs if they are < than depthmap_resolution @@ -179,6 +182,8 @@ class ODMOpenSfMCell(ecto.Cell): system.run('PYTHONPATH=%s %s/bin/opensfm compute_depthmaps %s' % (context.pyopencv_path, context.opensfm_path, tree.opensfm)) + # Filter + point_cloud.filter(tree.opensfm_model, standard_deviation=args.pc_maxsd, verbose=args.verbose) else: log.ODM_WARNING('Found a valid OpenSfM reconstruction file in: %s' % tree.opensfm_reconstruction) diff --git a/scripts/smvs.py b/scripts/smvs.py index 2ce9f58f..5279a754 100644 --- a/scripts/smvs.py +++ b/scripts/smvs.py @@ -4,6 +4,7 @@ from opendm import log from opendm import io from opendm import system from opendm import context +from opendm import point_cloud class ODMSmvsCell(ecto.Cell): @@ -91,6 +92,9 @@ class ODMSmvsCell(ecto.Cell): old_file = smvs_files[-1] if not (io.rename_file(old_file, tree.smvs_model)): log.ODM_WARNING("File %s does not exist, cannot be renamed. " % old_file) + + # Filter + point_cloud.filter(tree.smvs_model, standard_deviation=args.pc_maxsd, verbose=args.verbose) else: log.ODM_WARNING("Cannot find a valid point cloud (smvs-XX.ply) in %s. Check the console output for errors." % tree.smvs) else: