Replacing Cartopy’s Background Image

metadata

Whilst discussing a friend’s summary of 2017 I found it difficult to place parts of Michigan that I had visited as The Great Lakes were missing from the state boundaries. My friend then countered that my own maps did not feature The Great Lakes either. Disbelieving, I went away and checked; he was correct - Cartopy’s default background image does not show The Great Lakes (shown below).

Download:
  1. 512 px × 256 px (0.1 Mpx; 171.7 KiB)
  2. 720 px × 360 px (0.3 Mpx; 320.5 KiB)

If you would like to check for yourself then the following Python code will show you where the default image is located for your installation of Cartopy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

#!/usr/bin/env python3

# Use the proper idiom in the main module ...
# NOTE: See https://docs.python.org/3.11/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
if __name__ == "__main__":
    # Import standard modules ...
    import os

    # Import special modules ...
    try:
        import cartopy
        cartopy.config.update(
            {
                "cache_dir" : os.path.expanduser("~/.local/share/cartopy_cache"),
            }
        )
    except:
        raise Exception("\"cartopy\" is not installed; run \"pip install --user Cartopy\"") from None

    # Print path ...
    print(f"{cartopy.__path__[0]}/data/raster/natural_earth")

              
You may also download “replacing-cartopy-background-image-location.py” directly or view “replacing-cartopy-background-image-location.py” on GitHub Gist (you may need to manually checkout the “main” branch).

As of January 2018, that folder only contains one image, called 50-natural-earth-1-downsampled.png, which has dimensions 720 px × 360 px and size 327 KiB. The sidecar JSON file (shown below) describes the contents of the folder.

1
2
3
4
5
6
7
8
9

{
    "__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.", 
    "ne_shaded": {
        "__comment__": "Natural Earth shaded relief", 
        "__projection__": "PlateCarree", 
        "__source__": "https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-1/", 
        "low": "50-natural-earth-1-downsampled.png"
    }
}

              
You may also download “replacing-cartopy-background-image-images1.json” directly or view “replacing-cartopy-background-image-images1.json” on GitHub Gist (you may need to manually checkout the “main” branch).

I decided to create my own folder of background images, using the same specification that Cartopy uses, so that I could have both higher resolution background images and ones that contained large inland bodies of water. The script that I wrote to download the ZIP archives from Natural Earth, extract the TIFFs, create the PNGs and create the JSON is shown below. It runs both convert and optipng during its execution.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

#!/usr/bin/env python3

# Use the proper idiom in the main module ...
# NOTE: See https://docs.python.org/3.11/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
if __name__ == "__main__":
    # Import standard modules ...
    import json
    import os
    import tempfile
    import zipfile

    # Import my modules ...
    try:
        import pyguymer3
        import pyguymer3.image
    except:
        raise Exception("\"pyguymer3\" is not installed; you need to have the Python module from https://github.com/Guymer/PyGuymer3 located somewhere in your $PYTHONPATH") from None

    # Define the background image datasets ...
    imgs = {
        "cross-blend-hypso" : {
            "description" : "Cross-blended Hypsometric Tints with Relief, Water, Drains and Ocean Bottom from Natural Earth",
            "rasters" : {
                "large"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/HYP_HR_SR_OB_DR.zip",
                "medium" : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/HYP_LR_SR_OB_DR.zip",
                "small"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/HYP_50M_SR_W.zip",
            },
            "source" : "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-cross-blend-hypso/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-cross-blend-hypso/",
        },
        "gray-earth" : {
            "description" : "Gray Earth with Shaded Relief, Hypsography, Ocean Bottom and Drainages from Natural Earth",
            "rasters" : {
                "large"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/GRAY_HR_SR_OB_DR.zip",
                "medium" : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/GRAY_LR_SR_OB_DR.zip",
                "small"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/GRAY_50M_SR_OB.zip",
            },
            "source" : "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-gray-earth/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-gray-earth/",
        },
        "natural-earth-1" : {
            "description" : "Natural Earth I with Shaded Relief, Water and Drainages from Natural Earth",
            "rasters" : {
                "large"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/NE1_HR_LC_SR_W_DR.zip",
                "medium" : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/NE1_LR_LC_SR_W_DR.zip",
                "small"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/NE1_50M_SR_W.zip",
            },
            "source" : "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-natural-earth-1/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-1/",
        },
        "natural-earth-2" : {
            "description" : "Natural Earth II with Shaded Relief, Water and Drainages from Natural Earth",
            "rasters" : {
                "large"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/NE2_HR_LC_SR_W_DR.zip",
                "medium" : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/NE2_LR_LC_SR_W_DR.zip",
                "small"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/NE2_50M_SR_W.zip",
            },
            "source" : "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-natural-earth-2/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-2/",
        },
        "shaded-relief" : {
            "description" : "Shaded Relief Basic from Natural Earth",
            "rasters" : {
                "large"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/SR_HR.zip",
                "medium" : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/raster/SR_LR.zip",
                "small"  : "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/SR_50M.zip",
            },
            "source" : "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-shaded-relief/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-shaded-relief/",
        },
    }

    # Create JSON dictionary ...
    data = {}
    data["__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."

    # **************************************************************************
    # *                 CREATE PNG IMAGES FROM REMOTE SOURCES                  *
    # **************************************************************************

    # Start session ...
    with pyguymer3.start_session() as sess:
        # Create temporary directory ...
        with tempfile.TemporaryDirectory(prefix = "replacing-cartopy-background-image-process.") as tname:
            # Loop over background image datasets ...
            for img, info in imgs.items():
                # Add to JSON dictionary ...
                data[img] = {}
                data[img]["__comment__"] = info["description"]
                data[img]["__projection__"] = "PlateCarree"
                data[img]["__source__"] = info["source"]

                # Loop over sizes ...
                for size in info["rasters"]:
                    # Deduce ZIP file name and download it if it is missing ...
                    zfile = f"{img}_{size}.zip"
                    if not os.path.exists(zfile):
                        print(f"Downloading \"{zfile}\" ...")
                        if not pyguymer3.download_file(sess, info["rasters"][size], zfile):
                            raise Exception("download failed", info["rasters"][size]) from None

                    # Deduce PNG file name ...
                    pfile = f"{img}_{size}.png"

                    # Add to JSON dictionary ...
                    data[img][size] = pfile

                    # Skip if PNG already exists ...
                    if os.path.exists(pfile):
                        continue

                    print(f"Creating \"{pfile}\" ...")

                    # Extract TIF to a temporary folder ...
                    with zipfile.ZipFile(zfile, "r") as zobj:
                        for member in zobj.namelist():
                            if member.lower().endswith(".tif"):
                                tfile = zobj.extract(member, tname)
                                break

                    # Convert TIF to PNG ...
                    pyguymer3.image.image2png(tfile, pfile, strip = True)

    # **************************************************************************
    # *            CREATE DOWNSCALED PNG IMAGES FROM LOCAL SOURCES             *
    # **************************************************************************

    # Loop over background image datasets ...
    for img, info in imgs.items():
        # Loop over sizes ...
        for size in info["rasters"]:
            # Deduce PNG file name and skip if it is missing ...
            pfile1 = f"{img}_{size}.png"
            if not os.path.exists(pfile1):
                continue

            # Loop over downscaled sizes ...
            for width in [512, 1024, 2048, 4096, 8192]:
                # Deduce downscaled PNG file name ...
                pfile2 = f"{img}_{size}{width:04d}px.png"

                # Add to JSON dictionary ...
                data[img][f"{size}{width:04d}px"] = pfile2

                # Skip if downscaled PNG already exists ...
                if os.path.exists(pfile2):
                    continue

                print(f"Creating \"{pfile2}\" ...")

                # Convert PNG to downscaled PNG ...
                pyguymer3.image.image2png(pfile1, pfile2, screenHeight = width, screenWidth = width, strip = True)

    # Save JSON dictionary ...
    with open("images.json", "wt", encoding = "utf-8") as fObj:
        json.dump(
            data,
            fObj,
            ensure_ascii = False,
                  indent = 4,
               sort_keys = True,
        )

              
You may also download “replacing-cartopy-background-image-process.py” directly or view “replacing-cartopy-background-image-process.py” on GitHub Gist (you may need to manually checkout the “main” branch).

The JSON file that it creates is shown below for comparison with the original one.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103

{
    "__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.",
    "cross-blend-hypso": {
        "__comment__": "Cross-blended Hypsometric Tints with Relief, Water, Drains and Ocean Bottom from Natural Earth",
        "__projection__": "PlateCarree",
        "__source__": "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-cross-blend-hypso/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-cross-blend-hypso/",
        "large": "cross-blend-hypso_large.png",
        "large0512px": "cross-blend-hypso_large0512px.png",
        "large1024px": "cross-blend-hypso_large1024px.png",
        "large2048px": "cross-blend-hypso_large2048px.png",
        "large4096px": "cross-blend-hypso_large4096px.png",
        "medium": "cross-blend-hypso_medium.png",
        "medium0512px": "cross-blend-hypso_medium0512px.png",
        "medium1024px": "cross-blend-hypso_medium1024px.png",
        "medium2048px": "cross-blend-hypso_medium2048px.png",
        "medium4096px": "cross-blend-hypso_medium4096px.png",
        "small": "cross-blend-hypso_small.png",
        "small0512px": "cross-blend-hypso_small0512px.png",
        "small1024px": "cross-blend-hypso_small1024px.png",
        "small2048px": "cross-blend-hypso_small2048px.png",
        "small4096px": "cross-blend-hypso_small4096px.png"
    },
    "gray-earth": {
        "__comment__": "Gray Earth with Shaded Relief, Hypsography, Ocean Bottom and Drainages from Natural Earth",
        "__projection__": "PlateCarree",
        "__source__": "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-gray-earth/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-gray-earth/",
        "large": "gray-earth_large.png",
        "large0512px": "gray-earth_large0512px.png",
        "large1024px": "gray-earth_large1024px.png",
        "large2048px": "gray-earth_large2048px.png",
        "large4096px": "gray-earth_large4096px.png",
        "medium": "gray-earth_medium.png",
        "medium0512px": "gray-earth_medium0512px.png",
        "medium1024px": "gray-earth_medium1024px.png",
        "medium2048px": "gray-earth_medium2048px.png",
        "medium4096px": "gray-earth_medium4096px.png",
        "small": "gray-earth_small.png",
        "small0512px": "gray-earth_small0512px.png",
        "small1024px": "gray-earth_small1024px.png",
        "small2048px": "gray-earth_small2048px.png",
        "small4096px": "gray-earth_small4096px.png"
    },
    "natural-earth-1": {
        "__comment__": "Natural Earth I with Shaded Relief, Water and Drainages from Natural Earth",
        "__projection__": "PlateCarree",
        "__source__": "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-natural-earth-1/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-1/",
        "large": "natural-earth-1_large.png",
        "large0512px": "natural-earth-1_large0512px.png",
        "large1024px": "natural-earth-1_large1024px.png",
        "large2048px": "natural-earth-1_large2048px.png",
        "large4096px": "natural-earth-1_large4096px.png",
        "medium": "natural-earth-1_medium.png",
        "medium0512px": "natural-earth-1_medium0512px.png",
        "medium1024px": "natural-earth-1_medium1024px.png",
        "medium2048px": "natural-earth-1_medium2048px.png",
        "medium4096px": "natural-earth-1_medium4096px.png",
        "small": "natural-earth-1_small.png",
        "small0512px": "natural-earth-1_small0512px.png",
        "small1024px": "natural-earth-1_small1024px.png",
        "small2048px": "natural-earth-1_small2048px.png",
        "small4096px": "natural-earth-1_small4096px.png"
    },
    "natural-earth-2": {
        "__comment__": "Natural Earth II with Shaded Relief, Water and Drainages from Natural Earth",
        "__projection__": "PlateCarree",
        "__source__": "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-natural-earth-2/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-natural-earth-2/",
        "large": "natural-earth-2_large.png",
        "large0512px": "natural-earth-2_large0512px.png",
        "large1024px": "natural-earth-2_large1024px.png",
        "large2048px": "natural-earth-2_large2048px.png",
        "large4096px": "natural-earth-2_large4096px.png",
        "medium": "natural-earth-2_medium.png",
        "medium0512px": "natural-earth-2_medium0512px.png",
        "medium1024px": "natural-earth-2_medium1024px.png",
        "medium2048px": "natural-earth-2_medium2048px.png",
        "medium4096px": "natural-earth-2_medium4096px.png",
        "small": "natural-earth-2_small.png",
        "small0512px": "natural-earth-2_small0512px.png",
        "small1024px": "natural-earth-2_small1024px.png",
        "small2048px": "natural-earth-2_small2048px.png",
        "small4096px": "natural-earth-2_small4096px.png"
    },
    "shaded-relief": {
        "__comment__": "Shaded Relief Basic from Natural Earth",
        "__projection__": "PlateCarree",
        "__source__": "https://www.naturalearthdata.com/downloads/10m-raster-data/10m-shaded-relief/ and https://www.naturalearthdata.com/downloads/50m-raster-data/50m-shaded-relief/",
        "large": "shaded-relief_large.png",
        "large0512px": "shaded-relief_large0512px.png",
        "large1024px": "shaded-relief_large1024px.png",
        "large2048px": "shaded-relief_large2048px.png",
        "large4096px": "shaded-relief_large4096px.png",
        "medium": "shaded-relief_medium.png",
        "medium0512px": "shaded-relief_medium0512px.png",
        "medium1024px": "shaded-relief_medium1024px.png",
        "medium2048px": "shaded-relief_medium2048px.png",
        "medium4096px": "shaded-relief_medium4096px.png",
        "small": "shaded-relief_small.png",
        "small0512px": "shaded-relief_small0512px.png",
        "small1024px": "shaded-relief_small1024px.png",
        "small2048px": "shaded-relief_small2048px.png",
        "small4096px": "shaded-relief_small4096px.png"
    }
}

              
You may also download “replacing-cartopy-background-image-images2.json” directly or view “replacing-cartopy-background-image-images2.json” on GitHub Gist (you may need to manually checkout the “main” branch).

The PNG files that it creates are shown below for comparison with the original one too. Note that The Great Lakes and Lake Victoria are easily visible (to name a few).

Download:
  1. 512 px × 256 px (0.1 Mpx; 167.3 KiB)
  2. 1,024 px × 512 px (0.5 Mpx; 649.4 KiB)
  3. 2,048 px × 1,024 px (2.1 Mpx; 2.6 MiB)
  4. 4,096 px × 2,048 px (8.4 Mpx; 11.0 MiB)
  5. 8,192 px × 4,096 px (33.6 Mpx; 41.2 MiB)
  6. 21,600 px × 10,800 px (233.3 Mpx; 222.6 MiB)

Great - but how am I going to use this new folder that has the same organisation as Cartopy’s folder? I have written a replacement function, called pyguymer3.geo.add_map_background(), that I can use so that my scripts use this new folder. Now all that I need to do is set the environment variable $CARTOPY_USER_BACKGROUNDS to the path that my new images are in and then replace all instances of ax.stock_img() in all of my Python scripts with either pyguymer3.geo.add_map_background(ax) (for a low-resolution background image for testing purposes) or pyguymer3.geo.add_map_background(ax, resolution = "large4096px") (for a high-resolution background image for publishing purposes) for example.

You can see the fruits of this work by looking at the example image in my Flight Map Creator project on GitHub. The following webpages have been useful during this project: