How to apply Earth Features and Land/Ocean masks in high resolution coastlines in Cartopy? - matplotlib

I am using the coastlines of the GSHHS dataset in Cartopy. This has a high resolution for coastlines. But I want to not only plot the high resolution coastline but also apply a mask for the ocean.
import matplotlib.pyplot as plt
import cartopy
fig = plt.figure(figsize=(20,12))
ax = plt.axes(projection=cartopy.crs.PlateCarree())
coast = cartopy.feature.GSHHSFeature(scale="full")
ax.add_feature(coast, linewidth=2)
ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m"))
ax.set_extent([-17, -16, 27.9, 28.7])
Executing the code there're differences in the images, since I guess that ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m")) is using the "10m" resolution, while GSHHS has a higher resolution.
How can I mask using GSHHS higher resolution? Thx.

Before one can answer the question how to apply a mask to hide features in the main plot, we need to investigate the available masks first.
In our case, the main plot is Natural_Earth 10m resolution Physical Land features, and various resolutions of GSHHSFeature as the available masks.
The code and the output plot below reveals the insight.
# Code adapted from:-
# Src: https://ctroupin.github.io/posts/2019-09-02-fine-coast/
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
resolutions = {"c": "crude",
"l": "low",
"i": "intermediate",
"h": "high",
"f": "full"}
coordinates = (8.7, 8.81, 42.55, 42.60)
myproj = ccrs.PlateCarree()
fig = plt.figure(figsize=(8, 4))
for i, res in enumerate(resolutions):
ax = plt.subplot(2, 3, i+1, projection=myproj)
coast = cfeature.GSHHSFeature(scale=res)
ax.add_feature(coast, facecolor="lightgray")
ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m"),
ec="red", fc="yellow", lw=2, alpha=0.4)
ax.set_xlim(coordinates[0], coordinates[1])
ax.set_ylim(coordinates[2], coordinates[3])
plt.title(resolutions[res])
plt.suptitle("GSHHS: gray Versus 10m_Physical_Land: yellow/red")
plt.show()
Suppose we need a plot at this zoom level. It is clearly that the outlines from 2 data sources do not fit well enough to the eyes of the viewers. We may conclude that none of the available masks is fit for the target plot.
But if the plot extents is wider, or smaller scale plots, coupled with some cartographic techniques, e.g. using thicker coastlines, one may get acceptable plots. The process is trial-and-error approach.
Edit1
With (Global_land_mask) added, more choices can be plotted for
comparison.
from global_land_mask import globe
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
# Extent of map in degrees
minlon,maxlon,minlat,maxlat = (8.7, 8.81, 42.55, 42.60)
# Lat/lon points to get for `global_land_mask` uses
# Finer than 500x250 has no improvement
lons = np.linspace(minlon,maxlon, 500)
lats = np.linspace(minlat,maxlat, 250)
# Make a grid
lon_grid, lat_grid = np.meshgrid(lons,lats)
# Get whether the points are on land.
z = globe.is_land(lat_grid, lon_grid)
# GSHHS ...
resolutions = {"c": "crude",
"l": "low",
"i": "intermediate",
"h": "high",
"f": "full"}
myproj = ccrs.PlateCarree()
fig = plt.figure(figsize=(8, 4))
for i, res in enumerate(resolutions):
ax = plt.subplot(2, 3, i+1, projection=myproj)
# GSHHSFeature
coast = cfeature.GSHHSFeature(scale=res)
ax.add_feature(coast, facecolor="brown", alpha=0.5)
# 10m physical_land
ax.add_feature(cfeature.NaturalEarthFeature("physical", "land", "10m"),
ec="red", fc="yellow", lw=2, alpha=0.4)
# Global_land_mask data is used to create fillcontour
# The fillcontour with proper (colormap, zorder, alpha) can be used as land `mask`
ax.contourf(lon_grid, lat_grid, z, cmap="Greys_r", alpha=0.4)
ax.set_xlim(minlon, maxlon)
ax.set_ylim(minlat, maxlat)
plt.title(resolutions[res])
plt.suptitle("GSHHS:brown/black | 10m_Land:yellow/red | Global_land_mask:light_gray")
plt.show()
# The best resolutuion from `Global_land_mask` is plotted in `lightgray` covering the sea areas

Related

Animate a point moving along path between two points

I want to animate a point moving along a path from one location to another on the map.
For example, I drawn a path from New York to New Delhi, using Geodetic transform. Eg. taken from docs Adding data to the map
plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
color='blue', linewidth=2, marker='o',
transform=ccrs.Geodetic(),
)
Now i want to move a point along this path.
My idea was to somehow get some (say 50) points, along the path and plot a marker on each point for each frame. But I am not able to find a way to get the points on the path.
I found a function transform_points under classCRS, but I am unable to use this, as this gives me the same number of points i have, not the points in between.
Thanks in advance!
There are a couple of approaches to this.
The matplotlib approach
I'll start with perhaps the most basic if you are familiar with matplotlib, but this approach suffers from indirectly using cartopy's functionality, and is therefore harder to configure/extend.
There is a private _get_transformed_path method on a Line2D object (the thing that is returned from plt.plot). The resulting TransformedPath object has a get_transformed_path_and_affine method, which basically will give us the projected line (in the coordinate system of the Axes being drawn).
In [1]: import cartopy.crs as ccrs
In [3]: import matplotlib.pyplot as plt
In [4]: ax = plt.axes(projection=ccrs.Robinson())
In [6]: ny_lon, ny_lat = -75, 43
In [7]: delhi_lon, delhi_lat = 77.23, 28.61
In [8]: [line] = plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
...: color='blue', linewidth=2, marker='o',
...: transform=ccrs.Geodetic(),
...: )
In [9]: t_path = line._get_transformed_path()
In [10]: path_in_data_coords, _ = t_path.get_transformed_path_and_affine()
In [11]: path_in_data_coords.vertices
Out[11]:
array([[-6425061.82215208, 4594257.92617961],
[-5808923.84969279, 5250795.00604155],
[-5206753.88613758, 5777772.51828996],
[-4554622.94040482, 6244967.03723341],
[-3887558.58343227, 6627927.97123701],
[-3200922.19194864, 6932398.19937816],
[-2480001.76507805, 7165675.95095855],
[-1702269.5101901 , 7332885.72276795],
[ -859899.12295981, 7431215.78426759],
[ 23837.23431173, 7453455.61302756],
[ 889905.10635756, 7397128.77301289],
[ 1695586.66856764, 7268519.87627204],
[ 2434052.81300274, 7073912.54130764],
[ 3122221.22299409, 6812894.40443648],
[ 3782033.80448001, 6478364.28561403],
[ 4425266.18173684, 6062312.15662039],
[ 5049148.25986903, 5563097.6328901 ],
[ 5616318.74912886, 5008293.21452795],
[ 6213232.98764984, 4307186.23400115],
[ 6720608.93929235, 3584542.06839575],
[ 7034261.06659143, 3059873.62740856]])
We can pull this together with matplotlib's animation functionality to do as requested:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.Robinson())
ax.stock_img()
ny_lon, ny_lat = -75, 43
delhi_lon, delhi_lat = 77.23, 28.61
[line] = plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
color='blue', linewidth=2, marker='o',
transform=ccrs.Geodetic(),
)
t_path = line._get_transformed_path()
path_in_data_coords, _ = t_path.get_transformed_path_and_affine()
# Draw the point that we want to animate.
[point] = plt.plot(ny_lon, ny_lat, marker='o', transform=ax.projection)
def animate_point(i):
verts = path_in_data_coords.vertices
i = i % verts.shape[0]
# Set the coordinates of the line to the coordinate of the path.
point.set_data(verts[i, 0], verts[i, 1])
ani = animation.FuncAnimation(
ax.figure, animate_point,
frames= path_in_data_coords.vertices.shape[0],
interval=125, repeat=True)
ani.save('point_ani.gif', writer='imagemagick')
plt.show()
The cartopy approach
Under the hood, cartopy's matplotlib implementation (as used above), is calling the project_geometry method. We may as well make use of this directly as it is often more convenient to be using Shapely geometries than it is matplotlib Paths.
With this approach, we simply define a shapely geometry, and then construct the source and target coordinate reference systems that we want to convert the geometry from/to:
target_cs.project_geometry(geometry, source_cs)
The only thing we have to watch out for is that the result can be a MultiLineString (or more generally, any Multi- geometry type). However, in our simple case, we don't need to deal with that (incidentally, the same was true of the simple Path returned in the first example).
The code to produce a similar plot to above:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sgeom
ax = plt.axes(projection=ccrs.Robinson())
ax.stock_img()
ny_lon, ny_lat = -75, 43
delhi_lon, delhi_lat = 77.23, 28.61
line = sgeom.LineString([[ny_lon, ny_lat], [delhi_lon, delhi_lat]])
projected_line = ccrs.PlateCarree().project_geometry(line, ccrs.Geodetic())
# We only animate along one of the projected lines.
if isinstance(projected_line, sgeom.MultiLineString):
projected_line = projected_line.geoms[0]
ax.add_geometries(
[projected_line], ccrs.PlateCarree(),
edgecolor='blue', facecolor='none')
[point] = plt.plot(ny_lon, ny_lat, marker='o', transform=ccrs.PlateCarree())
def animate_point(i):
verts = np.array(projected_line.coords)
i = i % verts.shape[0]
# Set the coordinates of the line to the coordinate of the path.
point.set_data(verts[i, 0], verts[i, 1])
ani = animation.FuncAnimation(
ax.figure, animate_point,
frames=len(projected_line.coords),
interval=125, repeat=True)
ani.save('projected_line_ani.gif', writer='imagemagick')
plt.show()
Final remaaaaarrrrrrks....
The approach naturally generalises to animating any type of matplotlib Arrrrtist.... in this case, I took a bit more control over the great circle resolution, and I animated an image along the great circle:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sgeom
ax = plt.axes(projection=ccrs.Mercator())
ax.stock_img()
line = sgeom.LineString([[-5.9845, 37.3891], [-82.3666, 23.1136]])
# Higher resolution version of Mercator. Same workaround as found in
# https://github.com/SciTools/cartopy/issues/8#issuecomment-326987465.
class HighRes(ax.projection.__class__):
#property
def threshold(self):
return super(HighRes, self).threshold / 100
projected_line = HighRes().project_geometry(line, ccrs.Geodetic())
# We only animate along one of the projected lines.
if isinstance(projected_line, sgeom.MultiLineString):
projected_line = projected_line.geoms[0]
# Add the projected line to the map.
ax.add_geometries(
[projected_line], ax.projection,
edgecolor='blue', facecolor='none')
def ll_to_extent(x, y, ax_size=(4000000, 4000000)):
"""
Return an image extent in centered on the given
point with the given width and height.
"""
return [x - ax_size[0] / 2, x + ax_size[0] / 2,
y - ax_size[1] / 2, y + ax_size[1] / 2]
# Image from https://pixabay.com/en/sailing-ship-boat-sail-pirate-28930/.
pirate = plt.imread('pirates.png')
img = ax.imshow(pirate, extent=ll_to_extent(0, 0), transform=ax.projection, origin='upper')
ax.set_global()
def animate_ship(i):
verts = np.array(projected_line.coords)
i = i % verts.shape[0]
# Set the extent of the image to the coordinate of the path.
img.set_extent(ll_to_extent(verts[i, 0], verts[i, 1]))
ani = animation.FuncAnimation(
ax.figure, animate_ship,
frames=len(projected_line.coords),
interval=125, repeat=False)
ani.save('arrrr.gif', writer='imagemagick')
plt.show()
All code and images for this answer can be found at https://gist.github.com/pelson/618a5f4ca003e56f06d43815b21848f6.

Limit extent of orthograpic projection (zooming)

I would like to produce orthographic (polar) plots of Antarctica that are 'zoomed' with respect to the default settings. By default I get this:
Antarctica polar
The following script produced this.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.Orthographic(central_longitude=0.0, central_latitude=-90.))
ax.stock_img()
plt.show()
My best attempt to tell Cartopy 'limit the latitude to 60S to 90S' was:
ax.set_extent([-180,180,-60,-90], ccrs.PlateCarree())
unfortunately it does not give the desired result. Any ideas? Thanks in advance.
I'm not sure I fully understand what you're trying to do. Your example looks like a bounding box that was defined, but you'd like it rounded like your first example?
cartopy documentation has an example of this http://scitools.org.uk/cartopy/docs/latest/examples/always_circular_stereo.html:
import matplotlib.path as mpath
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
import cartopy.feature
def main():
fig = plt.figure(figsize=[10, 5])
ax1 = plt.subplot(1, 2, 1, projection=ccrs.SouthPolarStereo())
ax2 = plt.subplot(1, 2, 2, projection=ccrs.SouthPolarStereo(),
sharex=ax1, sharey=ax1)
fig.subplots_adjust(bottom=0.05, top=0.95,
left=0.04, right=0.95, wspace=0.02)
# Limit the map to -60 degrees latitude and below.
ax1.set_extent([-180, 180, -90, -60], ccrs.PlateCarree())
ax1.add_feature(cartopy.feature.LAND)
ax1.add_feature(cartopy.feature.OCEAN)
ax1.gridlines()
ax2.gridlines()
ax2.add_feature(cartopy.feature.LAND)
ax2.add_feature(cartopy.feature.OCEAN)
# Compute a circle in axes coordinates, which we can use as a boundary
# for the map. We can pan/zoom as much as we like - the boundary will be
# permanently circular.
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax2.set_boundary(circle, transform=ax2.transAxes)
plt.show()
if __name__ == '__main__':
main()

How to plot a tissot with cartopy and matplotlib?

For plotting skymaps I just switched from Basemap to cartopy, I like it a lot more
.
(The main reason was segfaulting of Basemap on some computers, which I could not fix).
The only thing I struggle with, is getting a tissot circle (used to show the view cone of our telescope.)
This is some example code plotting random stars (I use a catalogue for the real thing):
import matplotlib.pyplot as plt
from cartopy import crs
import numpy as np
# create some random stars:
n_stars = 100
azimuth = np.random.uniform(0, 360, n_stars)
altitude = np.random.uniform(75, 90, n_stars)
brightness = np.random.normal(8, 2, n_stars)
fig = plt.figure()
ax = fig.add_subplot(1,1,1, projection=crs.NorthPolarStereo())
ax.background_patch.set_facecolor('black')
ax.set_extent([-180, 180, 75, 90], crs.PlateCarree())
plot = ax.scatter(
azimuth,
altitude,
c=brightness,
s=0.5*(-brightness + brightness.max())**2,
transform=crs.PlateCarree(),
cmap='gray_r',
)
plt.show()
How would I add a tissot circle with a certain radius in degrees to that image?
https://en.wikipedia.org/wiki/Tissot%27s_indicatrix
I keep meaning to go back and add the two functions from GeographicLib which provide the forward and inverse geodesic calculations, with this it is simply a matter of computing a geodetic circle by sampling at appropriate azimuths for a given lat/lon/radius. Alas, I haven't yet done that, but there is a fairly primitive (but effective) wrapper in pyproj for the functionality.
To implement a tissot indicatrix then, the code might look something like:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
from pyproj import Geod
import shapely.geometry as sgeom
def circle(geod, lon, lat, radius, n_samples=360):
"""
Return the coordinates of a geodetic circle of a given
radius about a lon/lat point.
Radius is in meters in the geodetic's coordinate system.
"""
lons, lats, back_azim = geod.fwd(np.repeat(lon, n_samples),
np.repeat(lat, n_samples),
np.linspace(360, 0, n_samples),
np.repeat(radius, n_samples),
radians=False,
)
return lons, lats
def main():
ax = plt.axes(projection=ccrs.Robinson())
ax.coastlines()
geod = Geod(ellps='WGS84')
radius_km = 500
n_samples = 80
geoms = []
for lat in np.linspace(-80, 80, 10):
for lon in np.linspace(-180, 180, 7, endpoint=False):
lons, lats = circle(geod, lon, lat, radius_km * 1e3, n_samples)
geoms.append(sgeom.Polygon(zip(lons, lats)))
ax.add_geometries(geoms, ccrs.Geodetic(), facecolor='blue', alpha=0.7)
plt.show()
if __name__ == '__main__':
main()

Setting up a map which crosses the dateline in cartopy

I received the following email and wanted to make sure the answer to this question was available to everybody:
Hi,
I would like to setup a simple latitude longitude map, using cartopy, which crosses the dateline and shows east Asia on the left hand side with the west of North America on the right. The following google map is roughly what I am after:
https://maps.google.co.uk/?ll=56.559482,-175.253906&spn=47.333523,133.066406&t=m&z=4
Can this be done with Cartopy?
Good question. This is probably something which will come up time-and-time again, so I will go through this step-by-step before actually answering your specific question. For future reference, the following examples were written with cartopy v0.5.
Firstly, it is important to note that the default "latitude longitude" (or more technically PlateCarree) projection works in the forward range of -180 to 180. This means that you cannot plot the standard PlateCarree projection beyond this. There are several good reasons for this, most of which boil down to the fact that cartopy would have to do a lot more work when projecting both vectors and rasters (simple coastlines for example). Unfortunately the plot you are trying to produce requires precisely this functionality. To put this limitation into pictures, the default PlateCarree projection looks like:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
proj = ccrs.PlateCarree(central_longitude=0)
ax1 = plt.axes(projection=proj)
ax1.stock_img()
plt.title('Global')
plt.show()
Any single rectangle that you can draw on this map can legally be a zoomed in area (there is some slightly more advanced code in here, but the picture is worth a 1000 words):
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import shapely.geometry as sgeom
box = sgeom.box(minx=-90, maxx=45, miny=15, maxy=70)
x0, y0, x1, y1 = box.bounds
proj = ccrs.PlateCarree(central_longitude=0)
ax1 = plt.subplot(211, projection=proj)
ax1.stock_img()
ax1.add_geometries([box], proj, facecolor='coral',
edgecolor='black', alpha=0.5)
plt.title('Global')
ax2 = plt.subplot(212, projection=proj)
ax2.stock_img()
ax2.set_extent([x0, x1, y0, y1], proj)
plt.title('Zoomed in area')
plt.show()
Unfortunately the plot you want would require 2 rectangles with this projection:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import shapely.geometry as sgeom
box = sgeom.box(minx=120, maxx=260, miny=15, maxy=80)
proj = ccrs.PlateCarree(central_longitude=0)
ax1 = plt.axes(projection=proj)
ax1.stock_img()
ax1.add_geometries([box], proj, facecolor='coral',
edgecolor='black', alpha=0.5)
plt.title('Target area')
plt.show()
Hence it is not possible to draw a map that crosses the dateline using the standard PlateCarree definition. Instead we could change the PlateCarree definition's central longitude to allow a single box to be drawn of the area we are targeting:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import shapely.geometry as sgeom
box = sgeom.box(minx=120, maxx=260, miny=15, maxy=80)
x0, y0, x1, y1 = box.bounds
proj = ccrs.PlateCarree(central_longitude=180)
box_proj = ccrs.PlateCarree(central_longitude=0)
ax1 = plt.subplot(211, projection=proj)
ax1.stock_img()
ax1.add_geometries([box], box_proj, facecolor='coral',
edgecolor='black', alpha=0.5)
plt.title('Global')
ax2 = plt.subplot(212, projection=proj)
ax2.stock_img()
ax2.set_extent([x0, x1, y0, y1], box_proj)
plt.title('Zoomed in area')
plt.show()
Hopefully that shows you what it is you have to do to achieve your target map, the code above might be a little complex to achieve your goal, so to simplify slightly, the code I would write to produce the plot you want would be something like:
import cartopy.feature
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))
ax.set_extent([120, 260, 15, 80], crs=ccrs.PlateCarree())
# add some features to make the map a little more polished
ax.add_feature(cartopy.feature.LAND)
ax.add_feature(cartopy.feature.OCEAN)
ax.coastlines('50m')
plt.show()
This was a long answer, hopefully I have not only answered the question, but made some of the more complex details of map production and cartopy more clear to help smooth any future problems you may have.
Cheers,
For more details of above benjimin's comment,
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from matplotlib.ticker import AutoMinorLocator, FixedLocator, MultipleLocator
def map_common(ax1,gl_loc=[True,True,False,True],gl_lon_info=range(-180,180,60),gl_dlat=30):
ax1.coastlines(color='silver',linewidth=1.)
gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=True,
linewidth=0.6, color='gray', alpha=0.5, linestyle='--')
gl.ylabels_left = gl_loc[0]
gl.ylabels_right = gl_loc[1]
gl.xlabels_top = gl_loc[2]
gl.xlabels_bottom = gl_loc[3]
gl.xlocator = FixedLocator(gl_lon_info)
gl.ylocator = MultipleLocator(gl_dlat)
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
gl.xlabel_style = {'size': 11, 'color': 'k'}
gl.ylabel_style = {'size': 11, 'color': 'k'}
lon_boundary=np.arange(-240,-60,1.)
lat_boundary=np.arange(15,75,1.)
data=np.ones([lat_boundary.shape[0]-1,lon_boundary.shape[0]-1]) ## Data dimension is 1 less than boundaries
data=data*lat_boundary[:-1,None]
lon_offset=-150 ##
x,y=np.meshgrid(lon_boundary-lon_offset,lat_boundary)
fig=plt.figure()
fig.set_size_inches(7.5,5) ## (xsize, ysize)
ax1=fig.add_subplot(111,projection=ccrs.PlateCarree(central_longitude=lon_offset))
ax1.set_extent([-250,-50,10,80],crs=ccrs.PlateCarree())
props=dict(vmin=0,vmax=90,cmap=plt.cm.get_cmap('bone'),alpha=0.8)
cs=ax1.pcolormesh(x,y,data,**props)
ax1.set_title('Lon_Offset=-90')
map_common(ax1,gl_lon_info=[-180,-120,-60,120,],gl_dlat=15)
fnout='./map_over_dateline.png'
#plt.show()
plt.savefig(fnout,bbox_inches='tight',dpi=150)
Output of this program

matplotlib: Stretch image to cover the whole figure

I am quite used to working with matlab and now trying to make the shift matplotlib and numpy. Is there a way in matplotlib that an image you are plotting occupies the whole figure window.
import numpy as np
import matplotlib.pyplot as plt
# get image im as nparray
# ........
plt.figure()
plt.imshow(im)
plt.set_cmap('hot')
plt.savefig("frame.png")
I want the image to maintain its aspect ratio and scale to the size of the figure ... so when I do savefig it exactly the same size as the input figure, and it is completely covered by the image.
Thanks.
I did this using the following snippet.
#!/usr/bin/env python
import numpy as np
import matplotlib.cm as cm
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
from pylab import *
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
Z = Z2-Z1 # difference of Gaussians
ax = Axes(plt.gcf(),[0,0,1,1],yticks=[],xticks=[],frame_on=False)
plt.gcf().delaxes(plt.gca())
plt.gcf().add_axes(ax)
im = plt.imshow(Z, cmap=cm.gray)
plt.show()
Note the grey border on the sides is related to the aspect rario of the Axes which is altered by setting aspect='equal', or aspect='auto' or your ratio.
Also as mentioned by Zhenya in the comments Similar StackOverflow Question
mentions the parameters to savefig of bbox_inches='tight' and pad_inches=-1 or pad_inches=0
You can use a function like the one below.
It calculates the needed size for the figure (in inches) according to the resolution in dpi you want.
import numpy as np
import matplotlib.pyplot as plt
def plot_im(image, dpi=80):
px,py = im.shape # depending of your matplotlib.rc you may
have to use py,px instead
#px,py = im[:,:,0].shape # if image has a (x,y,z) shape
size = (py/np.float(dpi), px/np.float(dpi)) # note the np.float()
fig = plt.figure(figsize=size, dpi=dpi)
ax = fig.add_axes([0, 0, 1, 1])
# Customize the axis
# remove top and right spines
ax.spines['right'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
# turn off ticks
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.imshow(im)
plt.show()
Here's a minimal object-oriented solution:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, xticks=[], yticks=[])
Testing it out with
ax.imshow([[0]])
fig.savefig('test.png')
saves out a uniform purple block.
edit: As #duhaime points out below, this requires the figure to have the same aspect as the axes.
If you'd like the axes to resize to the figure, add aspect='auto' to imshow.
If you'd like the figure to resize to be resized to the axes, add
from matplotlib import tight_bbox
bbox = fig.get_tightbbox(fig.canvas.get_renderer())
tight_bbox.adjust_bbox(fig, bbox, fig.canvas.fixed_dpi)
after the imshow call. This is the important bit of matplotlib's tight_layout functionality which is implicitly called by things like Jupyter's renderer.