Mapping Elevation Data in Python’s Matplotlib

Learn how to add beautiful 3D terrain to your plots using accurate earth elevation data with this step-by-step guide.

Northern Africa’s matplotlib hight resolution render

STEP 1 : Get the elevation data

In order to plot the 3D terrain — commonly refered to as “elevation” in the geographers’ field — we first need to acquire this data from a trusted source. Many websites can give you such information, with various degrees of accuracy, and among them is the United State’s Geological Survey’s Earth Explorer, which will perfectly match our needs in this case.

USGS’s Earth Explorer Website:

USGS’s Earth Explorer Website

In order to find the data of interest, go to the Data Sets section in the left hand-side menu, and select the GTOPO30 option. On a side note, this elevation map is not the most precise, and covers the globe using large tiles, which do not provide good data for very small scales. However, they are largely sufficient for most usecases, as we will resize and crop the image data as we like.

Dataset selection:

Dataset selection

Next, find the elevation data picture that encompasses the land you would like to map. Note that the tiles are named after the latitude and longitude they cover. For this exemple, we will use the North-West African Tile (GT30W020N40) as to get both mountains and oceans to make this look interesting.

North-West Africa Tile, GT30W020N40:

North-West Africa Tile, GT30W020N40

STEP 2 : Get the python script ready

We first import all the necessary packages : PIL.Image to load and resize/crop the picture, Numpy to deal with the picture’s data and a subset of matplotlib’s libraries to plot nicely the result.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
from matplotlib import cm
from PIL import Image

Then we’ll add some lines to get a custom color map from the “terrain” default map, that will differentiate between land (elevation above 1m) and water (yes, you guessed it, below 1m). In a few words, this piece of code firstly loads the “terrain” color map (you can find more matplotlib color maps here), samples it from 20% of the spectrum to the end (to cut off the blue color and save it for the ocean), and finally adds the blue RGB values at the beginning of the spectrum, before declaring it as a new usable color map.

terrain = cm.get_cmap("terrain").resampled(256)
newcolors = terrain(np.linspace(0.2, 1, 256))
newcolors[:1, :] = np.array([39/256, 30/256, 100/256, 1])
newcmp = ListedColormap(newcolors)

STEP 3: Make the plotting function

In order to be able to plot from any data picture, we’ll define a function map. In the first lines of the function, we take the image from the file path input (always make sure the path has the backslashes “\” escaped as “\”) and we crop it to one interesting part (note that 1° of latitude or longitude is represented by 120 pixels in the picture, which is why i use multiples of 120).

Then, we use some numpy magic to define the plotting grid, using our reduction parameter : set to 1, you get full details (might crash or slow down your computer noticeably for big areas) — set to 10, you divide by 10 the number of sample points in your picture, and so on for bigger values. One may also decrease the detail level of the plot by increasing the rstride and cstride values in the plot_surface function. The exageration input is used to increase the height of the surface’s features for a more dramatic effect.

def map(path,reduction=10,exageration=4):
    img = Image.open(path) #MAKE SURE THE "\" ARE ESCAPED AS "\\" IN THE PATH !
    img = img.crop((10*120,5*120,15*120,10*120)) #CROP TO YOUR DESIRED SPOT HERE !
    x,y = img.size
    img = np.flipud(exageration*np.asarray(img.resize((int(x/reduction),int(y/reduction))),dtype=int))
    X,Y = np.meshgrid(np.linspace(0,int(x/reduction),int(x/reduction)),np.linspace(0,int(y/reduction),int(y/reduction)))
    ax = plt.figure().add_subplot(projection='3d')
    ax.set_zlim([0,5000])
    ax.plot_surface(X,Y,img,rstride=1,cstride=1,alpha=1,cmap=newcmp,antialiased=False)
    plt.show()

STEP 4: Enjoy

Once our function is ready, we can now get a clean elevation plot in two lines of code:

file_path = "C:\\Users\\Username\\Pictures\\gt30w020n40.jpg"

map(file_path,reduction=15,exageration=2)

Here are a few results you can get from this setup, varying the inputs :

1. Varying the localisation (cropping)

Different croping values, exageration=4 & reduction=10:

2. Varying the reduction

Different reduction values (100;30;10), exageration=4:

3. Varying the exageration

Different exageration values (1;4;10):

Here’s the full code in case you need a quick copy/paste:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
from matplotlib import cm
from PIL import Image

terrain = cm.get_cmap("terrain").resampled(256)
newcolors = terrain(np.linspace(0.2, 1, 256))
newcolors[:1, :] = np.array([39/256, 30/256, 100/256, 1])
newcmp = ListedColormap(newcolors)

def map(path,reduction=10,exageration=4):
    img = Image.open(path) #MAKE SURE THE "\" ARE ESCAPED AS "\\" IN THE PATH !
    img = img.crop((10*120,5*120,15*120,10*120)) #CROP TO YOUR DESIRED SPOT HERE !
    x,y = img.size
    img = np.flipud(exageration*np.asarray(img.resize((int(x/reduction),int(y/reduction))),dtype=int))
    X,Y = np.meshgrid(np.linspace(0,int(x/reduction),int(x/reduction)),np.linspace(0,int(y/reduction),int(y/reduction)))
    ax = plt.figure().add_subplot(projection='3d')
    ax.set_zlim([0,5000])
    ax.plot_surface(X, Y,img,rstride=1, cstride=1,alpha=1,cmap=newcmp,antialiased=False)
    plt.show()

file_path = "C:\\Users\\Username\\Pictures\\gt30w020n40.jpg"

map(file_path,reduction=15,exageration=2)

Thank you for reading!

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics