Creating High Resolution Satellite Images with Mapbox and Python
By: Kyle Pastor
Tags: Programming, Visualization, Python, Code, Data Science

For some of my personal projects I wanted to get super high resolution satellite and elevation imagery for specific points on Earth. When I tried to Google search for some existing data there wasn't anything flexible enough. I finally stumbled upon the Mapbox API which allows me to use URL requests to get images back for any location and zoom level. On top of this it also has an option to return high resolution elevation data encoded as PNG images (see second image in the above).

Getting a Mapbox API Key

The first step in this is to visit https://docs.mapbox.com/api/ to signup and get an API key. I will not go into detail here as it is super easy but all we want is the key which should look something like this:

Once we have the key it will allow us to parse the Mapbox API and get some nice image tilesets back! Keep in mind that there is a lot of free access so you do not need to worry about API limits unless you are deploying this to a wider audience.

A Note on Tilesets

One if the biggest issues I had was understanding tilesets. Basically I assumed I could use the Latitude/Longitude values in the API, however the way global imaging works is by having a set of “tilesets” per zoom level. The zoom level is from 0–15 where 0 is just a picture of all the world and 15 lets you see your own house. The tilesets are index as a matrix (x/y) which basically represent images of the globe sliced up. At zoom 0 there is only 1 tile at x=y=1 since it is just one big image. Imagine we zoomed to level 1 and it actually takes 4 images to cover the map. This means we would have the four combinations (0,0),(0,1),(1,0),(1,1). As you can image the deeper the zoom the more tiles needed.

I will show you the code needed to get a set of tiles for a given lat/lng combination so you don't have to worry about it! Classic python having a package for everything.

Installing Python Packages and Folder Setup

As is tradition we have to install the non standard packages. To figure out the translation between lat/lng to tileset we use the mercantile package (pretty sure a portmanteau of Mercator and tile). We will also install pillow which is a widely used image processing library for python.

pip install mercantile

pip install pillow

We will also set up some directories to store temporary images for both satellite and elevation imagery.

mkdir ./satellite_images

mkdir ./elevation_images

mkdir ./composite_images

The composite image directory is where our final images will go.

Converting From Lat/Lng to Tilesets

The first thing to do is determine what location we want to scan. I live near Toronto so I wanted to see what the waterfront looks like in super high resolution! I set up the coordinates (just Google the location and right click on the map and select “Whats Here?” to get the coordinates)

lat_lng = [43.640918, -79.371478]

Next I need to decide on the scan range I would like to view. I represent this by a lat/lng delta factor and apply it to find the top left and bottom right selection of the area I am interested in.

delta=0.05

tl = [lat_lng[0]+delta, lat_lng[1]-delta]

br = [lat_lng[0]-delta, lat_lng[1]+delta]

z = 15 # Set the resolution (max at 15)

Pulling the Images

The next step is to pull the image data from the specific API URLs. Mapbox has two endpoints that we are interested in. One gives high detail satellite imagery and the other gives an encoded elevation mapping. The endpoints require the same inputs so I will summarise below:

Mapbox API URLs

https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}@2x.pngraw?access_token={Your Key}

https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.pngraw?access_token={Your Key}

mapbox.terrain-rgb : This is the endpoint for the elevation data

This is the endpoint for the elevation data mapbox. satellite : This is the endpoint for the satellite data

This is the endpoint for the satellite data {z}: This is the zoom level we want to select (0–15)

This is the zoom level we want to select (0–15) {x}: The x tile we are requesting at the given zoom level

The x tile we are requesting at the given zoom level {y}: The y tile we are requesting at the given zoom level

The y tile we are requesting at the given zoom level {Your Key}: The API key you requested when you registered with Mapbox API (Looks like pk.kjsalfjaiei9789f7ah …)

The API key you requested when you registered with Mapbox API (Looks like …) @2x.pngraw: This says that we want the images as raw PNG files at double resolution (512x512)

If you just go into your browser and navigate to the URL with a set of valid inputs you should see a nice image returned. This is what we are going to automate with python.

First we need to collect our array of images using the x/y/z we defined in the previous sections.

import requests # The requests package allows use to call URLS

import shutil # shutil will be used to copy the image to the local # Loop over the tile ranges

for i,x in enumerate(range(x_tile_range[0],x_tile_range[1]+1)):

for j,y in enumerate(range(y_tile_range[0],y_tile_range[1]+1)) # Call the URL to get the image back

r = requests.get('https://api.mapbox.com/v4/mapbox.terrain-

rgb/'+str(z)+'/'+str(x)+'/'+str(y)+'

access_token=pk.eyJ1I....', stream=True) r = requests.get('https://api.mapbox.com/v4/mapbox.terrain-rgb/'+str(z)+'/'+str(x)+'/'+str(y)+' @2x .pngraw?access_token=pk.eyJ1I....', stream=True) # Next we will write the raw content to an image

with open(‘./elevation_images/’ + str(i) + ‘.’ + str(j) + ‘.png’,

‘wb’) as f:

r.raw.decode_content = True

shutil.copyfileobj(r.raw, f)

# Do the same for the satellite data

r =requests.get('https://api.mapbox.com/v4/mapbox.satellite/'+

str(z)+'/'+str(x)+'/'+str(y)+'

access_token=pk.eyJ1I....', stream=True) r =requests.get('https://api.mapbox.com/v4/mapbox.satellite/'+str(z)+'/'+str(x)+'/'+str(y)+' @2x .pngraw?access_token=pk.eyJ1I....', stream=True) with open(‘./satellite_images/’ + str(i) + ‘.’ + str(j) + ‘.png’,

‘wb’) as f:

r.raw.decode_content = True

shutil.copyfileobj(r.raw, f)

At this point we have a set of images that are within their folders and are named in the x.y.png format, which will make it easier to loop over later when we compose our final images.

Composing the Final Image

We are almost at the end of our little project. What we want to do now is compose all of the smaller images into a very large image of crazy resolution. To do this we are going to make a new png using Pillow (python package).

First we make a new blank image set to the final size we want. Then we will just be pasting in the smaller images with offsets that make sense. Below is a GIF showing visually what the code does.

Animation showing how the individual images are composed.

# Import the image, math and os libraries

import PIL

import mathfrom os import listdir

from os.path import isfile, join # Loop over the elevation and satellite image set

for img_name in [‘elevation’,’satellite’]: # Make a list of the image names

image_files = [‘./’+img_name+’_images/’ + f for f in

listdir(‘./’+img_name+’_images/’)] # Open the image set using pillow

images = [PIL.Image.open(x) for x in image_files] # Calculate the number of image tiles in each direction

edge_length_x = x_tile_range[1] — x_tile_range[0]

edge_length_y = y_tile_range[1] — y_tile_range[0]

edge_length_x = max(1,edge_length_x)

edge_length_y = max(1,edge_length_y) # Find the final composed image dimensions

width, height = images[0].size

total_width = width*edge_length_x

total_height = height*edge_length_y # Create a new blank image we will fill in

composite = PIL.Image.new(‘RGB’, (total_width, total_height)) # Loop over the x and y ranges

y_offset = 0

for i in range(0,edge_length_x):

x_offset = 0

for j in range(0,edge_length_y): # Open up the image file and paste it into the composed

image at the given offset position

tmp_img = PIL.Image.open(‘./’+img_name+’_images/’ + str(i) +

‘.’ + str(j) + ‘.png’)

composite.paste(tmp_img, (y_offset,x_offset))

x_offset += width # Update the width y_offset += height # Update the height # Save the final image

composite.save(‘./composite_images/’+img_name+’.png’)

Voila. You should now have two images that are very very large and of amazing resolution. To give you a sense of the scale look at the image below which shows a zoomed in version of the GIF from above.

A zoom in of the above image showing the amazing resolution we have (Image is around 50Mb).

Man that looks crisp! For bonus points below is the GIF of the elevation data.

Animation for the composition of the elevation data.

At this point you can do what you need with your final image. This whole thing has two benefits.

The images are super resolution You can easily choose any rectangle of a map you are interested in viewing

For my own projects I really wanted the decoded elevation data. Mapbox actually tells you how to decode the pixels to get the elevation down to 0.1 meter divisions but I just put together a python script below.

Decoding the Elevation Data

# Load the elevation image and convert it to RGBA

elevation_raw = PIL.Image.open(‘./composite_images/elevation.png’)

rgb_elevation = elevation_raw.convert(‘RGBA’) # Loop over the image and save the data in a list

elevation_data = []

for h in range(rgb_elevation.height):

for w in range(rgb_elevation.width): # Extract the pixel values

R, G, B, A = rgb_elevation.getpixel((w, h)) # Use Mapbox conversion from pixel to meters

height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1) # Append to our elevation data list

elevation_data.append(height) # Save to a json file

import json

with open(‘./elevation.json’, ‘w’) as outfile:

json.dump(elevation_data, outfile)

All done! Now you can take this to perform analysis or slicing on the raw image. (you can also make contour lines and visualize in 3D or a whole bunch of things!)

References and Links