Skip to content

Commit cb22128

Browse files
authored
Merge pull request #10 from scottstanie/bbox-integer
enhancement: make integer bbox align with tiles
2 parents 164c10e + 6d6e44d commit cb22128

File tree

10 files changed

+243
-165
lines changed

10 files changed

+243
-165
lines changed

sardem/cli.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
Command line interface for running sardem
33
"""
4-
from sardem.download import Downloader
54
import json
65
from argparse import (
76
ArgumentError,
@@ -11,6 +10,9 @@
1110
RawTextHelpFormatter,
1211
)
1312

13+
from sardem.download import Downloader
14+
from sardem import utils
15+
1416

1517
def positive_small_int(argstring):
1618
try:
@@ -135,19 +137,24 @@ def get_cli_args():
135137
" Default is GDAL's top-left edge convention."
136138
),
137139
)
138-
140+
parser.add_argument(
141+
"--cache-dir",
142+
help=(
143+
"Location to save downloaded files (Default = {})".format(utils.get_cache_dir())
144+
),
145+
)
139146
return parser.parse_args()
140147

141148

142149
def cli():
143150
args = get_cli_args()
144151
import sardem.dem
145152

146-
if args.left_lon and args.geojson or args.left_lon and args.bbox:
153+
if (args.left_lon and args.geojson) or (args.left_lon and args.bbox):
147154
raise ArgumentError(
148155
args.geojson,
149-
"Can only use one of positional arguments (left_lon top_lat dlon dlat) "
150-
", --geojson, or --bbox",
156+
"Can only use one type of bounding box specifier: (left_lon top_lat dlon dlat) "
157+
", --geojson, --bbox, or --wkt-file",
151158
)
152159
# Need all 4 positionals, or the --geosjon
153160
elif (
@@ -156,19 +163,15 @@ def cli():
156163
and not args.bbox
157164
and not args.wkt_file
158165
):
159-
raise ValueError("Need --bbox, --geojoin, or --wkt-file")
166+
raise ValueError("Need --bbox, --geojson, or --wkt-file")
160167

161168
geojson_dict = json.load(args.geojson) if args.geojson else None
162-
if args.bbox:
163-
left, bot, right, top = args.bbox
164-
left_lon, top_lat = left, top
165-
dlon = right - left
166-
dlat = top - bot
167-
elif args.wkt_file:
168-
left_lon, top_lat, dlon, dlat = None, None, None, None
169-
elif args.left_lon:
169+
if args.left_lon:
170170
left_lon, top_lat = args.left_lon, args.top_lat
171171
dlon, dlat = args.dlon, args.dlat
172+
bbox = utils.bounding_box(left_lon, top_lat, dlon, dlat)
173+
elif args.bbox:
174+
bbox = args.bbox
172175

173176
if not args.output:
174177
output = (
@@ -178,10 +181,8 @@ def cli():
178181
output = args.output
179182

180183
sardem.dem.main(
181-
left_lon,
182-
top_lat,
183-
dlon,
184-
dlat,
184+
output_name=output,
185+
bbox=bbox,
185186
geojson=geojson_dict,
186187
wkt_file=args.wkt_file,
187188
data_source=args.data_source,
@@ -190,5 +191,5 @@ def cli():
190191
make_isce_xml=args.make_isce_xml,
191192
keep_egm=args.keep_egm,
192193
shift_rsc=args.shift_rsc,
193-
output_name=output,
194+
cache_dir=args.cache_dir,
194195
)

sardem/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
NUM_PIXELS_SRTM1 = 3601 # For SRTM1
2+
DEFAULT_RES = 1 / 3600.0

sardem/conversions.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _get_size(filename):
6868
return xsize, ysize
6969

7070

71-
def convert_dem_to_wgs84(dem_filename, geoid="egm96", using_gdal_bounds=False):
71+
def convert_dem_to_wgs84(dem_filename, geoid="egm96"):
7272
"""Convert the file `dem_filename` from EGM96 heights to WGS84 ellipsoidal heights
7373
7474
Overwrites file, requires GDAL to be installed
@@ -79,8 +79,6 @@ def convert_dem_to_wgs84(dem_filename, geoid="egm96", using_gdal_bounds=False):
7979

8080
path_, fname = os.path.split(dem_filename)
8181
rsc_filename = os.path.join(path_, fname + ".rsc")
82-
if not using_gdal_bounds:
83-
utils.shift_rsc_file(rsc_filename, to_gdal=True)
8482

8583
output_egm = os.path.join(path_, "egm_" + fname)
8684
# output_wgs = dem_filename.replace(ext, ".wgs84" + ext)
@@ -97,8 +95,4 @@ def convert_dem_to_wgs84(dem_filename, geoid="egm96", using_gdal_bounds=False):
9795
logger.error("Failed to convert DEM:", exc_info=True)
9896
logger.error("Reverting back, using EGM dem as output")
9997
os.rename(output_egm, dem_filename)
100-
os.rename(rsc_filename_egm, rsc_filename)
101-
102-
if not using_gdal_bounds:
103-
# Now shift back to the .rsc is pointing to the middle of the pixel
104-
utils.shift_rsc_file(rsc_filename, to_gdal=False)
98+
os.rename(rsc_filename_egm, rsc_filename)

sardem/cop_dem.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import requests
55

66
from sardem import conversions, utils
7+
from sardem.constants import DEFAULT_RES
78

89
TILE_LIST_URL = "https://copernicus-dem-30m.s3.amazonaws.com/tileList.txt"
910
URL_TEMPLATE = "https://copernicus-dem-30m.s3.amazonaws.com/{t}/{t}.tif"
10-
DEFAULT_RES = 1 / 3600
11+
1112
logger = logging.getLogger("sardem")
1213
utils.set_logger_handler(logger)
1314

@@ -44,6 +45,7 @@ def download_and_stitch(
4445
t_srs = 'epsg:4326'
4546
xres = DEFAULT_RES / xrate
4647
yres = DEFAULT_RES / yrate
48+
resamp = "bilinear" if (xrate > 1 or yrate > 1) else "nearest"
4749

4850
# access_mode = "overwrite" if overwrite else None
4951
option_dict = dict(
@@ -54,7 +56,7 @@ def download_and_stitch(
5456
xRes=xres,
5557
yRes=yres,
5658
outputType=gdal.GDT_Int16,
57-
resampleAlg="bilinear",
59+
resampleAlg=resamp,
5860
multithread=True,
5961
warpMemoryLimit=5000,
6062
warpOptions=["NUM_THREADS=4"],
@@ -82,7 +84,7 @@ def _gdal_cmd_from_options(src, dst, option_dict):
8284
from osgeo import gdal
8385

8486
opts = deepcopy(option_dict)
85-
# To see what the list of cli options are
87+
# To see what the list of cli options are (gdal >= 3.5.0)
8688
opts["options"] = "__RETURN_OPTION_LIST__"
8789
opt_list = gdal.WarpOptions(**opts)
8890
out_opt_list = deepcopy(opt_list)

sardem/dem.py

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Digital Elevation Map (DEM) downloading/stitching/upsampling
1+
"""Digital Elevation Map (DEM) downloading, stitching, upsampling
22
33
Module contains utilities for downloading all necessary .hgt files
44
for a lon/lat rectangle, stiches them into one DEM, and creates a
@@ -38,15 +38,17 @@
3838
PROJECTION LL
3939
"""
4040
from __future__ import division, print_function
41+
4142
import collections
4243
import logging
4344
import os
45+
4446
import numpy as np
4547

46-
from sardem import utils, loading, upsample_cy, conversions
47-
from sardem.download import Tile, Downloader
48+
from sardem import conversions, loading, upsample_cy, utils
49+
from sardem.constants import DEFAULT_RES, NUM_PIXELS_SRTM1
50+
from sardem.download import Downloader, Tile
4851

49-
NUM_PIXELS = 3601 # For SRTM1
5052
RSC_KEYS = [
5153
"WIDTH",
5254
"FILE_LENGTH",
@@ -79,7 +81,7 @@ class Stitcher:
7981
"""
8082

8183
def __init__(
82-
self, tile_names, filenames=[], data_source="NASA", num_pixels=NUM_PIXELS
84+
self, tile_names, filenames=[], data_source="NASA", num_pixels=NUM_PIXELS_SRTM1
8385
):
8486
"""List should come from Tile.srtm1_tile_names()"""
8587
self.tile_file_list = list(tile_names)
@@ -191,7 +193,7 @@ def _load_tile(self, tile_name):
191193
else:
192194
return loading.load_elevation(filename)
193195
else:
194-
return np.zeros((NUM_PIXELS, NUM_PIXELS), dtype=self.dtype)
196+
return np.zeros((NUM_PIXELS_SRTM1, NUM_PIXELS_SRTM1), dtype=self.dtype)
195197

196198
def load_and_stitch(self):
197199
"""Function to load combine .hgt tiles
@@ -279,11 +281,11 @@ def create_dem_rsc(self):
279281
return rsc_dict
280282

281283

282-
def crop_stitched_dem(bounds, stitched_dem, rsc_data):
283-
"""Takes the output of Stitcher.load_and_stitch, crops to bounds
284+
def crop_stitched_dem(bbox, stitched_dem, rsc_data):
285+
"""Takes the output of Stitcher.load_and_stitch, crops to bbox
284286
285287
Args:
286-
bounds (tuple[float]): (left, bot, right, top) lats and lons of
288+
bbox (tuple[float]): (left, bot, right, top) lats and lons of
287289
desired bounding box for the DEM
288290
stitched_dem (numpy.array, 2D): result from files
289291
through Stitcher.load_and_stitch()
@@ -293,7 +295,7 @@ def crop_stitched_dem(bounds, stitched_dem, rsc_data):
293295
numpy.array: a cropped version of the bigger stitched_dem
294296
"""
295297
indexes, new_starts = utils.find_bounding_idxs(
296-
bounds,
298+
bbox,
297299
rsc_data["X_STEP"],
298300
rsc_data["Y_STEP"],
299301
rsc_data["X_FIRST"],
@@ -305,11 +307,13 @@ def crop_stitched_dem(bounds, stitched_dem, rsc_data):
305307
return cropped_dem, new_starts, new_sizes
306308

307309

310+
def _float_is_on_bounds(x):
311+
return int(x) == x
312+
313+
308314
def main(
309-
left_lon=None,
310-
top_lat=None,
311-
dlon=None,
312-
dlat=None,
315+
output_name=None,
316+
bbox=None,
313317
geojson=None,
314318
wkt_file=None,
315319
data_source=None,
@@ -318,16 +322,16 @@ def main(
318322
make_isce_xml=False,
319323
keep_egm=False,
320324
shift_rsc=False,
321-
output_name=None,
325+
cache_dir=None,
322326
):
323327
"""Function for entry point to create a DEM with `sardem`
324328
325329
Args:
326-
left_lon (float): Left most longitude of DEM box
327-
top_lat (float): Top most longitude of DEM box
328-
dlon (float): Width of box in longitude degrees
329-
dlat (float): Height of box in latitude degrees
330-
geojson (dict): geojson object outlining DEM (alternative to lat/lon)
330+
output_name (str): name of file to save final DEM (default = elevation.dem)
331+
bbox (tuple[float]): (left, bot, right, top)
332+
Longitude/latitude desired bounding box for the DEM
333+
geojson (dict): geojson object outlining DEM (alternative to bbox)
334+
wkt_file (str): path to .wkt file outlining DEM (alternative to bbox)
331335
data_source (str): 'NASA' or 'AWS', where to download .hgt tiles from
332336
xrate (int): x-rate (columns) to upsample DEM (positive int)
333337
yrate (int): y-rate (rows) to upsample DEM (positive int)
@@ -337,47 +341,53 @@ def main(
337341
shift_rsc (bool): Shift the .dem.rsc file down/right so that the
338342
X_FIRST and Y_FIRST values represent the pixel *center* (instead of
339343
GDAL's convention of pixel edge). Default = False.
340-
output_name (str): name of file to save final DEM (default = elevation.dem)
344+
cache_dir (str): directory to cache downloaded tiles
341345
"""
342-
if geojson:
343-
bounds = utils.bounding_box(geojson=geojson)
344-
elif wkt_file:
345-
bounds = utils.get_wkt_bbox(wkt_file)
346-
else:
347-
bounds = utils.bounding_box(left_lon, top_lat, dlon, dlat)
348-
logger.info("Bounds: %s", " ".join(str(b) for b in bounds))
349-
outrows, outcols = utils.get_output_size(bounds, xrate, yrate)
346+
if bbox is None:
347+
if geojson:
348+
bbox = utils.bounding_box(geojson=geojson)
349+
elif wkt_file:
350+
bbox = utils.get_wkt_bbox(wkt_file)
351+
352+
if bbox is None:
353+
raise ValueError("Must provide either bbox or geojson or wkt_file")
354+
logger.info("Bounds: %s", " ".join(str(b) for b in bbox))
355+
356+
if all(_float_is_on_bounds(b) for b in bbox):
357+
logger.info("Shifting bbox to nearest tile bounds")
358+
bbox = utils.shift_integer_bbox(bbox)
359+
logger.info("New edge bounds: %s", " ".join(str(b) for b in bbox))
360+
# Now we're assuming that `bbox` refers to the edges of the desired bounding box
361+
362+
# Print a warning if they're possibly requesting too-large a box by mistake
363+
outrows, outcols = utils.get_output_size(bbox, xrate, yrate)
350364
if outrows * outcols > WARN_LIMIT:
351365
logger.warning(
352366
"Caution: Output size is {} x {} pixels.".format(outrows, outcols)
353367
)
354-
logger.warning("Are the bounds correct?")
355-
356-
# Are we using GDAL's convention (pixel edge) or the center?
357-
# i.e. if `shift_rsc` is False, then we are `using_gdal_bounds`
358-
using_gdal_bounds = not shift_rsc
368+
logger.warning("Are the bounds correct (left, bottom, right, top)?")
359369

370+
# For copernicus, use GDAL to warp from the VRT
360371
if data_source == "COP":
361372
utils._gdal_installed_correctly()
362373
from sardem import cop_dem
363374

364375
cop_dem.download_and_stitch(
365376
output_name,
366-
bounds,
377+
bbox,
367378
keep_egm=keep_egm,
368379
xrate=xrate,
369380
yrate=yrate,
370381
)
371382
if make_isce_xml:
372383
logger.info("Creating ISCE2 XML file")
373-
utils.gdal2isce_xml(
374-
output_name, keep_egm=keep_egm, using_gdal_bounds=using_gdal_bounds
375-
)
384+
utils.gdal2isce_xml(output_name, keep_egm=keep_egm)
376385
return
377386

378-
tile_names = list(Tile(*bounds).srtm1_tile_names())
387+
# If using SRTM, download tiles manually and stitch
388+
tile_names = list(Tile(*bbox).srtm1_tile_names())
379389

380-
d = Downloader(tile_names, data_source=data_source)
390+
d = Downloader(tile_names, data_source=data_source, cache_dir=cache_dir)
381391
local_filenames = d.download_all()
382392

383393
s = Stitcher(tile_names, filenames=local_filenames, data_source=data_source)
@@ -386,10 +396,10 @@ def main(
386396
# Now create corresponding rsc file
387397
rsc_dict = s.create_dem_rsc()
388398

389-
# Cropping: get very close to the bounds asked for:
399+
# Cropping: get very close to the bbox asked for:
390400
logger.info("Cropping stitched DEM to boundaries")
391401
stitched_dem, new_starts, new_sizes = crop_stitched_dem(
392-
bounds, stitched_dem, rsc_dict
402+
bbox, stitched_dem, rsc_dict
393403
)
394404
new_x_first, new_y_first = new_starts
395405
new_rows, new_cols = new_sizes
@@ -398,8 +408,8 @@ def main(
398408
rsc_dict["Y_FIRST"] = new_y_first
399409
rsc_dict["FILE_LENGTH"] = new_rows
400410
rsc_dict["WIDTH"] = new_cols
401-
if shift_rsc:
402-
rsc_dict = utils.shift_rsc_dict(rsc_dict, to_gdal=True)
411+
412+
rsc_dict = utils.shift_rsc_dict(rsc_dict, to_gdal=True)
403413

404414
rsc_filename = output_name + ".rsc"
405415

@@ -452,17 +462,17 @@ def main(
452462

453463
if make_isce_xml:
454464
logger.info("Creating ISCE2 XML file")
455-
utils.gdal2isce_xml(
456-
output_name, keep_egm=keep_egm, using_gdal_bounds=using_gdal_bounds
457-
)
465+
utils.gdal2isce_xml(output_name, keep_egm=keep_egm)
458466

459467
if keep_egm or data_source == "NASA_WATER":
460468
logger.info("Keeping DEM as EGM96 geoid heights")
461469
else:
462470
logger.info("Correcting DEM to heights above WGS84 ellipsoid")
463-
conversions.convert_dem_to_wgs84(
464-
output_name, using_gdal_bounds=using_gdal_bounds
465-
)
471+
conversions.convert_dem_to_wgs84(output_name, geoid="egm96")
472+
473+
# If the user wants the .rsc file to point to pixel center:
474+
if shift_rsc:
475+
utils.shift_rsc_file(rsc_filename, to_gdal=False)
466476

467477
# Overwrite with smaller dtype for water mask
468478
if data_source == "NASA_WATER":

0 commit comments

Comments
 (0)