07 Raster2 Demo#

Reprojection, Clipping, Sampling, Zonal Stats#

Featuring the best* raster of all: DEMs#

UW Geospatial Data Analysis
CEE467/CEWA567
David Shean

*An objective conclusion

Objectives#

  • Demonstrate multiple approaches for “on-the-fly” raster data download

  • Understand additional fundamental raster processing/analysis:

    • Reprojection

    • Clipping

    • Interpolation and sampling strategies

  • Combine vector points and polygons with rasters for zonal statistics

  • Understand processing strategies, derivative products, and common applications for a fundamental raster data product: DEMs

    • Slope and Aspect

    • Contour generation

    • Volume estimation (cut/fill analysis)

What is a DEM?#

DEM = Digital Elevation Model

A generic term for a 2D raster grid with values representing surface elevation above some datum (e.g., WGS84 ellispoid or a geoid model representing mean sea level). Sometimes called 2.5D, as it’s not a true 3D dataset containing some value (e.g., temperature) at each (x,y,z) point.

There are subtypes:

  • DSM = Digital Surface Model (“first-return” model includes top of canopy, buildings, etc.)

  • DTM = Digital Terrain Model (bare ground model, with canopy, buildings, etc. removed)

Great resource on LiDAR and derivative products:

Airborne LiDAR example#

SRTM#

SRTM = Shuttle Radar Topography Mission

This week, we’ll play with the landmark SRTM dataset. We briefly introduced this during Lab03, as I sampled the SRTM products for the GLAS point locations and included as the dem_z column in the csv.

  • Collected February 11-22, 2000 (winter)

  • Single-track InSAR (interferometric synthetic aperture radar) instrument

  • Coverage: 56°S to 60°N (the shuttle orbit, plus radar look direction)

  • Tiled in 1x1° raster data at different resolutions:

    • 1-arcsecond (~30 m)

    • 3-arcsecond (~90 m)

  • Default elevation values in the SRTM tiles are relative to the EGM96 geoid (approximates mean sea level), not the WGS84 ellipsoid (as with the GLAS points)

For this lab, we’ll use the 3-arcsec (90 m) SRTM product for WA state to learn some new concepts. This is a relatively small dataset, with a limited number of 1x1° tiles required for WA state. However, the approaches we’ll learn (e.g., API subsetting, using vrt datasets), scale to larger datasets that are too big to fit in memory (like operations on the global SRTM dataset). In future weeks, we’ll explore xarray, which can also be used to efficiently process raster data (and raster time series).

Copernicus DEM#

Interactive discussion topics#

⚠️ Suggestion - shut down other unneeded kernels before running#

import os
import requests
import numpy as np
import pandas as pd
import geopandas as gpd
import rasterio as rio
from rasterio import plot, mask
from rasterio.warp import calculate_default_transform, reproject, Resampling
import matplotlib.pyplot as plt
import rasterstats
import rioxarray
from matplotlib_scalebar.scalebar import ScaleBar
#%matplotlib widget

Download sample DEM data for WA state#

pwd
'/home/jovyan/jupyterbook/book/modules/07_Raster2_DEMs_Warp_Clip_Sample'
#Create local directory to store images
demdir = 'dem_data'
if not os.path.exists(demdir):
    os.makedirs(demdir)
if os.path.basename(os.path.abspath('.')) != demdir:
    %cd $demdir
%pwd
/home/jovyan/jupyterbook/book/modules/07_Raster2_DEMs_Warp_Clip_Sample/dem_data
'/home/jovyan/jupyterbook/book/modules/07_Raster2_DEMs_Warp_Clip_Sample/dem_data'

Define the Washington state bounds from previous notebook#

  • Use the lat/lon bounds here in decimal degrees

wa_bounds = (-124.733174, 45.543541, -116.915989, 49.002494)
seattle_bounds = (-122.4487793446, 47.4510910088, -122.1548950672, 47.7917227803)

Use OpenTopography GlobalDEM API to fetch DEM for WA state#

#We are using OpenTopography demo API key here - only valid for 25 requests from a single IP address
#please save local output and use moving forward
#API_key="demoapikeyot2022"
#GDA key
API_key="e93e356203e00da7a5532e19ae4c2012"

base_url="https://portal.opentopography.org/API/globaldem?demtype={}&API_Key={}&west={}&south={}&east={}&north={}&outputFormat=GTiff"
def get_OT_GlobalDEM(demtype, bounds, out_fn=None):
    if out_fn is None:
        out_fn = '{}.tif'.format(demtype)
    
    if not os.path.exists(out_fn):
        print(f'Querying API for {demtype}')
        #Prepare API request url
        #Bounds should be [minlon, minlat, maxlon, maxlat]
        url = base_url.format(demtype, API_key, *bounds)
        print(url)
        #Get
        print(f'Downloading and Saving {out_fn}')
        response = requests.get(url)
        if response.status_code == 200:
            #Write to disk
            open(out_fn, 'wb').write(response.content)
        else:
            print(response.text)
    else:
        print(f'Found existing file: {out_fn}')
#List of all products hosted by OpenTopography GlobalDEM API
demtype_list = ['SRTMGL3', 'SRTMGL1', 'SRTMGL1_E', 'AW3D30', 'AW3D30_E', 'SRTM15Plus',\
                'NASADEM', 'COP30', 'COP90']
#Get Seattle data
for demtype in ['SRTMGL1', 'COP30']:
    
    #Output filename
    out_fn = f"SEA_{demtype}.tif"
    #Call the function to query API and download if file doesn't exist
    get_OT_GlobalDEM(demtype, seattle_bounds, out_fn)
Found existing file: SEA_SRTMGL1.tif
Found existing file: SEA_COP30.tif
#Get WA state data
for demtype in ['SRTMGL3', 'COP90']:
    
    #Output filename
    out_fn = f"WA_{demtype}.tif"
    #Call the function to query API and download if file doesn't exist
    get_OT_GlobalDEM(demtype, wa_bounds, out_fn)
Found existing file: WA_SRTMGL3.tif
Found existing file: WA_COP90.tif
!ls -lh
total 1.3G
drwxrwsr-x 2 jovyan users 4.0K Feb 17 06:28 dem_data
-rw-r--r-- 1 jovyan users 1.3M Feb 17 20:29 SEA_COP30_hs.tif
-rw-rw-r-- 1 jovyan users 5.2M Feb 17 20:25 SEA_COP30.tif
-rw-r--r-- 1 jovyan users 1.2M Feb 17 20:35 SEA_COP30_utm_hs.tif
-rw-r--r-- 1 jovyan users 4.8M Feb 17 20:33 SEA_COP30_utm.tif
-rw-rw-r-- 1 jovyan users 780K Feb 17 00:35 SEA_SRTMGL1.tif
-rw-r--r-- 1 jovyan users 199M Feb 17 20:12 WA_COP90_SRTMGL3_diff.tif
-rw-rw-r-- 1 jovyan users 142M Feb 17 01:04 WA_COP90.tif
-rw-rw-r-- 1 jovyan users 176M Feb 17 01:07 WA_COP90_utm_gdalwarp_lzw.tif
-rw-rw-r-- 1 jovyan users 199M Feb 17 01:06 WA_COP90_utm_gdalwarp.tif
-rw-rw-r-- 1 jovyan users  50M Feb 17 05:42 WA_COP90_utm_riowarp_hs.tif
-rw-rw-r-- 1 jovyan users 176M Feb 17 01:07 WA_COP90_utm_riowarp.tif
-rw-rw-r-- 1 jovyan users  46M Feb 17 00:30 WA_SRTMGL3.tif
-rw-rw-r-- 1 jovyan users  50M Feb 17 07:27 WA_SRTMGL3_utm_gdalwarp_lzw_hs.tif
-rw-rw-r-- 1 jovyan users 199M Feb 17 07:37 WA_SRTMGL3_utm_gdalwarp_lzw_slope.tif
-rw-rw-r-- 1 jovyan users  57M Feb 17 07:27 WA_SRTMGL3_utm_gdalwarp_lzw.tif
!gdalinfo $out_fn
Warning 1: WA_COP90.tif: This file used to have optimizations in its layout, but those have been, at least partly, invalidated by later changes
Driver: GTiff/GeoTIFF
Files: WA_COP90.tif
Size is 9381, 4151
Coordinate System is:
GEOGCRS["WGS 84",
    ENSEMBLE["World Geodetic System 1984 ensemble",
        MEMBER["World Geodetic System 1984 (Transit)"],
        MEMBER["World Geodetic System 1984 (G730)"],
        MEMBER["World Geodetic System 1984 (G873)"],
        MEMBER["World Geodetic System 1984 (G1150)"],
        MEMBER["World Geodetic System 1984 (G1674)"],
        MEMBER["World Geodetic System 1984 (G1762)"],
        MEMBER["World Geodetic System 1984 (G2139)"],
        ELLIPSOID["WGS 84",6378137,298.257223563,
            LENGTHUNIT["metre",1]],
        ENSEMBLEACCURACY[2.0]],
    PRIMEM["Greenwich",0,
        ANGLEUNIT["degree",0.0174532925199433]],
    CS[ellipsoidal,2],
        AXIS["geodetic latitude (Lat)",north,
            ORDER[1],
            ANGLEUNIT["degree",0.0174532925199433]],
        AXIS["geodetic longitude (Lon)",east,
            ORDER[2],
            ANGLEUNIT["degree",0.0174532925199433]],
    USAGE[
        SCOPE["Horizontal component of 3D system."],
        AREA["World."],
        BBOX[-90,-180,90,180]],
    ID["EPSG",4326]]
Data axis to CRS axis mapping: 2,1
Origin = (-124.733333333333320,49.002916666666664)
Pixel Size = (0.000833333333333,-0.000833333333333)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  COMPRESSION=LZW
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (-124.7333333,  49.0029167) (124d44' 0.00"W, 49d 0'10.50"N)
Lower Left  (-124.7333333,  45.5437500) (124d44' 0.00"W, 45d32'37.50"N)
Upper Right (-116.9158333,  49.0029167) (116d54'57.00"W, 49d 0'10.50"N)
Lower Right (-116.9158333,  45.5437500) (116d54'57.00"W, 45d32'37.50"N)
Center      (-120.8245833,  47.2733333) (120d49'28.50"W, 47d16'24.00"N)
Band 1 Block=256x256 Type=Float32, ColorInterp=Gray
  NoData Value=0

Open the file with rasterio#

src = rio.open(out_fn)

Review the metadata#

  • Note the input data type - are these values unsigned or signed (meaning values can be positive or negative)?

src.profile
{'driver': 'GTiff', 'dtype': 'float32', 'nodata': 0.0, 'width': 9381, 'height': 4151, 'count': 1, 'crs': CRS.from_epsg(4326), 'transform': Affine(0.0008333333333333334, 0.0, -124.73333333333332,
       0.0, -0.0008333333333333334, 49.002916666666664), 'blockxsize': 256, 'blockysize': 256, 'tiled': True, 'compress': 'lzw', 'interleave': 'band'}

Plot using rio.plot.show()#

  • Use the with construct here to cleanly open and close the vrt file, with better handling of any exceptions

#%matplotlib widget
#%matplotlib inline
with rio.open(out_fn) as src:
    rio.plot.show(src)
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_31_0.png

Nice! WA state really does have interesting topography.

Much more interesting than Kansas:

Compare SRTM And Copernicus DEM#

Read the data and plot with matplotlib#

  • Read the data to a NumPy array

    • Note that the tiles on disk are not actually loaded until you read the dataset

    • Make sure you deal with nodata, either with a NumPy masked array, or setting nodata to np.nan (remember np.nan is a float, your input data type is int16)

  • View with matplotlib imshow - verify you have a DEM of washington state

srtm_fn = 'WA_SRTMGL3.tif'
cop_fn = 'WA_COP90.tif'
f, axa = plt.subplots(1,2, sharex=True, sharey=True, figsize=(10,3))
#One call to open, read and plot with imshow. Stores output from imshow (mappapble object) to create colorbar
m = axa[0].imshow(rio.open(srtm_fn).read(1, masked=True))
axa[0].set_title('SRTM-GL3')
f.colorbar(m, ax=axa[0])
m = axa[1].imshow(rio.open(cop_fn).read(1, masked=True))
axa[1].set_title('COP90')
f.colorbar(m, ax=axa[1]);
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_36_0.png

Set nodata value for Copernicus DEM to 0#

  • Note: The Copernicus DEM creators filled water with values of 0.0 above the EGM2008 geoid (approximates mean sea level)

  • We can set these to nodata to conly consider land. This is a bit of a hack, and it is possible that there are real elevations at 0 m that will be masked here.

#Temporarily open file in read/write mode to set nodata value
#https://rasterio.readthedocs.io/en/latest/api/rasterio.html?highlight=open#rasterio.open
with rio.open(cop_fn, 'r+') as src:
    src.nodata = 0.0

Raster reprojection using GDAL and rasterio#

Reproject the SRTM DEM to UTM 10N#

  • The SRTM grids are distributed with crs of EPSG:4326

  • Let’s reproject to a more appropriate coordinate system for WA state

    • Use EPSG:32610

dst_crs = 'EPSG:32610'
#Sadly, there is no `to_crs` method as in GeoPandas
#src.to_crs(dst_crs)

First, reproject using gdalwarp command-line utility#

proj_fn = os.path.splitext(out_fn)[0]+'_utm_gdalwarp.tif'
%%time
if not os.path.exists(proj_fn):
    !gdalwarp -overwrite -r cubic -t_srs $dst_crs $out_fn $proj_fn
CPU times: user 0 ns, sys: 48 µs, total: 48 µs
Wall time: 52.7 µs
ls -lh $proj_fn
-rw-rw-r-- 1 jovyan users 199M Feb 17 01:06 WA_COP90_utm_gdalwarp.tif
!gdalinfo $proj_fn
Driver: GTiff/GeoTIFF
Files: WA_COP90_utm_gdalwarp.tif
Size is 8877, 5851
Coordinate System is:
PROJCRS["WGS 84 / UTM zone 10N",
    BASEGEOGCRS["WGS 84",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4326]],
    CONVERSION["UTM zone 10N",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-123,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Between 126°W and 120°W, northern hemisphere between equator and 84°N, onshore and offshore. Canada - British Columbia (BC); Northwest Territories (NWT); Nunavut; Yukon. United States (USA) - Alaska (AK)."],
        BBOX[0,-126,84,-120]],
    ID["EPSG",32610]]
Data axis to CRS axis mapping: 1,2
Origin = (364685.490899736876599,5445638.415841511450708)
Pixel Size = (68.748471281554814,-68.748471281554814)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (  364685.491, 5445638.416) (124d51'20.11"W, 49d 8'55.14"N)
Lower Left  (  364685.491, 5043391.110) (124d43'58.58"W, 45d31'51.28"N)
Upper Right (  974965.670, 5445638.416) (116d30'24.81"W, 48d58'49.93"N)
Lower Right (  974965.670, 5043391.110) (116d55'58.87"W, 45d22'57.49"N)
Center      (  669825.581, 5244514.763) (120d45' 7.90"W, 47d19'55.38"N)
Band 1 Block=8877x1 Type=Float32, ColorInterp=Gray
  NoData Value=0

A note about “creation options” when writing a new file to disk#

  • Review this great reference on GeoTiff file format: https://www.gdal.org/frmt_gtiff.html

  • I almost always use -co COMPRESS=LZW -co TILED=YES -co BIGTIFF=IF_SAFER

    • Uses lossless LZW compression (we explored in LS8 example comparing filesize on disk to your calculated array filesize)

    • Writes the tif image in “tiles” of 256x256 px instead of one large block - makes it much more efficient to read and extract a subwindow from the tif

    • If necessary, use the BIGTIFF format for files that are >4 GB

Write out the same file, but this time use tiling and LZW compression#

  • Compare the new filesize on disk

proj_fn_lzw = os.path.splitext(out_fn)[0]+'_utm_gdalwarp_lzw.tif'
%%time
if not os.path.exists(proj_fn_lzw):
    !gdalwarp -r cubic -t_srs $dst_crs -co COMPRESS=LZW -co TILED=YES -co BIGTIFF=IF_SAFER $out_fn $proj_fn_lzw
CPU times: user 869 µs, sys: 0 ns, total: 869 µs
Wall time: 944 µs
ls -lh $proj_fn_lzw
-rw-rw-r-- 1 jovyan users 176M Feb 17 01:07 WA_COP90_utm_gdalwarp_lzw.tif
#Let's use the compressed file from here on out
#Should compare runtimes when using compressed vs. uncompressed
proj_fn = proj_fn_lzw
!gdalinfo $proj_fn
Driver: GTiff/GeoTIFF
Files: WA_COP90_utm_gdalwarp_lzw.tif
Size is 8877, 5851
Coordinate System is:
PROJCRS["WGS 84 / UTM zone 10N",
    BASEGEOGCRS["WGS 84",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4326]],
    CONVERSION["UTM zone 10N",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-123,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Between 126°W and 120°W, northern hemisphere between equator and 84°N, onshore and offshore. Canada - British Columbia (BC); Northwest Territories (NWT); Nunavut; Yukon. United States (USA) - Alaska (AK)."],
        BBOX[0,-126,84,-120]],
    ID["EPSG",32610]]
Data axis to CRS axis mapping: 1,2
Origin = (364685.490899736876599,5445638.415841511450708)
Pixel Size = (68.748471281554814,-68.748471281554814)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  COMPRESSION=LZW
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (  364685.491, 5445638.416) (124d51'20.11"W, 49d 8'55.14"N)
Lower Left  (  364685.491, 5043391.110) (124d43'58.58"W, 45d31'51.28"N)
Upper Right (  974965.670, 5445638.416) (116d30'24.81"W, 48d58'49.93"N)
Lower Right (  974965.670, 5043391.110) (116d55'58.87"W, 45d22'57.49"N)
Center      (  669825.581, 5244514.763) (120d45' 7.90"W, 47d19'55.38"N)
Band 1 Block=256x256 Type=Float32, ColorInterp=Gray
  NoData Value=0

Load your reprojected dataset and plot#

with rio.open(proj_fn) as src_proj:
    rio.plot.show(src_proj)
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_56_0.png

Reprojection using rasterio API#

def rio_reproj_write(src, proj_fn, dst_crs, driver='GTiff'):
    #Check to see if output filename already exists
    if os.path.exists(proj_fn):
        print("File exists: ", proj_fn)
    else:
        #Get the required transformation and output width and height
        transform, width, height = calculate_default_transform(src.crs, dst_crs, src.width, src.height, *src.bounds)
        #Create a copy of input metadata, then update with output values
        kwargs = src.meta.copy()
        print("Source metadata:\n", kwargs)
        kwargs.update({'crs':dst_crs,'transform':transform,'width':width,'height':height})
        kwargs['driver'] = driver
        #Set output creation options
        kwargs['compress'] = 'LZW'
        kwargs['tiled'] = True
        kwargs['BIGTIFF'] = 'IF_SAFER'
        print("Destination metadata:\n", kwargs)

        #Open the destination dataset in write mode
        with rio.open(proj_fn, 'w', **kwargs) as dst:
            print("Writing: ", proj_fn)
            #Loop through each input band
            for i in range(1, src.count + 1):
                #Reproject!
                reproject(source=rio.band(src, i), destination=rio.band(dst, i), src_transform=src.transform,\
                          src_crs=src.crs, dst_transform=transform, dst_crs=dst_crs, resampling=Resampling.cubic)
            print("Complete")
proj_fn = os.path.splitext(out_fn)[0]+'_utm_riowarp.tif'

with rio.open(out_fn) as src:
    rio_reproj_write(src, proj_fn, dst_crs)
File exists:  WA_COP90_utm_riowarp.tif
src_proj = rio.open(proj_fn)
src_proj.profile
{'driver': 'GTiff', 'dtype': 'float32', 'nodata': 0.0, 'width': 8877, 'height': 5851, 'count': 1, 'crs': CRS.from_epsg(32610), 'transform': Affine(68.74847128155481, 0.0, 364685.4908997369,
       0.0, -68.74847128155481, 5445638.415841511), 'blockxsize': 256, 'blockysize': 256, 'tiled': True, 'compress': 'lzw', 'interleave': 'band'}
src_proj.res
(68.74847128155481, 68.74847128155481)
#Load as a masked array
#dem_proj = src_proj.read(1, masked=True)
#dem_proj.shape

Discussion Questions#

src.bounds
BoundingBox(left=-124.73333333333332, bottom=45.543749999999996, right=-116.91583333333332, top=49.002916666666664)
src.res
(0.0008333333333333334, 0.0008333333333333334)
#Approximate length of a degree of latitude
mpd = 111000
src.res[0] * mpd
92.5
src.res[0] * mpd*np.cos(np.radians(src.bounds.top))
60.68190636208666
src.res[0] * mpd*np.cos(np.radians(src.bounds.bottom))
64.78371025007438
src_proj.res
(68.74847128155481, 68.74847128155481)

…resolution is computed with the intent that the length of the distance from the top left corner of the output imagery to the bottom right corner would represent the same number of pixels as in the source image. Note that if the image is somewhat rotated the diagonal taken isn’t of the whole output bounding rectangle, but instead of the locations where the top/left and bottom/right corners transform. The output pixel size is always square. This is intended to approximately preserve the resolution of the input data in the output file.

  • But you can also specify this output resolution (-tr argument in gdalwarp), if, for example, you wanted to resample your output to 180 m resolution.

Part 2: Create a shaded relief map (hillshade) from your projected DEM#

hs_fn = os.path.splitext(proj_fn)[0]+'_hs.tif'
%%time
if not os.path.exists(hs_fn):
    !gdaldem hillshade $proj_fn $hs_fn
CPU times: user 25 µs, sys: 9 µs, total: 34 µs
Wall time: 38.6 µs

Use rasterio to load your GDAL shaded relief map as a NumPy masked array#

  • Note that the datatype is 8-bit (Byte), with values from 0-255 (it’s a grayscale image, not elevation data)

  • Make sure you mask nodata values

hs_src = rio.open(hs_fn)
hs = hs_src.read(1, masked=True)
hs_src.profile
{'driver': 'GTiff', 'dtype': 'uint8', 'nodata': 0.0, 'width': 8877, 'height': 5851, 'count': 1, 'crs': CRS.from_epsg(32610), 'transform': Affine(68.74847128155481, 0.0, 364685.4908997369,
       0.0, -68.74847128155481, 5445638.415841511), 'blockysize': 1, 'tiled': False, 'interleave': 'band'}

Get the extent of the hillshade dataset in the projected coordinate system#

  • See rio.plot.plotting_extent

  • Pass this to imshow extent and verify that your coordinates look good

hs_extent = rio.plot.plotting_extent(hs_src)
hs_extent
(364685.4908997369, 974965.6704660989, 5043391.110373134, 5445638.415841511)
f, ax = plt.subplots()
ax.imshow(hs, cmap='gray', extent=hs_extent);
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_81_0.png

Example of hillshade using GDAL API#

def gdal_hs_ds(fn):
    from osgeo import gdal
    #Open the GDAL dataset
    dem_ds = gdal.Open(fn)
    #Specify the type of product we want (e.g., hillshade, slope, aspect) - see gdaldem doc for list of avaiable options
    producttype = 'hillshade'
    #Create a GDAL hillshade dataset in memory
    hs_ds = gdal.DEMProcessing('', dem_ds, 'hillshade', format='MEM')
    return hs_ds

def gdal_hs_ma(fn):
    #Get GDAL Dataset
    hs_ds = gdal_hs_ds(fn)
    #Read the dataset as a NumPy array
    hs = hs_ds.ReadAsArray()
    return hs
#Create hillshade from a filename
#hs_ma = gdal_hs_ma(fn)
#plt.imshow(hs_ma, cmap='gray')

Work in progress: dynamically create shaded relief, slope and aspect using numpy#

#Function to return hillshade (for visualization), slope magnitude (deg) and aspect on the fly
#This is a work in progress, aspect values are incorrect!
def hillshade(a, res=1.0, sun_az=315, sun_el=45):
    sun_az = 360.0 - sun_az
    dx, dy = np.gradient(a, res)
    slope = np.pi/2. - np.arctan(np.sqrt(dx*dx + dy*dy))
    aspect = np.arctan2(-dx, dy)
    shaded = np.sin(np.radians(sun_az)) * np.sin(slope) + \
    np.cos(np.radians(sun_el)) * np.cos(slope) * \
    np.cos((np.radians(sun_az) - np.pi/2.) - aspect)
    hs = 255 * (shaded + 1)/2
    return hs, np.degrees(np.pi/2. - slope), np.degrees(aspect)

Part 3: Clipping Raster data#

Load the states GeoDataFrame#

#states_url = 'http://eric.clst.org/assets/wiki/uploads/Stuff/gz_2010_us_040_00_5m.json'
states_url = 'http://eric.clst.org/assets/wiki/uploads/Stuff/gz_2010_us_040_00_500k.json'
states_gdf = gpd.read_file(states_url)
states_gdf.head()
GEO_ID STATE NAME LSAD CENSUSAREA geometry
0 0400000US23 23 Maine 30842.923 MULTIPOLYGON (((-67.61976 44.51975, -67.61541 ...
1 0400000US25 25 Massachusetts 7800.058 MULTIPOLYGON (((-70.83204 41.60650, -70.82373 ...
2 0400000US26 26 Michigan 56538.901 MULTIPOLYGON (((-88.68443 48.11579, -88.67563 ...
3 0400000US30 30 Montana 145545.801 POLYGON ((-104.05770 44.99743, -104.25015 44.9...
4 0400000US32 32 Nevada 109781.180 POLYGON ((-114.05060 37.00040, -114.04999 36.9...

Reproject the GeoDataFrame to match your DEM#

states_gdf_proj = states_gdf.to_crs(dst_crs)

Isolate the WA state geometry object#

  • We want the geometry, not a GeoDataFrame or GeoSeries

wa_state = states_gdf_proj.loc[states_gdf_proj['NAME'] == 'Washington']
wa_geom = wa_state.iloc[0].geometry
wa_geom
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_94_0.svg

Clip your projected DEM dataset (not the hillshade) to the WA state multipolygon#

src_proj
<open DatasetReader name='WA_COP90_utm_riowarp.tif' mode='r'>
#rio.mask.mask?
rio_mask_kwargs = {'filled':False, 'crop':True, 'indexes':1}
rio.mask.mask(src_proj, wa_geom.geoms, **rio_mask_kwargs)
(masked_array(
   data=[[--, --, --, ..., --, --, --],
         [--, --, --, ..., --, --, --],
         [--, --, --, ..., --, --, --],
         ...,
         [--, --, --, ..., --, --, --],
         [--, --, --, ..., --, --, --],
         [--, --, --, ..., --, --, --]],
   mask=[[ True,  True,  True, ...,  True,  True,  True],
         [ True,  True,  True, ...,  True,  True,  True],
         [ True,  True,  True, ...,  True,  True,  True],
         ...,
         [ True,  True,  True, ...,  True,  True,  True],
         [ True,  True,  True, ...,  True,  True,  True],
         [ True,  True,  True, ...,  True,  True,  True]],
   fill_value=0.0,
   dtype=float32),
 Affine(68.74847128155481, 0.0, 371079.0987289215,
        0.0, -68.74847128155481, 5444607.188772288))
src_proj.transform
Affine(68.74847128155481, 0.0, 364685.4908997369,
       0.0, -68.74847128155481, 5445638.415841511)
#No need to do read here, masked array directly loaded from disk
wa_ma, wa_ma_transform = rio.mask.mask(src_proj, wa_geom.geoms, **rio_mask_kwargs)
wa_ma.shape
(5834, 8728)
wa_ma_transform
Affine(68.74847128155481, 0.0, 371079.0987289215,
       0.0, -68.74847128155481, 5444607.188772288)
#Get clipped extent in UTM coordaintes
wa_ma_extent = rio.plot.plotting_extent(wa_ma, wa_ma_transform)
wa_ma_extent
(371079.0987289215, 971115.7560743319, 5043528.607315698, 5444607.188772288)
#Original unclipped extent for comparison
src_proj.bounds
BoundingBox(left=364685.4908997369, bottom=5043391.110373134, right=974965.6704660989, top=5445638.415841511)
f, axa = plt.subplots(1, 2, sharex=True, sharey=True)
axa[0].imshow(src_proj.read(1, masked=True), extent=wa_ma_extent)
wa_state.plot(ax=axa[0], facecolor='none', edgecolor='r')
axa[1].imshow(wa_ma, extent=wa_ma_extent);
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_106_0.png

Part 4: Comparing and differencing rasters#

  • Often have two rasters with different resolution, extent, projection

  • Want to combine, or use for some kind of raster math (e.g., subtract one from the other to compute difference)

  • Relatively new package, rioxarray makes this very easy

    • https://corteva.github.io/rioxarray/stable/index.html

    • More on this in Week09 and Week10

    • Built for NetCDF model, but wraps rasterio for working with rasters

    • Lazy evaluation, doesn’t read from disk util necessary

    • Built-in support for parallel operations using Dask

#Original SRTM DEM, still in EPSG:4326
srtm_fn
'WA_SRTMGL3.tif'
#Projected COP DEM, reprojected in EPSG:32610
proj_fn
'WA_COP90_utm_riowarp.tif'

Open using rioxarray#

rioxarray.open_rasterio(proj_fn, masked=True)
<xarray.DataArray (band: 1, y: 5851, x: 8877)>
[51939327 values with dtype=float32]
Coordinates:
  * band         (band) int64 1
  * x            (x) float64 3.647e+05 3.648e+05 ... 9.749e+05 9.749e+05
  * y            (y) float64 5.446e+06 5.446e+06 ... 5.043e+06 5.043e+06
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Area
    scale_factor:   1.0
    add_offset:     0.0
#Note: since nodata is set, we can use masked=True to set those values to nan
#Using squeeze here to reduce the band=1 dimension
cop_xds = rioxarray.open_rasterio(proj_fn, masked=True).squeeze()
cop_xds
<xarray.DataArray (y: 5851, x: 8877)>
[51939327 values with dtype=float32]
Coordinates:
    band         int64 1
  * x            (x) float64 3.647e+05 3.648e+05 ... 9.749e+05 9.749e+05
  * y            (y) float64 5.446e+06 5.446e+06 ... 5.043e+06 5.043e+06
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Area
    scale_factor:   1.0
    add_offset:     0.0

Access rasterio functions/attributes#

#cop_xds.rio.
cop_xds.rio.crs
CRS.from_epsg(32610)

Quick plotting using xarray#

#Default xarray plot uses the contourf method - can fill memory!
#cop_xds.plot()

xarray use RdBu and symmetrical color bar here when negative values detected#

cop_xds.plot.imshow();

f, ax = plt.subplots()
cop_xds.plot.imshow(cmap='inferno', robust=True, ax=ax)
ax.set_aspect('equal')
ax.set_title(proj_fn);
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_119_0.png

Clipping with rioxarray#

#One liner
cop_xds_wa = cop_xds.rio.clip(wa_state.geometry)
f, ax = plt.subplots()
cop_xds_wa.plot.imshow(cmap='inferno', robust=True, ax=ax)
ax.set_aspect('equal')
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_122_0.png
cop_xds_wa.max()
<xarray.DataArray ()>
array(4399.989, dtype=float32)
Coordinates:
    band         int64 1
    spatial_ref  int64 0

Open unprojected SRTM file#

srtm_xds = rioxarray.open_rasterio(srtm_fn, masked=True).squeeze()
srtm_xds
<xarray.DataArray (y: 4151, x: 9381)>
[38940531 values with dtype=float32]
Coordinates:
    band         int64 1
  * x            (x) float64 -124.7 -124.7 -124.7 ... -116.9 -116.9 -116.9
  * y            (y) float64 49.0 49.0 49.0 49.0 ... 45.55 45.55 45.55 45.54
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Area
    scale_factor:   1.0
    add_offset:     0.0
srtm_xds.rio.crs
CRS.from_epsg(4326)
f, ax = plt.subplots()
srtm_xds.plot.imshow(cmap='inferno', robust=True, ax=ax)
ax.set_aspect('equal')
ax.set_title(srtm_fn);
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_128_0.png
srtm_xds.max()
<xarray.DataArray ()>
array(4371., dtype=float32)
Coordinates:
    band         int64 1
    spatial_ref  int64 0
#Try to difference - no output, need to reproject
#cop_xds - srtm_xds

Reproject and match#

#Using cubic resampling (3)
srtm_xds_proj = srtm_xds.rio.reproject_match(cop_xds, resampling=3)
srtm_xds_proj
<xarray.DataArray (y: 5851, x: 8877)>
array([[nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan],
       ...,
       [nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan]], dtype=float32)
Coordinates:
  * x            (x) float64 3.647e+05 3.648e+05 ... 9.749e+05 9.749e+05
  * y            (y) float64 5.446e+06 5.446e+06 ... 5.043e+06 5.043e+06
    band         int64 1
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Area
    scale_factor:   1.0
    add_offset:     0.0
srtm_xds_proj.rio.crs
CRS.from_epsg(32610)
f, ax = plt.subplots()
srtm_xds_proj.plot.imshow(cmap='inferno', robust=True, ax=ax)
ax.set_aspect('equal')
ax.set_title(srtm_fn+' reproject_match');
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_135_0.png

Create a difference map#

  • Now that both rasters have identical projection, extent, and resolution, this is a simple subtraction

diff = cop_xds - srtm_xds_proj
f, ax = plt.subplots(dpi=150)
diff.plot.imshow(cmap='RdBu', robust=True, ax=ax)
ax.set_aspect('equal')
ax.set_title('COP90 minus SRTMGL3')
Text(0.5, 1.0, 'COP90 minus SRTMGL3')
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_138_1.png
f, ax = plt.subplots(dpi=150)
diff.plot.imshow(cmap='RdBu', robust=True, vmin=-5, vmax=5, ax=ax)
ax.set_aspect('equal')
ax.set_title('COP90 minus SRTMGL3')
Text(0.5, 1.0, 'COP90 minus SRTMGL3')
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_139_1.png
diff.median()
<xarray.DataArray ()>
array(1.4493103, dtype=float32)
Coordinates:
    band         int64 1
    spatial_ref  int64 0
diff.mean()
<xarray.DataArray ()>
array(1.1973335, dtype=float32)
Coordinates:
    band         int64 1
    spatial_ref  int64 0
diff.std()
<xarray.DataArray ()>
array(9.297225, dtype=float32)
Coordinates:
    band         int64 1
    spatial_ref  int64 0

Save output raster to disk#

diff_fn = 'WA_COP90_SRTMGL3_diff.tif'
diff.rio.to_raster(diff_fn)
!gdalinfo $diff_fn
Driver: GTiff/GeoTIFF
Files: WA_COP90_SRTMGL3_diff.tif
Size is 8877, 5851
Coordinate System is:
PROJCRS["WGS 84 / UTM zone 10N",
    BASEGEOGCRS["WGS 84",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4326]],
    CONVERSION["UTM zone 10N",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-123,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Between 126°W and 120°W, northern hemisphere between equator and 84°N, onshore and offshore. Canada - British Columbia (BC); Northwest Territories (NWT); Nunavut; Yukon. United States (USA) - Alaska (AK)."],
        BBOX[0,-126,84,-120]],
    ID["EPSG",32610]]
Data axis to CRS axis mapping: 1,2
Origin = (364685.490899736876599,5445638.415841511450708)
Pixel Size = (68.748471281554799,-68.748471281554743)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (  364685.491, 5445638.416) (124d51'20.11"W, 49d 8'55.14"N)
Lower Left  (  364685.491, 5043391.110) (124d43'58.58"W, 45d31'51.28"N)
Upper Right (  974965.670, 5445638.416) (116d30'24.81"W, 48d58'49.93"N)
Lower Right (  974965.670, 5043391.110) (116d55'58.87"W, 45d22'57.49"N)
Center      (  669825.581, 5244514.763) (120d45' 7.90"W, 47d19'55.38"N)
Band 1 Block=8877x1 Type=Float32, ColorInterp=Gray

Profile plot#

#y=5300000
y=5190000
f, ax = plt.subplots()
cop_xds_wa.plot.imshow(cmap='inferno', robust=True, ax=ax)
ax.set_aspect('equal')
ax.axhline(y)

f, axa = plt.subplots(2, 1)
cop_xds_wa.sel(y=y, method='nearest').plot(ax=axa[0], label='COP90')
srtm_xds_proj.sel(y=y, method='nearest').plot(ax=axa[0], label='SRTM_GL1')
axa[0].legend()
axa[0].set_ylabel('Elevation (m)')
diff.sel(y=y, method='nearest').plot(ax=axa[1])
axa[1].axhline(0, color='k')
axa[1].set_ylabel('Difference (m)')
plt.tight_layout()
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_148_0.png ../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_148_1.png

Using hvplot#

import hvplot.xarray
#Nice for interactive viewing, but results in huge notebook file size
#diff.hvplot(x='x', y='y', cmap='RdBu', clim=(-10,10), aspect='equal')

Part 5: Raster Filtering#

  • Applying digital filters is a common operation in image processing

  • Can be useful to filter outliers, reduce noise, find edges, etc.

  • Lots of useful kernel-based filters in scipy.ndimage: https://docs.scipy.org/doc/scipy/reference/ndimage.html

    • Note that many of these do not elegantly handle NaN or masked values!

Extract a portion of the DEM around Mt Rainier#

#From Raster1 lab
window_bounds = (581385.0, 5174595.0, 612105.0, 5205315.0)
print("Window bounds: ", window_bounds)
window_extent = [window_bounds[0], window_bounds[2], window_bounds[1], window_bounds[3]]
print("Window extent: ", window_extent)
Window bounds:  (581385.0, 5174595.0, 612105.0, 5205315.0)
Window extent:  [581385.0, 612105.0, 5174595.0, 5205315.0]
rainier_window = rio.windows.from_bounds(*window_bounds, transform=src_proj.transform)
rainier_window
Window(col_off=3152.0629486113885, row_off=3495.691051183996, width=446.846299668081, height=446.8462996680755)
rainier_ma = src_proj.read(1, window=rainier_window)
#rainier_ma = np.ma.masked_equal(rainier_ma, src_proj.nodata)
plt.imshow(rainier_ma)
<matplotlib.image.AxesImage at 0x7f552dfd4e80>
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_157_1.png
rainier_hs, slope, aspect = hillshade(rainier_ma, src_proj.res[0])
print(slope.max(), aspect.max())
66.14135 179.99565
f, axa = plt.subplots(1, 3, sharex=True, sharey=True, figsize=(15,4))
axa[0].imshow(rainier_hs, extent=window_extent, cmap='gray')
m = axa[0].imshow(rainier_ma, cmap='inferno', extent=window_extent, alpha=0.5)
#axa[1].imshow(hs)
f.colorbar(m, ax=axa[0])
axa[0].set_title('Color shaded relief')
m = axa[1].imshow(slope, extent=window_extent, vmax=50)
f.colorbar(m, ax=axa[1])
axa[1].set_title('Surface slope (deg)')
m = axa[2].imshow(aspect, cmap='twilight', extent=window_extent)
f.colorbar(m, ax=axa[2])
axa[2].set_title('Surface azimuth (deg from N, clockwise)')
plt.tight_layout()
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_159_0.png

Gaussian smoothing#

from scipy.ndimage import gaussian_filter
#This controls width of gaussian kernel standard deviation in pixels
#Play around with a few values
sigma = 3
rainier_ma_smooth = gaussian_filter(rainier_ma, sigma=sigma)
rainier_hs_smooth, slope, aspect = hillshade(rainier_ma_smooth, src_proj.res[0])
f, axa = plt.subplots(1,3, sharex=True, sharey=True, figsize=(15,4))
axa[0].imshow(rainier_hs, extent=window_extent, cmap='gray')
m = axa[0].imshow(rainier_ma, extent=window_extent, alpha=0.5)
f.colorbar(m, ax=axa[0])
axa[0].set_title("Original")
axa[1].imshow(rainier_hs_smooth, extent=window_extent, cmap='gray')
m = axa[1].imshow(rainier_ma_smooth, extent=window_extent, alpha=0.5)
f.colorbar(m, ax=axa[1])
axa[1].set_title("Gaussian filter (sigma=%s px)" % sigma)
m = axa[2].imshow(rainier_ma - rainier_ma_smooth, extent=window_extent, cmap='RdBu', vmin=-100, vmax=100)
f.colorbar(m, ax=axa[2])
axa[2].set_title("Difference (high-pass)")
plt.tight_layout()
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_162_0.png

Overviews and COGs#

#Landsat-8 image is a COG
tir_fn = '../../05_Raster1_GDAL_rasterio_LS8/LS8_sample/LC08_L2SP_046027_20181224_20200829_02_T1_ST_B10.TIF'
!gdalinfo $tir_fn
Driver: GTiff/GeoTIFF
Files: ../../05_Raster1_GDAL_rasterio_LS8/LS8_sample/LC08_L2SP_046027_20181224_20200829_02_T1_ST_B10.TIF
       ../../05_Raster1_GDAL_rasterio_LS8/LS8_sample/LC08_L2SP_046027_20181224_20200829_02_T1_ST_B10.TIF.aux.xml
Size is 7771, 7881
Coordinate System is:
PROJCRS["WGS 84 / UTM zone 10N",
    BASEGEOGCRS["WGS 84",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4326]],
    CONVERSION["UTM zone 10N",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-123,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Between 126°W and 120°W, northern hemisphere between equator and 84°N, onshore and offshore. Canada - British Columbia (BC); Northwest Territories (NWT); Nunavut; Yukon. United States (USA) - Alaska (AK)."],
        BBOX[0,-126,84,-120]],
    ID["EPSG",32610]]
Data axis to CRS axis mapping: 1,2
Origin = (472785.000000000000000,5373315.000000000000000)
Pixel Size = (30.000000000000000,-30.000000000000000)
Metadata:
  AREA_OR_POINT=Point
Image Structure Metadata:
  COMPRESSION=DEFLATE
  INTERLEAVE=BAND
Corner Coordinates:
Upper Left  (  472785.000, 5373315.000) (123d22' 6.59"W, 48d30'44.50"N)
Lower Left  (  472785.000, 5136885.000) (123d21'14.15"W, 46d23' 5.97"N)
Upper Right (  705915.000, 5373315.000) (120d12'48.76"W, 48d28'45.16"N)
Lower Right (  705915.000, 5136885.000) (120d19'24.73"W, 46d21'15.14"N)
Center      (  589350.000, 5255100.000) (121d48'53.62"W, 47d26'35.59"N)
Band 1 Block=256x256 Type=UInt16, ColorInterp=Gray
  Min=23752.000 Max=42219.000 
  Minimum=23752.000, Maximum=42219.000, Mean=35708.987, StdDev=2100.959
  NoData Value=0
  Overviews: 3886x3941, 1943x1971, 972x986, 486x493, 243x247, 122x124
  Metadata:
    STATISTICS_MAXIMUM=42219
    STATISTICS_MEAN=35708.986610756
    STATISTICS_MINIMUM=23752
    STATISTICS_STDDEV=2100.9585113563
    STATISTICS_VALID_PERCENT=66.34
tir = rioxarray.open_rasterio(tir_fn, overview_level=5, mask_and_scale=True).squeeze()
tir
<xarray.DataArray (y: 124, x: 122)>
[15128 values with dtype=float32]
Coordinates:
    band         int64 1
  * x            (x) float64 4.737e+05 4.757e+05 4.776e+05 ... 7.03e+05 7.05e+05
  * y            (y) float64 5.372e+06 5.37e+06 5.369e+06 ... 5.14e+06 5.138e+06
    spatial_ref  int64 0
Attributes:
    AREA_OR_POINT:  Point
tir.plot.imshow()
<matplotlib.image.AxesImage at 0x7f552dcbb3d0>
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_167_1.png

Add overviews to an existing GeoTiff#

sea_fn = 'SEA_COP30.tif'
#If your file doesn't include overviews
#!gdaladdo -r gauss $sea_fn
#But since we need to reproject, can specify output file format as COG, which will tile, compress and build overviews
sea_fn_proj = 'SEA_COP30_utm.tif'
if not os.path.exists(sea_fn_proj):
    !gdalwarp -of COG -r cubic -t_srs $dst_crs $sea_fn $sea_fn_proj
CPU times: user 326 µs, sys: 113 µs, total: 439 µs
Wall time: 650 µs
!gdalinfo $sea_fn_proj
Driver: GTiff/GeoTIFF
Files: SEA_COP30_utm.tif
Size is 829, 1406
Coordinate System is:
PROJCRS["WGS 84 / UTM zone 10N",
    BASEGEOGCRS["WGS 84",
        DATUM["World Geodetic System 1984",
            ELLIPSOID["WGS 84",6378137,298.257223563,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4326]],
    CONVERSION["UTM zone 10N",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-123,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Between 126°W and 120°W, northern hemisphere between equator and 84°N, onshore and offshore. Canada - British Columbia (BC); Northwest Territories (NWT); Nunavut; Yukon. United States (USA) - Alaska (AK)."],
        BBOX[0,-126,84,-120]],
    ID["EPSG",32610]]
Data axis to CRS axis mapping: 1,2
Origin = (541275.482176973484457,5293506.379035037010908)
Pixel Size = (27.060262602922780,-27.060262602922780)
Metadata:
  AREA_OR_POINT=Area
Image Structure Metadata:
  COMPRESSION=LZW
  INTERLEAVE=BAND
  LAYOUT=COG
Corner Coordinates:
Upper Left  (  541275.482, 5293506.379) (122d26'55.93"W, 47d47'36.93"N)
Lower Left  (  541275.482, 5255459.650) (122d27' 8.84"W, 47d27' 4.58"N)
Upper Right (  563708.440, 5293506.379) (122d 8'57.71"W, 47d47'30.35"N)
Lower Right (  563708.440, 5255459.650) (122d 9'17.63"W, 47d26'58.07"N)
Center      (  552491.961, 5274483.014) (122d18' 5.05"W, 47d37'17.84"N)
Band 1 Block=512x512 Type=Float32, ColorInterp=Gray
  Overviews: 415x703, 208x352, 104x177
hs_fn = os.path.splitext(sea_fn_proj)[0]+'_hs.tif'
!gdaldem hillshade $sea_fn_proj $hs_fn
0...10...20...30...40...50...60...70...80...90...100 - done.
sea_COP30 = rioxarray.open_rasterio(sea_fn_proj, mask_and_scale=True).squeeze()
sea_COP30_hs = rioxarray.open_rasterio(hs_fn, mask_and_scale=True).squeeze()
f, ax = plt.subplots(dpi=220)
sea_COP30_hs.plot.imshow(ax=ax, cmap='gray', robust=True, add_colorbar=False)
sea_COP30.plot.imshow(ax=ax, cmap='viridis', robust=True, alpha=0.5)
ax.set_aspect('equal')
ax.set_title('Copernicus 30 m DEM');
../../_images/07_Raster2_DEMs_Warp_Clip_Sample_demo_176_0.png

Some other newer packages worth exploring#

Work in Progress: xarray-spatial#

import xrspatial
#Run the hillshade operation
#hs = xrspatial.hillshade(cop_xds, azimuth=315, angle_altitude=45)
#Note: there are some coordinate scaling and nodata issues here
#hs.plot.imshow(cmap='gray');

xdem#