OSFM get_submodel_argv refactoring, testing, radiometric calibration for 3-channel images

pull/1082/head
Piero Toffanin 2020-03-18 19:29:43 +00:00
rodzic 7b94d25c0c
commit 59edf1fd0f
11 zmienionych plików z 276 dodań i 118 usunięć

Wyświetl plik

@ -48,19 +48,38 @@ def url_string(string):
class RerunFrom(argparse.Action): class RerunFrom(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, processopts[processopts.index(values):]) setattr(namespace, self.dest, processopts[processopts.index(values):])
setattr(namespace, self.dest + '_is_set', True)
class StoreTrue(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, True)
setattr(namespace, self.dest + '_is_set', True)
parser = SettingsParser(description='OpenDroneMap', class StoreValue(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
setattr(namespace, self.dest + '_is_set', True)
args = None
def config(argv=None, settings_yaml=context.settings_path):
global args
if args is not None and argv is None:
return args
parser = SettingsParser(description='OpenDroneMap',
usage='%(prog)s [options] <project name>', usage='%(prog)s [options] <project name>',
yaml_file=open(context.settings_path)) yaml_file=open(settings_yaml))
def config():
parser.add_argument('--project-path', parser.add_argument('--project-path',
metavar='<path>', metavar='<path>',
action=StoreValue,
help='Path to the project folder') help='Path to the project folder')
parser.add_argument('name', parser.add_argument('name',
metavar='<project name>', metavar='<project name>',
action=StoreValue,
type=alphanumeric_string, type=alphanumeric_string,
default='code', default='code',
nargs='?', nargs='?',
@ -68,6 +87,7 @@ def config():
parser.add_argument('--resize-to', parser.add_argument('--resize-to',
metavar='<integer>', metavar='<integer>',
action=StoreValue,
default=2048, default=2048,
type=int, type=int,
help='Resizes images by the largest side for feature extraction purposes only. ' help='Resizes images by the largest side for feature extraction purposes only. '
@ -76,6 +96,7 @@ def config():
parser.add_argument('--end-with', '-e', parser.add_argument('--end-with', '-e',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='odm_orthophoto', default='odm_orthophoto',
choices=processopts, choices=processopts,
help=('Can be one of:' + ' | '.join(processopts))) help=('Can be one of:' + ' | '.join(processopts)))
@ -84,11 +105,13 @@ def config():
rerun.add_argument('--rerun', '-r', rerun.add_argument('--rerun', '-r',
metavar='<string>', metavar='<string>',
action=StoreValue,
choices=processopts, choices=processopts,
help=('Can be one of:' + ' | '.join(processopts))) help=('Can be one of:' + ' | '.join(processopts)))
rerun.add_argument('--rerun-all', rerun.add_argument('--rerun-all',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='force rerun of all tasks') help='force rerun of all tasks')
@ -108,6 +131,7 @@ def config():
parser.add_argument('--min-num-features', parser.add_argument('--min-num-features',
metavar='<integer>', metavar='<integer>',
action=StoreValue,
default=8000, default=8000,
type=int, type=int,
help=('Minimum number of features to extract per image. ' help=('Minimum number of features to extract per image. '
@ -115,17 +139,19 @@ def config():
'execution. Default: %(default)s')) 'execution. Default: %(default)s'))
parser.add_argument('--feature-type', parser.add_argument('--feature-type',
metavar='<string>', metavar='<string>',
default='sift', action=StoreValue,
choices=['sift', 'hahog'], default='sift',
help=('Choose the algorithm for extracting keypoints and computing descriptors. ' choices=['sift', 'hahog'],
'Can be one of: [sift, hahog]. Default: ' help=('Choose the algorithm for extracting keypoints and computing descriptors. '
'%(default)s')) 'Can be one of: [sift, hahog]. Default: '
'%(default)s'))
parser.add_argument('--matcher-neighbors', parser.add_argument('--matcher-neighbors',
type=int,
metavar='<integer>', metavar='<integer>',
action=StoreValue,
default=8, default=8,
type=int,
help='Number of nearest images to pre-match based on GPS ' help='Number of nearest images to pre-match based on GPS '
'exif data. Set to 0 to skip pre-matching. ' 'exif data. Set to 0 to skip pre-matching. '
'Neighbors works together with Distance parameter, ' 'Neighbors works together with Distance parameter, '
@ -136,6 +162,7 @@ def config():
parser.add_argument('--matcher-distance', parser.add_argument('--matcher-distance',
metavar='<integer>', metavar='<integer>',
action=StoreValue,
default=0, default=0,
type=int, type=int,
help='Distance threshold in meters to find pre-matching ' help='Distance threshold in meters to find pre-matching '
@ -144,13 +171,15 @@ def config():
'pre-matching. Default: %(default)s') 'pre-matching. Default: %(default)s')
parser.add_argument('--use-fixed-camera-params', parser.add_argument('--use-fixed-camera-params',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Turn off camera parameter optimization during bundler') help='Turn off camera parameter optimization during bundler')
parser.add_argument('--cameras', parser.add_argument('--cameras',
default='', default='',
metavar='<json>', metavar='<json>',
action=StoreValue,
type=path_or_json_string, type=path_or_json_string,
help='Use the camera parameters computed from ' help='Use the camera parameters computed from '
'another dataset instead of calculating them. ' 'another dataset instead of calculating them. '
@ -160,6 +189,7 @@ def config():
parser.add_argument('--camera-lens', parser.add_argument('--camera-lens',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='auto', default='auto',
choices=['auto', 'perspective', 'brown', 'fisheye', 'spherical'], choices=['auto', 'perspective', 'brown', 'fisheye', 'spherical'],
help=('Set a camera projection type. Manually setting a value ' help=('Set a camera projection type. Manually setting a value '
@ -170,6 +200,7 @@ def config():
parser.add_argument('--radiometric-calibration', parser.add_argument('--radiometric-calibration',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='none', default='none',
choices=['none', 'camera', 'camera+sun'], choices=['none', 'camera', 'camera+sun'],
help=('Set the radiometric calibration to perform on images. ' help=('Set the radiometric calibration to perform on images. '
@ -182,6 +213,7 @@ def config():
parser.add_argument('--max-concurrency', parser.add_argument('--max-concurrency',
metavar='<positive integer>', metavar='<positive integer>',
action=StoreValue,
default=context.num_cores, default=context.num_cores,
type=int, type=int,
help=('The maximum number of processes to use in various ' help=('The maximum number of processes to use in various '
@ -190,6 +222,7 @@ def config():
parser.add_argument('--depthmap-resolution', parser.add_argument('--depthmap-resolution',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=640, default=640,
help=('Controls the density of the point cloud by setting the resolution of the depthmap images. Higher values take longer to compute ' help=('Controls the density of the point cloud by setting the resolution of the depthmap images. Higher values take longer to compute '
@ -198,6 +231,7 @@ def config():
parser.add_argument('--opensfm-depthmap-min-consistent-views', parser.add_argument('--opensfm-depthmap-min-consistent-views',
metavar='<integer: 2 <= x <= 9>', metavar='<integer: 2 <= x <= 9>',
action=StoreValue,
type=int, type=int,
default=3, default=3,
help=('Minimum number of views that should reconstruct a point for it to be valid. Use lower values ' help=('Minimum number of views that should reconstruct a point for it to be valid. Use lower values '
@ -207,6 +241,7 @@ def config():
parser.add_argument('--opensfm-depthmap-method', parser.add_argument('--opensfm-depthmap-method',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='PATCH_MATCH', default='PATCH_MATCH',
choices=['PATCH_MATCH', 'BRUTE_FORCE', 'PATCH_MATCH_SAMPLE'], choices=['PATCH_MATCH', 'BRUTE_FORCE', 'PATCH_MATCH_SAMPLE'],
help=('Raw depthmap computation algorithm. ' help=('Raw depthmap computation algorithm. '
@ -216,6 +251,7 @@ def config():
parser.add_argument('--opensfm-depthmap-min-patch-sd', parser.add_argument('--opensfm-depthmap-min-patch-sd',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=1, default=1,
help=('When using PATCH_MATCH or PATCH_MATCH_SAMPLE, controls the standard deviation threshold to include patches. ' help=('When using PATCH_MATCH or PATCH_MATCH_SAMPLE, controls the standard deviation threshold to include patches. '
@ -223,13 +259,15 @@ def config():
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--use-hybrid-bundle-adjustment', parser.add_argument('--use-hybrid-bundle-adjustment',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Run local bundle adjustment for every image added to the reconstruction and a global ' help='Run local bundle adjustment for every image added to the reconstruction and a global '
'adjustment every 100 images. Speeds up reconstruction for very large datasets.') 'adjustment every 100 images. Speeds up reconstruction for very large datasets.')
parser.add_argument('--mve-confidence', parser.add_argument('--mve-confidence',
metavar='<float: 0 <= x <= 1>', metavar='<float: 0 <= x <= 1>',
action=StoreValue,
type=float, type=float,
default=0.60, default=0.60,
help=('Discard points that have less than a certain confidence threshold. ' help=('Discard points that have less than a certain confidence threshold. '
@ -238,22 +276,26 @@ def config():
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--use-3dmesh', parser.add_argument('--use-3dmesh',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas.') help='Use a full 3D mesh to compute the orthophoto instead of a 2.5D mesh. This option is a bit faster and provides similar results in planar areas.')
parser.add_argument('--skip-3dmodel', parser.add_argument('--skip-3dmodel',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs.') help='Skip generation of a full 3D model. This can save time if you only need 2D results such as orthophotos and DEMs.')
parser.add_argument('--use-opensfm-dense', parser.add_argument('--use-opensfm-dense',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Use opensfm to compute dense point cloud alternatively') help='Use opensfm to compute dense point cloud alternatively')
parser.add_argument('--ignore-gsd', parser.add_argument('--ignore-gsd',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Ignore Ground Sampling Distance (GSD). GSD ' help='Ignore Ground Sampling Distance (GSD). GSD '
'caps the maximum resolution of image outputs and ' 'caps the maximum resolution of image outputs and '
@ -262,6 +304,7 @@ def config():
parser.add_argument('--mesh-size', parser.add_argument('--mesh-size',
metavar='<positive integer>', metavar='<positive integer>',
action=StoreValue,
default=200000, default=200000,
type=int, type=int,
help=('The maximum vertex count of the output mesh. ' help=('The maximum vertex count of the output mesh. '
@ -269,6 +312,7 @@ def config():
parser.add_argument('--mesh-octree-depth', parser.add_argument('--mesh-octree-depth',
metavar='<positive integer>', metavar='<positive integer>',
action=StoreValue,
default=10, default=10,
type=int, type=int,
help=('Oct-tree depth used in the mesh reconstruction, ' help=('Oct-tree depth used in the mesh reconstruction, '
@ -277,6 +321,7 @@ def config():
parser.add_argument('--mesh-samples', parser.add_argument('--mesh-samples',
metavar='<float >= 1.0>', metavar='<float >= 1.0>',
action=StoreValue,
default=1.0, default=1.0,
type=float, type=float,
help=('Number of points per octree node, recommended ' help=('Number of points per octree node, recommended '
@ -284,6 +329,7 @@ def config():
parser.add_argument('--mesh-point-weight', parser.add_argument('--mesh-point-weight',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
default=4, default=4,
type=float, type=float,
help=('This floating point value specifies the importance' help=('This floating point value specifies the importance'
@ -294,7 +340,8 @@ def config():
'Default= %(default)s')) 'Default= %(default)s'))
parser.add_argument('--fast-orthophoto', parser.add_argument('--fast-orthophoto',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Skips dense reconstruction and 3D model generation. ' help='Skips dense reconstruction and 3D model generation. '
'It generates an orthophoto directly from the sparse reconstruction. ' 'It generates an orthophoto directly from the sparse reconstruction. '
@ -302,6 +349,7 @@ def config():
parser.add_argument('--crop', parser.add_argument('--crop',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
default=3, default=3,
type=float, type=float,
help=('Automatically crop image outputs by creating a smooth buffer ' help=('Automatically crop image outputs by creating a smooth buffer '
@ -310,7 +358,8 @@ def config():
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--pc-classify', parser.add_argument('--pc-classify',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Classify the point cloud outputs using a Simple Morphological Filter. ' help='Classify the point cloud outputs using a Simple Morphological Filter. '
'You can control the behavior of this option by tweaking the --dem-* parameters. ' 'You can control the behavior of this option by tweaking the --dem-* parameters. '
@ -318,22 +367,26 @@ def config():
'%(default)s') '%(default)s')
parser.add_argument('--pc-csv', parser.add_argument('--pc-csv',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Export the georeferenced point cloud in CSV format. Default: %(default)s') help='Export the georeferenced point cloud in CSV format. Default: %(default)s')
parser.add_argument('--pc-las', parser.add_argument('--pc-las',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Export the georeferenced point cloud in LAS format. Default: %(default)s') help='Export the georeferenced point cloud in LAS format. Default: %(default)s')
parser.add_argument('--pc-ept', parser.add_argument('--pc-ept',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s') help='Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s')
parser.add_argument('--pc-filter', parser.add_argument('--pc-filter',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=2.5, default=2.5,
help='Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering.' help='Filters the point cloud by removing points that deviate more than N standard deviations from the local mean. Set to 0 to disable filtering.'
@ -342,6 +395,7 @@ def config():
parser.add_argument('--pc-sample', parser.add_argument('--pc-sample',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=0, default=0,
help='Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud. Set to 0 to disable sampling.' help='Filters the point cloud by keeping only a single point around a radius N (in meters). This can be useful to limit the output resolution of the point cloud. Set to 0 to disable sampling.'
@ -350,6 +404,7 @@ def config():
parser.add_argument('--smrf-scalar', parser.add_argument('--smrf-scalar',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=1.25, default=1.25,
help='Simple Morphological Filter elevation scalar parameter. ' help='Simple Morphological Filter elevation scalar parameter. '
@ -358,6 +413,7 @@ def config():
parser.add_argument('--smrf-slope', parser.add_argument('--smrf-slope',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=0.15, default=0.15,
help='Simple Morphological Filter slope parameter (rise over run). ' help='Simple Morphological Filter slope parameter (rise over run). '
@ -366,6 +422,7 @@ def config():
parser.add_argument('--smrf-threshold', parser.add_argument('--smrf-threshold',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=0.5, default=0.5,
help='Simple Morphological Filter elevation threshold parameter (meters). ' help='Simple Morphological Filter elevation threshold parameter (meters). '
@ -374,6 +431,7 @@ def config():
parser.add_argument('--smrf-window', parser.add_argument('--smrf-window',
metavar='<positive float>', metavar='<positive float>',
action=StoreValue,
type=float, type=float,
default=18.0, default=18.0,
help='Simple Morphological Filter window radius parameter (meters). ' help='Simple Morphological Filter window radius parameter (meters). '
@ -382,6 +440,7 @@ def config():
parser.add_argument('--texturing-data-term', parser.add_argument('--texturing-data-term',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='gmi', default='gmi',
choices=['gmi', 'area'], choices=['gmi', 'area'],
help=('Data term: [area, gmi]. Default: ' help=('Data term: [area, gmi]. Default: '
@ -389,6 +448,7 @@ def config():
parser.add_argument('--texturing-nadir-weight', parser.add_argument('--texturing-nadir-weight',
metavar='<integer: 0 <= x <= 32>', metavar='<integer: 0 <= x <= 32>',
action=StoreValue,
default=16, default=16,
type=int, type=int,
help=('Affects orthophotos only. ' help=('Affects orthophotos only. '
@ -399,6 +459,7 @@ def config():
parser.add_argument('--texturing-outlier-removal-type', parser.add_argument('--texturing-outlier-removal-type',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='gauss_clamping', default='gauss_clamping',
choices=['none', 'gauss_clamping', 'gauss_damping'], choices=['none', 'gauss_clamping', 'gauss_damping'],
help=('Type of photometric outlier removal method: ' help=('Type of photometric outlier removal method: '
@ -406,36 +467,42 @@ def config():
'%(default)s')) '%(default)s'))
parser.add_argument('--texturing-skip-visibility-test', parser.add_argument('--texturing-skip-visibility-test',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Skip geometric visibility test. Default: ' help=('Skip geometric visibility test. Default: '
' %(default)s')) ' %(default)s'))
parser.add_argument('--texturing-skip-global-seam-leveling', parser.add_argument('--texturing-skip-global-seam-leveling',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Skip global seam leveling. Useful for IR data.' help=('Skip global seam leveling. Useful for IR data.'
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--texturing-skip-local-seam-leveling', parser.add_argument('--texturing-skip-local-seam-leveling',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Skip local seam blending. Default: %(default)s') help='Skip local seam blending. Default: %(default)s')
parser.add_argument('--texturing-skip-hole-filling', parser.add_argument('--texturing-skip-hole-filling',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Skip filling of holes in the mesh. Default: ' help=('Skip filling of holes in the mesh. Default: '
' %(default)s')) ' %(default)s'))
parser.add_argument('--texturing-keep-unseen-faces', parser.add_argument('--texturing-keep-unseen-faces',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Keep faces in the mesh that are not seen in any camera. ' help=('Keep faces in the mesh that are not seen in any camera. '
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--texturing-tone-mapping', parser.add_argument('--texturing-tone-mapping',
metavar='<string>', metavar='<string>',
action=StoreValue,
choices=['none', 'gamma'], choices=['none', 'gamma'],
default='none', default='none',
help='Turn on gamma tone mapping or none for no tone ' help='Turn on gamma tone mapping or none for no tone '
@ -444,6 +511,7 @@ def config():
parser.add_argument('--gcp', parser.add_argument('--gcp',
metavar='<path string>', metavar='<path string>',
action=StoreValue,
default=None, default=None,
help=('path to the file containing the ground control ' help=('path to the file containing the ground control '
'points used for georeferencing. Default: ' 'points used for georeferencing. Default: '
@ -452,25 +520,29 @@ def config():
'northing height pixelrow pixelcol imagename')) 'northing height pixelrow pixelcol imagename'))
parser.add_argument('--use-exif', parser.add_argument('--use-exif',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Use this tag if you have a gcp_list.txt but ' help=('Use this tag if you have a gcp_list.txt but '
'want to use the exif geotags instead')) 'want to use the exif geotags instead'))
parser.add_argument('--dtm', parser.add_argument('--dtm',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple ' help='Use this tag to build a DTM (Digital Terrain Model, ground only) using a simple '
'morphological filter. Check the --dem* and --smrf* parameters for finer tuning.') 'morphological filter. Check the --dem* and --smrf* parameters for finer tuning.')
parser.add_argument('--dsm', parser.add_argument('--dsm',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive ' help='Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive '
'morphological filter. Check the --dem* parameters for finer tuning.') 'morphological filter. Check the --dem* parameters for finer tuning.')
parser.add_argument('--dem-gapfill-steps', parser.add_argument('--dem-gapfill-steps',
metavar='<positive integer>', metavar='<positive integer>',
action=StoreValue,
default=3, default=3,
type=int, type=int,
help='Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. ' help='Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. '
@ -481,6 +553,7 @@ def config():
parser.add_argument('--dem-resolution', parser.add_argument('--dem-resolution',
metavar='<float>', metavar='<float>',
action=StoreValue,
type=float, type=float,
default=5, default=5,
help='DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.' help='DSM/DTM resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.'
@ -488,6 +561,7 @@ def config():
parser.add_argument('--dem-decimation', parser.add_argument('--dem-decimation',
metavar='<positive integer>', metavar='<positive integer>',
action=StoreValue,
default=1, default=1,
type=int, type=int,
help='Decimate the points before generating the DEM. 1 is no decimation (full quality). ' help='Decimate the points before generating the DEM. 1 is no decimation (full quality). '
@ -495,7 +569,8 @@ def config():
'generation.\nDefault=%(default)s') 'generation.\nDefault=%(default)s')
parser.add_argument('--dem-euclidean-map', parser.add_argument('--dem-euclidean-map',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Computes an euclidean raster map for each DEM. ' help='Computes an euclidean raster map for each DEM. '
'The map reports the distance from each cell to the nearest ' 'The map reports the distance from each cell to the nearest '
@ -506,25 +581,29 @@ def config():
parser.add_argument('--orthophoto-resolution', parser.add_argument('--orthophoto-resolution',
metavar='<float > 0.0>', metavar='<float > 0.0>',
action=StoreValue,
default=5, default=5,
type=float, type=float,
help=('Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.\n' help=('Orthophoto resolution in cm / pixel. Note that this value is capped by a ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.\n'
'Default: %(default)s')) 'Default: %(default)s'))
parser.add_argument('--orthophoto-no-tiled', parser.add_argument('--orthophoto-no-tiled',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Set this parameter if you want a stripped geoTIFF.\n' help='Set this parameter if you want a stripped geoTIFF.\n'
'Default: %(default)s') 'Default: %(default)s')
parser.add_argument('--orthophoto-png', parser.add_argument('--orthophoto-png',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Set this parameter if you want to generate a PNG rendering of the orthophoto.\n' help='Set this parameter if you want to generate a PNG rendering of the orthophoto.\n'
'Default: %(default)s') 'Default: %(default)s')
parser.add_argument('--orthophoto-compression', parser.add_argument('--orthophoto-compression',
metavar='<string>', metavar='<string>',
action=StoreValue,
type=str, type=str,
choices=['JPEG', 'LZW', 'PACKBITS', 'DEFLATE', 'LZMA', 'NONE'], choices=['JPEG', 'LZW', 'PACKBITS', 'DEFLATE', 'LZMA', 'NONE'],
default='DEFLATE', default='DEFLATE',
@ -533,7 +612,8 @@ def config():
'are doing. Options: %(choices)s.\nDefault: %(default)s') 'are doing. Options: %(choices)s.\nDefault: %(default)s')
parser.add_argument('--orthophoto-cutline', parser.add_argument('--orthophoto-cutline',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Generates a polygon around the cropping area ' help='Generates a polygon around the cropping area '
'that cuts the orthophoto around the edges of features. This polygon ' 'that cuts the orthophoto around the edges of features. This polygon '
@ -542,24 +622,28 @@ def config():
'%(default)s') '%(default)s')
parser.add_argument('--build-overviews', parser.add_argument('--build-overviews',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Build orthophoto overviews using gdaladdo.') help='Build orthophoto overviews using gdaladdo.')
parser.add_argument('--verbose', '-v', parser.add_argument('--verbose', '-v',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Print additional messages to the console\n' help='Print additional messages to the console\n'
'Default: %(default)s') 'Default: %(default)s')
parser.add_argument('--time', parser.add_argument('--time',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Generates a benchmark file with runtime info\n' help='Generates a benchmark file with runtime info\n'
'Default: %(default)s') 'Default: %(default)s')
parser.add_argument('--debug', parser.add_argument('--debug',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help='Print debug messages\n' help='Print debug messages\n'
'Default: %(default)s') 'Default: %(default)s')
@ -571,6 +655,7 @@ def config():
parser.add_argument('--split', parser.add_argument('--split',
type=int, type=int,
action=StoreValue,
default=999999, default=999999,
metavar='<positive integer>', metavar='<positive integer>',
help='Average number of images per submodel. When ' help='Average number of images per submodel. When '
@ -581,6 +666,7 @@ def config():
parser.add_argument('--split-overlap', parser.add_argument('--split-overlap',
type=float, type=float,
action=StoreValue,
metavar='<positive integer>', metavar='<positive integer>',
default=150, default=150,
help='Radius of the overlap between submodels. ' help='Radius of the overlap between submodels. '
@ -591,6 +677,7 @@ def config():
parser.add_argument('--sm-cluster', parser.add_argument('--sm-cluster',
metavar='<string>', metavar='<string>',
action=StoreValue,
type=url_string, type=url_string,
default=None, default=None,
help='URL to a ClusterODM instance ' help='URL to a ClusterODM instance '
@ -600,6 +687,7 @@ def config():
parser.add_argument('--merge', parser.add_argument('--merge',
metavar='<string>', metavar='<string>',
action=StoreValue,
default='all', default='all',
choices=['all', 'pointcloud', 'orthophoto', 'dem'], choices=['all', 'pointcloud', 'orthophoto', 'dem'],
help=('Choose what to merge in the merge step in a split dataset. ' help=('Choose what to merge in the merge step in a split dataset. '
@ -608,20 +696,22 @@ def config():
'%(default)s')) '%(default)s'))
parser.add_argument('--force-gps', parser.add_argument('--force-gps',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Use images\' GPS exif data for reconstruction, even if there are GCPs present.' help=('Use images\' GPS exif data for reconstruction, even if there are GCPs present.'
'This flag is useful if you have high precision GPS measurements. ' 'This flag is useful if you have high precision GPS measurements. '
'If there are no GCPs, this flag does nothing. Default: %(default)s')) 'If there are no GCPs, this flag does nothing. Default: %(default)s'))
parser.add_argument('--pc-rectify', parser.add_argument('--pc-rectify',
action='store_true', action=StoreTrue,
nargs=0,
default=False, default=False,
help=('Perform ground rectification on the point cloud. This means that wrongly classified ground ' help=('Perform ground rectification on the point cloud. This means that wrongly classified ground '
'points will be re-classified and gaps will be filled. Useful for generating DTMs. ' 'points will be re-classified and gaps will be filled. Useful for generating DTMs. '
'Default: %(default)s')) 'Default: %(default)s'))
args = parser.parse_args() args = parser.parse_args(argv)
# check that the project path setting has been set properly # check that the project path setting has been set properly
if not args.project_path: if not args.project_path:
@ -662,5 +752,4 @@ def config():
# log.ODM_WARNING("radiometric-calibration is turned on, automatically setting --texturing-skip-global-seam-leveling") # log.ODM_WARNING("radiometric-calibration is turned on, automatically setting --texturing-skip-global-seam-leveling")
# args.texturing_skip_global_seam_leveling = True # args.texturing_skip_global_seam_leveling = True
return args return args

Wyświetl plik

@ -277,9 +277,9 @@ class OSFMContext:
def name(self): def name(self):
return os.path.basename(os.path.abspath(self.path(".."))) return os.path.basename(os.path.abspath(self.path("..")))
def get_submodel_argv(project_name = None, submodels_path = None, submodel_name = None): def get_submodel_argv(args, submodels_path = None, submodel_name = None):
""" """
Gets argv for a submodel starting from the argv passed to the application startup. Gets argv for a submodel starting from the args passed to the application startup.
Additionally, if project_name, submodels_path and submodel_name are passed, the function Additionally, if project_name, submodels_path and submodel_name are passed, the function
handles the <project name> value and --project-path detection / override. handles the <project name> value and --project-path detection / override.
When all arguments are set to None, --project-path and project name are always removed. When all arguments are set to None, --project-path and project name are always removed.
@ -295,82 +295,73 @@ def get_submodel_argv(project_name = None, submodels_path = None, submodel_name
removing --gcp (the GCP path if specified is always "gcp_list.txt") removing --gcp (the GCP path if specified is always "gcp_list.txt")
reading the contents of --cameras reading the contents of --cameras
""" """
assure_always = ['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'] assure_always = ['orthophoto_cutline', 'dem_euclidean_map', 'skip_3dmodel']
remove_always_2 = ['--split', '--split-overlap', '--rerun-from', '--rerun', '--gcp', '--end-with', '--sm-cluster'] remove_always = ['split', 'split_overlap', 'rerun_from', 'rerun', 'gcp', 'end_with', 'sm_cluster', 'rerun_all', 'pc_csv', 'pc_las', 'pc_ept']
remove_always_1 = ['--rerun-all', '--pc-csv', '--pc-las', '--pc-ept'] read_json_always = ['cameras']
read_json_always = ['--cameras']
argv = sys.argv argv = sys.argv
result = [argv[0]] # Startup script (/path/to/run.py)
result = [argv[0]] args_dict = vars(args).copy()
i = 1 set_keys = [k[:-len("_is_set")] for k in args_dict.keys() if k.endswith("_is_set")]
found_args = {}
while i < len(argv): # Handle project name and project path (special case)
arg = argv[i] if "name" in set_keys:
del args_dict["name"]
if i == 1 and project_name and submodel_name and arg == project_name: set_keys.remove("name")
i += 1
continue if "project_path" in set_keys:
elif i == len(argv) - 1: del args_dict["project_path"]
# Project name? set_keys.remove("project_path")
if project_name and submodel_name and arg == project_name:
result.append(submodel_name) # Remove parameters
found_args['project_name'] = True set_keys = [k for k in set_keys if k not in remove_always]
i += 1
continue # Assure parameters
for k in assure_always:
if arg == '--project-path': if not k in set_keys:
if submodels_path: set_keys.append(k)
result.append(arg) args_dict[k] = True
result.append(submodels_path)
found_args[arg] = True # Read JSON always
i += 2 for k in read_json_always:
elif arg in assure_always: if k in set_keys:
result.append(arg)
found_args[arg] = True
i += 1
elif arg == '--crop':
result.append(arg)
crop_value = float(argv[i + 1])
if crop_value == 0:
crop_value = 0.015625
result.append(str(crop_value))
found_args[arg] = True
i += 2
elif arg in read_json_always:
try: try:
jsond = io.path_or_json_string_to_dict(argv[i + 1]) if isinstance(args_dict[k], str):
result.append(arg) args_dict[k] = io.path_or_json_string_to_dict(args_dict[k])
result.append(json.dumps(jsond)) if isinstance(args_dict[k], dict):
found_args[arg] = True args_dict[k] = json.dumps(args_dict[k])
except ValueError as e: except ValueError as e:
log.ODM_WARNING("Cannot parse/read JSON: {}".format(str(e))) log.ODM_WARNING("Cannot parse/read JSON: {}".format(str(e)))
finally:
i += 2 # Handle crop (cannot be zero for split/merge)
elif arg in remove_always_2: if "crop" in set_keys:
i += 2 crop_value = float(args_dict["crop"])
elif arg in remove_always_1: if crop_value == 0:
i += 1 crop_value = 0.015625
else: args_dict["crop"] = crop_value
result.append(arg)
i += 1 # Populate result
for k in set_keys:
result.append("--%s" % k.replace("_", "-"))
# No second value for booleans
if isinstance(args_dict[k], bool) and args_dict[k] == True:
continue
result.append(str(args_dict[k]))
if not found_args.get('--project-path') and submodels_path: if submodels_path:
result.append('--project-path') result.append("--project-path")
result.append(submodels_path) result.append(submodels_path)
for arg in assure_always: if submodel_name:
if not found_args.get(arg):
result.append(arg)
if not found_args.get('project_name') and submodel_name:
result.append(submodel_name) result.append(submodel_name)
return result return result
def get_submodel_args_dict(): def get_submodel_args_dict(args):
submodel_argv = get_submodel_argv() submodel_argv = get_submodel_argv(args)
result = {} result = {}
i = 0 i = 0

Wyświetl plik

@ -199,7 +199,7 @@ class ODM_Photo:
# ], float) # ], float)
self.width, self.height = get_image_size.get_image_size(_path_file) self.width, self.height = get_image_size.get_image_size(_path_file)
# Sanitize band name since we use it in folder paths # Sanitize band name since we use it in folder paths
self.band_name = re.sub('[^A-Za-z0-9]+', '', self.band_name) self.band_name = re.sub('[^A-Za-z0-9]+', '', self.band_name)
@ -286,7 +286,7 @@ class ODM_Photo:
return " ".join(map(str, tag.values)) return " ".join(map(str, tag.values))
def get_radiometric_calibration(self): def get_radiometric_calibration(self):
if self.radiometric_calibration: if isinstance(self.radiometric_calibration, str):
parts = self.radiometric_calibration.split(" ") parts = self.radiometric_calibration.split(" ")
if len(parts) == 3: if len(parts) == 3:
return list(map(float, parts)) return list(map(float, parts))

Wyświetl plik

@ -8,6 +8,7 @@ import zipfile
import glob import glob
from opendm import log from opendm import log
from opendm import system from opendm import system
from opendm import config
from pyodm import Node, exceptions from pyodm import Node, exceptions
from pyodm.utils import AtomicCounter from pyodm.utils import AtomicCounter
from pyodm.types import TaskStatus from pyodm.types import TaskStatus
@ -354,7 +355,7 @@ class Task:
# Upload task # Upload task
task = self.node.create_task(images, task = self.node.create_task(images,
get_submodel_args_dict(), get_submodel_args_dict(config.config()),
progress_callback=print_progress, progress_callback=print_progress,
skip_post_processing=True, skip_post_processing=True,
outputs=outputs) outputs=outputs)
@ -470,8 +471,7 @@ class ToolchainTask(Task):
log.ODM_INFO("=============================") log.ODM_INFO("=============================")
submodels_path = os.path.abspath(self.path("..")) submodels_path = os.path.abspath(self.path(".."))
project_name = os.path.basename(os.path.abspath(os.path.join(submodels_path, ".."))) argv = get_submodel_argv(config.config(), submodels_path, submodel_name)
argv = get_submodel_argv(project_name, submodels_path, submodel_name)
# Re-run the ODM toolchain on the submodel # Re-run the ODM toolchain on the submodel
system.run(" ".join(map(quote, argv)), env_vars=os.environ.copy()) system.run(" ".join(map(quote, argv)), env_vars=os.environ.copy())

6
run.py
Wyświetl plik

@ -14,12 +14,16 @@ from stages.odm_app import ODMApp
if __name__ == '__main__': if __name__ == '__main__':
args = config.config() args = config.config()
log.ODM_INFO('Initializing OpenDroneMap app - %s' % system.now()) log.ODM_INFO('Initializing ODM - %s' % system.now())
# Print args # Print args
args_dict = vars(args) args_dict = vars(args)
log.ODM_INFO('==============') log.ODM_INFO('==============')
for k in sorted(args_dict.keys()): for k in sorted(args_dict.keys()):
# Skip _is_set keys
if k.endswith("_is_set"):
continue
# Don't leak token # Don't leak token
if k == 'sm_cluster' and args_dict[k] is not None: if k == 'sm_cluster' and args_dict[k] is not None:
log.ODM_INFO('%s: True' % k) log.ODM_INFO('%s: True' % k)

Wyświetl plik

@ -9,6 +9,7 @@ from opendm import system
from shutil import copyfile from shutil import copyfile
from opendm import progress from opendm import progress
def save_images_database(photos, database_file): def save_images_database(photos, database_file):
with open(database_file, 'w') as f: with open(database_file, 'w') as f:
f.write(json.dumps(map(lambda p: p.__dict__, photos))) f.write(json.dumps(map(lambda p: p.__dict__, photos)))
@ -38,7 +39,6 @@ def load_images_database(database_file):
class ODMLoadDatasetStage(types.ODM_Stage): class ODMLoadDatasetStage(types.ODM_Stage):
def process(self, args, outputs): def process(self, args, outputs):
# Load tree
tree = types.ODM_Tree(args.project_path, args.gcp) tree = types.ODM_Tree(args.project_path, args.gcp)
outputs['tree'] = tree outputs['tree'] = tree

Wyświetl plik

@ -61,9 +61,6 @@ class ODMOpenSfMStage(types.ODM_Stage):
if args.radiometric_calibration == "none": if args.radiometric_calibration == "none":
octx.convert_and_undistort(self.rerun()) octx.convert_and_undistort(self.rerun())
else: else:
# TODO: does this work for RGB images?
def radiometric_calibrate(shot_id, image): def radiometric_calibrate(shot_id, image):
photo = reconstruction.get_photo(shot_id) photo = reconstruction.get_photo(shot_id)
return multispectral.dn_to_reflectance(photo, image, use_sun_sensor=args.radiometric_calibration=="camera+sun") return multispectral.dn_to_reflectance(photo, image, use_sun_sensor=args.radiometric_calibration=="camera+sun")

Wyświetl plik

@ -144,7 +144,7 @@ class ODMSplitStage(types.ODM_Stage):
log.ODM_INFO("Processing %s" % sp_octx.name()) log.ODM_INFO("Processing %s" % sp_octx.name())
log.ODM_INFO("========================") log.ODM_INFO("========================")
argv = get_submodel_argv(args.name, tree.submodels_path, sp_octx.name()) argv = get_submodel_argv(tree.submodels_path, sp_octx.name())
# Re-run the ODM toolchain on the submodel # Re-run the ODM toolchain on the submodel
system.run(" ".join(map(quote, argv)), env_vars=os.environ.copy()) system.run(" ".join(map(quote, argv)), env_vars=os.environ.copy())

Wyświetl plik

@ -0,0 +1,3 @@
{
"test": "1"
}

Wyświetl plik

@ -0,0 +1,2 @@
---
project_path: '/test'

72
tests/test_osfm.py 100644
Wyświetl plik

@ -0,0 +1,72 @@
import unittest
import os
from opendm.osfm import get_submodel_argv, get_submodel_args_dict
from opendm import config
class TestOSFM(unittest.TestCase):
def setUp(self):
pass
def test_get_submodel_argv(self):
# Base
args = config.config(["--project-path", "/datasets"])
self.assertEqual(get_submodel_argv(args)[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# Base + project name
args = config.config(["--project-path", "/datasets", "brighton"])
self.assertEqual(get_submodel_argv(args)[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# Project name + base
args = config.config(["brighton", "--project-path", "/datasets"])
self.assertEqual(get_submodel_argv(args)[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# Crop
args = config.config(["brighton", "--project-path", "/datasets", "--crop", "0"])
self.assertEqual(get_submodel_argv(args)[1:],
['--crop', '0.015625', '--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--crop', '0.015625', '--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# Using settings.yaml with project-path
args = config.config(["brighton"], os.path.join(os.path.dirname(os.path.realpath(__file__)), "assets", "settings.yaml"))
self.assertEqual(get_submodel_argv(args)[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# With sm-cluster, pc-csv and others
args = config.config(["--project-path", "/datasets", "--split", "200", "--pc-csv"])
self.assertEqual(get_submodel_argv(args)[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
self.assertEqual(get_submodel_argv(args, "/submodels", "submodel_0000")[1:],
['--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel', '--project-path', '/submodels', 'submodel_0000'])
# Cameras JSON
args = config.config(["--project-path", "/datasets", "--cameras", os.path.join(os.path.dirname(os.path.realpath(__file__)), "assets", "sample.json")])
self.assertEqual(get_submodel_argv(args)[1:],
['--cameras', '{"test": "1"}', '--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
# Camera JSON string
args = config.config(["--project-path", "/datasets", "--cameras", '{"test": "1"}'])
self.assertEqual(get_submodel_argv(args)[1:],
['--cameras', '{"test": "1"}', '--orthophoto-cutline', '--dem-euclidean-map', '--skip-3dmodel'])
def test_get_submodel_argv_dict(self):
# Base
args = config.config(["--project-path", "/datasets"])
self.assertEqual(get_submodel_args_dict(args),
{'orthophoto-cutline': True, 'skip-3dmodel': True, 'dem-euclidean-map': True})
if __name__ == '__main__':
unittest.main()