pythonmatplotlibpatchcartopyclip

How to clip tile imagery by patch?


I'm trying to clip the satellite background image by circle:

import io
from PIL import Image

import cartopy.crs as ccrs
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import cartopy.io.img_tiles as cimgt
from urllib.request import urlopen, Request

def image_spoof(self, tile):
    '''this function reformats web requests from OSM for cartopy'''

    url = self._image_url(tile)                # get the url of the street map API
    req = Request(url)                         # start request
    req.add_header('User-agent','Anaconda 3')  # add user agent to request
    fh = urlopen(req) 
    im_data = io.BytesIO(fh.read())            # get image
    fh.close()                                 # close url
    img = Image.open(im_data)                  # open image with PIL
    img = img.convert(self.desired_tile_form)  # set image format
    return img, self.tileextent(tile), 'lower' # reformat for cartopy

proj = ccrs.PlateCarree()
ax = plt.axes(projection=proj)

# set extent
lon_min = -98.853627
lon_max = -98.752037
lat_min = 19.274685
lat_max = 19.376275

ax.set_extent((lon_min, lon_max, lat_min, lat_max), crs=proj)

cimgt.QuadtreeTiles.get_image = image_spoof # reformat web request for street map spoofing
osm_img = cimgt.QuadtreeTiles() # spoofed, downloaded street map

osm_img = ax.add_image(osm_img, 16) # add OSM with zoom specification

patch = patches.Circle((-98.802832, 19.32548), radius=0.03, transform=ax.transData)
ax.add_patch(patch)

# osm_img.set_clip_path(patch)

However, I see set_clip_path does not work for this case. Is it possible to clip osm_img by the circle patch? Or any other method to plot a circled satellite background?

example


Solution

  • I figure out one method which gets the image array and clip the image:

    import cartopy.crs as ccrs
    import matplotlib
    import contextily as cx
    import matplotlib.patches as patches
    import matplotlib.pyplot as plt
    
    ax = plt.axes(projection=ccrs.PlateCarree())
    
    proj = ccrs.PlateCarree()
    
    # set extent
    lon_min = -98.853627
    lon_max = -98.752037
    lat_min = 19.274685
    lat_max = 19.376275
    extent = (lon_min, lon_max, lat_min, lat_max)
    ax.set_extent(extent, crs=proj)
    
    cx.add_basemap(ax, crs=proj, source=cx.providers.Esri.WorldImagery)
    
    # get the background as image array
    for child in ax.get_children():
        if isinstance(child, matplotlib.image.AxesImage):
            print("* Found image object.")
            child0 = child
            ccmap = child0.get_cmap()
            img_array = child0.get_array()
            break
        pass
    
    # clear the axis
    plt.cla()
    
    # create patch
    patch = patches.Circle((-98.802832, 19.32548), radius=0.03, transform=ax.transData)
    
    # clip the image
    osm_img = ax.imshow(img_array, extent=extent, origin='lower', cmap=ccmap)
    osm_img.set_clip_path(patch)
    

    example