OpenDroneMap-ODM/opendm/dem/merge.py

176 wiersze
6.9 KiB
Python
Czysty Zwykły widok Historia

import math
import numpy as np
from scipy import ndimage
import rasterio
from rasterio.transform import Affine, rowcol
from opendm import system
from opendm.dem.commands import compute_euclidean_map
from opendm import log
from opendm import io
import os
def euclidean_merge_dems(input_dems, output_dem, creation_options={}, euclidean_map_source=None):
"""
Based on https://github.com/mapbox/rio-merge-rgba
and ideas from Anna Petrasova
implementation by Piero Toffanin
Computes a merged DEM by computing/using a euclidean
distance to NODATA cells map for all DEMs and then blending all overlapping DEM cells
by a weighted average based on such euclidean distance.
"""
inputs = []
bounds=None
precision=7
existing_dems = []
for dem in input_dems:
if not io.file_exists(dem):
log.ODM_WARNING("%s does not exist. Will skip from merged DEM." % dem)
continue
existing_dems.append(dem)
if len(existing_dems) == 0:
log.ODM_WARNING("No input DEMs, skipping euclidean merge.")
return
with rasterio.open(existing_dems[0]) as first:
src_nodata = first.nodatavals[0]
res = first.res
dtype = first.dtypes[0]
profile = first.profile
for dem in existing_dems:
eumap = compute_euclidean_map(dem, io.related_file_path(dem, postfix=".euclideand", replace_base=euclidean_map_source), overwrite=False)
if eumap and io.file_exists(eumap):
inputs.append((dem, eumap))
log.ODM_INFO("%s valid DEM rasters to merge" % len(inputs))
sources = [(rasterio.open(d), rasterio.open(e)) for d,e in inputs]
# Extent from option or extent of all inputs.
if bounds:
dst_w, dst_s, dst_e, dst_n = bounds
else:
# scan input files.
# while we're at it, validate assumptions about inputs
xs = []
ys = []
for src_d, src_e in sources:
left, bottom, right, top = src_d.bounds
xs.extend([left, right])
ys.extend([bottom, top])
if src_d.profile["count"] != 1 or src_e.profile["count"] != 1:
raise ValueError("Inputs must be 1-band rasters")
dst_w, dst_s, dst_e, dst_n = min(xs), min(ys), max(xs), max(ys)
log.ODM_INFO("Output bounds: %r %r %r %r" % (dst_w, dst_s, dst_e, dst_n))
output_transform = Affine.translation(dst_w, dst_n)
output_transform *= Affine.scale(res[0], -res[1])
# Compute output array shape. We guarantee it will cover the output
# bounds completely.
output_width = int(math.ceil((dst_e - dst_w) / res[0]))
output_height = int(math.ceil((dst_n - dst_s) / res[1]))
# Adjust bounds to fit.
dst_e, dst_s = output_transform * (output_width, output_height)
log.ODM_INFO("Output width: %d, height: %d" % (output_width, output_height))
log.ODM_INFO("Adjusted bounds: %r %r %r %r" % (dst_w, dst_s, dst_e, dst_n))
profile["transform"] = output_transform
profile["height"] = output_height
profile["width"] = output_width
profile["tiled"] = creation_options.get('TILED', 'YES') == 'YES'
profile["blockxsize"] = creation_options.get('BLOCKXSIZE', 512)
profile["blockysize"] = creation_options.get('BLOCKYSIZE', 512)
profile["compress"] = creation_options.get('COMPRESS', 'LZW')
profile["nodata"] = src_nodata
# Creation opts
profile.update(creation_options)
# create destination file
2022-06-29 19:38:27 +00:00
with rasterio.open(output_dem, "w", BIGTIFF="IF_SAFER", **profile) as dstrast:
for idx, dst_window in dstrast.block_windows():
left, bottom, right, top = dstrast.window_bounds(dst_window)
blocksize = dst_window.width
dst_rows, dst_cols = (dst_window.height, dst_window.width)
# initialize array destined for the block
dst_count = first.count
dst_shape = (dst_count, dst_rows, dst_cols)
dstarr = np.zeros(dst_shape, dtype=dtype)
distsum = np.zeros(dst_shape, dtype=dtype)
small_distance = 0.001953125
for src_d, src_e in sources:
# The full_cover behavior is problematic here as it includes
# extra pixels along the bottom right when the sources are
# slightly misaligned
#
# src_window = get_window(left, bottom, right, top,
# src.transform, precision=precision)
#
# With rio merge this just adds an extra row, but when the
# imprecision occurs at each block, you get artifacts
nodata = src_d.nodatavals[0]
# Alternative, custom get_window using rounding
src_window_d = tuple(zip(rowcol(
src_d.transform, left, top, op=round, precision=precision
), rowcol(
src_d.transform, right, bottom, op=round, precision=precision
)))
src_window_e = tuple(zip(rowcol(
src_e.transform, left, top, op=round, precision=precision
), rowcol(
src_e.transform, right, bottom, op=round, precision=precision
)))
temp_d = np.zeros(dst_shape, dtype=dtype)
temp_d = src_d.read(
out=temp_d, window=src_window_d, boundless=True, masked=False
)
temp_e = np.zeros(dst_shape, dtype=dtype)
temp_e = src_e.read(
out=temp_e, window=src_window_e, boundless=True, masked=False
)
# Set NODATA areas in the euclidean map to a very low value
# so that:
# - Areas with overlap prioritize DEM layers' cells that
# are far away from NODATA areas
# - Areas that have no overlap are included in the final result
# even if they are very close to a NODATA cell
temp_e[temp_e==0] = small_distance
temp_e[temp_d==nodata] = 0
np.multiply(temp_d, temp_e, out=temp_d)
np.add(dstarr, temp_d, out=dstarr)
np.add(distsum, temp_e, out=distsum)
np.divide(dstarr, distsum, out=dstarr, where=distsum[0] != 0.0)
# Perform nearest neighbor interpolation on areas where two or more rasters overlap
# but where both rasters have only interpolated data. This prevents the creation
# of artifacts that average areas of interpolation.
indices = ndimage.distance_transform_edt(np.logical_and(distsum < 1, distsum > small_distance),
return_distances=False,
return_indices=True)
dstarr = dstarr[tuple(indices)]
dstarr[dstarr == 0.0] = src_nodata
dstrast.write(dstarr, window=dst_window)
return output_dem