Improve resolution of Cartopy map

Viewed 3458

The following code generates a map of the US with a contour line for every state. The problem is that the image looks like it was generated with 1990s technology. How can I significantly improve the quality of the figure, i.e. increase the resolution of the background?

import cartopy.crs as ccrs
import cartopy.io.shapereader as shpreader

import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 400

fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.LambertConformal())
ax.set_extent([-125, -66.5, 20, 50], ccrs.Geodetic())
ax.stock_img()


shapename = 'admin_1_states_provinces_lakes_shp'
states_shp = shpreader.natural_earth(resolution='110m', category='cultural',
                                     name=shapename)

ax.outline_patch.set_visible(False)  # don't draw the map border
ax.set_title('My map of the lower 48')

# example state coloring
colors = {
    'Minnesota': [0, 1, 0],
    'Texas': "#FF0000",
    'Montana': "blue",
}
default_color = [0.9375, 0.9375, 0.859375]

for state in shpreader.Reader(states_shp).records():
    facecolor = colors.get(state.attributes['name'], default_color)
    ax.add_geometries([state.geometry], ccrs.PlateCarree(),
                      facecolor=facecolor, edgecolor='black', alpha=0.5,linewidth=0.1)

# example data
df = pd.DataFrame(columns=['city', 'lat', 'lon'], data=[
    ('Hoboken', 40.745255, -74.034775),
    ('Port Hueneme', 34.155834, -119.202789),
    ('Auburn', 42.933334, -76.566666),
    ('Jamestown', 42.095554, -79.238609),
    ('Fulton', 38.846668, -91.948059),
    ('Bedford', 41.392502, -81.534447)
])

ax.plot(df['lon'], df['lat'], transform=ccrs.PlateCarree(),
        ms=8, ls='', marker='*')
#plt.show()
plt.savefig("usa.png")
plt.close()

enter image description here

1 Answers

The low quality image is caused by the stock image (ax.stock_img()) which for reasons unknown to me of very poor quality. I found the following solutions:

(1) Don't use any background, but add a number of features:

fig = plt.figure()
ax = plt.axes(projection=cartopy.crs.PlateCarree())
ax.add_feature(cartopy.feature.LAND)
ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.COASTLINE,linewidth=0.3)
ax.add_feature(cartopy.feature.BORDERS, linestyle=':',linewidth=0.3)
ax.add_feature(cartopy.feature.LAKES, alpha=0.5)
ax.add_feature(cartopy.feature.RIVERS)
ax.set_extent([-125, -66.5, 20, 50])

Another example for this solution can be found here

Simple map using features

(2) Use a custom background image. This solution requires downloading a basemap background image such as Blue Marble or ETOPO Global Relief. As per these instructions, once downloaded, you must set a global environment variable: CARTOPY_USER_BACKGROUNDS=path_to_map_folder which points to the folder where you stored the downloaded maps. In addition, you must create a JSON file called images.json and store it in the same folder. Example contents of the JSON file:

{"__comment__": "JSON file specifying the image to use for a given type/name and resolution. Read in by cartopy.mpl.geoaxes.read_user_background_images.",
  "BM": {
    "__comment__": "Blue Marble Next Generation, July ",
    "__source__": "https://neo.sci.gsfc.nasa.gov/view.php?datasetId=BlueMarbleNG-TB",
    "__projection__": "PlateCarree",
    "low": "bluemarble1degrees.jpeg",
    "high": "bluemarble01degrees.jpeg"},
  "ETOPO": {
    "__comment__": "ETOPO",
    "__source__": "https://www.ngdc.noaa.gov/mgg/global/global.html",
    "__projection__": "PlateCarree",
    "high": "color_etopo1_ice_low.jpg"}
}

Finally, one can use the following code to add the desired background:

fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.PlateCarree())
ax.set_extent([-125, -66.5, 20, 50], ccrs.Geodetic())
ax.background_img(name='ETOPO', resolution='high')

The above code would use the ETOPO background. To use the Blue Marble background, change the last line to: ax.background_img(name='BM', resolution='high'). Note that the name and resolution parameters must match the entries in the JSON file.

ETOPO: Etopo map BLue Marble: Blue marble map

Finally, as per this Kaggle post, it must also be possible to load the basemap as a image background directly. This would be more convenient as it bypasses the step of setting an Environment variable and writing a JSON file:

fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.PlateCarree())
ax.set_extent([-125, -66.5, 20, 50], ccrs.Geodetic())
img = plt.imread('./bluemarble01degrees.jpeg')
img_extent = (-125, -66.5, 20, 50)
ax.imshow(img, origin='upper', extent=img_extent, transform=ccrs.PlateCarree())

Unfortunately I did not get the above solution to work, as it does not zoom in on the relevant section of the map:

enter image description here

Related