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: