Esempio n. 1
0
def split(input_point_cloud, outdir, filename_template, capacity, dims=None):
    log.ODM_INFO(
        "Splitting point cloud filtering in chunks of {} vertices".format(
            capacity))

    if not os.path.exists(input_point_cloud):
        log.ODM_ERROR(
            "{} does not exist, cannot split point cloud. The program will now exit."
            .format(input_point_cloud))
        sys.exit(1)

    if not os.path.exists(outdir):
        system.mkdir_p(outdir)

    if len(os.listdir(outdir)) != 0:
        log.ODM_ERROR(
            "%s already contains some files. The program will now exit.".
            format(outdir))
        sys.exit(1)

    cmd = 'pdal split -i "%s" -o "%s" --capacity %s ' % (
        input_point_cloud, os.path.join(outdir, filename_template), capacity)

    if filename_template.endswith(".ply"):
        cmd += ("--writers.ply.sized_types=false "
                "--writers.ply.storage_mode='little endian' ")
    if dims is not None:
        cmd += '--writers.ply.dims="%s"' % dims
    system.run(cmd)

    return [os.path.join(outdir, f) for f in os.listdir(outdir)]
Esempio n. 2
0
    def process(self, inputs, outputs):

        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running ODM Resize Cell')

        # get inputs
        args = self.inputs.args
        tree = self.inputs.tree
        photos = self.inputs.photos

        if not photos:
            log.ODM_ERROR('Not enough photos in photos to resize')
            return ecto.QUIT

        if self.params.resize_to <= 0:
            log.ODM_ERROR('Resize parameter must be greater than 0')
            return ecto.QUIT

        # create working directory
        system.mkdir_p(tree.dataset_resize)

        log.ODM_DEBUG('Resizing dataset to: %s' % tree.dataset_resize)

        # check if we rerun cell or not
        rerun_cell = (args.rerun is not None and
                      args.rerun == 'resize') or \
                     (args.rerun_all) or \
                     (args.rerun_from is not None and
                      'resize' in args.rerun_from)

        # loop over photos
        if self.params.skip_resize:
            photos = Pool().map(
                partial(no_resize, tree.dataset_raw, tree.dataset_resize,
                        rerun_cell), photos)
            log.ODM_INFO('Copied %s images' % len(photos))
        else:
            photos = Pool().map(
                partial(resize, tree.dataset_raw, tree.dataset_resize,
                        self.params.resize_to, rerun_cell), photos)
            log.ODM_INFO('Resized %s images' % len(photos))

        # append photos to cell output
        self.outputs.photos = photos

        if args.time:
            system.benchmark(start_time, tree.benchmarking, 'Resizing')

        log.ODM_INFO('Running ODM Resize Cell - Finished')
        return ecto.OK if args.end_with != 'resize' else ecto.QUIT
Esempio n. 3
0
    def process(self, inputs, outputs):
        # check if the extension is sopported
        def supported_extension(file_name):
            (pathfn, ext) = os.path.splitext(file_name)
            return ext.lower() in context.supported_extensions

        log.ODM_INFO('Running ODM Load Dataset Cell')

        # get inputs
        tree = self.inputs.tree

        # set images directory
        images_dir = tree.dataset_resize

        if not io.dir_exists(images_dir):
            images_dir = tree.dataset_raw
            if not io.dir_exists(images_dir):
                log.ODM_ERROR(
                    "You must put your pictures into an <images> directory")
                return ecto.QUIT

        log.ODM_DEBUG('Loading dataset from: %s' % images_dir)

        # find files in the given directory
        files = io.get_files_list(images_dir)

        # filter images for its extension type
        files = [f for f in files if supported_extension(f)]

        if files:
            # create ODMPhoto list
            photos = []
            for f in files:
                path_file = io.join_paths(images_dir, f)
                photo = types.ODM_Photo(path_file, self.params.force_focal,
                                        self.params.force_ccd)
                photos.append(photo)

            log.ODM_INFO('Found %s usable images' % len(photos))
        else:
            log.ODM_ERROR('Not enough supported images in %s' % images_dir)
            return ecto.QUIT

        # append photos to cell output
        outputs.photos = photos

        log.ODM_INFO('Running ODM Load Dataset Cell - Finished')
        return ecto.OK
Esempio n. 4
0
    def process(self, args, outputs):
        cm = outputs["cm"]
        outputs["sparse_dir"] = os.path.join(outputs["project_path"], "sparse")
        if not os.path.exists(outputs["sparse_dir"]):
            system.mkdir_p(outputs["sparse_dir"])

        sparse_dir_empty = len(os.listdir(outputs["sparse_dir"])) == 0
        if sparse_dir_empty or self.rerun():
            kwargs = {}

            # TODO: Expose some useful parameters here...

            # TODO: use hierarchical_mapper for larger datasets

            cm.run("mapper", database_path=outputs["db_path"],
                             image_path=outputs["images_dir"],
                             output_path=outputs["sparse_dir"],
                             log_level=1,
                             **kwargs)
        else:
            log.ODM_WARNING("Found existing sparse results %s" % outputs["sparse_dir"])

        outputs["sparse_reconstruction_dirs"] = list(map(lambda p: os.path.join(outputs["sparse_dir"], p), os.listdir(outputs["sparse_dir"])))

        if len(outputs["sparse_reconstruction_dirs"]) == 0:
            log.ODM_ERROR("The program could not process this dataset using the current settings. "
                            "Check that the images have enough overlap, "
                            "that there are enough recognizable features "
                            "and that the images are in focus. "
                            "The program will now exit.")
            exit(1)
Esempio n. 5
0
    def reconstruct(self, rerun=False):
        tracks_file = os.path.join(self.opensfm_project_path, 'tracks.csv')
        reconstruction_file = os.path.join(self.opensfm_project_path,
                                           'reconstruction.json')

        if not io.file_exists(tracks_file) or rerun:
            self.run('create_tracks')
        else:
            log.ODM_WARNING('Found a valid OpenSfM tracks file in: %s' %
                            tracks_file)

        if not io.file_exists(reconstruction_file) or rerun:
            self.run('reconstruct')
        else:
            log.ODM_WARNING(
                'Found a valid OpenSfM reconstruction file in: %s' %
                reconstruction_file)

        # Check that a reconstruction file has been created
        if not self.reconstructed():
            log.ODM_ERROR(
                "The program could not process this dataset using the current settings. "
                "Check that the images have enough overlap, "
                "that there are enough recognizable features "
                "and that the images are in focus. "
                "You could also try to increase the --min-num-features parameter."
                "The program will now exit.")
            exit(1)
Esempio n. 6
0
    def process(self, args, outputs):
        cm = outputs["cm"]


        georegistration_file = os.path.join(outputs["project_path"], "georegistration.txt")
        if not os.path.exists(georegistration_file) or self.rerun():
            if len(outputs["photos"]) < 3:
                log.ODM_ERROR("You need at least 3 photos to georegister this dataset")
                exit(1)

            extract_georegistration(outputs["photos"], georegistration_file)
        else:
            log.ODM_WARNING("Found existing georegistration file %s" % georegistration_file)

        # TODO: handle multiple reconstructions
        if len(outputs["sparse_reconstruction_dirs"]) > 1:
            log.ODM_WARNING("Multiple reconstructions found. We will only reconstruct the first. "
                            "Part of your dataset might not be reconstructed")
        
        reconstruction_dir = outputs["sparse_reconstruction_dirs"][0]
        georeconstruction_dir = os.path.join(reconstruction_dir, "geo")

        if not os.path.exists(georeconstruction_dir) or self.rerun():
            if os.path.exists(georeconstruction_dir):
                log.ODM_WARNING("Deleting %s" % georeconstruction_dir)
                shutil.rmtree(georeconstruction_dir)
            
            system.mkdir_p(georeconstruction_dir)

            cm.run("model_aligner", input_path=reconstruction_dir,
                                    output_path=georeconstruction_dir,
                                    robust_alignment_max_error=15, # TODO: use GPS DOP
                                    ref_images_path=georegistration_file)

        outputs["georeconstruction_dir"] = georeconstruction_dir
    def process(self, inputs, outputs):
        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running ODM Georeferencing Cell')

        # get inputs
        args = self.inputs.args
        tree = self.inputs.tree
        gcpfile = io.join_paths(tree.root_path, self.params.gcp_file)

        # define paths and create working directories
        system.mkdir_p(tree.odm_georeferencing)

        # in case a gcp file it's not provided, let's try to generate it using
        # images metadata. Internally calls jhead.
        if not self.params.use_gcp and \
           not io.file_exists(tree.odm_georeferencing_coords):

            log.ODM_WARNING('Warning: No coordinates file. '
                            'Generating coordinates file in: %s' %
                            tree.odm_georeferencing_coords)
            try:
                # odm_georeference definitions
                kwargs = {
                    'bin': context.odm_modules_path,
                    'imgs': tree.dataset_raw,
                    'imgs_list': tree.opensfm_bundle_list,
                    'coords': tree.odm_georeferencing_coords,
                    'log': tree.odm_georeferencing_utm_log
                }

                # run UTM extraction binary
                system.run(
                    '{bin}/odm_extract_utm -imagesPath {imgs}/ '
                    '-imageListFile {imgs_list} -outputCoordFile {coords} '
                    '-logFile {log}'.format(**kwargs))

            except Exception, e:
                log.ODM_ERROR(
                    'Could not generate GCP file from images metadata.'
                    'Consider rerunning with argument --odm_georeferencing-useGcp'
                    ' and provide a proper GCP file')
                log.ODM_ERROR(e)
                return ecto.QUIT
Esempio n. 8
0
    def extract_offsets(self, geo_sys_file):
        if not io.file_exists(geo_sys_file):
            log.ODM_ERROR('Could not find file %s' % geo_sys_file)
            return

        with open(geo_sys_file) as f:
            offsets = f.readlines()[1].split(' ')
            self.utm_east_offset = float(offsets[0])
            self.utm_north_offset = float(offsets[1])
Esempio n. 9
0
def run(cmd):
    """Run a system command"""
    log.ODM_DEBUG('running %s' % cmd)
    returnCode = os.system(cmd)

    if (returnCode != 0):
        log.ODM_ERROR("quitting cause: \n\t" + cmd + "\nreturned with code " +
                 str(returnCode) + ".\n")
        sys.exit('An error occurred. Check stdout above or the logs.')
Esempio n. 10
0
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))
Esempio n. 11
0
def extract_utm_coords(photos, images_path, output_coords_file):
    """
    Create a coordinate file containing the GPS positions of all cameras 
    to be used later in the ODM toolchain for automatic georeferecing
    :param photos ([ODM_Photo]) list of photos
    :param images_path (str) path to dataset images
    :param output_coords_file (str) path to output coordinates file
    :return None
    """
    if len(photos) == 0:
        raise Exception(
            "No input images, cannot create coordinates file of GPS positions")

    utm_zone = None
    hemisphere = None
    coords = []
    reference_photo = None
    for photo in photos:
        if photo.latitude is None or photo.longitude is None or photo.altitude is None:
            log.ODM_ERROR("Failed parsing GPS position for %s, skipping" %
                          photo.filename)
            continue

        if utm_zone is None:
            utm_zone, hemisphere = get_utm_zone_and_hemisphere_from(
                photo.longitude, photo.latitude)

        try:
            coord = convert_to_utm(photo.longitude, photo.latitude,
                                   photo.altitude, utm_zone, hemisphere)
        except:
            raise Exception("Failed to convert GPS position to UTM for %s" %
                            photo.filename)

        coords.append(coord)

    if utm_zone is None:
        raise Exception("No images seem to have GPS information")

    # Calculate average
    dx = 0.0
    dy = 0.0
    num = float(len(coords))
    for coord in coords:
        dx += coord[0] / num
        dy += coord[1] / num

    dx = int(math.floor(dx))
    dy = int(math.floor(dy))

    # Open output file
    with open(output_coords_file, "w") as f:
        f.write("WGS84 UTM %s%s\n" % (utm_zone, hemisphere))
        f.write("%s %s\n" % (dx, dy))
        for coord in coords:
            f.write("%s %s %s\n" % (coord[0] - dx, coord[1] - dy, coord[2]))
Esempio n. 12
0
def parse_srs_header(header):
    """
    Parse a header coming from GCP or coordinate file
    :param header (str) line
    :return Proj object
    """
    log.ODM_DEBUG('Parsing SRS header: %s' % header)
    header = header.strip()
    ref = header.split(' ')
    try:
        if ref[0] == 'WGS84' and ref[1] == 'UTM':
            datum = ref[0]
            utm_pole = (ref[2][len(ref[2]) - 1]).upper()
            utm_zone = int(ref[2][:len(ref[2]) - 1])

            proj_args = {'zone': utm_zone, 'datum': datum}

            proj4 = '+proj=utm +zone={zone} +datum={datum} +units=m +no_defs=True'
            if utm_pole == 'S':
                proj4 += ' +south=True'

            srs = CRS.from_proj4(proj4.format(**proj_args))
        elif '+proj' in header:
            srs = CRS.from_proj4(header.strip('\''))
        elif header.lower().startswith("epsg:"):
            srs = CRS.from_epsg(header.lower()[5:])
        else:
            log.ODM_ERROR('Could not parse coordinates. Bad SRS supplied: %s' %
                          header)
    except RuntimeError as e:
        log.ODM_ERROR(
            'Uh oh! There seems to be a problem with your coordinates/GCP file.\n\n'
            'The line: %s\n\n'
            'Is not valid. Projections that are valid include:\n'
            ' - EPSG:*****\n'
            ' - WGS84 UTM **(N|S)\n'
            ' - Any valid proj4 string (for example, +proj=utm +zone=32 +north +ellps=WGS84 +datum=WGS84 +units=m +no_defs)\n\n'
            'Modify your input and try again.' % header)
        raise RuntimeError(e)

    return srs
Esempio n. 13
0
    def execute(self):
        try:
            self.first_stage.run()
            return 0
        except system.SubprocessException as e:
            print("")
            print(
                "===== Dumping Info for Geeks (developers need this to fix bugs) ====="
            )
            print(str(e))
            traceback.print_exc()
            print("===== Done, human-readable information to follow... =====")
            print("")

            code = e.errorCode

            if code == 139 or code == 134 or code == 1:
                # Segfault
                log.ODM_ERROR(
                    "Uh oh! Processing stopped because of strange values in the reconstruction. This is often a sign that the input data has some issues or the software cannot deal with it. Have you followed best practices for data acquisition? See https://docs.opendronemap.org/flying.html"
                )
            elif code == 137:
                log.ODM_ERROR(
                    "Whoops! You ran out of memory! Add more RAM to your computer, if you're using docker configure it to use more memory, for WSL2 make use of .wslconfig (https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig), resize your images, lower the quality settings or process the images using a cloud provider (e.g. https://webodm.net)."
                )
            elif code == 132:
                log.ODM_ERROR(
                    "Oh no! It looks like your CPU is not supported (is it fairly old?). You can still use ODM, but you will need to build your own docker image. See https://github.com/OpenDroneMap/ODM#build-from-source"
                )
            elif code == 3:
                log.ODM_ERROR(
                    "ODM can't find a program that is required for processing to run! Did you do a custom build of ODM? (cool!) Make sure that all programs required by ODM are in the right place and are built correctly."
                )
            else:
                log.ODM_ERROR(
                    "The program exited with a strange error code. Please report it at https://community.opendronemap.org"
                )

            # TODO: more?

            return code
Esempio n. 14
0
    def parse_transformation_matrix(self, matrix_file):
        if not io.file_exists(matrix_file):
            log.ODM_ERROR('Could not find file %s' % matrix_file)
            return

        # Create a nested list for the transformation matrix
        with open(matrix_file) as f:
            for line in f:
                # Handle matrix formats that either
                # have leading or trailing brakets or just plain numbers.
                line = re.sub(r"[\[\],]", "", line).strip()
                self.transform += [[float(i) for i in line.split()]]

        self.utm_east_offset = self.transform[0][3]
        self.utm_north_offset = self.transform[1][3]
Esempio n. 15
0
def filter(input_point_cloud,
           output_point_cloud,
           standard_deviation=2.5,
           meank=16,
           sample_radius=0,
           boundary=None,
           verbose=False,
           max_concurrency=1):
    """
    Filters a point cloud
    """
    if not os.path.exists(input_point_cloud):
        log.ODM_ERROR("{} does not exist. The program will now exit.".format(
            input_point_cloud))
        sys.exit(1)

    args = [
        '--input "%s"' % input_point_cloud,
        '--output "%s"' % output_point_cloud,
        '--concurrency %s' % max_concurrency,
        '--verbose' if verbose else '',
    ]

    if sample_radius > 0:
        log.ODM_INFO("Sampling points around a %sm radius" % sample_radius)
        args.append('--radius %s' % sample_radius)

    if standard_deviation > 0 and meank > 0:
        log.ODM_INFO(
            "Filtering {} (statistical, meanK {}, standard deviation {})".
            format(input_point_cloud, meank, standard_deviation))
        args.append('--meank %s' % meank)
        args.append('--std %s' % standard_deviation)

    if boundary is not None:
        log.ODM_INFO("Boundary {}".format(boundary))
        fd, boundary_json_file = tempfile.mkstemp(suffix='.boundary.json')
        os.close(fd)
        with open(boundary_json_file, 'w') as f:
            f.write(as_geojson(boundary))
        args.append('--boundary "%s"' % boundary_json_file)

    system.run('"%s" %s' % (context.fpcfilter_path, " ".join(args)))

    if not os.path.exists(output_point_cloud):
        log.ODM_WARNING(
            "{} not found, filtering has failed.".format(output_point_cloud))
Esempio n. 16
0
def extract_georegistration(photos, georegistration_file):
    if len(photos) == 0:
        raise Exception(
            "No input images, cannot create coordinates file of GPS positions")

    utm_zone = None
    hemisphere = None
    coords = []
    reference_photo = None
    for photo in photos:
        if photo.latitude is None or photo.longitude is None or photo.altitude is None:
            log.ODM_ERROR("Failed parsing GPS position for %s, skipping" %
                          photo.filename)
            continue

        if utm_zone is None:
            utm_zone, hemisphere = get_utm_zone_and_hemisphere_from(
                photo.longitude, photo.latitude)

        try:
            coord = convert_to_utm(photo.longitude, photo.latitude,
                                   photo.altitude, utm_zone, hemisphere)
        except:
            raise Exception("Failed to convert GPS position to UTM for %s" %
                            photo.filename)

        coords.append((photo.filename, coord))

    if utm_zone is None:
        raise Exception("No images seem to have GPS information")

    # Calculate average
    dx = 0.0
    dy = 0.0
    num = float(len(coords))
    for _, coord in coords:
        dx += coord[0] / num
        dy += coord[1] / num

    dx = int(math.floor(dx))
    dy = int(math.floor(dy))

    # Open output file
    with open(georegistration_file, "w") as f:
        for fname, coord in coords:
            f.write("%s %s %s %s\n" %
                    (fname, coord[0] - dx, coord[1] - dy, coord[2]))
Esempio n. 17
0
def extract_temperatures_dji(photo, image, dataset_tree):
    """Extracts the DJI-encoded thermal image as 2D floating-point numpy array with temperatures in degC.
        The raw sensor values are obtained using the sample binaries provided in the official Thermal SDK by DJI.
        The executable file is run and generates a 16 bit unsigned RAW image with Little Endian byte order.
        Link to DJI Forum post: https://forum.dji.com/forum.php?mod=redirect&goto=findpost&ptid=230321&pid=2389016
        """
    # Hardcoded metadata for mean of values
    # This is added to support the possibility of extracting RJPEG from DJI M2EA
    meta = {
        "Emissivity": 0.95,
        "ObjectDistance":
        50,  #This is mean value of flights for better results. Need to be changed later, or improved by bypassing options from task broker
        "AtmosphericTemperature": 20,
        "ReflectedApparentTemperature": 30,
        "IRWindowTemperature": 20,
        "IRWindowTransmission": 1,
        "RelativeHumidity": 40,
        "PlanckR1": 21106.77,
        "PlanckB": 1501,
        "PlanckF": 1,
        "PlanckO": -7340,
        "PlanckR2": 0.012545258,
    }

    if photo.camera_model == "MAVIC2-ENTERPRISE-ADVANCED":
        # Adding support for MAVIC2-ENTERPRISE-ADVANCED Camera images
        im = Image.open(f"{dataset_tree}/{photo.filename}")
        # concatenate APP3 chunks of data
        a = im.applist[3][1]
        for i in range(4, 14):
            a += im.applist[i][1]
        # create image from bytes
        try:
            img = Image.frombytes("I;16L", (640, 512), a)
        except ValueError as e:
            log.ODM_ERROR(
                "Error during extracting temperature values for file %s : %s" %
                photo.filename, e)
    else:
        log.ODM_DEBUG(
            "Only DJI M2EA currently supported, please wait for new updates")
        return image
    # Extract raw sensor values from generated image into numpy array
    raw_sensor_np = np.array(img)
    ## extracting the temperatures from thermal images
    thermal_np = sensor_vals_to_temp(raw_sensor_np, **meta)
    return thermal_np
Esempio n. 18
0
def decomposeProjection(projectionMatrix):

    # Check input:
    if projectionMatrix.shape != (3,4):
        log.ODM_ERROR('Unable to decompose projection matrix, shape != (3,4)')

    RQ = rq(projectionMatrix[:,:3])
    
    # Fix sign, since we know K is upper triangular and has a positive diagonal.
    signMat = np.diag(np.diag(np.sign(RQ[0])))
    K = signMat*RQ[0]
    R = signMat*RQ[1]
    
    # Calculate camera position from translation vector.
    t = np.linalg.inv(-1.0*projectionMatrix[:,:3])*projectionMatrix[:,3]

    return K, R, t
Esempio n. 19
0
    def __init__(self, nodeUrl):
        self.node = Node.from_url(nodeUrl)
        self.params = {
            'tasks': [],
            'threads': []
        }
        self.node_online = True

        log.ODM_INFO("LRE: Initializing using cluster node %s:%s" % (self.node.host, self.node.port))
        try:
            odm_version = self.node.info().odm_version
            log.ODM_INFO("LRE: Node is online and running ODM version: %s"  % odm_version)
        except exceptions.NodeConnectionError:
            log.ODM_WARNING("LRE: The node seems to be offline! We'll still process the dataset, but it's going to run entirely locally.")
            self.node_online = False
        except Exception as e:
            log.ODM_ERROR("LRE: An unexpected problem happened while opening the node connection: %s" % str(e))
            exit(1)
Esempio n. 20
0
    def detect_multi_camera(self):
        """
        Looks at the reconstruction photos and determines if this
        is a single or multi-camera setup.
        """
        band_photos = {}
        band_indexes = {}

        for p in self.photos:
            if not p.band_name in band_photos:
                band_photos[p.band_name] = []
            if not p.band_name in band_indexes:
                band_indexes[p.band_name] = str(p.band_index)

            band_photos[p.band_name].append(p)

        bands_count = len(band_photos)
        if bands_count >= 2 and bands_count <= 8:
            # Validate that all bands have the same number of images,
            # otherwise this is not a multi-camera setup
            img_per_band = len(band_photos[p.band_name])
            for band in band_photos:
                if len(band_photos[band]) != img_per_band:
                    log.ODM_ERROR(
                        "Multi-camera setup detected, but band \"%s\" (identified from \"%s\") has only %s images (instead of %s), perhaps images are missing or are corrupted. Please include all necessary files to process all bands and try again."
                        % (band, band_photos[band][0].filename,
                           len(band_photos[band]), img_per_band))
                    raise RuntimeError("Invalid multi-camera images")

            mc = []
            for band_name in band_indexes:
                mc.append({
                    'name': band_name,
                    'photos': band_photos[band_name]
                })

            # Sort by band index
            mc.sort(key=lambda x: band_indexes[x['name']])

            return mc

        return None
Esempio n. 21
0
def rot2quat(R):
    
    # Float epsilon (use square root to be well with the stable region).
    eps = np.sqrt(np.finfo(float).eps)
    
    # If the determinant is not 1, it's not a rotation matrix
    if np.abs(np.linalg.det(R) - 1.0) > eps:
        log.ODM_ERROR('Matrix passed to rot2quat was not a rotation matrix, det != 1.0')

    tr = np.trace(R)

    quat = np.zeros((1,4))

    # Is trace big enough be computationally stable?
    if tr > eps:
        S = 0.5 / np.sqrt(tr + 1.0)
        quat[0,0] = 0.25 / S
        quat[0,1] = (R[2,1] - R[1,2]) * S
        quat[0,2] = (R[0,2] - R[2,0]) * S
        quat[0,3] = (R[1,0] - R[0,1]) * S
    else: # It's not, use the largest diagonal.
        if R[0,0] > R[1,1] and R[0,0] > R[2,2]:
            S = np.sqrt(1.0 + R[0,0] - R[1,1] - R[2,2]) * 2.0
            quat[0,0] = (R[2,1] - R[1,2]) / S
            quat[0,1] = 0.25 * S
            quat[0,2] = (R[0,1] + R[1,0]) / S
            quat[0,3] = (R[0,2] + R[2,0]) / S
        elif R[1,1] > R[2,2]:
            S = np.sqrt(1.0 - R[0,0] + R[1,1] - R[2,2]) * 2.0
            quat[0,0] = (R[0,2] - R[2,0]) / S
            quat[0,1] = (R[0,1] + R[1,0]) / S
            quat[0,2] = 0.25 * S
            quat[0,3] = (R[1,2] + R[2,1]) / S
        else:
            S = np.sqrt(1.0 - R[0,0] - R[1,1] + R[2,2]) * 2.0
            quat[0,0] = (R[1,0] - R[0,1]) / S
            quat[0,1] = (R[0,2] + R[2,0]) / S
            quat[0,2] = (R[1,2] + R[2,1]) / S
            quat[0,3] = 0.25 * S

    return quat
Esempio n. 22
0
def filter(input_point_cloud, output_point_cloud, standard_deviation=2.5, meank=16, confidence=None, verbose=False):
    """
    Filters a point cloud
    """
    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 confidence:
        log.ODM_INFO("Keeping only points with > %s confidence" % confidence)

    if not os.path.exists(input_point_cloud):
        log.ODM_ERROR("{} does not exist, cannot filter point cloud. The program will now exit.".format(input_point_cloud))
        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.")
        shutil.copy(input_point_cloud, output_point_cloud)
        return

    filterArgs = {
      'bin': filter_program,
      'inputFile': input_point_cloud,
      'outputFile': output_point_cloud,
      'sd': standard_deviation,
      'meank': meank,
      'verbose': '-verbose' if verbose else '',
      'confidence': '-confidence %s' % confidence if confidence else '',
    }

    system.run('{bin} -inputFile {inputFile} '
         '-outputFile {outputFile} '
         '-sd {sd} '
         '-meank {meank} {confidence} {verbose} '.format(**filterArgs))

    # Remove input file, swap temp file
    if not os.path.exists(output_point_cloud):
        log.ODM_WARNING("{} not found, filtering has failed.".format(output_point_cloud))
Esempio n. 23
0
    def process(self, args, outputs):
        outputs['start_time'] = system.now_raw()
        tree = types.ODM_Tree(args.project_path, args.gcp, args.geo)
        outputs['tree'] = tree

        if args.time and io.file_exists(tree.benchmarking):
            # Delete the previously made file
            os.remove(tree.benchmarking)
            with open(tree.benchmarking, 'a') as b:
                b.write(
                    'ODM Benchmarking file created %s\nNumber of Cores: %s\n\n'
                    % (system.now(), context.num_cores))

        # check if the image filename is supported
        def valid_image_filename(filename):
            (pathfn, ext) = os.path.splitext(filename)
            return ext.lower(
            ) in context.supported_extensions and pathfn[-5:] != "_mask"

        # Get supported images from dir
        def get_images(in_dir):
            log.ODM_DEBUG(in_dir)
            entries = os.listdir(in_dir)
            valid, rejects = [], []
            for f in entries:
                if valid_image_filename(f):
                    valid.append(f)
                else:
                    rejects.append(f)
            return valid, rejects

        def find_mask(photo_path, masks):
            (pathfn, ext) = os.path.splitext(os.path.basename(photo_path))
            k = "{}_mask".format(pathfn)

            mask = masks.get(k)
            if mask:
                # Spaces are not supported due to OpenSfM's mask_list.txt format reqs
                if not " " in mask:
                    return mask
                else:
                    log.ODM_WARNING(
                        "Image mask {} has a space. Spaces are currently not supported for image masks."
                        .format(mask))

        # get images directory
        images_dir = tree.dataset_raw

        # define paths and create working directories
        system.mkdir_p(tree.odm_georeferencing)

        log.ODM_INFO('Loading dataset from: %s' % images_dir)

        # check if we rerun cell or not
        images_database_file = os.path.join(tree.root_path, 'images.json')
        if not io.file_exists(images_database_file) or self.rerun():
            if not os.path.exists(images_dir):
                log.ODM_ERROR(
                    "There are no images in %s! Make sure that your project path and dataset name is correct. The current is set to: %s"
                    % (images_dir, args.project_path))
                exit(1)

            files, rejects = get_images(images_dir)
            if files:
                # create ODMPhoto list
                path_files = [os.path.join(images_dir, f) for f in files]

                # Lookup table for masks
                masks = {}
                for r in rejects:
                    (p, ext) = os.path.splitext(r)
                    if p[-5:] == "_mask" and ext.lower(
                    ) in context.supported_extensions:
                        masks[p] = r

                photos = []
                with open(tree.dataset_list, 'w') as dataset_list:
                    log.ODM_INFO("Loading %s images" % len(path_files))
                    for f in path_files:
                        p = types.ODM_Photo(f)
                        p.set_mask(find_mask(f, masks))
                        photos += [p]
                        dataset_list.write(photos[-1].filename + '\n')

                # Check if a geo file is available
                if tree.odm_geo_file is not None and os.path.exists(
                        tree.odm_geo_file):
                    log.ODM_INFO("Found image geolocation file")
                    gf = GeoFile(tree.odm_geo_file)
                    updated = 0
                    for p in photos:
                        entry = gf.get_entry(p.filename)
                        if entry:
                            p.update_with_geo_entry(entry)
                            updated += 1
                    log.ODM_INFO("Updated %s image positions" % updated)

                # Save image database for faster restart
                save_images_database(photos, images_database_file)
            else:
                log.ODM_ERROR('Not enough supported images in %s' % images_dir)
                exit(1)
        else:
            # We have an images database, just load it
            photos = load_images_database(images_database_file)

        log.ODM_INFO('Found %s usable images' % len(photos))

        # Create reconstruction object
        reconstruction = types.ODM_Reconstruction(photos)

        if tree.odm_georeferencing_gcp and not args.use_exif:
            reconstruction.georeference_with_gcp(
                tree.odm_georeferencing_gcp,
                tree.odm_georeferencing_coords,
                tree.odm_georeferencing_gcp_utm,
                tree.odm_georeferencing_model_txt_geo,
                rerun=self.rerun())
        else:
            reconstruction.georeference_with_gps(
                tree.dataset_raw,
                tree.odm_georeferencing_coords,
                tree.odm_georeferencing_model_txt_geo,
                rerun=self.rerun())

        reconstruction.save_proj_srs(
            os.path.join(tree.odm_georeferencing,
                         tree.odm_georeferencing_proj))
        outputs['reconstruction'] = reconstruction
Esempio n. 24
0
def config():
    parser.add_argument('--images',
                        '-i',
                        metavar='<path>',
                        help='Path to input images'),

    parser.add_argument('--project-path',
                        metavar='<path>',
                        help='Path to the project folder')

    parser.add_argument(
        'name',
        metavar='<project name>',
        type=alphanumeric_string,
        help='Name of Project (i.e subdirectory of projects folder)')

    parser.add_argument('--resize-to',
                        metavar='<integer>',
                        default=2048,
                        type=int,
                        help='resizes images by the largest side for opensfm. '
                        'Set to -1 to disable. Default:  %(default)s')

    parser.add_argument('--start-with',
                        '-s',
                        metavar='<string>',
                        default='resize',
                        choices=processopts,
                        help=('Can be one of: ' + ' | '.join(processopts)))

    parser.add_argument('--end-with',
                        '-e',
                        metavar='<string>',
                        default='odm_orthophoto',
                        choices=processopts,
                        help=('Can be one of:' + ' | '.join(processopts)))

    rerun = parser.add_mutually_exclusive_group()

    rerun.add_argument('--rerun',
                       '-r',
                       metavar='<string>',
                       choices=processopts,
                       help=('Can be one of:' + ' | '.join(processopts)))

    rerun.add_argument('--rerun-all',
                       action='store_true',
                       default=False,
                       help='force rerun of all tasks')

    rerun.add_argument('--rerun-from',
                       action=RerunFrom,
                       metavar='<string>',
                       choices=processopts,
                       help=('Can be one of:' + ' | '.join(processopts)))

    parser.add_argument('--video',
                        metavar='<string>',
                        help='Path to the video file to process')

    parser.add_argument('--slam-config',
                        metavar='<string>',
                        help='Path to config file for orb-slam')

    parser.add_argument('--force-focal',
                        metavar='<positive float>',
                        type=float,
                        help=('Override the focal length information for the '
                              'images'))

    parser.add_argument(
        '--proj',
        metavar='<PROJ4 string>',
        help=
        'Projection used to transform the model into geographic coordinates')

    parser.add_argument(
        '--force-ccd',
        metavar='<positive float>',
        type=float,
        help='Override the ccd width information for the images')

    parser.add_argument(
        '--min-num-features',
        metavar='<integer>',
        default=8000,
        type=int,
        help=('Minimum number of features to extract per image. '
              'More features leads to better results but slower '
              'execution. Default: %(default)s'))

    parser.add_argument(
        '--matcher-neighbors',
        type=int,
        metavar='<integer>',
        default=8,
        help='Number of nearest images to pre-match based on GPS '
        'exif data. Set to 0 to skip pre-matching. '
        'Neighbors works together with Distance parameter, '
        'set both to 0 to not use pre-matching. OpenSFM '
        'uses both parameters at the same time, Bundler '
        'uses only one which has value, prefering the '
        'Neighbors parameter. Default: %(default)s')

    parser.add_argument(
        '--matcher-distance',
        metavar='<integer>',
        default=0,
        type=int,
        help='Distance threshold in meters to find pre-matching '
        'images based on GPS exif data. Set both '
        'matcher-neighbors and this to 0 to skip '
        'pre-matching. Default: %(default)s')

    parser.add_argument(
        '--use-fixed-camera-params',
        action='store_true',
        default=False,
        help='Turn off camera parameter optimization during bundler')

    parser.add_argument(
        '--max-concurrency',
        metavar='<positive integer>',
        default=context.num_cores,
        type=int,
        help=('The maximum number of processes to use in various '
              'processes. Peak memory requirement is ~1GB per '
              'thread and 2 megapixel image resolution. Default: %(default)s'))

    parser.add_argument(
        '--depthmap-resolution',
        metavar='<positive float>',
        type=float,
        default=640,
        help=
        ('Controls the density of the point cloud by setting the resolution of the depthmap images. Higher values take longer to compute '
         'but produce denser point clouds. '
         'Default: %(default)s'))

    parser.add_argument(
        '--opensfm-depthmap-min-consistent-views',
        metavar='<integer: 2 <= x <= 9>',
        type=int,
        default=3,
        help=
        ('Minimum number of views that should reconstruct a point for it to be valid. Use lower values '
         'if your images have less overlap. Lower values result in denser point clouds '
         'but with more noise. '
         'Default: %(default)s'))

    parser.add_argument(
        '--opensfm-depthmap-method',
        metavar='<string>',
        default='PATCH_MATCH',
        choices=['PATCH_MATCH', 'BRUTE_FORCE', 'PATCH_MATCH_SAMPLE'],
        help=
        ('Raw depthmap computation algorithm. '
         'PATCH_MATCH and PATCH_MATCH_SAMPLE are faster, but might miss some valid points. '
         'BRUTE_FORCE takes longer but produces denser reconstructions. '
         'Default: %(default)s'))

    parser.add_argument(
        '--opensfm-depthmap-min-patch-sd',
        metavar='<positive float>',
        type=float,
        default=1,
        help=
        ('When using PATCH_MATCH or PATCH_MATCH_SAMPLE, controls the standard deviation threshold to include patches. '
         'Patches with lower standard deviation are ignored. '
         'Default: %(default)s'))

    parser.add_argument(
        '--use-hybrid-bundle-adjustment',
        action='store_true',
        default=False,
        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.'
    )

    parser.add_argument(
        '--use-3dmesh',
        action='store_true',
        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.'
    )

    parser.add_argument(
        '--skip-3dmodel',
        action='store_true',
        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.'
    )

    parser.add_argument(
        '--use-opensfm-dense',
        action='store_true',
        default=False,
        help='Use opensfm to compute dense point cloud alternatively')

    parser.add_argument(
        '--ignore-gsd',
        action='store_true',
        default=False,
        help='Ignore Ground Sampling Distance (GSD). GSD '
        'caps the maximum resolution of image outputs and '
        'resizes images when necessary, resulting in faster processing and '
        'lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality.'
    )

    parser.add_argument(
        '--smvs-alpha',
        metavar='<float>',
        default=1.0,
        type=float,
        help='Regularization parameter, a higher alpha leads to '
        'smoother surfaces. Default: %(default)s')

    parser.add_argument(
        '--smvs-output-scale',
        metavar='<positive integer>',
        default=2,
        type=int,
        help='The scale of the optimization - the '
        'finest resolution of the bicubic patches will have the'
        ' size of the respective power of 2 (e.g. 2 will '
        'optimize patches covering down to 4x4 pixels). '
        'Default: %(default)s')

    parser.add_argument(
        '--smvs-enable-shading',
        action='store_true',
        default=False,
        help='Use shading-based optimization. This model cannot '
        'handle complex scenes. Try to supply linear images to '
        'the reconstruction pipeline that are not tone mapped '
        'or altered as this can also have very negative effects '
        'on the reconstruction. If you have simple JPGs with SRGB '
        'gamma correction you can remove it with the --smvs-gamma-srgb '
        'option. Default: %(default)s')

    parser.add_argument(
        '--smvs-gamma-srgb',
        action='store_true',
        default=False,
        help='Apply inverse SRGB gamma correction. To be used '
        'with --smvs-enable-shading when you have simple JPGs with '
        'SRGB gamma correction. Default: %(default)s')

    parser.add_argument('--mesh-size',
                        metavar='<positive integer>',
                        default=100000,
                        type=int,
                        help=('The maximum vertex count of the output mesh. '
                              'Default: %(default)s'))

    parser.add_argument(
        '--mesh-octree-depth',
        metavar='<positive integer>',
        default=9,
        type=int,
        help=('Oct-tree depth used in the mesh reconstruction, '
              'increase to get more vertices, recommended '
              'values are 8-12. Default: %(default)s'))

    parser.add_argument('--mesh-samples',
                        metavar='<float >= 1.0>',
                        default=1.0,
                        type=float,
                        help=('Number of points per octree node, recommended '
                              'and default value: %(default)s'))

    parser.add_argument(
        '--mesh-point-weight',
        metavar='<interpolation weight>',
        default=4,
        type=float,
        help=('This floating point value specifies the importance'
              ' that interpolation of the point samples is given in the '
              'formulation of the screened Poisson equation. The results '
              'of the original (unscreened) Poisson Reconstruction can '
              'be obtained by setting this value to 0.'
              'Default= %(default)s'))

    parser.add_argument(
        '--fast-orthophoto',
        action='store_true',
        default=False,
        help='Skips dense reconstruction and 3D model generation. '
        'It generates an orthophoto directly from the sparse reconstruction. '
        'If you just need an orthophoto and do not need a full 3D model, turn on this option. '
        'Experimental.')

    parser.add_argument(
        '--crop',
        metavar='<positive float>',
        default=3,
        type=float,
        help=('Automatically crop image outputs by creating a smooth buffer '
              'around the dataset boundaries, shrinked by N meters. '
              'Use 0 to disable cropping. '
              'Default: %(default)s'))

    parser.add_argument(
        '--pc-classify',
        metavar='<string>',
        default='none',
        choices=['none', 'smrf', 'pmf'],
        help='Classify the .LAS point cloud output using either '
        'a Simple Morphological Filter or a Progressive Morphological Filter. '
        'If --dtm is set this parameter defaults to smrf. '
        'You can control the behavior of both smrf and pmf by tweaking the --dem-* parameters. '
        'Default: '
        '%(default)s')

    parser.add_argument(
        '--pc-csv',
        action='store_true',
        default=False,
        help=
        'Export the georeferenced point cloud in CSV format. Default:  %(default)s'
    )

    parser.add_argument('--texturing-data-term',
                        metavar='<string>',
                        default='gmi',
                        choices=['gmi', 'area'],
                        help=('Data term: [area, gmi]. Default: '
                              '%(default)s'))

    parser.add_argument(
        '--texturing-nadir-weight',
        metavar='<integer: 0 <= x <= 32>',
        default=16,
        type=int,
        help=
        ('Affects orthophotos only. '
         'Higher values result in sharper corners, but can affect color distribution and blurriness. '
         'Use lower values for planar areas and higher values for urban areas. '
         'The default value works well for most scenarios. Default: '
         '%(default)s'))

    parser.add_argument(
        '--texturing-outlier-removal-type',
        metavar='<string>',
        default='gauss_clamping',
        choices=['none', 'gauss_clamping', 'gauss_damping'],
        help=('Type of photometric outlier removal method: '
              '[none, gauss_damping, gauss_clamping]. Default: '
              '%(default)s'))

    parser.add_argument('--texturing-skip-visibility-test',
                        action='store_true',
                        default=False,
                        help=('Skip geometric visibility test. Default: '
                              ' %(default)s'))

    parser.add_argument('--texturing-skip-global-seam-leveling',
                        action='store_true',
                        default=False,
                        help=('Skip global seam leveling. Useful for IR data.'
                              'Default: %(default)s'))

    parser.add_argument('--texturing-skip-local-seam-leveling',
                        action='store_true',
                        default=False,
                        help='Skip local seam blending. Default:  %(default)s')

    parser.add_argument('--texturing-skip-hole-filling',
                        action='store_true',
                        default=False,
                        help=('Skip filling of holes in the mesh. Default: '
                              ' %(default)s'))

    parser.add_argument(
        '--texturing-keep-unseen-faces',
        action='store_true',
        default=False,
        help=('Keep faces in the mesh that are not seen in any camera. '
              'Default:  %(default)s'))

    parser.add_argument('--texturing-tone-mapping',
                        metavar='<string>',
                        choices=['none', 'gamma'],
                        default='none',
                        help='Turn on gamma tone mapping or none for no tone '
                        'mapping. Choices are  \'gamma\' or \'none\'. '
                        'Default: %(default)s ')

    parser.add_argument('--gcp',
                        metavar='<path string>',
                        default=None,
                        help=('path to the file containing the ground control '
                              'points used for georeferencing.  Default: '
                              '%(default)s. The file needs to '
                              'be on the following line format: \neasting '
                              'northing height pixelrow pixelcol imagename'))

    parser.add_argument('--use-exif',
                        action='store_true',
                        default=False,
                        help=('Use this tag if you have a gcp_list.txt but '
                              'want to use the exif geotags instead'))

    parser.add_argument(
        '--dtm',
        action='store_true',
        default=False,
        help=
        'Use this tag to build a DTM (Digital Terrain Model, ground only) using a progressive '
        'morphological filter. Check the --dem* parameters for fine tuning.')

    parser.add_argument(
        '--dsm',
        action='store_true',
        default=False,
        help=
        'Use this tag to build a DSM (Digital Surface Model, ground + objects) using a progressive '
        'morphological filter. Check the --dem* parameters for fine tuning.')

    parser.add_argument(
        '--dem-gapfill-steps',
        metavar='<positive integer>',
        default=3,
        type=int,
        help=
        'Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. '
        'Starting with a radius equal to the output resolution, N different DEMs are generated with '
        'progressively bigger radius using the inverse distance weighted (IDW) algorithm '
        'and merged together. Remaining gaps are then merged using nearest neighbor interpolation. '
        '\nDefault=%(default)s')

    parser.add_argument('--dem-resolution',
                        metavar='<float>',
                        type=float,
                        default=5,
                        help='DSM/DTM resolution in cm / pixel.'
                        '\nDefault: %(default)s')

    parser.add_argument(
        '--dem-maxangle',
        metavar='<positive float>',
        type=float,
        default=20,
        help=
        'Points that are more than maxangle degrees off-nadir are discarded. '
        '\nDefault: '
        '%(default)s')

    parser.add_argument(
        '--dem-maxsd',
        metavar='<positive float>',
        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-initial-distance',
        metavar='<positive float>',
        type=float,
        default=0.15,
        help=
        'Used to classify ground vs non-ground points. Set this value to account for Z noise in meters. '
        'If you have an uncertainty of around 15 cm, set this value large enough to not exclude these points. '
        'Too small of a value will exclude valid ground points, while too large of a value will misclassify non-ground points for ground ones. '
        '\nDefault: '
        '%(default)s')

    parser.add_argument('--dem-approximate',
                        action='store_true',
                        default=False,
                        help='Use this tag use the approximate progressive  '
                        'morphological filter, which computes DEMs faster '
                        'but is not as accurate.')

    parser.add_argument(
        '--dem-decimation',
        metavar='<positive integer>',
        default=1,
        type=int,
        help=
        'Decimate the points before generating the DEM. 1 is no decimation (full quality). '
        '100 decimates ~99%% of the points. Useful for speeding up '
        'generation.\nDefault=%(default)s')

    parser.add_argument(
        '--dem-terrain-type',
        metavar='<string>',
        choices=[
            'FlatNonForest', 'FlatForest', 'ComplexNonForest', 'ComplexForest'
        ],
        default='ComplexForest',
        help=
        'One of: %(choices)s. Specifies the type of terrain. This mainly helps reduce processing time. '
        '\nFlatNonForest: Relatively flat region with little to no vegetation'
        '\nFlatForest: Relatively flat region that is forested'
        '\nComplexNonForest: Varied terrain with little to no vegetation'
        '\nComplexForest: Varied terrain that is forested'
        '\nDefault=%(default)s')

    parser.add_argument('--orthophoto-resolution',
                        metavar='<float > 0.0>',
                        default=5,
                        type=float,
                        help=('Orthophoto resolution in cm / pixel.\n'
                              'Default: %(default)s'))

    parser.add_argument(
        '--orthophoto-target-srs',
        metavar="<EPSG:XXXX>",
        type=str,
        default=None,
        help='Target spatial reference for orthophoto creation. '
        'Not implemented yet.\n'
        'Default: %(default)s')

    parser.add_argument(
        '--orthophoto-no-tiled',
        action='store_true',
        default=False,
        help='Set this parameter if you want a stripped geoTIFF.\n'
        'Default: %(default)s')

    parser.add_argument(
        '--orthophoto-compression',
        metavar='<string>',
        type=str,
        choices=['JPEG', 'LZW', 'PACKBITS', 'DEFLATE', 'LZMA', 'NONE'],
        default='DEFLATE',
        help='Set the compression to use. Note that this could '
        'break gdal_translate if you don\'t know what you '
        'are doing. Options: %(choices)s.\nDefault: %(default)s')

    parser.add_argument(
        '--orthophoto-bigtiff',
        type=str,
        choices=['YES', 'NO', 'IF_NEEDED', 'IF_SAFER'],
        default='IF_SAFER',
        help='Control whether the created orthophoto is a BigTIFF or '
        'classic TIFF. BigTIFF is a variant for files larger than '
        '4GiB of data. Options are %(choices)s. See GDAL specs: '
        'https://www.gdal.org/frmt_gtiff.html for more info. '
        '\nDefault: %(default)s')

    parser.add_argument('--build-overviews',
                        action='store_true',
                        default=False,
                        help='Build orthophoto overviews using gdaladdo.')

    parser.add_argument('--zip-results',
                        action='store_true',
                        default=False,
                        help='compress the results using gunzip')

    parser.add_argument('--verbose',
                        '-v',
                        action='store_true',
                        default=False,
                        help='Print additional messages to the console\n'
                        'Default: %(default)s')

    parser.add_argument('--time',
                        action='store_true',
                        default=False,
                        help='Generates a benchmark file with runtime info\n'
                        'Default: %(default)s')

    parser.add_argument('--version',
                        action='version',
                        version='OpenDroneMap {0}'.format(__version__),
                        help='Displays version number and exits. ')

    args = parser.parse_args()

    # check that the project path setting has been set properly
    if not args.project_path:
        log.ODM_ERROR('You need to set the project path in the '
                      'settings.yaml file before you can run ODM, '
                      'or use `--project-path <path>`. Run `python '
                      'run.py --help` for more information. ')
        sys.exit(1)

    if args.fast_orthophoto:
        log.ODM_INFO(
            'Fast orthophoto is turned on, automatically setting --skip-3dmodel'
        )
        args.skip_3dmodel = True

    if args.dtm and args.pc_classify == 'none':
        log.ODM_INFO(
            "DTM is turned on, automatically turning on point cloud classification"
        )
        args.pc_classify = "smrf"

    if args.skip_3dmodel and args.use_3dmesh:
        log.ODM_WARNING(
            '--skip-3dmodel is set, but so is --use-3dmesh. You can\'t have both!'
        )
        sys.exit(1)

    return args
Esempio n. 25
0
    def process(self, args, outputs):
        tree = outputs['tree']

        if outputs['large']:
            if not os.path.exists(tree.submodels_path):
                log.ODM_ERROR(
                    "We reached the merge stage, but %s folder does not exist. Something must have gone wrong at an earlier stage. Check the log and fix possible problem before restarting?" % tree.submodels_path)
                exit(1)

            # Merge point clouds
            if args.merge in ['all', 'pointcloud']:
                if not io.file_exists(tree.odm_georeferencing_model_laz) or self.rerun():
                    all_point_clouds = get_submodel_paths(tree.submodels_path, "odm_georeferencing",
                                                          "odm_georeferenced_model.laz")

                    try:
                        point_cloud.merge(all_point_clouds, tree.odm_georeferencing_model_laz)
                        point_cloud.post_point_cloud_steps(args, tree)
                    except Exception as e:
                        log.ODM_WARNING("Could not merge point cloud: %s (skipping)" % str(e))
                else:
                    log.ODM_WARNING("Found merged point cloud in %s" % tree.odm_georeferencing_model_laz)

            self.update_progress(25)

            # Merge crop bounds
            merged_bounds_file = os.path.join(tree.odm_georeferencing, 'odm_georeferenced_model.bounds.gpkg')
            if not io.file_exists(merged_bounds_file) or self.rerun():
                all_bounds = get_submodel_paths(tree.submodels_path, 'odm_georeferencing',
                                                'odm_georeferenced_model.bounds.gpkg')
                log.ODM_INFO("Merging all crop bounds: %s" % all_bounds)
                if len(all_bounds) > 0:
                    # Calculate a new crop area
                    # based on the convex hull of all crop areas of all submodels
                    # (without a buffer, otherwise we are double-cropping)
                    Cropper.merge_bounds(all_bounds, merged_bounds_file, 0)
                else:
                    log.ODM_WARNING("No bounds found for any submodel.")

            # Merge orthophotos
            if args.merge in ['all', 'orthophoto']:
                if not io.dir_exists(tree.odm_orthophoto):
                    system.mkdir_p(tree.odm_orthophoto)

                if not io.file_exists(tree.odm_orthophoto_tif) or self.rerun():
                    all_orthos_and_ortho_cuts = get_all_submodel_paths(tree.submodels_path,
                                                                       os.path.join("odm_orthophoto",
                                                                                    "odm_orthophoto_feathered.tif"),
                                                                       os.path.join("odm_orthophoto",
                                                                                    "odm_orthophoto_cut.tif"),
                                                                       )

                    if len(all_orthos_and_ortho_cuts) > 1:
                        log.ODM_INFO(
                            "Found %s submodels with valid orthophotos and cutlines" % len(all_orthos_and_ortho_cuts))

                        # TODO: histogram matching via rasterio
                        # currently parts have different color tones

                        if io.file_exists(tree.odm_orthophoto_tif):
                            os.remove(tree.odm_orthophoto_tif)

                        orthophoto_vars = orthophoto.get_orthophoto_vars(args)
                        orthophoto.merge(all_orthos_and_ortho_cuts, tree.odm_orthophoto_tif, orthophoto_vars)
                        orthophoto.post_orthophoto_steps(args, merged_bounds_file, tree.odm_orthophoto_tif,
                                                         tree.orthophoto_tiles)
                    elif len(all_orthos_and_ortho_cuts) == 1:
                        # Simply copy
                        log.ODM_WARNING("A single orthophoto/cutline pair was found between all submodels.")
                        shutil.copyfile(all_orthos_and_ortho_cuts[0][0], tree.odm_orthophoto_tif)
                    else:
                        log.ODM_WARNING(
                            "No orthophoto/cutline pairs were found in any of the submodels. No orthophoto will be generated.")
                else:
                    log.ODM_WARNING("Found merged orthophoto in %s" % tree.odm_orthophoto_tif)

            self.update_progress(75)

            # Merge DEMs
            def merge_dems(dem_filename, human_name):
                if not io.dir_exists(tree.path('odm_dem')):
                    system.mkdir_p(tree.path('odm_dem'))

                dem_file = tree.path("odm_dem", dem_filename)
                if not io.file_exists(dem_file) or self.rerun():
                    all_dems = get_submodel_paths(tree.submodels_path, "odm_dem", dem_filename)
                    log.ODM_INFO("Merging %ss" % human_name)

                    # Merge
                    dem_vars = utils.get_dem_vars(args)
                    eu_map_source = None  # Default

                    # Use DSM's euclidean map for DTMs
                    # (requires the DSM to be computed)
                    if human_name == "DTM":
                        eu_map_source = "dsm"

                    euclidean_merge_dems(all_dems, dem_file, dem_vars, euclidean_map_source=eu_map_source)

                    if io.file_exists(dem_file):
                        # Crop
                        if args.crop > 0:
                            Cropper.crop(merged_bounds_file, dem_file, dem_vars,
                                         keep_original=not args.optimize_disk_space)
                        log.ODM_INFO("Created %s" % dem_file)

                        if args.tiles:
                            generate_dem_tiles(dem_file, tree.path("%s_tiles" % human_name.lower()),
                                               args.max_concurrency)
                    else:
                        log.ODM_WARNING("Cannot merge %s, %s was not created" % (human_name, dem_file))

                else:
                    log.ODM_WARNING("Found merged %s in %s" % (human_name, dem_filename))

            if args.merge in ['all', 'dem'] and args.dsm:
                merge_dems("dsm.tif", "DSM")

            if args.merge in ['all', 'dem'] and args.dtm:
                merge_dems("dtm.tif", "DTM")

            self.update_progress(95)

            # Merge reports
            if not io.dir_exists(tree.odm_report):
                system.mkdir_p(tree.odm_report)

            geojson_shots = tree.path(tree.odm_report, "shots.geojson")
            if not io.file_exists(geojson_shots) or self.rerun():
                geojson_shots_files = get_submodel_paths(tree.submodels_path, "odm_report", "shots.geojson")
                log.ODM_INFO("Merging %s shots.geojson files" % len(geojson_shots_files))
                merge_geojson_shots(geojson_shots_files, geojson_shots)
            else:
                log.ODM_WARNING("Found merged shots.geojson in %s" % tree.odm_report)

            # Stop the pipeline short! We're done.
            self.next_stage = None
        else:
            log.ODM_INFO("Normal dataset, nothing to merge.")
            self.progress = 0.0
Esempio n. 26
0
    def process(self, inputs, outputs):

        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running SMVS Cell')

        # get inputs
        tree = inputs.tree
        args = inputs.args
        reconstruction = inputs.reconstruction
        photos = reconstruction.photos

        if not photos:
            log.ODM_ERROR('Not enough photos in photos array to start SMVS')
            return ecto.QUIT

        # check if we rerun cell or not
        rerun_cell = (args.rerun is not None and
                      args.rerun == 'smvs') or \
                     (args.rerun_all) or \
                     (args.rerun_from is not None and
                      'smvs' in args.rerun_from)

        # check if reconstruction was done before
        if not io.file_exists(tree.smvs_model) or rerun_cell:
            # cleanup if a rerun
            if io.dir_exists(tree.mve_path) and rerun_cell:
                shutil.rmtree(tree.mve_path)

            # make bundle directory
            if not io.file_exists(tree.mve_bundle):
                system.mkdir_p(tree.mve_path)
                system.mkdir_p(io.join_paths(tree.mve_path, 'bundle'))
                io.copy(tree.opensfm_image_list, tree.mve_image_list)
                io.copy(tree.opensfm_bundle, tree.mve_bundle)

            # mve makescene wants the output directory
            # to not exists before executing it (otherwise it
            # will prompt the user for confirmation)
            if io.dir_exists(tree.smvs):
                shutil.rmtree(tree.smvs)

            # run mve makescene
            if not io.dir_exists(tree.mve_views):
                system.run('%s %s %s' % (context.makescene_path, tree.mve_path, tree.smvs))

            # config
            config = [
                "-t%s" % self.params.threads,
                "-a%s" % self.params.alpha,
                "--max-pixels=%s" % int(self.params.max_pixels),
                "-o%s" % self.params.output_scale,
                "--debug-lvl=%s" % ('1' if self.params.verbose else '0'),
                "%s" % '-S' if self.params.shading else '',
                "%s" % '-g' if self.params.gamma_srgb and self.params.shading else '',
                "--force" if rerun_cell else ''
            ]

            # run smvs
            system.run('%s %s %s' % (context.smvs_path, ' '.join(config), tree.smvs))
            
            # find and rename the output file for simplicity
            smvs_files = glob.glob(os.path.join(tree.smvs, 'smvs-*'))
            smvs_files.sort(key=os.path.getmtime) # sort by last modified date
            if len(smvs_files) > 0:
                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_filter, 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:
            log.ODM_WARNING('Found a valid SMVS reconstruction file in: %s' %
                            tree.smvs_model)

        outputs.reconstruction = reconstruction

        if args.time:
            system.benchmark(start_time, tree.benchmarking, 'SMVS')

        log.ODM_INFO('Running ODM SMVS Cell - Finished')
        return ecto.OK if args.end_with != 'smvs' else ecto.QUIT
Esempio n. 27
0
    def process(self, inputs, outputs):

        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running ODM OpenSfM Cell')

        # get inputs
        tree = inputs.tree
        args = inputs.args
        reconstruction = inputs.reconstruction
        photos = reconstruction.photos

        if not photos:
            log.ODM_ERROR('Not enough photos in photos array to start OpenSfM')
            return ecto.QUIT

        # create working directories
        system.mkdir_p(tree.opensfm)
        system.mkdir_p(tree.pmvs)

        # check if we rerun cell or not
        rerun_cell = (args.rerun is not None and
                      args.rerun == 'opensfm') or \
                     (args.rerun_all) or \
                     (args.rerun_from is not None and
                      'opensfm' in args.rerun_from)

        if not args.use_pmvs:
            output_file = tree.opensfm_model
            if args.fast_orthophoto:
                output_file = io.join_paths(tree.opensfm, 'reconstruction.ply')
        else:
            output_file = tree.opensfm_reconstruction

        # check if reconstruction was done before
        if not io.file_exists(output_file) or rerun_cell:
            # create file list
            list_path = io.join_paths(tree.opensfm, 'image_list.txt')
            has_alt = True
            with open(list_path, 'w') as fout:
                for photo in photos:
                    if not photo.altitude:
                        has_alt = False
                    fout.write('%s\n' % photo.path_file)

            # create config file for OpenSfM
            config = [
                "use_exif_size: %s" %
                ('no' if not self.params.use_exif_size else 'yes'),
                "feature_process_size: %s" % self.params.feature_process_size,
                "feature_min_frames: %s" % self.params.feature_min_frames,
                "processes: %s" % self.params.processes,
                "matching_gps_neighbors: %s" %
                self.params.matching_gps_neighbors,
                "depthmap_method: %s" % args.opensfm_depthmap_method,
                "depthmap_resolution: %s" % args.opensfm_depthmap_resolution,
                "depthmap_min_patch_sd: %s" %
                args.opensfm_depthmap_min_patch_sd,
                "depthmap_min_consistent_views: %s" %
                args.opensfm_depthmap_min_consistent_views,
                "optimize_camera_parameters: %s" %
                ('no' if self.params.fixed_camera_params else 'yes')
            ]

            if has_alt:
                log.ODM_DEBUG(
                    "Altitude data detected, enabling it for GPS alignment")
                config.append("use_altitude_tag: True")
                config.append("align_method: naive")

            if args.use_hybrid_bundle_adjustment:
                log.ODM_DEBUG("Enabling hybrid bundle adjustment")
                config.append(
                    "bundle_interval: 100"
                )  # Bundle after adding 'bundle_interval' cameras
                config.append(
                    "bundle_new_points_ratio: 1.2"
                )  # Bundle when (new points) / (bundled points) > bundle_new_points_ratio
                config.append(
                    "local_bundle_radius: 1"
                )  # Max image graph distance for images to be included in local bundle adjustment

            if args.matcher_distance > 0:
                config.append("matching_gps_distance: %s" %
                              self.params.matching_gps_distance)

            if tree.odm_georeferencing_gcp:
                config.append("bundle_use_gcp: yes")
                io.copy(tree.odm_georeferencing_gcp, tree.opensfm)

            # write config file
            log.ODM_DEBUG(config)
            config_filename = io.join_paths(tree.opensfm, 'config.yaml')
            with open(config_filename, 'w') as fout:
                fout.write("\n".join(config))

            # run OpenSfM reconstruction
            matched_done_file = io.join_paths(tree.opensfm,
                                              'matching_done.txt')
            if not io.file_exists(matched_done_file) or rerun_cell:
                system.run('PYTHONPATH=%s %s/bin/opensfm extract_metadata %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
                system.run('PYTHONPATH=%s %s/bin/opensfm detect_features %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
                system.run('PYTHONPATH=%s %s/bin/opensfm match_features %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
                with open(matched_done_file, 'w') as fout:
                    fout.write("Matching done!\n")
            else:
                log.ODM_WARNING(
                    'Found a feature matching done progress file in: %s' %
                    matched_done_file)

            if not io.file_exists(tree.opensfm_tracks) or rerun_cell:
                system.run('PYTHONPATH=%s %s/bin/opensfm create_tracks %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
            else:
                log.ODM_WARNING('Found a valid OpenSfM tracks file in: %s' %
                                tree.opensfm_tracks)

            if not io.file_exists(tree.opensfm_reconstruction) or rerun_cell:
                system.run('PYTHONPATH=%s %s/bin/opensfm reconstruct %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
            else:
                log.ODM_WARNING(
                    'Found a valid OpenSfM reconstruction file in: %s' %
                    tree.opensfm_reconstruction)

            if not io.file_exists(
                    tree.opensfm_reconstruction_meshed) or rerun_cell:
                system.run('PYTHONPATH=%s %s/bin/opensfm mesh %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))
            else:
                log.ODM_WARNING(
                    'Found a valid OpenSfM meshed reconstruction file in: %s' %
                    tree.opensfm_reconstruction_meshed)

            if not args.use_pmvs:
                if not io.file_exists(
                        tree.opensfm_reconstruction_nvm) or rerun_cell:
                    system.run(
                        'PYTHONPATH=%s %s/bin/opensfm export_visualsfm %s' %
                        (context.pyopencv_path, context.opensfm_path,
                         tree.opensfm))
                else:
                    log.ODM_WARNING(
                        'Found a valid OpenSfM NVM reconstruction file in: %s'
                        % tree.opensfm_reconstruction_nvm)

                system.run('PYTHONPATH=%s %s/bin/opensfm undistort %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm))

                # Skip dense reconstruction if necessary and export
                # sparse reconstruction instead
                if args.fast_orthophoto:
                    system.run(
                        'PYTHONPATH=%s %s/bin/opensfm export_ply --no-cameras %s'
                        % (context.pyopencv_path, context.opensfm_path,
                           tree.opensfm))
                else:
                    system.run(
                        'PYTHONPATH=%s %s/bin/opensfm compute_depthmaps %s' %
                        (context.pyopencv_path, context.opensfm_path,
                         tree.opensfm))

        else:
            log.ODM_WARNING(
                'Found a valid OpenSfM reconstruction file in: %s' %
                tree.opensfm_reconstruction)

        # check if reconstruction was exported to bundler before
        if not io.file_exists(tree.opensfm_bundle_list) or rerun_cell:
            # convert back to bundler's format
            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)

        if args.use_pmvs:
            # check if reconstruction was exported to pmvs before
            if not io.file_exists(tree.pmvs_visdat) or rerun_cell:
                # run PMVS converter
                system.run('PYTHONPATH=%s %s/bin/export_pmvs %s --output %s' %
                           (context.pyopencv_path, context.opensfm_path,
                            tree.opensfm, tree.pmvs))
            else:
                log.ODM_WARNING('Found a valid CMVS file in: %s' %
                                tree.pmvs_visdat)

        if reconstruction.georef:
            system.run(
                'PYTHONPATH=%s %s/bin/opensfm export_geocoords %s --transformation --proj \'%s\''
                % (context.pyopencv_path, context.opensfm_path, tree.opensfm,
                   reconstruction.georef.projection.srs))

        outputs.reconstruction = reconstruction

        if args.time:
            system.benchmark(start_time, tree.benchmarking, 'OpenSfM')

        log.ODM_INFO('Running ODM OpenSfM Cell - Finished')
        return ecto.OK if args.end_with != 'opensfm' else ecto.QUIT
Esempio n. 28
0
def config():
    parser.add_argument('--project-path',
                        metavar='<path>',
                        help='Path to the project folder')

    parser.add_argument('name',
                        metavar='<project name>',
                        type=alphanumeric_string,
                        default='code',
                        nargs='?',
                        help='Name of Project (i.e subdirectory of projects folder)')

    parser.add_argument('--resize-to',
                        metavar='<integer>',
                        default=2048,
                        type=int,
                        help='Resizes images by the largest side for feature extraction purposes only. '
                             'Set to -1 to disable. This does not affect the final orthophoto '
                             ' resolution quality and will not resize the original images. Default:  %(default)s')

    parser.add_argument('--end-with', '-e',
                        metavar='<string>',
                        default='odm_orthophoto',
                        choices=processopts,
                        help=('Can be one of:' + ' | '.join(processopts)))

    rerun = parser.add_mutually_exclusive_group()

    rerun.add_argument('--rerun', '-r',
                       metavar='<string>',
                       choices=processopts,
                       help=('Can be one of:' + ' | '.join(processopts)))

    rerun.add_argument('--rerun-all',
                       action='store_true',
                       default=False,
                       help='force rerun of all tasks')

    rerun.add_argument('--rerun-from',
                       action=RerunFrom,
                       metavar='<string>',
                       choices=processopts,
                       help=('Can be one of:' + ' | '.join(processopts)))

    # parser.add_argument('--video',
    #                     metavar='<string>',
    #                     help='Path to the video file to process')

    # parser.add_argument('--slam-config',
    #                     metavar='<string>',
    #                     help='Path to config file for orb-slam')

    parser.add_argument('--min-num-features',
                        metavar='<integer>',
                        default=8000,
                        type=int,
                        help=('Minimum number of features to extract per image. '
                              'More features leads to better results but slower '
                              'execution. Default: %(default)s'))

    parser.add_argument('--matcher-neighbors',
                        type=int,
                        metavar='<integer>',
                        default=8,
                        help='Number of nearest images to pre-match based on GPS '
                             'exif data. Set to 0 to skip pre-matching. '
                             'Neighbors works together with Distance parameter, '
                             'set both to 0 to not use pre-matching. OpenSFM '
                             'uses both parameters at the same time, Bundler '
                             'uses only one which has value, prefering the '
                             'Neighbors parameter. Default: %(default)s')

    parser.add_argument('--matcher-distance',
                        metavar='<integer>',
                        default=0,
                        type=int,
                        help='Distance threshold in meters to find pre-matching '
                             'images based on GPS exif data. Set both '
                             'matcher-neighbors and this to 0 to skip '
                             'pre-matching. Default: %(default)s')

    parser.add_argument('--use-fixed-camera-params',
                        action='store_true',
                        default=False,
                        help='Turn off camera parameter optimization during bundler')

    parser.add_argument('--cameras',
                        default='',
                        metavar='<json>',
                        type=path_or_json_string,
                        help='Use the camera parameters computed from '
                             'another dataset instead of calculating them. '
                             'Can be specified either as path to a cameras.json file or as a '
                             'JSON string representing the contents of a '
                             'cameras.json file. Default: %(default)s')
    
    parser.add_argument('--camera-lens',
            metavar='<string>',
            default='auto',
            choices=['auto', 'perspective', 'brown', 'fisheye', 'spherical'],
            help=('Set a camera projection type. Manually setting a value '
                'can help improve geometric undistortion. By default the application '
                'tries to determine a lens type from the images metadata. Can be '
                'set to one of: [auto, perspective, brown, fisheye, spherical]. Default: '
                '%(default)s'))

    parser.add_argument('--max-concurrency',
                        metavar='<positive integer>',
                        default=context.num_cores,
                        type=int,
                        help=('The maximum number of processes to use in various '
                              'processes. Peak memory requirement is ~1GB per '
                              'thread and 2 megapixel image resolution. Default: %(default)s'))

    parser.add_argument('--depthmap-resolution',
                        metavar='<positive float>',
                        type=float,
                        default=640,
                        help=('Controls the density of the point cloud by setting the resolution of the depthmap images. Higher values take longer to compute '
                              'but produce denser point clouds. '
                              'Default: %(default)s'))

    parser.add_argument('--opensfm-depthmap-min-consistent-views',
                      metavar='<integer: 2 <= x <= 9>',
                      type=int,
                      default=3,
                      help=('Minimum number of views that should reconstruct a point for it to be valid. Use lower values '
                            'if your images have less overlap. Lower values result in denser point clouds '
                            'but with more noise. '
                            'Default: %(default)s'))

    parser.add_argument('--opensfm-depthmap-method',
                      metavar='<string>',
                      default='PATCH_MATCH',
                      choices=['PATCH_MATCH', 'BRUTE_FORCE', 'PATCH_MATCH_SAMPLE'],
                      help=('Raw depthmap computation algorithm. '
                            'PATCH_MATCH and PATCH_MATCH_SAMPLE are faster, but might miss some valid points. '
                            'BRUTE_FORCE takes longer but produces denser reconstructions. '
                            'Default: %(default)s'))

    parser.add_argument('--opensfm-depthmap-min-patch-sd',
                      metavar='<positive float>',
                      type=float,
                      default=1,
                      help=('When using PATCH_MATCH or PATCH_MATCH_SAMPLE, controls the standard deviation threshold to include patches. '
                            'Patches with lower standard deviation are ignored. '
                            'Default: %(default)s'))

    parser.add_argument('--use-hybrid-bundle-adjustment',
                        action='store_true',
                        default=False,
                        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.')

    parser.add_argument('--mve-confidence',
                        metavar='<float: 0 <= x <= 1>',
                        type=float,
                        default=0.60,
                        help=('Discard points that have less than a certain confidence threshold. '
                              'This only affects dense reconstructions performed with MVE. '
                              'Higher values discard more points. '
                              'Default: %(default)s'))

    parser.add_argument('--use-3dmesh',
                    action='store_true',
                    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.')

    parser.add_argument('--skip-3dmodel',
                    action='store_true',
                    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.')

    parser.add_argument('--use-opensfm-dense',
                        action='store_true',
                        default=False,
                        help='Use opensfm to compute dense point cloud alternatively')

    parser.add_argument('--ignore-gsd',
                        action='store_true',
                        default=False,
                        help='Ignore Ground Sampling Distance (GSD). GSD '
                        'caps the maximum resolution of image outputs and '
                        'resizes images when necessary, resulting in faster processing and '
                        'lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality.')

    parser.add_argument('--mesh-size',
                        metavar='<positive integer>',
                        default=100000,
                        type=int,
                        help=('The maximum vertex count of the output mesh. '
                              'Default: %(default)s'))

    parser.add_argument('--mesh-octree-depth',
                        metavar='<positive integer>',
                        default=9,
                        type=int,
                        help=('Oct-tree depth used in the mesh reconstruction, '
                              'increase to get more vertices, recommended '
                              'values are 8-12. Default: %(default)s'))

    parser.add_argument('--mesh-samples',
                        metavar='<float >= 1.0>',
                        default=1.0,
                        type=float,
                        help=('Number of points per octree node, recommended '
                              'and default value: %(default)s'))

    parser.add_argument('--mesh-point-weight',
                        metavar='<positive float>',
                        default=4,
                        type=float,
                        help=('This floating point value specifies the importance'
                        ' that interpolation of the point samples is given in the '
                        'formulation of the screened Poisson equation. The results '
                        'of the original (unscreened) Poisson Reconstruction can '
                        'be obtained by setting this value to 0.'
                        'Default= %(default)s'))

    parser.add_argument('--fast-orthophoto',
                action='store_true',
                default=False,
                help='Skips dense reconstruction and 3D model generation. '
                'It generates an orthophoto directly from the sparse reconstruction. '
                'If you just need an orthophoto and do not need a full 3D model, turn on this option. '
                'Experimental.')

    parser.add_argument('--crop',
                    metavar='<positive float>',
                    default=3,
                    type=float,
                    help=('Automatically crop image outputs by creating a smooth buffer '
                          'around the dataset boundaries, shrinked by N meters. '
                          'Use 0 to disable cropping. '
                          'Default: %(default)s'))

    parser.add_argument('--pc-classify',
            action='store_true',
            default=False,
            help='Classify the point cloud outputs using a Simple Morphological Filter. '
            'You can control the behavior of this option by tweaking the --dem-* parameters. '
            'Default: '
            '%(default)s')

    parser.add_argument('--pc-csv',
                        action='store_true',
                        default=False,
                        help='Export the georeferenced point cloud in CSV format. Default:  %(default)s')
    
    parser.add_argument('--pc-las',
                action='store_true',
                default=False,
                help='Export the georeferenced point cloud in LAS format. Default:  %(default)s')

    parser.add_argument('--pc-ept',
                action='store_true',
                default=False,
                help='Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default:  %(default)s')

    parser.add_argument('--pc-filter',
                        metavar='<positive float>',
                        type=float,
                        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.'
                             '\nDefault: '
                             '%(default)s')
    
    parser.add_argument('--pc-sample',
                        metavar='<positive float>',
                        type=float,
                        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.'
                             '\nDefault: '
                             '%(default)s')

    parser.add_argument('--smrf-scalar',
                        metavar='<positive float>',
                        type=float,
                        default=1.25,
                        help='Simple Morphological Filter elevation scalar parameter. '
                             '\nDefault: '
                             '%(default)s')

    parser.add_argument('--smrf-slope',
        metavar='<positive float>',
        type=float,
        default=0.15,
        help='Simple Morphological Filter slope parameter (rise over run). '
                '\nDefault: '
                '%(default)s')
    
    parser.add_argument('--smrf-threshold',
        metavar='<positive float>',
        type=float,
        default=0.5,
        help='Simple Morphological Filter elevation threshold parameter (meters). '
                '\nDefault: '
                '%(default)s')
    
    parser.add_argument('--smrf-window',
        metavar='<positive float>',
        type=float,
        default=18.0,
        help='Simple Morphological Filter window radius parameter (meters). '
                '\nDefault: '
                '%(default)s')

    parser.add_argument('--texturing-data-term',
                        metavar='<string>',
                        default='gmi',
                        choices=['gmi', 'area'],
                        help=('Data term: [area, gmi]. Default: '
                              '%(default)s'))

    parser.add_argument('--texturing-nadir-weight',
                        metavar='<integer: 0 <= x <= 32>',
                        default=16,
                        type=int,
                        help=('Affects orthophotos only. '
                              'Higher values result in sharper corners, but can affect color distribution and blurriness. '
                              'Use lower values for planar areas and higher values for urban areas. '
                              'The default value works well for most scenarios. Default: '
                              '%(default)s'))

    parser.add_argument('--texturing-outlier-removal-type',
                        metavar='<string>',
                        default='gauss_clamping',
                        choices=['none', 'gauss_clamping', 'gauss_damping'],
                        help=('Type of photometric outlier removal method: '
                              '[none, gauss_damping, gauss_clamping]. Default: '
                              '%(default)s'))

    parser.add_argument('--texturing-skip-visibility-test',
                        action='store_true',
                        default=False,
                        help=('Skip geometric visibility test. Default: '
                              ' %(default)s'))

    parser.add_argument('--texturing-skip-global-seam-leveling',
                        action='store_true',
                        default=False,
                        help=('Skip global seam leveling. Useful for IR data.'
                              'Default: %(default)s'))

    parser.add_argument('--texturing-skip-local-seam-leveling',
                        action='store_true',
                        default=False,
                        help='Skip local seam blending. Default:  %(default)s')

    parser.add_argument('--texturing-skip-hole-filling',
                        action='store_true',
                        default=False,
                        help=('Skip filling of holes in the mesh. Default: '
                              ' %(default)s'))

    parser.add_argument('--texturing-keep-unseen-faces',
                        action='store_true',
                        default=False,
                        help=('Keep faces in the mesh that are not seen in any camera. '
                              'Default:  %(default)s'))

    parser.add_argument('--texturing-tone-mapping',
                        metavar='<string>',
                        choices=['none', 'gamma'],
                        default='none',
                        help='Turn on gamma tone mapping or none for no tone '
                             'mapping. Choices are  \'gamma\' or \'none\'. '
                             'Default: %(default)s ')

    parser.add_argument('--gcp',
                        metavar='<path string>',
                        default=None,
                        help=('path to the file containing the ground control '
                              'points used for georeferencing.  Default: '
                              '%(default)s. The file needs to '
                              'be on the following line format: \neasting '
                              'northing height pixelrow pixelcol imagename'))

    parser.add_argument('--use-exif',
                        action='store_true',
                        default=False,
                        help=('Use this tag if you have a gcp_list.txt but '
                              'want to use the exif geotags instead'))

    parser.add_argument('--dtm',
                        action='store_true',
                        default=False,
                        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.')

    parser.add_argument('--dsm',
                        action='store_true',
                        default=False,
                        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.')

    parser.add_argument('--dem-gapfill-steps',
                        metavar='<positive integer>',
                        default=3,
                        type=int,
                        help='Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. '
                             'Starting with a radius equal to the output resolution, N different DEMs are generated with '
                             'progressively bigger radius using the inverse distance weighted (IDW) algorithm '
                             'and merged together. Remaining gaps are then merged using nearest neighbor interpolation. '
                             '\nDefault=%(default)s')

    parser.add_argument('--dem-resolution',
                        metavar='<float>',
                        type=float,
                        default=5,
                        help='DSM/DTM resolution in cm / pixel.'
                             '\nDefault: %(default)s')

    parser.add_argument('--dem-decimation',
                        metavar='<positive integer>',
                        default=1,
                        type=int,
                        help='Decimate the points before generating the DEM. 1 is no decimation (full quality). '
                             '100 decimates ~99%% of the points. Useful for speeding up '
                             'generation.\nDefault=%(default)s')
    
    parser.add_argument('--dem-euclidean-map',
            action='store_true',
            default=False,
            help='Computes an euclidean raster map for each DEM. '
            'The map reports the distance from each cell to the nearest '
            'NODATA value (before any hole filling takes place). '
            'This can be useful to isolate the areas that have been filled. '
            'Default: '
            '%(default)s')

    parser.add_argument('--orthophoto-resolution',
                        metavar='<float > 0.0>',
                        default=5,
                        type=float,
                        help=('Orthophoto resolution in cm / pixel.\n'
                              'Default: %(default)s'))

    parser.add_argument('--orthophoto-no-tiled',
                        action='store_true',
                        default=False,
                        help='Set this parameter if you want a stripped geoTIFF.\n'
                             'Default: %(default)s')

    parser.add_argument('--orthophoto-compression',
                        metavar='<string>',
                        type=str,
                        choices=['JPEG', 'LZW', 'PACKBITS', 'DEFLATE', 'LZMA', 'NONE'],
                        default='DEFLATE',
                        help='Set the compression to use. Note that this could '
                             'break gdal_translate if you don\'t know what you '
                             'are doing. Options: %(choices)s.\nDefault: %(default)s')
    
    parser.add_argument('--orthophoto-cutline',
            action='store_true',
            default=False,
            help='Generates a polygon around the cropping area '
            'that cuts the orthophoto around the edges of features. This polygon '
            'can be useful for stitching seamless mosaics with multiple overlapping orthophotos. '
            'Default: '
            '%(default)s')

    parser.add_argument('--build-overviews',
                        action='store_true',
                        default=False,
                        help='Build orthophoto overviews using gdaladdo.')

    parser.add_argument('--verbose', '-v',
                        action='store_true',
                        default=False,
                        help='Print additional messages to the console\n'
                             'Default: %(default)s')

    parser.add_argument('--time',
                        action='store_true',
                        default=False,
                        help='Generates a benchmark file with runtime info\n'
                             'Default: %(default)s')
    
    parser.add_argument('--debug',
                        action='store_true',
                        default=False,
                        help='Print debug messages\n'
                             'Default: %(default)s')

    parser.add_argument('--version',
                        action='version',
                        version='OpenDroneMap {0}'.format(__version__),
                        help='Displays version number and exits. ')

    parser.add_argument('--split',
                        type=int,
                        default=999999,
                        metavar='<positive integer>',
                        help='Average number of images per submodel. When '
                                'splitting a large dataset into smaller '
                                'submodels, images are grouped into clusters. '
                                'This value regulates the number of images that '
                                'each cluster should have on average.')

    parser.add_argument('--split-overlap',
                        type=float,
                        metavar='<positive integer>',
                        default=150,
                        help='Radius of the overlap between submodels. '
                        'After grouping images into clusters, images '
                        'that are closer than this radius to a cluster '
                        'are added to the cluster. This is done to ensure '
                        'that neighboring submodels overlap.')

    parser.add_argument('--sm-cluster',
                        metavar='<string>',
                        type=url_string,
                        default=None,
                        help='URL to a ClusterODM instance '
                            'for distributing a split-merge workflow on '
                            'multiple nodes in parallel. '
                            'Default: %(default)s')

    parser.add_argument('--merge',
                    metavar='<string>',
                    default='all',
                    choices=['all', 'pointcloud', 'orthophoto', 'dem'],
                    help=('Choose what to merge in the merge step in a split dataset. '
                          'By default all available outputs are merged. '
                          'Options: %(choices)s. Default: '
                            '%(default)s'))

    parser.add_argument('--force-gps',
                    action='store_true',
                    default=False,
                    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. '
                          'If there are no GCPs, this flag does nothing. Default: %(default)s'))

    args = parser.parse_args()

    # check that the project path setting has been set properly
    if not args.project_path:
        log.ODM_ERROR('You need to set the project path in the '
                      'settings.yaml file before you can run ODM, '
                      'or use `--project-path <path>`. Run `python '
                      'run.py --help` for more information. ')
        sys.exit(1)

    if args.fast_orthophoto:
      log.ODM_INFO('Fast orthophoto is turned on, automatically setting --skip-3dmodel')
      args.skip_3dmodel = True

    if args.dtm and not args.pc_classify:
      log.ODM_INFO("DTM is turned on, automatically turning on point cloud classification")
      args.pc_classify = True

    if args.skip_3dmodel and args.use_3dmesh:
      log.ODM_WARNING('--skip-3dmodel is set, but so is --use-3dmesh. --skip-3dmodel will be ignored.')
      args.skip_3dmodel = False

    if args.orthophoto_cutline and not args.crop:
      log.ODM_WARNING("--orthophoto-cutline is set, but --crop is not. --crop will be set to 0.01")
      args.crop = 0.01

    if args.sm_cluster:
        try:
            Node.from_url(args.sm_cluster).info()
        except exceptions.NodeConnectionError as e:
            log.ODM_ERROR("Cluster node seems to be offline: %s"  % str(e))
            sys.exit(1)

    return args
Esempio n. 29
0
def filter(input_point_cloud, output_point_cloud, standard_deviation=2.5, meank=16, sample_radius=0, verbose=False, max_concurrency=1):
    """
    Filters a point cloud
    """
    if not os.path.exists(input_point_cloud):
        log.ODM_ERROR("{} does not exist. The program will now exit.".format(input_point_cloud))
        sys.exit(1)

    if (standard_deviation <= 0 or meank <= 0) and sample_radius <= 0:
        log.ODM_INFO("Skipping point cloud filtering")
        # if using the option `--pc-filter 0`, we need copy input_point_cloud
        shutil.copy(input_point_cloud, output_point_cloud)
        return

    filters = []

    if sample_radius > 0:
        log.ODM_INFO("Sampling points around a %sm radius" % sample_radius)
        filters.append('sample')

    if standard_deviation > 0 and meank > 0:
        log.ODM_INFO("Filtering {} (statistical, meanK {}, standard deviation {})".format(input_point_cloud, meank, standard_deviation))
        filters.append('outlier')

    if len(filters) > 0:
        filters.append('range')

    info = ply_info(input_point_cloud)
    dims = "x=float,y=float,z=float,"
    if info['has_normals']:
        dims += "nx=float,ny=float,nz=float,"
    dims += "red=uchar,blue=uchar,green=uchar"
    if info['has_views']:
        dims += ",views=uchar"

    if info['vertex_count'] == 0:
        log.ODM_ERROR("Cannot read vertex count for {}".format(input_point_cloud))
        sys.exit(1)

    # Do we need to split this?
    VERTEX_THRESHOLD = 250000
    should_split = max_concurrency > 1 and info['vertex_count'] > VERTEX_THRESHOLD*2

    if should_split:
        partsdir = os.path.join(os.path.dirname(output_point_cloud), "parts")
        if os.path.exists(partsdir):
            log.ODM_WARNING("Removing existing directory %s" % partsdir)
            shutil.rmtree(partsdir)

        point_cloud_submodels = split(input_point_cloud, partsdir, "part.ply", capacity=VERTEX_THRESHOLD, dims=dims)

        def run_filter(pcs):
            # Recurse
            filter(pcs['path'], io.related_file_path(pcs['path'], postfix="_filtered"), 
                        standard_deviation=standard_deviation, 
                        meank=meank, 
                        sample_radius=sample_radius, 
                        verbose=verbose,
                        max_concurrency=1)
        # Filter
        parallel_map(run_filter, [{'path': p} for p in point_cloud_submodels], max_concurrency)

        # Merge
        log.ODM_INFO("Merging %s point cloud chunks to %s" % (len(point_cloud_submodels), output_point_cloud))
        filtered_pcs = [io.related_file_path(pcs, postfix="_filtered") for pcs in point_cloud_submodels]
        #merge_ply(filtered_pcs, output_point_cloud, dims)
        fast_merge_ply(filtered_pcs, output_point_cloud)

        if os.path.exists(partsdir):
            shutil.rmtree(partsdir)
    else:
        # Process point cloud (or a point cloud submodel) in a single step
        filterArgs = {
            'inputFile': input_point_cloud,
            'outputFile': output_point_cloud,
            'stages': " ".join(filters),
            'dims': dims
        }

        cmd = ("pdal translate -i \"{inputFile}\" "
                "-o \"{outputFile}\" "
                "{stages} "
                "--writers.ply.sized_types=false "
                "--writers.ply.storage_mode='little endian' "
                "--writers.ply.dims=\"{dims}\" "
                "").format(**filterArgs)

        if 'sample' in filters:
            cmd += "--filters.sample.radius={} ".format(sample_radius)
        
        if 'outlier' in filters:
            cmd += ("--filters.outlier.method='statistical' "
                "--filters.outlier.mean_k={} "
                "--filters.outlier.multiplier={} ").format(meank, standard_deviation)  
        
        if 'range' in filters:
            # Remove outliers
            cmd += "--filters.range.limits='Classification![7:7]' "

        system.run(cmd)

    if not os.path.exists(output_point_cloud):
        log.ODM_WARNING("{} not found, filtering has failed.".format(output_point_cloud))
Esempio n. 30
0
def config(argv=None, parser=None):
    global args

    if args is not None and argv is None:
        return args

    if sys.platform == 'win32':
        usage_bin = 'run'
    else:
        usage_bin = 'run.sh'

    if parser is None:
        parser = SettingsParser(
            description=
            'ODM is a command line toolkit to generate maps, point clouds, 3D models and DEMs from drone, balloon or kite images.',
            usage='%s [options] <dataset name>' % usage_bin,
            yaml_file=open(context.settings_path))

    parser.add_argument(
        '--project-path',
        metavar='<path>',
        action=StoreValue,
        help=
        'Path to the project folder. Your project folder should contain subfolders for each dataset. Each dataset should have an "images" folder.'
    )
    parser.add_argument(
        'name',
        metavar='<dataset name>',
        action=StoreValue,
        type=str,
        default='code',
        nargs='?',
        help=
        'Name of dataset (i.e subfolder name within project folder). Default: %(default)s'
    )

    parser.add_argument(
        '--resize-to',
        metavar='<integer>',
        action=StoreValue,
        default=2048,
        type=int,
        help=
        'Legacy option (use --feature-quality instead). Resizes images by the largest side for feature extraction purposes only. '
        'Set to -1 to disable. This does not affect the final orthophoto '
        'resolution quality and will not resize the original images. Default: %(default)s'
    )

    parser.add_argument(
        '--end-with',
        '-e',
        metavar='<string>',
        action=StoreValue,
        default='odm_postprocess',
        choices=processopts,
        help=
        'End processing at this stage. Can be one of: %(choices)s. Default: %(default)s'
    )

    rerun = parser.add_mutually_exclusive_group()

    rerun.add_argument(
        '--rerun',
        '-r',
        metavar='<string>',
        action=StoreValue,
        choices=processopts,
        help=
        ('Rerun this stage only and stop. Can be one of: %(choices)s. Default: %(default)s'
         ))

    rerun.add_argument(
        '--rerun-all',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Permanently delete all previous results and rerun the processing pipeline.'
    )

    rerun.add_argument(
        '--rerun-from',
        action=RerunFrom,
        metavar='<string>',
        choices=processopts,
        help=
        ('Rerun processing from this stage. Can be one of: %(choices)s. Default: %(default)s'
         ))

    parser.add_argument(
        '--min-num-features',
        metavar='<integer>',
        action=StoreValue,
        default=10000,
        type=int,
        help=
        ('Minimum number of features to extract per image. '
         'More features can be useful for finding more matches between images, '
         'potentially allowing the reconstruction of areas with little overlap or insufficient features. '
         'More features also slow down processing. Default: %(default)s'))

    parser.add_argument(
        '--feature-type',
        metavar='<string>',
        action=StoreValue,
        default='sift',
        choices=['akaze', 'hahog', 'orb', 'sift'],
        help=
        ('Choose the algorithm for extracting keypoints and computing descriptors. '
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--feature-quality',
        metavar='<string>',
        action=StoreValue,
        default='high',
        choices=['ultra', 'high', 'medium', 'low', 'lowest'],
        help=
        ('Set feature extraction quality. Higher quality generates better features, but requires more memory and takes longer. '
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--matcher-type',
        metavar='<string>',
        action=StoreValue,
        default='flann',
        choices=['bow', 'bruteforce', 'flann'],
        help=
        ('Matcher algorithm, Fast Library for Approximate Nearest Neighbors or Bag of Words. FLANN is slower, but more stable. BOW is faster, but can sometimes miss valid matches. BRUTEFORCE is very slow but robust.'
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--matcher-neighbors',
        metavar='<positive integer>',
        action=StoreValue,
        default=0,
        type=int,
        help=
        'Perform image matching with the nearest images based on GPS exif data. Set to 0 to match by triangulation. Default: %(default)s'
    )

    parser.add_argument(
        '--use-fixed-camera-params',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Turn off camera parameter optimization during bundle adjustment. This can be sometimes useful for improving results that exhibit doming/bowling or when images are taken with a rolling shutter camera. Default: %(default)s'
    )

    parser.add_argument(
        '--cameras',
        default='',
        metavar='<json>',
        action=StoreValue,
        type=path_or_json_string,
        help='Use the camera parameters computed from '
        'another dataset instead of calculating them. '
        'Can be specified either as path to a cameras.json file or as a '
        'JSON string representing the contents of a '
        'cameras.json file. Default: %(default)s')

    parser.add_argument(
        '--camera-lens',
        metavar='<string>',
        action=StoreValue,
        default='auto',
        choices=[
            'auto', 'perspective', 'brown', 'fisheye', 'spherical',
            'equirectangular', 'dual'
        ],
        help=
        ('Set a camera projection type. Manually setting a value '
         'can help improve geometric undistortion. By default the application '
         'tries to determine a lens type from the images metadata. Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--radiometric-calibration',
        metavar='<string>',
        action=StoreValue,
        default='none',
        choices=['none', 'camera', 'camera+sun'],
        help=
        ('Set the radiometric calibration to perform on images. '
         'When processing multispectral and thermal images you should set this option '
         'to obtain reflectance/temperature values (otherwise you will get digital number values). '
         '[camera] applies black level, vignetting, row gradient gain/exposure compensation (if appropriate EXIF tags are found) and computes absolute temperature values. '
         '[camera+sun] is experimental, applies all the corrections of [camera], plus compensates for spectral radiance registered via a downwelling light sensor (DLS) taking in consideration the angle of the sun. '
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--max-concurrency',
        metavar='<positive integer>',
        action=StoreValue,
        default=context.num_cores,
        type=int,
        help=('The maximum number of processes to use in various '
              'processes. Peak memory requirement is ~1GB per '
              'thread and 2 megapixel image resolution. Default: %(default)s'))

    parser.add_argument(
        '--depthmap-resolution',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        default=640,
        help=
        ('Controls the density of the point cloud by setting the resolution of the depthmap images. Higher values take longer to compute '
         'but produce denser point clouds. Overrides the value calculated by --pc-quality.'
         'Default: %(default)s'))

    parser.add_argument(
        '--use-hybrid-bundle-adjustment',
        action=StoreTrue,
        nargs=0,
        default=False,
        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. Default: %(default)s'
    )

    parser.add_argument(
        '--sfm-algorithm',
        metavar='<string>',
        action=StoreValue,
        default='incremental',
        choices=['incremental', 'triangulation', 'planar'],
        help=
        ('Choose the structure from motion algorithm. For aerial datasets, if camera GPS positions and angles are available, triangulation can generate better results. For planar scenes captured at fixed altitude with nadir-only images, planar can be much faster. '
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--use-3dmesh',
        action=StoreTrue,
        nargs=0,
        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. Default: %(default)s'
    )

    parser.add_argument(
        '--skip-3dmodel',
        action=StoreTrue,
        nargs=0,
        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. Default: %(default)s'
    )

    parser.add_argument(
        '--skip-report',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Skip generation of PDF report. This can save time if you don\'t need a report. Default: %(default)s'
    )

    parser.add_argument(
        '--skip-orthophoto',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Skip generation of the orthophoto. This can save time if you only need 3D results or DEMs. Default: %(default)s'
    )

    parser.add_argument(
        '--ignore-gsd',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Ignore Ground Sampling Distance (GSD). GSD '
        'caps the maximum resolution of image outputs and '
        'resizes images when necessary, resulting in faster processing and '
        'lower memory usage. Since GSD is an estimate, sometimes ignoring it can result in slightly better image output quality. Default: %(default)s'
    )

    parser.add_argument(
        '--no-gpu',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Do not use GPU acceleration, even if it\'s available. Default: %(default)s'
    )

    parser.add_argument('--mesh-size',
                        metavar='<positive integer>',
                        action=StoreValue,
                        default=200000,
                        type=int,
                        help=('The maximum vertex count of the output mesh. '
                              'Default: %(default)s'))

    parser.add_argument('--mesh-octree-depth',
                        metavar='<integer: 1 <= x <= 14>',
                        action=StoreValue,
                        default=11,
                        type=int,
                        help=('Octree depth used in the mesh reconstruction, '
                              'increase to get more vertices, recommended '
                              'values are 8-12. Default: %(default)s'))

    parser.add_argument(
        '--fast-orthophoto',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Skips dense reconstruction and 3D model generation. '
        'It generates an orthophoto directly from the sparse reconstruction. '
        'If you just need an orthophoto and do not need a full 3D model, turn on this option. Default: %(default)s'
    )

    parser.add_argument(
        '--crop',
        metavar='<positive float>',
        action=StoreValue,
        default=3,
        type=float,
        help=('Automatically crop image outputs by creating a smooth buffer '
              'around the dataset boundaries, shrunk by N meters. '
              'Use 0 to disable cropping. '
              'Default: %(default)s'))

    parser.add_argument(
        '--boundary',
        default='',
        metavar='<json>',
        action=StoreValue,
        type=path_or_json_string,
        help='GeoJSON polygon limiting the area of the reconstruction. '
        'Can be specified either as path to a GeoJSON file or as a '
        'JSON string representing the contents of a '
        'GeoJSON file. Default: %(default)s')

    parser.add_argument(
        '--auto-boundary',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Automatically set a boundary using camera shot locations to limit the area of the reconstruction. '
        'This can help remove far away background artifacts (sky, background landscapes, etc.). See also --boundary. '
        'Default: %(default)s')

    parser.add_argument(
        '--pc-quality',
        metavar='<string>',
        action=StoreValue,
        default='medium',
        choices=['ultra', 'high', 'medium', 'low', 'lowest'],
        help=
        ('Set point cloud quality. Higher quality generates better, denser point clouds, but requires more memory and takes longer. Each step up in quality increases processing time roughly by a factor of 4x.'
         'Can be one of: %(choices)s. Default: '
         '%(default)s'))

    parser.add_argument(
        '--pc-classify',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Classify the point cloud outputs using a Simple Morphological Filter. '
        'You can control the behavior of this option by tweaking the --dem-* parameters. '
        'Default: '
        '%(default)s')

    parser.add_argument(
        '--pc-csv',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Export the georeferenced point cloud in CSV format. Default: %(default)s'
    )

    parser.add_argument(
        '--pc-las',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Export the georeferenced point cloud in LAS format. Default: %(default)s'
    )

    parser.add_argument(
        '--pc-ept',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Export the georeferenced point cloud in Entwine Point Tile (EPT) format. Default: %(default)s'
    )

    parser.add_argument(
        '--pc-copc',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Save the georeferenced point cloud in Cloud Optimized Point Cloud (COPC) format. Default: %(default)s'
    )

    parser.add_argument(
        '--pc-filter',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        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. '
        'Default: %(default)s')

    parser.add_argument(
        '--pc-sample',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        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 and remove duplicate points. Set to 0 to disable sampling. '
        'Default: %(default)s')

    parser.add_argument(
        '--pc-tile',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Reduce the memory usage needed for depthmap fusion by splitting large scenes into tiles. Turn this on if your machine doesn\'t have much RAM and/or you\'ve set --pc-quality to high or ultra. Experimental. '
        'Default: %(default)s')

    parser.add_argument(
        '--pc-geometric',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Improve the accuracy of the point cloud by computing geometrically consistent depthmaps. This increases processing time, but can improve results in urban scenes. '
        'Default: %(default)s')

    parser.add_argument(
        '--smrf-scalar',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        default=1.25,
        help='Simple Morphological Filter elevation scalar parameter. '
        'Default: %(default)s')

    parser.add_argument(
        '--smrf-slope',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        default=0.15,
        help='Simple Morphological Filter slope parameter (rise over run). '
        'Default: %(default)s')

    parser.add_argument(
        '--smrf-threshold',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        default=0.5,
        help=
        'Simple Morphological Filter elevation threshold parameter (meters). '
        'Default: %(default)s')

    parser.add_argument(
        '--smrf-window',
        metavar='<positive float>',
        action=StoreValue,
        type=float,
        default=18.0,
        help='Simple Morphological Filter window radius parameter (meters). '
        'Default: %(default)s')

    parser.add_argument(
        '--texturing-data-term',
        metavar='<string>',
        action=StoreValue,
        default='gmi',
        choices=['gmi', 'area'],
        help=
        ('When texturing the 3D mesh, for each triangle, choose to prioritize images with sharp features (gmi) or those that cover the largest area (area). Default: %(default)s'
         ))

    parser.add_argument(
        '--texturing-outlier-removal-type',
        metavar='<string>',
        action=StoreValue,
        default='gauss_clamping',
        choices=['none', 'gauss_clamping', 'gauss_damping'],
        help=
        ('Type of photometric outlier removal method. Can be one of: %(choices)s. Default: %(default)s'
         ))

    parser.add_argument(
        '--texturing-skip-global-seam-leveling',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        ('Skip normalization of colors across all images. Useful when processing radiometric data. Default: %(default)s'
         ))

    parser.add_argument(
        '--texturing-skip-local-seam-leveling',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Skip the blending of colors near seams. Default: %(default)s')

    parser.add_argument(
        '--texturing-keep-unseen-faces',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=('Keep faces in the mesh that are not seen in any camera. '
              'Default:  %(default)s'))

    parser.add_argument('--texturing-tone-mapping',
                        metavar='<string>',
                        action=StoreValue,
                        choices=['none', 'gamma'],
                        default='none',
                        help='Turn on gamma tone mapping or none for no tone '
                        'mapping. Can be one of %(choices)s. '
                        'Default: %(default)s ')

    parser.add_argument(
        '--gcp',
        metavar='<path string>',
        action=StoreValue,
        default=None,
        help=
        ('Path to the file containing the ground control '
         'points used for georeferencing. '
         'The file needs to '
         'use the following format: \n'
         'EPSG:<code> or <+proj definition>\n'
         'geo_x geo_y geo_z im_x im_y image_name [gcp_name] [extra1] [extra2]\n'
         'Default: %(default)s'))

    parser.add_argument(
        '--geo',
        metavar='<path string>',
        action=StoreValue,
        default=None,
        help=
        ('Path to the image geolocation file containing the camera center coordinates used for georeferencing. '
         'Note that omega/phi/kappa are currently not supported (you can set them to 0). '
         'The file needs to '
         'use the following format: \n'
         'EPSG:<code> or <+proj definition>\n'
         'image_name geo_x geo_y geo_z [omega (degrees)] [phi (degrees)] [kappa (degrees)] [horz accuracy (meters)] [vert accuracy (meters)]\n'
         'Default: %(default)s'))

    parser.add_argument(
        '--use-exif',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        ('Use this tag if you have a GCP File but '
         'want to use the EXIF information for georeferencing instead. Default: %(default)s'
         ))

    parser.add_argument(
        '--dtm',
        action=StoreTrue,
        nargs=0,
        default=False,
        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. Default: %(default)s'
    )

    parser.add_argument(
        '--dsm',
        action=StoreTrue,
        nargs=0,
        default=False,
        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. Default: %(default)s'
    )

    parser.add_argument(
        '--dem-gapfill-steps',
        metavar='<positive integer>',
        action=StoreValue,
        default=3,
        type=int,
        help=
        'Number of steps used to fill areas with gaps. Set to 0 to disable gap filling. '
        'Starting with a radius equal to the output resolution, N different DEMs are generated with '
        'progressively bigger radius using the inverse distance weighted (IDW) algorithm '
        'and merged together. Remaining gaps are then merged using nearest neighbor interpolation. '
        'Default: %(default)s')

    parser.add_argument(
        '--dem-resolution',
        metavar='<float>',
        action=StoreValue,
        type=float,
        default=5,
        help=
        'DSM/DTM resolution in cm / pixel. Note that this value is capped to 2x the ground sampling distance (GSD) estimate. To remove the cap, check --ignore-gsd also.'
        ' Default: %(default)s')

    parser.add_argument(
        '--dem-decimation',
        metavar='<positive integer>',
        action=StoreValue,
        default=1,
        type=int,
        help=
        'Decimate the points before generating the DEM. 1 is no decimation (full quality). '
        '100 decimates ~99%% of the points. Useful for speeding up generation of DEM results in very large datasets. Default: %(default)s'
    )

    parser.add_argument(
        '--dem-euclidean-map',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Computes an euclidean raster map for each DEM. '
        'The map reports the distance from each cell to the nearest '
        'NODATA value (before any hole filling takes place). '
        'This can be useful to isolate the areas that have been filled. '
        'Default: '
        '%(default)s')

    parser.add_argument(
        '--orthophoto-resolution',
        metavar='<float > 0.0>',
        action=StoreValue,
        default=5,
        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. '
         'Default: %(default)s'))

    parser.add_argument(
        '--orthophoto-no-tiled',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Set this parameter if you want a striped GeoTIFF. '
        'Default: %(default)s')

    parser.add_argument(
        '--orthophoto-png',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Set this parameter if you want to generate a PNG rendering of the orthophoto. '
        'Default: %(default)s')

    parser.add_argument(
        '--orthophoto-kmz',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Set this parameter if you want to generate a Google Earth (KMZ) rendering of the orthophoto. '
        'Default: %(default)s')

    parser.add_argument(
        '--orthophoto-compression',
        metavar='<string>',
        action=StoreValue,
        type=str,
        choices=['JPEG', 'LZW', 'PACKBITS', 'DEFLATE', 'LZMA', 'NONE'],
        default='DEFLATE',
        help=
        'Set the compression to use for orthophotos. Can be one of: %(choices)s. Default: %(default)s'
    )

    parser.add_argument(
        '--orthophoto-cutline',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Generates a polygon around the cropping area '
        'that cuts the orthophoto around the edges of features. This polygon '
        'can be useful for stitching seamless mosaics with multiple overlapping orthophotos. '
        'Default: '
        '%(default)s')

    parser.add_argument(
        '--tiles',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Generate static tiles for orthophotos and DEMs that are '
        'suitable for viewers like Leaflet or OpenLayers. '
        'Default: %(default)s')

    parser.add_argument(
        '--3d-tiles',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Generate OGC 3D Tiles outputs. Default: %(default)s')

    parser.add_argument(
        '--rolling-shutter',
        action=StoreTrue,
        nargs=0,
        default=False,
        help='Turn on rolling shutter correction. If the camera '
        'has a rolling shutter and the images were taken in motion, you can turn on this option '
        'to improve the accuracy of the results. See also --rolling-shutter-readout. '
        'Default: %(default)s')

    parser.add_argument(
        '--rolling-shutter-readout',
        type=float,
        action=StoreValue,
        metavar='<positive integer>',
        default=0,
        help=
        'Override the rolling shutter readout time for your camera sensor (in milliseconds), instead of using the rolling shutter readout database. '
        'Note that not all cameras are present in the database. Set to 0 to use the database value. '
        'Default: %(default)s')

    parser.add_argument(
        '--build-overviews',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Build orthophoto overviews for faster display in programs such as QGIS. Default: %(default)s'
    )

    parser.add_argument(
        '--cog',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        'Create Cloud-Optimized GeoTIFFs instead of normal GeoTIFFs. Default: %(default)s'
    )

    parser.add_argument('--verbose',
                        '-v',
                        action=StoreTrue,
                        nargs=0,
                        default=False,
                        help='Print additional messages to the console. '
                        'Default: %(default)s')

    parser.add_argument(
        '--copy-to',
        metavar='<path>',
        action=StoreValue,
        help='Copy output results to this folder after processing.')

    parser.add_argument('--time',
                        action=StoreTrue,
                        nargs=0,
                        default=False,
                        help='Generates a benchmark file with runtime info. '
                        'Default: %(default)s')

    parser.add_argument('--debug',
                        action=StoreTrue,
                        nargs=0,
                        default=False,
                        help='Print debug messages. Default: %(default)s')

    parser.add_argument('--version',
                        action='version',
                        version='ODM {0}'.format(__version__),
                        help='Displays version number and exits. ')

    parser.add_argument(
        '--split',
        type=int,
        action=StoreValue,
        default=999999,
        metavar='<positive integer>',
        help='Average number of images per submodel. When '
        'splitting a large dataset into smaller '
        'submodels, images are grouped into clusters. '
        'This value regulates the number of images that '
        'each cluster should have on average. Default: %(default)s')

    parser.add_argument(
        '--split-overlap',
        type=float,
        action=StoreValue,
        metavar='<positive integer>',
        default=150,
        help='Radius of the overlap between submodels. '
        'After grouping images into clusters, images '
        'that are closer than this radius to a cluster '
        'are added to the cluster. This is done to ensure '
        'that neighboring submodels overlap. Default: %(default)s')

    parser.add_argument(
        '--split-image-groups',
        metavar='<path string>',
        action=StoreValue,
        default=None,
        help=
        ('Path to the image groups file that controls how images should be split into groups. '
         'The file needs to use the following format: \n'
         'image_name group_name\n'
         'Default: %(default)s'))
    # parser.add_argument('--split-multitracks',
    #                    action=StoreTrue,
    #                    nargs=0,
    #                    default=False,
    #                    help='Split multi-track reconstructions.')

    parser.add_argument('--sm-cluster',
                        metavar='<string>',
                        action=StoreValue,
                        type=url_string,
                        default=None,
                        help='URL to a ClusterODM instance '
                        'for distributing a split-merge workflow on '
                        'multiple nodes in parallel. '
                        'Default: %(default)s')

    parser.add_argument(
        '--merge',
        metavar='<string>',
        action=StoreValue,
        default='all',
        choices=['all', 'pointcloud', 'orthophoto', 'dem'],
        help=('Choose what to merge in the merge step in a split dataset. '
              'By default all available outputs are merged. '
              'Options: %(choices)s. Default: '
              '%(default)s'))

    parser.add_argument(
        '--force-gps',
        action=StoreTrue,
        nargs=0,
        default=False,
        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. '
         'If there are no GCPs, this flag does nothing. Default: %(default)s'))

    parser.add_argument(
        '--gps-accuracy',
        type=float,
        action=StoreValue,
        metavar='<positive float>',
        default=10,
        help='Set a value in meters for the GPS Dilution of Precision (DOP) '
        'information for all images. If your images are tagged '
        'with high precision GPS information (RTK), this value will be automatically '
        'set accordingly. You can use this option to manually set it in case the reconstruction '
        'fails. Lowering this option can sometimes help control bowling-effects over large areas. Default: %(default)s'
    )

    parser.add_argument(
        '--optimize-disk-space',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        ('Delete heavy intermediate files to optimize disk space usage. This '
         'affects the ability to restart the pipeline from an intermediate stage, '
         'but allows datasets to be processed on machines that don\'t have sufficient '
         'disk space available. Default: %(default)s'))

    parser.add_argument(
        '--pc-rectify',
        action=StoreTrue,
        nargs=0,
        default=False,
        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. '
         'Default: %(default)s'))

    parser.add_argument(
        '--primary-band',
        metavar='<string>',
        action=StoreValue,
        default="auto",
        type=str,
        help=
        ('When processing multispectral datasets, you can specify the name of the primary band that will be used for reconstruction. '
         'It\'s recommended to choose a band which has sharp details and is in focus. '
         'Default: %(default)s'))

    parser.add_argument(
        '--skip-band-alignment',
        action=StoreTrue,
        nargs=0,
        default=False,
        help=
        ('When processing multispectral datasets, ODM will automatically align the images for each band. '
         'If the images have been postprocessed and are already aligned, use this option. '
         'Default: %(default)s'))

    args = parser.parse_args(argv)

    # check that the project path setting has been set properly
    if not args.project_path:
        log.ODM_ERROR('You need to set the project path in the '
                      'settings.yaml file before you can run ODM, '
                      'or use `--project-path <path>`. Run `python3 '
                      'run.py --help` for more information. ')
        sys.exit(1)

    if args.fast_orthophoto:
        log.ODM_INFO(
            'Fast orthophoto is turned on, automatically setting --skip-3dmodel'
        )
        args.skip_3dmodel = True

    if args.pc_rectify and not args.pc_classify:
        log.ODM_INFO(
            "Ground rectify is turned on, automatically turning on point cloud classification"
        )
        args.pc_classify = True

    if args.dtm and not args.pc_classify:
        log.ODM_INFO(
            "DTM is turned on, automatically turning on point cloud classification"
        )
        args.pc_classify = True

    if args.skip_3dmodel and args.use_3dmesh:
        log.ODM_WARNING(
            '--skip-3dmodel is set, but so is --use-3dmesh. --skip-3dmodel will be ignored.'
        )
        args.skip_3dmodel = False

    if args.orthophoto_cutline and not args.crop:
        log.ODM_WARNING(
            "--orthophoto-cutline is set, but --crop is not. --crop will be set to 0.01"
        )
        args.crop = 0.01

    if args.sm_cluster:
        try:
            Node.from_url(args.sm_cluster).info()
        except exceptions.NodeConnectionError as e:
            log.ODM_ERROR("Cluster node seems to be offline: %s" % str(e))
            sys.exit(1)

    return args