Пример #1
0
def generate_png(orthophoto_file, output_file=None, outsize=None):
    if output_file is None:
        base, ext = os.path.splitext(orthophoto_file)
        output_file = base + '.png'

    # See if we need to select top three bands
    bandparam = ""

    gtif = gdal.Open(orthophoto_file)
    if gtif.RasterCount > 4:
        bands = []
        for idx in range(1, gtif.RasterCount + 1):
            bands.append(gtif.GetRasterBand(idx).GetColorInterpretation())
        bands = dict(zip(bands, range(1, len(bands) + 1)))

        try:
            red = bands.get(gdal.GCI_RedBand)
            green = bands.get(gdal.GCI_GreenBand)
            blue = bands.get(gdal.GCI_BlueBand)
            if red is None or green is None or blue is None:
                raise Exception("Cannot find bands")

            bandparam = "-b %s -b %s -b %s -a_nodata 0" % (red, green, blue)
        except:
            bandparam = "-b 1 -b 2 -b 3 -a_nodata 0"
    gtif = None

    osparam = ""
    if outsize is not None:
        osparam = "-outsize %s 0" % outsize

    system.run(
        'gdal_translate -of png "%s" "%s" %s %s '
        '--config GDAL_CACHEMAX %s%% ' %
        (orthophoto_file, output_file, osparam, bandparam, get_max_memory()))
Пример #2
0
def compute_cutline(orthophoto_file, crop_area_file, destination, max_concurrency=1, tmpdir=None, scale=1):
    if io.file_exists(orthophoto_file) and io.file_exists(crop_area_file):
        from opendm.grass_engine import grass
        log.ODM_DEBUG("Computing cutline")

        if tmpdir and not io.dir_exists(tmpdir):
            system.mkdir_p(tmpdir)

        scale = max(0.0001, min(1, scale))
        scaled_orthophoto = None

        if scale < 1:
            log.ODM_DEBUG("Scaling orthophoto to %s%% to compute cutline" % (scale * 100))

            scaled_orthophoto = os.path.join(tmpdir, os.path.basename(io.related_file_path(orthophoto_file, postfix=".scaled")))
            # Scale orthophoto before computing cutline
            system.run("gdal_translate -outsize {}% 0 "
                "-co NUM_THREADS={} "
                "--config GDAL_CACHEMAX {}% "
                "{} {}".format(
                scale * 100,
                max_concurrency,
                concurrency.get_max_memory(),
                orthophoto_file,
                scaled_orthophoto
            ))
            orthophoto_file = scaled_orthophoto

        try:
            ortho_width,ortho_height = get_image_size.get_image_size(orthophoto_file, fallback_on_error=False)
            log.ODM_DEBUG("Orthophoto dimensions are %sx%s" % (ortho_width, ortho_height))
            number_lines = int(max(8, math.ceil(min(ortho_width, ortho_height) / 256.0)))
        except:
            log.ODM_DEBUG("Cannot compute orthophoto dimensions, setting arbitrary number of lines.")
            number_lines = 32
        
        log.ODM_DEBUG("Number of lines: %s" % number_lines)

        gctx = grass.create_context({'auto_cleanup' : False, 'tmpdir': tmpdir})
        gctx.add_param('orthophoto_file', orthophoto_file)
        gctx.add_param('crop_area_file', crop_area_file)
        gctx.add_param('number_lines', number_lines)
        gctx.add_param('max_concurrency', max_concurrency)
        gctx.add_param('memory', int(concurrency.get_max_memory_mb(300)))
        gctx.set_location(orthophoto_file)

        cutline_file = gctx.execute(os.path.join("opendm", "grass", "compute_cutline.grass"))
        if cutline_file != 'error':
            if io.file_exists(cutline_file):
                shutil.move(cutline_file, destination)
                log.ODM_INFO("Generated cutline file: %s --> %s" % (cutline_file, destination))
                gctx.cleanup()
                return destination
            else:
                log.ODM_WARNING("Unexpected script result: %s. No cutline file has been generated." % cutline_file)
        else:
            log.ODM_WARNING("Could not generate orthophoto cutline. An error occured when running GRASS. No orthophoto will be generated.")
    else:
        log.ODM_WARNING("We've been asked to compute cutline, but either %s or %s is missing. Skipping..." % (orthophoto_file, crop_area_file))
Пример #3
0
def generate_png(orthophoto_file):
    log.ODM_INFO("Generating PNG")
    base, ext = os.path.splitext(orthophoto_file)
    orthophoto_png = base + '.png'

    system.run('gdal_translate -of png "%s" "%s" '
               '--config GDAL_CACHEMAX %s%% ' %
               (orthophoto_file, orthophoto_png, get_max_memory()))
Пример #4
0
    def crop(gpkg_path, geotiff_path, gdal_options, keep_original=True):
        if not os.path.exists(gpkg_path) or not os.path.exists(geotiff_path):
            log.ODM_WARNING(
                "Either {} or {} does not exist, will skip cropping.".format(
                    gpkg_path, geotiff_path))
            return geotiff_path

        log.ODM_INFO("Cropping %s" % geotiff_path)

        # Rename original file
        # path/to/odm_orthophoto.tif --> path/to/odm_orthophoto.original.tif

        path, filename = os.path.split(geotiff_path)
        # path = path/to
        # filename = odm_orthophoto.tif

        basename, ext = os.path.splitext(filename)
        # basename = odm_orthophoto
        # ext = .tif

        original_geotiff = os.path.join(path,
                                        "{}.original{}".format(basename, ext))
        os.rename(geotiff_path, original_geotiff)

        try:
            kwargs = {
                'gpkg_path':
                gpkg_path,
                'geotiffInput':
                original_geotiff,
                'geotiffOutput':
                geotiff_path,
                'options':
                ' '.join(
                    map(lambda k: '-co {}={}'.format(k, gdal_options[k]),
                        gdal_options)),
                'max_memory':
                get_max_memory()
            }

            run('gdalwarp -cutline {gpkg_path} '
                '-crop_to_cutline '
                '{options} '
                '{geotiffInput} '
                '{geotiffOutput} '
                '--config GDAL_CACHEMAX {max_memory}%'.format(**kwargs))

            if not keep_original:
                os.remove(original_geotiff)

        except Exception as e:
            log.ODM_WARNING('Something went wrong while cropping: {}'.format(
                e.message))

            # Revert rename
            os.rename(original_geotiff, geotiff_path)

        return geotiff_path
Пример #5
0
def convert_to_cogeo(src_path, blocksize=256, max_workers=1):
    """
    Guarantee that the .tif passed as an argument is a Cloud Optimized GeoTIFF (cogeo)
    The file is destructively converted into a cogeo.
    If the file cannot be converted, the function does not change the file
    :param src_path: path to GeoTIFF
    :return: True on success
    """

    if not os.path.isfile(src_path):
        logger.warning("Cannot convert to cogeo: %s (file does not exist)" %
                       src_path)
        return False

    log.ODM_INFO("Optimizing %s as Cloud Optimized GeoTIFF" % src_path)

    tmpfile = io.related_file_path(src_path, postfix='_cogeo')
    swapfile = io.related_file_path(src_path, postfix='_cogeo_swap')

    kwargs = {
        'threads': max_workers if max_workers else 'ALL_CPUS',
        'blocksize': blocksize,
        'max_memory': get_max_memory(),
        'src_path': src_path,
        'tmpfile': tmpfile,
    }

    try:
        system.run("gdal_translate "
                   "-of COG "
                   "-co NUM_THREADS={threads} "
                   "-co BLOCKSIZE={blocksize} "
                   "-co COMPRESS=deflate "
                   "-co BIGTIFF=IF_SAFER "
                   "-co RESAMPLING=NEAREST "
                   "--config GDAL_CACHEMAX {max_memory}% "
                   "--config GDAL_NUM_THREADS {threads} "
                   "\"{src_path}\" \"{tmpfile}\" ".format(**kwargs))
    except Exception as e:
        log.ODM_WARNING("Cannot create Cloud Optimized GeoTIFF: %s" % str(e))

    if os.path.isfile(tmpfile):
        shutil.move(src_path, swapfile)  # Move to swap location

        try:
            shutil.move(tmpfile, src_path)
        except IOError as e:
            log.ODM_WARNING("Cannot move %s to %s: %s" %
                            (tmpfile, src_path, str(e)))
            shutil.move(swapfile, src_path)  # Attempt to restore

        if os.path.isfile(swapfile):
            os.remove(swapfile)

        return True
    else:
        return False
Пример #6
0
def generate_kmz(orthophoto_file, output_file=None, outsize=None):
    if output_file is None:
        base, ext = os.path.splitext(orthophoto_file)
        output_file = base + '.kmz'

    # See if we need to select top three bands
    bandparam = ""
    gtif = gdal.Open(orthophoto_file)
    if gtif.RasterCount > 4:
        bandparam = "-b 1 -b 2 -b 3 -a_nodata 0"

    system.run(
        'gdal_translate -of KMLSUPEROVERLAY -co FORMAT=JPEG "%s" "%s" %s '
        '--config GDAL_CACHEMAX %s%% ' %
        (orthophoto_file, output_file, bandparam, get_max_memory()))
Пример #7
0
    def crop(shapefile_path, geotiff_path, gdal_options, keep_original=True):
        if not os.path.exists(shapefile_path) or not os.path.exists(geotiff_path):
            log.ODM_WARNING("Either {} or {} does not exist, will skip cropping.".format(shapefile_path, geotiff_path))
            return geotiff_path

        # Rename original file
        # path/to/odm_orthophoto.tif --> path/to/odm_orthophoto.original.tif
        
        path, filename = os.path.split(geotiff_path)
        # path = path/to
        # filename = odm_orthophoto.tif

        basename, ext = os.path.splitext(filename)
        # basename = odm_orthophoto
        # ext = .tif

        original_geotiff = os.path.join(path, "{}.original{}".format(basename, ext))
        os.rename(geotiff_path, original_geotiff)

        try:
            kwargs = {
                'shapefile_path': shapefile_path,
                'geotiffInput': original_geotiff,
                'geotiffOutput': geotiff_path,
                'options': ' '.join(map(lambda k: '-co {}={}'.format(k, gdal_options[k]), gdal_options)),
                'max_memory': get_max_memory()
            }

            run('gdalwarp -cutline {shapefile_path} '
                '-crop_to_cutline '
                '{options} '
                '{geotiffInput} '
                '{geotiffOutput} '
                '--config GDAL_CACHEMAX {max_memory}%'.format(**kwargs))

            if not keep_original:
                os.remove(original_geotiff)

        except Exception as e:
            log.ODM_WARNING('Something went wrong while cropping: {}'.format(e.message))
            
            # Revert rename
            os.rename(original_geotiff, geotiff_path)

        return geotiff_path
Пример #8
0
def generate_png(orthophoto_file, output_file=None, outsize=None):
    if output_file is None:
        base, ext = os.path.splitext(orthophoto_file)
        output_file = base + '.png'

    # See if we need to select top three bands
    bandparam = ""
    gtif = gdal.Open(orthophoto_file)
    if gtif.RasterCount > 4:
        bandparam = "-b 1 -b 2 -b 3 -a_nodata 0"

    osparam = ""
    if outsize is not None:
        osparam = "-outsize %s 0" % outsize

    system.run(
        'gdal_translate -of png "%s" "%s" %s %s '
        '--config GDAL_CACHEMAX %s%% ' %
        (orthophoto_file, output_file, osparam, bandparam, get_max_memory()))
Пример #9
0
def compute_cutline(orthophoto_file,
                    crop_area_file,
                    destination,
                    max_concurrency=1,
                    scale=1):
    if io.file_exists(orthophoto_file) and io.file_exists(crop_area_file):
        log.ODM_INFO("Computing cutline")

        scale = max(0.0001, min(1, scale))
        scaled_orthophoto = None
        if scale < 1:
            log.ODM_INFO("Scaling orthophoto to %s%% to compute cutline" %
                         (scale * 100))

            scaled_orthophoto = io.related_file_path(orthophoto_file,
                                                     postfix=".scaled")
            # Scale orthophoto before computing cutline
            system.run("gdal_translate -outsize {}% 0 "
                       "-co NUM_THREADS={} "
                       "--config GDAL_CACHEMAX {}% "
                       '"{}" "{}"'.format(scale * 100, max_concurrency,
                                          concurrency.get_max_memory(),
                                          orthophoto_file, scaled_orthophoto))

            orthophoto_file = scaled_orthophoto

        # open raster
        f = rasterio.open(orthophoto_file)
        rast = f.read(1)  # First band only
        height, width = rast.shape
        number_lines = int(max(8, math.ceil(min(width, height) / 256.0)))
        line_hor_offset = int(width / number_lines)
        line_ver_offset = int(height / number_lines)

        if line_hor_offset <= 2 or line_ver_offset <= 2:
            log.ODM_WARNING(
                "Cannot compute cutline, orthophoto is too small (%sx%spx)" %
                (width, height))
            return

        crop_f = fiona.open(crop_area_file, 'r')
        if len(crop_f) == 0:
            log.ODM_WARNING("Crop area is empty, cannot compute cutline")
            return

        crop_poly = shape(crop_f[1]['geometry'])
        crop_f.close()

        linestrings = []

        # Compute canny edges on first band
        edges = canny(rast)

        def compute_linestrings(direction):
            log.ODM_INFO("Computing %s cutlines" % direction)
            # Initialize cost map
            cost_map = np.full((height, width), 1, dtype=np.float32)

            # Write edges to cost map
            cost_map[edges == True] = 0  # Low cost

            # Write "barrier, floor is lava" costs
            if direction == 'vertical':
                lines = [((i, 0), (i, height - 1))
                         for i in range(line_hor_offset, width -
                                        line_hor_offset, line_hor_offset)]
                points = []
                pad_x = int(line_hor_offset / 2.0)
                for i in range(0, len(lines)):
                    a, b = lines[i]
                    points.append(((a[0] - pad_x, a[1]), (b[0] - pad_x, b[1])))
                a, b = lines[-1]
                points.append(((a[0] + pad_x, a[1]), (b[0] + pad_x, b[1])))
            else:
                lines = [((0, j), (width - 1, j))
                         for j in range(line_ver_offset, height -
                                        line_ver_offset, line_ver_offset)]
                points = []
                pad_y = int(line_ver_offset / 2.0)
                for i in range(0, len(lines)):
                    a, b = lines[i]
                    points.append(((a[0], a[1] - pad_y), (b[0], b[1] - pad_y)))
                a, b = lines[-1]
                points.append(((a[0], a[1] + pad_y), (b[0], b[1] + pad_y)))

            for a, b in lines:
                rr, cc = line(*a, *b)
                cost_map[cc, rr] = 9999  # Lava

            # Calculate route
            for a, b in points:
                line_coords, cost = route_through_array(cost_map, (a[1], a[0]),
                                                        (b[1], b[0]),
                                                        fully_connected=True,
                                                        geometric=True)

                # Convert to geographic
                geo_line_coords = [f.xy(*c) for c in line_coords]

                # Simplify
                ls = LineString(geo_line_coords)
                linestrings.append(ls.simplify(0.05, preserve_topology=False))

        compute_linestrings('vertical')
        compute_linestrings('horizontal')

        # Generate polygons and keep only those inside the crop area
        log.ODM_INFO("Generating polygons... this could take a bit.")
        polygons = []
        for p in polygonize(unary_union(linestrings)):
            if crop_poly.contains(p):
                polygons.append(p)

        # This should never happen
        if len(polygons) == 0:
            log.ODM_WARNING("No polygons, cannot compute cutline")
            return

        log.ODM_INFO("Merging polygons")
        cutline_polygons = unary_union(polygons)
        if not hasattr(cutline_polygons, '__getitem__'):
            cutline_polygons = [cutline_polygons]

        largest_cutline = cutline_polygons[0]
        max_area = largest_cutline.area
        for p in cutline_polygons:
            if p.area > max_area:
                max_area = p.area
                largest_cutline = p

        log.ODM_INFO("Largest cutline found: %s m^2" % max_area)

        meta = {
            'crs': {
                'init': str(f.crs).lower()
            },
            'driver': 'GPKG',
            'schema': {
                'properties': {},
                'geometry': 'Polygon'
            }
        }

        # Remove previous
        if os.path.exists(destination):
            os.remove(destination)

        with fiona.open(destination, 'w', **meta) as sink:
            sink.write({
                'geometry': mapping(largest_cutline),
                'properties': {}
            })
        f.close()
        log.ODM_INFO("Wrote %s" % destination)

        # Cleanup
        if scaled_orthophoto is not None and os.path.exists(scaled_orthophoto):
            os.remove(scaled_orthophoto)
    else:
        log.ODM_WARNING(
            "We've been asked to compute cutline, but either %s or %s is missing. Skipping..."
            % (orthophoto_file, crop_area_file))
Пример #10
0
def create_dem(input_point_cloud, dem_type, output_type='max', radiuses=['0.56'], gapfill=True,
                outdir='', resolution=0.1, max_workers=1, max_tile_size=4096,
                verbose=False, decimation=None, keep_unfilled_copy=False,
                apply_smoothing=True):
    """ Create DEM from multiple radii, and optionally gapfill """
    
    global error
    error = None

    start = datetime.now()

    if not os.path.exists(outdir):
        log.ODM_INFO("Creating %s" % outdir)
        os.mkdir(outdir)

    extent = point_cloud.get_extent(input_point_cloud)
    log.ODM_INFO("Point cloud bounds are [minx: %s, maxx: %s] [miny: %s, maxy: %s]" % (extent['minx'], extent['maxx'], extent['miny'], extent['maxy']))
    ext_width = extent['maxx'] - extent['minx']
    ext_height = extent['maxy'] - extent['miny']

    w, h = (int(math.ceil(ext_width / float(resolution))),
            int(math.ceil(ext_height / float(resolution))))

    # Set a floor, no matter the resolution parameter
    # (sometimes a wrongly estimated scale of the model can cause the resolution
    # to be set unrealistically low, causing errors)
    RES_FLOOR = 64
    if w < RES_FLOOR and h < RES_FLOOR:
        prev_w, prev_h = w, h
        
        if w >= h:
            w, h = (RES_FLOOR, int(math.ceil(ext_height / ext_width * RES_FLOOR)))
        else:
            w, h = (int(math.ceil(ext_width / ext_height * RES_FLOOR)), RES_FLOOR)
        
        floor_ratio = prev_w / float(w)
        resolution *= floor_ratio
        radiuses = [str(float(r) * floor_ratio) for r in radiuses]

        log.ODM_WARNING("Really low resolution DEM requested %s will set floor at %s pixels. Resolution changed to %s. The scale of this reconstruction might be off." % ((prev_w, prev_h), RES_FLOOR, resolution))
        
    final_dem_pixels = w * h

    num_splits = int(max(1, math.ceil(math.log(math.ceil(final_dem_pixels / float(max_tile_size * max_tile_size)))/math.log(2))))
    num_tiles = num_splits * num_splits
    log.ODM_INFO("DEM resolution is %s, max tile size is %s, will split DEM generation into %s tiles" % ((h, w), max_tile_size, num_tiles))

    tile_bounds_width = ext_width / float(num_splits)
    tile_bounds_height = ext_height / float(num_splits)

    tiles = []

    for r in radiuses:
        minx = extent['minx']

        for x in range(num_splits):
            miny = extent['miny']
            if x == num_splits - 1:
                maxx = extent['maxx']
            else:
                maxx = minx + tile_bounds_width

            for y in range(num_splits):
                if y == num_splits - 1:
                    maxy = extent['maxy']
                else:
                    maxy = miny + tile_bounds_height

                filename = os.path.join(os.path.abspath(outdir), '%s_r%s_x%s_y%s.tif' % (dem_type, r, x, y))

                tiles.append({
                    'radius': r,
                    'bounds': {
                        'minx': minx,
                        'maxx': maxx,
                        'miny': miny,
                        'maxy': maxy 
                    },
                    'filename': filename
                })

                miny = maxy
            minx = maxx

    # Sort tiles by increasing radius
    tiles.sort(key=lambda t: float(t['radius']), reverse=True)

    def process_tile(q):
        log.ODM_INFO("Generating %s (%s, radius: %s, resolution: %s)" % (q['filename'], output_type, q['radius'], resolution))
        
        d = pdal.json_gdal_base(q['filename'], output_type, q['radius'], resolution, q['bounds'])

        if dem_type == 'dtm':
            d = pdal.json_add_classification_filter(d, 2)

        if decimation is not None:
            d = pdal.json_add_decimation_filter(d, decimation)

        pdal.json_add_readers(d, [input_point_cloud])
        pdal.run_pipeline(d, verbose=verbose)

    parallel_map(process_tile, tiles, max_workers)

    output_file = "%s.tif" % dem_type
    output_path = os.path.abspath(os.path.join(outdir, output_file))

    # Verify tile results
    for t in tiles: 
        if not os.path.exists(t['filename']):
            raise Exception("Error creating %s, %s failed to be created" % (output_file, t['filename']))
    
    # Create virtual raster
    tiles_vrt_path = os.path.abspath(os.path.join(outdir, "tiles.vrt"))
    run('gdalbuildvrt "%s" "%s"' % (tiles_vrt_path, '" "'.join(map(lambda t: t['filename'], tiles))))

    merged_vrt_path = os.path.abspath(os.path.join(outdir, "merged.vrt"))
    geotiff_tmp_path = os.path.abspath(os.path.join(outdir, 'tiles.tmp.tif'))
    geotiff_small_path = os.path.abspath(os.path.join(outdir, 'tiles.small.tif'))
    geotiff_small_filled_path = os.path.abspath(os.path.join(outdir, 'tiles.small_filled.tif'))
    geotiff_path = os.path.abspath(os.path.join(outdir, 'tiles.tif'))

    # Build GeoTIFF
    kwargs = {
        'max_memory': get_max_memory(),
        'threads': max_workers if max_workers else 'ALL_CPUS',
        'tiles_vrt': tiles_vrt_path,
        'merged_vrt': merged_vrt_path,
        'geotiff': geotiff_path,
        'geotiff_tmp': geotiff_tmp_path,
        'geotiff_small': geotiff_small_path,
        'geotiff_small_filled': geotiff_small_filled_path
    }

    if gapfill:
        # Sometimes, for some reason gdal_fillnodata.py
        # behaves strangely when reading data directly from a .VRT
        # so we need to convert to GeoTIFF first.
        run('gdal_translate '
                '-co NUM_THREADS={threads} '
                '--config GDAL_CACHEMAX {max_memory}% '
                '{tiles_vrt} {geotiff_tmp}'.format(**kwargs))

        # Scale to 10% size
        run('gdal_translate '
            '-co NUM_THREADS={threads} '
            '--config GDAL_CACHEMAX {max_memory}% '
            '-outsize 10% 0 '
            '{geotiff_tmp} {geotiff_small}'.format(**kwargs))

        # Fill scaled
        run('gdal_fillnodata.py '
            '-co NUM_THREADS={threads} '
            '--config GDAL_CACHEMAX {max_memory}% '
            '-b 1 '
            '-of GTiff '
            '{geotiff_small} {geotiff_small_filled}'.format(**kwargs))

        # Merge filled scaled DEM with unfilled DEM using bilinear interpolation
        run('gdalbuildvrt -resolution highest -r bilinear "%s" "%s" "%s"' % (merged_vrt_path, geotiff_small_filled_path, geotiff_tmp_path))
        run('gdal_translate '
            '-co NUM_THREADS={threads} '
            '-co TILED=YES '
            '-co COMPRESS=DEFLATE '
            '--config GDAL_CACHEMAX {max_memory}% '
            '{merged_vrt} {geotiff}'.format(**kwargs))
    else:
        run('gdal_translate '
                '-co NUM_THREADS={threads} '
                '-co TILED=YES '
                '-co COMPRESS=DEFLATE '
                '--config GDAL_CACHEMAX {max_memory}% '
                '{tiles_vrt} {geotiff}'.format(**kwargs))

    if apply_smoothing:
        median_smoothing(geotiff_path, output_path)
        os.remove(geotiff_path)
    else:
        os.rename(geotiff_path, output_path)

    if os.path.exists(geotiff_tmp_path):
        if not keep_unfilled_copy: 
            os.remove(geotiff_tmp_path)
        else:
            os.rename(geotiff_tmp_path, io.related_file_path(output_path, postfix=".unfilled"))
    
    for cleanup_file in [tiles_vrt_path, merged_vrt_path, geotiff_small_path, geotiff_small_filled_path]:
        if os.path.exists(cleanup_file): os.remove(cleanup_file)
    for t in tiles:
        if os.path.exists(t['filename']): os.remove(t['filename'])
    
    log.ODM_INFO('Completed %s in %s' % (output_file, datetime.now() - start))
Пример #11
0
    def process(self, inputs, outputs):

        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running ODM Orthophoto Cell')

        # get inputs
        args = self.inputs.args
        tree = self.inputs.tree
        reconstruction = inputs.reconstruction
        verbose = '-verbose' if self.params.verbose else ''

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

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

        if not io.file_exists(tree.odm_orthophoto_file) or rerun_cell:

            # odm_orthophoto definitions
            kwargs = {
                'bin':
                context.odm_modules_path,
                'log':
                tree.odm_orthophoto_log,
                'ortho':
                tree.odm_orthophoto_file,
                'corners':
                tree.odm_orthophoto_corners,
                'res':
                1.0 / (gsd.cap_resolution(self.params.resolution,
                                          tree.opensfm_reconstruction,
                                          ignore_gsd=args.ignore_gsd) / 100.0),
                'verbose':
                verbose
            }

            # Have geo coordinates?
            georef = reconstruction.georef

            # Check if the georef object is initialized
            # (during a --rerun this might not be)
            # TODO: we should move this to a more central
            # location (perhaps during the dataset initialization)
            if georef and not georef.utm_east_offset:
                georeferencing_dir = tree.odm_georeferencing if args.use_3dmesh and not args.skip_3dmodel else tree.odm_25dgeoreferencing
                odm_georeferencing_model_txt_geo_file = os.path.join(
                    georeferencing_dir, tree.odm_georeferencing_model_txt_geo)

                if io.file_exists(odm_georeferencing_model_txt_geo_file):
                    georef.extract_offsets(
                        odm_georeferencing_model_txt_geo_file)
                else:
                    log.ODM_WARNING(
                        'Cannot read UTM offset from {}. An orthophoto will not be generated.'
                        .format(odm_georeferencing_model_txt_geo_file))

            if georef:
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(
                        tree.odm_texturing,
                        tree.odm_georeferencing_model_obj_geo)
                else:
                    kwargs['model_geo'] = os.path.join(
                        tree.odm_25dtexturing,
                        tree.odm_georeferencing_model_obj_geo)
            else:
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(
                        tree.odm_texturing, tree.odm_textured_model_obj)
                else:
                    kwargs['model_geo'] = os.path.join(
                        tree.odm_25dtexturing, tree.odm_textured_model_obj)

            # run odm_orthophoto
            system.run(
                '{bin}/odm_orthophoto -inputFile {model_geo} '
                '-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
                '-outputCornerFile {corners}'.format(**kwargs))

            # Create georeferenced GeoTiff
            geotiffcreated = False

            if georef and georef.projection and georef.utm_east_offset and georef.utm_north_offset:
                ulx = uly = lrx = lry = 0.0
                with open(tree.odm_orthophoto_corners) as f:
                    for lineNumber, line in enumerate(f):
                        if lineNumber == 0:
                            tokens = line.split(' ')
                            if len(tokens) == 4:
                                ulx = float(tokens[0]) + \
                                    float(georef.utm_east_offset)
                                lry = float(tokens[1]) + \
                                    float(georef.utm_north_offset)
                                lrx = float(tokens[2]) + \
                                    float(georef.utm_east_offset)
                                uly = float(tokens[3]) + \
                                    float(georef.utm_north_offset)
                log.ODM_INFO('Creating GeoTIFF')

                kwargs = {
                    'ulx':
                    ulx,
                    'uly':
                    uly,
                    'lrx':
                    lrx,
                    'lry':
                    lry,
                    'tiled':
                    '' if self.params.no_tiled else '-co TILED=yes ',
                    'compress':
                    self.params.compress,
                    'predictor':
                    '-co PREDICTOR=2 '
                    if self.params.compress in ['LZW', 'DEFLATE'] else '',
                    'proj':
                    georef.projection.srs,
                    'bigtiff':
                    self.params.bigtiff,
                    'png':
                    tree.odm_orthophoto_file,
                    'tiff':
                    tree.odm_orthophoto_tif,
                    'log':
                    tree.odm_orthophoto_tif_log,
                    'max_memory':
                    get_max_memory(),
                    'threads':
                    self.params.max_concurrency
                }

                system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                           '{tiled} '
                           '-co BIGTIFF={bigtiff} '
                           '-co COMPRESS={compress} '
                           '{predictor} '
                           '-co BLOCKXSIZE=512 '
                           '-co BLOCKYSIZE=512 '
                           '-co NUM_THREADS={threads} '
                           '-a_srs \"{proj}\" '
                           '--config GDAL_CACHEMAX {max_memory}% '
                           '{png} {tiff} > {log}'.format(**kwargs))

                if args.crop > 0:
                    shapefile_path = os.path.join(
                        tree.odm_georeferencing,
                        'odm_georeferenced_model.bounds.shp')
                    Cropper.crop(
                        shapefile_path, tree.odm_orthophoto_tif, {
                            'TILED':
                            'NO' if self.params.no_tiled else 'YES',
                            'COMPRESS':
                            self.params.compress,
                            'PREDICTOR':
                            '2' if self.params.compress in ['LZW', 'DEFLATE']
                            else '1',
                            'BIGTIFF':
                            self.params.bigtiff,
                            'BLOCKXSIZE':
                            512,
                            'BLOCKYSIZE':
                            512,
                            'NUM_THREADS':
                            self.params.max_concurrency
                        })

                if self.params.build_overviews:
                    log.ODM_DEBUG("Building Overviews")
                    kwargs = {
                        'orthophoto': tree.odm_orthophoto_tif,
                        'log': tree.odm_orthophoto_gdaladdo_log
                    }
                    # Run gdaladdo
                    system.run(
                        'gdaladdo -ro -r average '
                        '--config BIGTIFF_OVERVIEW IF_SAFER '
                        '--config COMPRESS_OVERVIEW JPEG '
                        '{orthophoto} 2 4 8 16 > {log}'.format(**kwargs))

                geotiffcreated = True
            if not geotiffcreated:
                log.ODM_WARNING(
                    'No geo-referenced orthophoto created due '
                    'to missing geo-referencing or corner coordinates.')

        else:
            log.ODM_WARNING('Found a valid orthophoto in: %s' %
                            tree.odm_orthophoto_file)

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

        log.ODM_INFO('Running ODM OrthoPhoto Cell - Finished')
        return ecto.OK if args.end_with != 'odm_orthophoto' else ecto.QUIT
Пример #12
0
def create_dem(input_point_cloud, dem_type, output_type='max', radiuses=['0.56'], gapfill=True,
                outdir='', resolution=0.1, max_workers=1, max_tile_size=2048,
                verbose=False, decimation=None):
    """ Create DEM from multiple radii, and optionally gapfill """
    global error
    error = None

    start = datetime.now()

    if not os.path.exists(outdir):
        log.ODM_INFO("Creating %s" % outdir)
        os.mkdir(outdir)

    extent = point_cloud.get_extent(input_point_cloud)
    log.ODM_INFO("Point cloud bounds are [minx: %s, maxx: %s] [miny: %s, maxy: %s]" % (extent['minx'], extent['maxx'], extent['miny'], extent['maxy']))
    ext_width = extent['maxx'] - extent['minx']
    ext_height = extent['maxy'] - extent['miny']

    final_dem_resolution = (int(math.ceil(ext_width / float(resolution))),
                            int(math.ceil(ext_height / float(resolution))))
    final_dem_pixels = final_dem_resolution[0] * final_dem_resolution[1]

    num_splits = int(max(1, math.ceil(math.log(math.ceil(final_dem_pixels / float(max_tile_size * max_tile_size)))/math.log(2))))
    num_tiles = num_splits * num_splits
    log.ODM_INFO("DEM resolution is %s, max tile size is %s, will split DEM generation into %s tiles" % (final_dem_resolution, max_tile_size, num_tiles))

    tile_bounds_width = ext_width / float(num_splits)
    tile_bounds_height = ext_height / float(num_splits)

    tiles = []

    for r in radiuses:
        minx = extent['minx']

        for x in range(num_splits):
            miny = extent['miny']
            if x == num_splits - 1:
                maxx = extent['maxx']
            else:
                maxx = minx + tile_bounds_width

            for y in range(num_splits):
                if y == num_splits - 1:
                    maxy = extent['maxy']
                else:
                    maxy = miny + tile_bounds_height

                filename = os.path.join(os.path.abspath(outdir), '%s_r%s_x%s_y%s.tif' % (dem_type, r, x, y))

                tiles.append({
                    'radius': r,
                    'bounds': {
                        'minx': minx,
                        'maxx': maxx,
                        'miny': miny,
                        'maxy': maxy 
                    },
                    'filename': filename
                })

                miny = maxy
            minx = maxx

    # Sort tiles by increasing radius
    tiles.sort(key=lambda t: float(t['radius']), reverse=True)

    def process_one(q):
        log.ODM_INFO("Generating %s (%s, radius: %s, resolution: %s)" % (q['filename'], output_type, q['radius'], resolution))
        
        d = pdal.json_gdal_base(q['filename'], output_type, q['radius'], resolution, q['bounds'])

        if dem_type == 'dsm':
            d = pdal.json_add_classification_filter(d, 2, equality='max')
        elif dem_type == 'dtm':
            d = pdal.json_add_classification_filter(d, 2)

        if decimation is not None:
            d = pdal.json_add_decimation_filter(d, decimation)

        pdal.json_add_readers(d, [input_point_cloud])
        pdal.run_pipeline(d, verbose=verbose)

    def worker():
        global error

        while True:
            (num, q) = pq.get()
            if q is None or error is not None:
                pq.task_done()
                break

            try:
                process_one(q)
            except Exception as e:
                error = e
            finally:
                pq.task_done()

    if max_workers > 1:
        use_single_thread = False
        pq = queue.PriorityQueue()
        threads = []
        for i in range(max_workers):
            t = threading.Thread(target=worker)
            t.start()
            threads.append(t)

        for t in tiles:
            pq.put((i, t.copy()))

        def stop_workers():
            for i in range(len(threads)):
                pq.put((-1, None))
            for t in threads:
                t.join()

        # block until all tasks are done
        try:
            while pq.unfinished_tasks > 0:
                time.sleep(0.5)
        except KeyboardInterrupt:
            print("CTRL+C terminating...")
            stop_workers()
            sys.exit(1)

        stop_workers()

        if error is not None:
            # Try to reprocess using a single thread
            # in case this was a memory error
            log.ODM_WARNING("DEM processing failed with multiple threads, let's retry with a single thread...")
            use_single_thread = True
    else:
        use_single_thread = True

    if use_single_thread:
        # Boring, single thread processing
        for q in tiles:
            process_one(q)

    output_file = "%s.tif" % dem_type
    output_path = os.path.abspath(os.path.join(outdir, output_file))

    # Verify tile results
    for t in tiles: 
        if not os.path.exists(t['filename']):
            raise Exception("Error creating %s, %s failed to be created" % (output_file, t['filename']))
    
    # Create virtual raster
    vrt_path = os.path.abspath(os.path.join(outdir, "merged.vrt"))
    run('gdalbuildvrt "%s" "%s"' % (vrt_path, '" "'.join(map(lambda t: t['filename'], tiles))))

    geotiff_tmp_path = os.path.abspath(os.path.join(outdir, 'merged.tmp.tif'))
    geotiff_path = os.path.abspath(os.path.join(outdir, 'merged.tif'))

    # Build GeoTIFF
    kwargs = {
        'max_memory': get_max_memory(),
        'threads': max_workers if max_workers else 'ALL_CPUS',
        'vrt': vrt_path,
        'geotiff': geotiff_path,
        'geotiff_tmp': geotiff_tmp_path
    }

    if gapfill:
        # Sometimes, for some reason gdal_fillnodata.py
        # behaves strangely when reading data directly from a .VRT
        # so we need to convert to GeoTIFF first.
        run('gdal_translate '
                '-co NUM_THREADS={threads} '
                '--config GDAL_CACHEMAX {max_memory}% '
                '{vrt} {geotiff_tmp}'.format(**kwargs))

        run('gdal_fillnodata.py '
            '-co NUM_THREADS={threads} '
            '--config GDAL_CACHEMAX {max_memory}% '
            '-b 1 '
            '-of GTiff '
            '{geotiff_tmp} {geotiff}'.format(**kwargs))
    else:
        run('gdal_translate '
                '-co NUM_THREADS={threads} '
                '--config GDAL_CACHEMAX {max_memory}% '
                '{vrt} {geotiff}'.format(**kwargs))

    post_process(geotiff_path, output_path)
    os.remove(geotiff_path)

    if os.path.exists(geotiff_tmp_path): os.remove(geotiff_tmp_path)
    if os.path.exists(vrt_path): os.remove(vrt_path)
    for t in tiles:
        if os.path.exists(t['filename']): os.remove(t['filename'])
    
    log.ODM_INFO('Completed %s in %s' % (output_file, datetime.now() - start))
Пример #13
0
    def process(self, args, outputs):
        tree = outputs['tree']
        reconstruction = outputs['reconstruction']
        verbose = '-verbose' if args.verbose else ''

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

        if not io.file_exists(tree.odm_orthophoto_file) or self.rerun():

            # odm_orthophoto definitions
            kwargs = {
                'bin': context.odm_modules_path,
                'log': tree.odm_orthophoto_log,
                'ortho': tree.odm_orthophoto_file,
                'corners': tree.odm_orthophoto_corners,
                'res': 1.0 / (gsd.cap_resolution(args.orthophoto_resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd) / 100.0),
                'verbose': verbose
            }

            # Have geo coordinates?
            georef = reconstruction.georef

            # Check if the georef object is initialized
            # (during a --rerun this might not be)
            # TODO: this should be moved to a more central location?
            if reconstruction.is_georeferenced() and not reconstruction.georef.valid_utm_offsets():
                georeferencing_dir = tree.odm_georeferencing if args.use_3dmesh and not args.skip_3dmodel else tree.odm_25dgeoreferencing
                odm_georeferencing_model_txt_geo_file = os.path.join(georeferencing_dir, tree.odm_georeferencing_model_txt_geo)

                if io.file_exists(odm_georeferencing_model_txt_geo_file):
                    reconstruction.georef.extract_offsets(odm_georeferencing_model_txt_geo_file)
                else:
                    log.ODM_WARNING('Cannot read UTM offset from {}. An orthophoto will not be generated.'.format(odm_georeferencing_model_txt_geo_file))

            if reconstruction.is_georeferenced():
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_georeferencing_model_obj_geo)
                else:
                    kwargs['model_geo'] = os.path.join(tree.odm_25dtexturing, tree.odm_georeferencing_model_obj_geo)
            else:
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_textured_model_obj)
                else:
                    kwargs['model_geo'] = os.path.join(tree.odm_25dtexturing, tree.odm_textured_model_obj)

            # run odm_orthophoto
            system.run('{bin}/odm_orthophoto -inputFile {model_geo} '
                       '-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
                       '-outputCornerFile {corners}'.format(**kwargs))

            # Create georeferenced GeoTiff
            geotiffcreated = False

            if reconstruction.is_georeferenced() and reconstruction.georef.valid_utm_offsets():
                ulx = uly = lrx = lry = 0.0
                with open(tree.odm_orthophoto_corners) as f:
                    for lineNumber, line in enumerate(f):
                        if lineNumber == 0:
                            tokens = line.split(' ')
                            if len(tokens) == 4:
                                ulx = float(tokens[0]) + \
                                    float(reconstruction.georef.utm_east_offset)
                                lry = float(tokens[1]) + \
                                    float(reconstruction.georef.utm_north_offset)
                                lrx = float(tokens[2]) + \
                                    float(reconstruction.georef.utm_east_offset)
                                uly = float(tokens[3]) + \
                                    float(reconstruction.georef.utm_north_offset)
                log.ODM_INFO('Creating GeoTIFF')

                orthophoto_vars = orthophoto.get_orthophoto_vars(args)

                kwargs = {
                    'ulx': ulx,
                    'uly': uly,
                    'lrx': lrx,
                    'lry': lry,
                    'vars': ' '.join(['-co %s=%s' % (k, orthophoto_vars[k]) for k in orthophoto_vars]),
                    'proj': reconstruction.georef.proj4(),
                    'png': tree.odm_orthophoto_file,
                    'tiff': tree.odm_orthophoto_tif,
                    'log': tree.odm_orthophoto_tif_log,
                    'max_memory': get_max_memory(),
                }

                system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                           '{vars} '
                           '-a_srs \"{proj}\" '
                           '--config GDAL_CACHEMAX {max_memory}% '
                           '{png} {tiff} > {log}'.format(**kwargs))

                bounds_file_path = os.path.join(tree.odm_georeferencing, 'odm_georeferenced_model.bounds.gpkg')
                    
                # Cutline computation, before cropping
                # We want to use the full orthophoto, not the cropped one.
                if args.orthophoto_cutline:
                    compute_cutline(tree.odm_orthophoto_tif, 
                                    bounds_file_path,
                                    os.path.join(tree.odm_orthophoto, "cutline.gpkg"),
                                    args.max_concurrency,
                                    tmpdir=os.path.join(tree.odm_orthophoto, "grass_cutline_tmpdir"),
                                    scale=0.25)

                if args.crop > 0:
                    Cropper.crop(bounds_file_path, tree.odm_orthophoto_tif, orthophoto_vars)

                if args.build_overviews:
                    orthophoto.build_overviews(tree.odm_orthophoto_tif)

                geotiffcreated = True
            if not geotiffcreated:
                log.ODM_WARNING('No geo-referenced orthophoto created due '
                                'to missing geo-referencing or corner coordinates.')

        else:
            log.ODM_WARNING('Found a valid orthophoto in: %s' % tree.odm_orthophoto_file)
Пример #14
0
def process(args, current_path, max_concurrency, reconstruction):
    #args = vars(args)
    orthophoto_cutline = True
    odm_orthophoto = io.join_paths(current_path, 'orthophoto')
    odm_orthophoto_path = odm_orthophoto
    odm_orthophoto_render = io.join_paths(odm_orthophoto_path,
                                          'odm_orthophoto_render.tif')
    odm_orthophoto_tif = io.join_paths(odm_orthophoto_path,
                                       'odm_orthophoto.tif')
    odm_orthophoto_corners = io.join_paths(odm_orthophoto_path,
                                           'odm_orthophoto_corners.tif')
    odm_orthophoto_log = io.join_paths(odm_orthophoto_path,
                                       'odm_orthophoto_log.tif')
    odm_orthophoto_tif_log = io.join_paths(odm_orthophoto_path,
                                           'gdal_translate_log.txt')
    odm_25dgeoreferencing = io.join_paths(current_path, 'odm_georeferencing')
    odm_georeferencing = io.join_paths(current_path, 'odm_georeferencing')

    odm_georeferencing_coords = io.join_paths(odm_georeferencing, 'coords.txt')
    odm_georeferencing_gcp = io.find('gcp_list.txt', current_path)
    odm_georeferencing_gcp_utm = io.join_paths(odm_georeferencing,
                                               'gcp_list_utm.txt')
    odm_georeferencing_utm_log = io.join_paths(
        odm_georeferencing, 'odm_georeferencing_utm_log.txt')
    odm_georeferencing_log = 'odm_georeferencing_log.txt'
    odm_georeferencing_transform_file = 'odm_georeferencing_transform.txt'
    odm_georeferencing_proj = 'proj.txt'
    odm_georeferencing_model_txt_geo = 'odm_georeferencing_model_geo.txt'
    odm_georeferencing_model_obj_geo = 'odm_textured_model_geo.obj'
    odm_georeferencing_xyz_file = io.join_paths(odm_georeferencing,
                                                'odm_georeferenced_model.csv')
    odm_georeferencing_las_json = io.join_paths(odm_georeferencing, 'las.json')
    odm_georeferencing_model_laz = io.join_paths(
        odm_georeferencing, 'odm_georeferenced_model.laz')
    odm_georeferencing_model_las = io.join_paths(
        odm_georeferencing, 'odm_georeferenced_model.las')
    odm_georeferencing_dem = io.join_paths(odm_georeferencing,
                                           'odm_georeferencing_model_dem.tif')

    opensfm_reconstruction = io.join_paths(current_path, 'reconstruction.json')
    odm_texturing = io.join_paths(current_path, 'mvs')
    odm_textured_model_obj = io.join_paths(odm_texturing,
                                           'odm_textured_model.obj')
    images_dir = io.join_paths(current_path, 'images')

    reconstruction = reconstruction

    verbose = ''
    #"-verbose"

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

    if not io.file_exists(odm_orthophoto_tif):
        gsd_error_estimate = 0.1
        ignore_resolution = False
        if not reconstruction.is_georeferenced():
            # Match DEMs
            gsd_error_estimate = -3
            ignore_resolution = True
        orthophoto_resolution = 5
        resolution = 1.0 / (
            gsd.cap_resolution(orthophoto_resolution,
                               opensfm_reconstruction,
                               gsd_error_estimate=gsd_error_estimate,
                               ignore_gsd=True,
                               ignore_resolution=ignore_resolution,
                               has_gcp=reconstruction.has_gcp()) / 100.0)

        # odm_orthophoto definitions
        kwargs = {
            'bin': context.odm_modules_path,
            'log': odm_orthophoto_log,
            'ortho': odm_orthophoto_render,
            'corners': odm_orthophoto_corners,
            'res': resolution,
            'bands': '',
            'verbose': verbose
        }

        # Check if the georef object is initialized
        # (during a --rerun this might not be)
        # TODO: this should be moved to a more central location?
        if reconstruction.is_georeferenced(
        ) and not reconstruction.georef.valid_utm_offsets():
            georeferencing_dir = odm_georeferencing  #if args.use_3dmesh and not args.skip_3dmodel else odm_25dgeoreferencing
            odm_georeferencing_model_txt_geo_file = os.path.join(
                georeferencing_dir, odm_georeferencing_model_txt_geo)

            if io.file_exists(odm_georeferencing_model_txt_geo_file):
                reconstruction.georef.extract_offsets(
                    odm_georeferencing_model_txt_geo_file)
            else:
                log.ODM_WARNING('Cannot read UTM offset from {}.'.format(
                    odm_georeferencing_model_txt_geo_file))

        models = []

        base_dir = odm_texturing

        if reconstruction.is_georeferenced():
            model_file = odm_georeferencing_model_obj_geo
        else:
            model_file = odm_textured_model_obj

        if reconstruction.multi_camera:
            for band in reconstruction.multi_camera:
                primary = band == reconstruction.multi_camera[0]
                subdir = ""
                if not primary:
                    subdir = band['name'].lower()
                models.append(os.path.join(base_dir, subdir, model_file))
            kwargs['bands'] = '-bands %s' % (','.join([
                quote(b['name'].lower()) for b in reconstruction.multi_camera
            ]))
        else:
            models.append(os.path.join(base_dir, model_file))

        kwargs['models'] = ','.join(map(quote, models))

        # run odm_orthophoto
        system.run(
            '{bin}/odm_orthophoto -inputFiles {models} '
            '-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
            '-outputCornerFile {corners} {bands}'.format(**kwargs))

        # Create georeferenced GeoTiff
        geotiffcreated = False

        if reconstruction.is_georeferenced(
        ) and reconstruction.georef.valid_utm_offsets():
            ulx = uly = lrx = lry = 0.0
            with open(odm_orthophoto_corners) as f:
                for lineNumber, line in enumerate(f):
                    if lineNumber == 0:
                        tokens = line.split(' ')
                        if len(tokens) == 4:
                            ulx = float(tokens[0]) + \
                                float(reconstruction.georef.utm_east_offset)
                            lry = float(tokens[1]) + \
                                float(reconstruction.georef.utm_north_offset)
                            lrx = float(tokens[2]) + \
                                float(reconstruction.georef.utm_east_offset)
                            uly = float(tokens[3]) + \
                                float(reconstruction.georef.utm_north_offset)
            log.ODM_INFO('Creating GeoTIFF')

            orthophoto_vars = orthophoto.get_orthophoto_vars(args)

            kwargs = {
                'ulx':
                ulx,
                'uly':
                uly,
                'lrx':
                lrx,
                'lry':
                lry,
                'vars':
                ' '.join([
                    '-co %s=%s' % (k, orthophoto_vars[k])
                    for k in orthophoto_vars
                ]),
                'proj':
                reconstruction.georef.proj4(),
                'input':
                odm_orthophoto_render,
                'output':
                odm_orthophoto_tif,
                'log':
                odm_orthophoto_tif_log,
                'max_memory':
                get_max_memory(),
            }

            system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                       '{vars} '
                       '-a_srs \"{proj}\" '
                       '--config GDAL_CACHEMAX {max_memory}% '
                       '--config GDAL_TIFF_INTERNAL_MASK YES '
                       '{input} {output} > {log}'.format(**kwargs))

            bounds_file_path = os.path.join(
                odm_georeferencing, 'odm_georeferenced_model.bounds.gpkg')

            # Cutline computation, before cropping
            # We want to use the full orthophoto, not the cropped one.
            pio = True
            if pio:
                cutline_file = os.path.join(odm_orthophoto, "cutline.gpkg")

                compute_cutline(odm_orthophoto_tif,
                                bounds_file_path,
                                cutline_file,
                                max_concurrency,
                                tmpdir=os.path.join(odm_orthophoto,
                                                    "grass_cutline_tmpdir"),
                                scale=0.25)

                orthophoto.compute_mask_raster(odm_orthophoto_tif,
                                               cutline_file,
                                               os.path.join(
                                                   odm_orthophoto,
                                                   "odm_orthophoto_cut.tif"),
                                               blend_distance=20,
                                               only_max_coords_feature=True)

            orthophoto.post_orthophoto_steps(args, bounds_file_path,
                                             odm_orthophoto_tif)

            # Generate feathered orthophoto also
            if pio:
                orthophoto.feather_raster(odm_orthophoto_tif,
                                          os.path.join(
                                              odm_orthophoto,
                                              "odm_orthophoto_feathered.tif"),
                                          blend_distance=20)

            geotiffcreated = True
        if not geotiffcreated:
            if io.file_exists(odm_orthophoto_render):
                pseudogeo.add_pseudo_georeferencing(odm_orthophoto_render)
                log.ODM_INFO("Renaming %s --> %s" %
                             (odm_orthophoto_render, odm_orthophoto_tif))
                os.rename(odm_orthophoto_render, odm_orthophoto_tif)
            else:
                log.ODM_WARNING(
                    "Could not generate an orthophoto (it did not render)")
    else:
        log.ODM_WARNING('Found a valid orthophoto in: %s' % odm_orthophoto_tif)

    #generate png

    orthophoto.generate_png(odm_orthophoto_tif)
Пример #15
0
    def process(self, inputs, outputs):

        # Benchmarking
        start_time = system.now_raw()

        log.ODM_INFO('Running ODM Orthophoto Cell')

        # get inputs
        args = self.inputs.args
        tree = self.inputs.tree
        reconstruction = inputs.reconstruction
        verbose = '-verbose' if self.params.verbose else ''

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

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

        if not io.file_exists(tree.odm_orthophoto_file) or rerun_cell:

            # odm_orthophoto definitions
            kwargs = {
                'bin': context.odm_modules_path,
                'log': tree.odm_orthophoto_log,
                'ortho': tree.odm_orthophoto_file,
                'corners': tree.odm_orthophoto_corners,
                'res': 1.0 / (gsd.cap_resolution(self.params.resolution, tree.opensfm_reconstruction, ignore_gsd=args.ignore_gsd) / 100.0),
                'verbose': verbose
            }

            # Have geo coordinates?
            georef = reconstruction.georef

            # Check if the georef object is initialized
            # (during a --rerun this might not be)
            # TODO: we should move this to a more central
            # location (perhaps during the dataset initialization)
            if georef and not georef.utm_east_offset:
                georeferencing_dir = tree.odm_georeferencing if args.use_3dmesh and not args.skip_3dmodel else tree.odm_25dgeoreferencing
                odm_georeferencing_model_txt_geo_file = os.path.join(georeferencing_dir, tree.odm_georeferencing_model_txt_geo)

                if io.file_exists(odm_georeferencing_model_txt_geo_file):
                    georef.extract_offsets(odm_georeferencing_model_txt_geo_file)
                else:
                    log.ODM_WARNING('Cannot read UTM offset from {}. An orthophoto will not be generated.'.format(odm_georeferencing_model_txt_geo_file))


            if georef:
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_georeferencing_model_obj_geo)
                else:
                    kwargs['model_geo'] = os.path.join(tree.odm_25dtexturing, tree.odm_georeferencing_model_obj_geo)
            else:
                if args.use_3dmesh:
                    kwargs['model_geo'] = os.path.join(tree.odm_texturing, tree.odm_textured_model_obj)
                else:
                    kwargs['model_geo'] = os.path.join(tree.odm_25dtexturing, tree.odm_textured_model_obj)

            # run odm_orthophoto
            system.run('{bin}/odm_orthophoto -inputFile {model_geo} '
                       '-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
                       '-outputCornerFile {corners}'.format(**kwargs))

            # Create georeferenced GeoTiff
            geotiffcreated = False

            if georef and georef.projection and georef.utm_east_offset and georef.utm_north_offset:
                ulx = uly = lrx = lry = 0.0
                with open(tree.odm_orthophoto_corners) as f:
                    for lineNumber, line in enumerate(f):
                        if lineNumber == 0:
                            tokens = line.split(' ')
                            if len(tokens) == 4:
                                ulx = float(tokens[0]) + \
                                    float(georef.utm_east_offset)
                                lry = float(tokens[1]) + \
                                    float(georef.utm_north_offset)
                                lrx = float(tokens[2]) + \
                                    float(georef.utm_east_offset)
                                uly = float(tokens[3]) + \
                                    float(georef.utm_north_offset)
                log.ODM_INFO('Creating GeoTIFF')

                kwargs = {
                    'ulx': ulx,
                    'uly': uly,
                    'lrx': lrx,
                    'lry': lry,
                    'tiled': '' if self.params.no_tiled else '-co TILED=yes ',
                    'compress': self.params.compress,
                    'predictor': '-co PREDICTOR=2 ' if self.params.compress in
                                                       ['LZW', 'DEFLATE'] else '',
                    'proj': georef.projection.srs,
                    'bigtiff': self.params.bigtiff,
                    'png': tree.odm_orthophoto_file,
                    'tiff': tree.odm_orthophoto_tif,
                    'log': tree.odm_orthophoto_tif_log,
                    'max_memory': get_max_memory(),
                    'threads': self.params.max_concurrency
                }

                system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                           '{tiled} '
                           '-co BIGTIFF={bigtiff} '
                           '-co COMPRESS={compress} '
                           '{predictor} '
                           '-co BLOCKXSIZE=512 '
                           '-co BLOCKYSIZE=512 '
                           '-co NUM_THREADS={threads} '
                           '-a_srs \"{proj}\" '
                           '--config GDAL_CACHEMAX {max_memory}% '
                           '{png} {tiff} > {log}'.format(**kwargs))

                if args.crop > 0:
                    shapefile_path = os.path.join(tree.odm_georeferencing, 'odm_georeferenced_model.bounds.shp')
                    Cropper.crop(shapefile_path, tree.odm_orthophoto_tif, {
                            'TILED': 'NO' if self.params.no_tiled else 'YES',
                            'COMPRESS': self.params.compress,
                            'PREDICTOR': '2' if self.params.compress in ['LZW', 'DEFLATE'] else '1',
                            'BIGTIFF': self.params.bigtiff,
                            'BLOCKXSIZE': 512,
                            'BLOCKYSIZE': 512,
                            'NUM_THREADS': self.params.max_concurrency
                        })

                if self.params.build_overviews:
                    log.ODM_DEBUG("Building Overviews")
                    kwargs = {
                        'orthophoto': tree.odm_orthophoto_tif,
                        'log': tree.odm_orthophoto_gdaladdo_log
                    }
                    # Run gdaladdo
                    system.run('gdaladdo -ro -r average '
                               '--config BIGTIFF_OVERVIEW IF_SAFER '
                               '--config COMPRESS_OVERVIEW JPEG '
                               '{orthophoto} 2 4 8 16 > {log}'.format(**kwargs))

                geotiffcreated = True
            if not geotiffcreated:
                log.ODM_WARNING('No geo-referenced orthophoto created due '
                                'to missing geo-referencing or corner coordinates.')

        else:
            log.ODM_WARNING('Found a valid orthophoto in: %s' % tree.odm_orthophoto_file)

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

        log.ODM_INFO('Running ODM OrthoPhoto Cell - Finished')
        return ecto.OK if args.end_with != 'odm_orthophoto' else ecto.QUIT
Пример #16
0
    def process(self, args, outputs):
        tree = outputs['tree']
        reconstruction = outputs['reconstruction']

        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:
                        # pdal.merge_point_clouds(all_point_clouds, tree.odm_georeferencing_model_laz, args.verbose)
                        entwine.build(all_point_clouds,
                                      tree.entwine_pointcloud,
                                      max_concurrency=args.max_concurrency,
                                      rerun=self.rerun())
                    except Exception as e:
                        log.ODM_WARNING(
                            "Could not merge point cloud: %s (skipping)" %
                            str(e))

                    if io.dir_exists(tree.entwine_pointcloud):
                        try:
                            system.run('pdal translate "ept://{}" "{}"'.format(
                                tree.entwine_pointcloud,
                                tree.odm_georeferencing_model_laz))
                        except Exception as e:
                            log.ODM_WARNING(
                                "Cannot export EPT dataset to LAZ: %s" %
                                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_cutlines = get_all_submodel_paths(
                        tree.submodels_path,
                        os.path.join("odm_orthophoto", "odm_orthophoto.tif"),
                        os.path.join("odm_orthophoto", "cutline.gpkg"),
                    )

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

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

                        merged_geotiff = os.path.join(
                            tree.odm_orthophoto, "odm_orthophoto.merged.tif")

                        kwargs = {
                            'orthophoto_merged':
                            merged_geotiff,
                            'input_files':
                            ' '.join(
                                map(lambda i: quote(i[0]),
                                    all_orthos_and_cutlines)),
                            'max_memory':
                            get_max_memory(),
                            'threads':
                            args.max_concurrency,
                        }

                        # use bounds as cutlines (blending)
                        if io.file_exists(merged_geotiff):
                            os.remove(merged_geotiff)

                        system.run('gdal_merge.py -o {orthophoto_merged} '
                                   #'-createonly '
                                   '-co "BIGTIFF=YES" '
                                   '-co "BLOCKXSIZE=512" '
                                   '-co "BLOCKYSIZE=512" '
                                   '--config GDAL_CACHEMAX {max_memory}% '
                                   '{input_files} '.format(**kwargs))

                        for ortho_cutline in all_orthos_and_cutlines:
                            kwargs['input_file'], kwargs[
                                'cutline'] = ortho_cutline

                            # Note: cblend has a high performance penalty
                            system.run(
                                'gdalwarp -cutline {cutline} '
                                '-cblend 20 '
                                '-r bilinear -multi '
                                '-wo NUM_THREADS={threads} '
                                '--config GDAL_CACHEMAX {max_memory}% '
                                '{input_file} {orthophoto_merged}'.format(
                                    **kwargs))

                        # Apply orthophoto settings (compression, tiling, etc.)
                        orthophoto_vars = orthophoto.get_orthophoto_vars(args)

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

                        kwargs = {
                            'vars':
                            ' '.join([
                                '-co %s=%s' % (k, orthophoto_vars[k])
                                for k in orthophoto_vars
                            ]),
                            'max_memory':
                            get_max_memory(),
                            'merged':
                            merged_geotiff,
                            'log':
                            tree.odm_orthophoto_tif_log,
                            'orthophoto':
                            tree.odm_orthophoto_tif,
                        }

                        system.run(
                            'gdal_translate '
                            '{vars} '
                            '--config GDAL_CACHEMAX {max_memory}% '
                            '{merged} {orthophoto} > {log}'.format(**kwargs))

                        os.remove(merged_geotiff)

                        # Crop
                        if args.crop > 0:
                            Cropper.crop(merged_bounds_file,
                                         tree.odm_orthophoto_tif,
                                         orthophoto_vars)

                        # Overviews
                        if args.build_overviews:
                            orthophoto.build_overviews(tree.odm_orthophoto_tif)

                    elif len(all_orthos_and_cutlines) == 1:
                        # Simply copy
                        log.ODM_WARNING(
                            "A single orthophoto/cutline pair was found between all submodels."
                        )
                        shutil.copyfile(all_orthos_and_cutlines[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)
                    euclidean_merge_dems(all_dems, dem_file, dem_vars)

                    if io.file_exists(dem_file):
                        # Crop
                        if args.crop > 0:
                            Cropper.crop(merged_bounds_file, dem_file,
                                         dem_vars)
                        log.ODM_INFO("Created %s" % dem_file)
                    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")

            # Stop the pipeline short! We're done.
            self.next_stage = None
        else:
            log.ODM_INFO("Normal dataset, nothing to merge.")
            self.progress = 0.0
Пример #17
0
    def process(self, args, outputs):
        orthophoto_file = os.path.join(args.project_path, "odm_orthophoto",
                                       "odm_orthophoto.tif")
        orthophoto_render_file = os.path.join(args.project_path,
                                              "odm_orthophoto",
                                              "odm_orthophoto_render.tif")
        orthophoto_corners_file = os.path.join(args.project_path,
                                               "odm_orthophoto",
                                               "odm_orthophoto_corners.txt")

        if not io.file_exists(orthophoto_file) or self.rerun():
            kwargs = {
                'ortho': orthophoto_render_file,
                'corners': orthophoto_corners_file,
                'res': 100.0 / args.orthophoto_resolution,
                'verbose': '--verbose' if args.verbose else '',
                'models': outputs["textured_model_obj"]
            }

            system.run('odm_orthophoto -inputFiles {models} '
                       '-outputFile {ortho} -resolution {res} {verbose} '
                       '-outputCornerFile {corners}'.format(**kwargs))

            utm_east_offset, utm_north_offset = outputs["utm_offset"]
            proj4_srs = outputs["utm_srs"].to_proj4()

            ulx = uly = lrx = lry = 0.0
            with open(orthophoto_corners_file) as f:
                for lineNumber, line in enumerate(f):
                    if lineNumber == 0:
                        tokens = line.split(' ')
                        if len(tokens) == 4:
                            ulx = float(tokens[0]) + utm_east_offset
                            lry = float(tokens[1]) + utm_north_offset
                            lrx = float(tokens[2]) + utm_east_offset
                            uly = float(tokens[3]) + utm_north_offset
                log.ODM_INFO('Creating GeoTIFF')

                orthophoto_vars = orthophoto.get_orthophoto_vars(args)

                kwargs = {
                    'ulx':
                    ulx,
                    'uly':
                    uly,
                    'lrx':
                    lrx,
                    'lry':
                    lry,
                    'vars':
                    ' '.join([
                        '-co %s=%s' % (k, orthophoto_vars[k])
                        for k in orthophoto_vars
                    ]),
                    'proj':
                    proj4_srs,
                    'input':
                    orthophoto_render_file,
                    'output':
                    orthophoto_file,
                    'max_memory':
                    get_max_memory(),
                }

                system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                           '{vars} '
                           '-a_srs \"{proj}\" '
                           '--config GDAL_CACHEMAX {max_memory}% '
                           '--config GDAL_TIFF_INTERNAL_MASK YES '
                           '{input} {output}'.format(**kwargs))

                #orthophoto.post_orthophoto_steps(args, bounds_file_path, tree.odm_orthophoto_tif)
        else:
            log.ODM_WARNING('Found a valid orthophoto in: %s' %
                            orthophoto_file)
Пример #18
0
    def process(self, args, outputs):
        tree = outputs['tree']
        reconstruction = outputs['reconstruction']
        verbose = '-verbose' if args.verbose else ''

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

        if not io.file_exists(tree.odm_orthophoto_tif) or self.rerun():
            gsd_error_estimate = 0.1
            ignore_resolution = False
            if not reconstruction.is_georeferenced():
                # Match DEMs
                gsd_error_estimate = -3
                ignore_resolution = True

            resolution = 1.0 / (
                gsd.cap_resolution(args.orthophoto_resolution,
                                   tree.opensfm_reconstruction,
                                   gsd_error_estimate=gsd_error_estimate,
                                   ignore_gsd=args.ignore_gsd,
                                   ignore_resolution=ignore_resolution,
                                   has_gcp=reconstruction.has_gcp()) / 100.0)

            # odm_orthophoto definitions
            kwargs = {
                'bin': context.odm_modules_path,
                'log': tree.odm_orthophoto_log,
                'ortho': tree.odm_orthophoto_render,
                'corners': tree.odm_orthophoto_corners,
                'res': resolution,
                'bands': '',
                'verbose': verbose
            }

            models = []

            if args.use_3dmesh:
                base_dir = tree.odm_texturing
            else:
                base_dir = tree.odm_25dtexturing

            model_file = tree.odm_textured_model_obj

            if reconstruction.multi_camera:
                for band in reconstruction.multi_camera:
                    primary = band['name'] == get_primary_band_name(
                        reconstruction.multi_camera, args.primary_band)
                    subdir = ""
                    if not primary:
                        subdir = band['name'].lower()
                    models.append(os.path.join(base_dir, subdir, model_file))
                kwargs['bands'] = '-bands %s' % (','.join(
                    [quote(b['name']) for b in reconstruction.multi_camera]))
            else:
                models.append(os.path.join(base_dir, model_file))

            kwargs['models'] = ','.join(map(quote, models))

            # run odm_orthophoto
            system.run(
                '{bin}/odm_orthophoto -inputFiles {models} '
                '-logFile {log} -outputFile {ortho} -resolution {res} {verbose} '
                '-outputCornerFile {corners} {bands}'.format(**kwargs))

            # Create georeferenced GeoTiff
            geotiffcreated = False

            if reconstruction.is_georeferenced():
                ulx = uly = lrx = lry = 0.0
                with open(tree.odm_orthophoto_corners) as f:
                    for lineNumber, line in enumerate(f):
                        if lineNumber == 0:
                            tokens = line.split(' ')
                            if len(tokens) == 4:
                                ulx = float(tokens[0]) + \
                                      float(reconstruction.georef.utm_east_offset)
                                lry = float(tokens[1]) + \
                                      float(reconstruction.georef.utm_north_offset)
                                lrx = float(tokens[2]) + \
                                      float(reconstruction.georef.utm_east_offset)
                                uly = float(tokens[3]) + \
                                      float(reconstruction.georef.utm_north_offset)
                log.ODM_INFO('Creating GeoTIFF')

                orthophoto_vars = orthophoto.get_orthophoto_vars(args)

                kwargs = {
                    'ulx':
                    ulx,
                    'uly':
                    uly,
                    'lrx':
                    lrx,
                    'lry':
                    lry,
                    'vars':
                    ' '.join([
                        '-co %s=%s' % (k, orthophoto_vars[k])
                        for k in orthophoto_vars
                    ]),
                    'proj':
                    reconstruction.georef.proj4(),
                    'input':
                    tree.odm_orthophoto_render,
                    'output':
                    tree.odm_orthophoto_tif,
                    'log':
                    tree.odm_orthophoto_tif_log,
                    'max_memory':
                    get_max_memory(),
                }

                system.run('gdal_translate -a_ullr {ulx} {uly} {lrx} {lry} '
                           '{vars} '
                           '-a_srs \"{proj}\" '
                           '--config GDAL_CACHEMAX {max_memory}% '
                           '--config GDAL_TIFF_INTERNAL_MASK YES '
                           '{input} {output} > {log}'.format(**kwargs))

                bounds_file_path = os.path.join(
                    tree.odm_georeferencing,
                    'odm_georeferenced_model.bounds.gpkg')

                # Cutline computation, before cropping
                # We want to use the full orthophoto, not the cropped one.
                if args.orthophoto_cutline:
                    cutline_file = os.path.join(tree.odm_orthophoto,
                                                "cutline.gpkg")

                    compute_cutline(tree.odm_orthophoto_tif,
                                    bounds_file_path,
                                    cutline_file,
                                    args.max_concurrency,
                                    tmpdir=os.path.join(
                                        tree.odm_orthophoto,
                                        "grass_cutline_tmpdir"),
                                    scale=0.25)

                    orthophoto.compute_mask_raster(
                        tree.odm_orthophoto_tif,
                        cutline_file,
                        os.path.join(tree.odm_orthophoto,
                                     "odm_orthophoto_cut.tif"),
                        blend_distance=20,
                        only_max_coords_feature=True)

                orthophoto.post_orthophoto_steps(args, bounds_file_path,
                                                 tree.odm_orthophoto_tif,
                                                 tree.orthophoto_tiles)

                # Generate feathered orthophoto also
                if args.orthophoto_cutline:
                    orthophoto.feather_raster(
                        tree.odm_orthophoto_tif,
                        os.path.join(tree.odm_orthophoto,
                                     "odm_orthophoto_feathered.tif"),
                        blend_distance=20)

                geotiffcreated = True
            if not geotiffcreated:
                if io.file_exists(tree.odm_orthophoto_render):
                    pseudogeo.add_pseudo_georeferencing(
                        tree.odm_orthophoto_render)
                    log.ODM_INFO(
                        "Renaming %s --> %s" %
                        (tree.odm_orthophoto_render, tree.odm_orthophoto_tif))
                    os.rename(tree.odm_orthophoto_render,
                              tree.odm_orthophoto_tif)
                else:
                    log.ODM_WARNING(
                        "Could not generate an orthophoto (it did not render)")
        else:
            log.ODM_WARNING('Found a valid orthophoto in: %s' %
                            tree.odm_orthophoto_tif)

        if args.optimize_disk_space and io.file_exists(
                tree.odm_orthophoto_render):
            os.remove(tree.odm_orthophoto_render)