I'd like to plot a transparent contour plot over an image file in matplotlib/pyplot.
Here's what I got so far...
I have a 600x600 pixel square image file test.png that looks like so:
I would like to plot a contour plot over this image (having the image file be 'below' and a semi-transparent version of the contour plot overlaid) using matplotlib and pyplot. As a bonus, the image would be automatically scaled to fit within the current plotting boundaries. My example plotting script is as follows:
from matplotlib import pyplot
from matplotlib.ticker import MultipleLocator, FormatStrFormatter
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
from pylab import *
import numpy as np
import random
# ----------------------------- #
dx, dy = 500.0, 500.0
y, x = np.mgrid[slice(-2500.0, 2500.0 + dy, dy),slice(-2500.0, 2500.0 + dx, dx)]
z = []
for i in x:
z.append([])
for j in y:
z[-1].append(random.uniform(80.0,100.0))
# ----------------------------- #
plot_aspect = 1.2
plot_height = 10.0
plot_width = int(plot_height*plot_aspect)
# ----------------------------- #
pyplot.figure(figsize=(plot_width, plot_height), dpi=100)
pyplot.subplots_adjust(left=0.10, right=1.00, top=0.90, bottom=0.06, hspace=0.30)
subplot1 = pyplot.subplot(111)
# ----------------------------- #
cbar_max = 100.0
cbar_min = 80.0
cbar_step = 1.0
cbar_num_colors = 200
cbar_num_format = "%d"
# ----------
levels = MaxNLocator(nbins=cbar_num_colors).tick_values(cbar_min, cbar_max)
cmap = pyplot.get_cmap('jet')
norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True)
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap)
cbar = pyplot.colorbar(pp, orientation='vertical', ticks=np.arange(cbar_min, cbar_max+cbar_step, cbar_step), format=cbar_num_format)
cbar.ax.set_ylabel('Color Scale [unit]', fontsize = 16, weight="bold")
# ----------
CS = pyplot.contour(x,y,z, alpha=0.5)
# ----------
majorLocator1 = MultipleLocator(500)
majorFormatter1 = FormatStrFormatter('%d')
minorLocator1 = MultipleLocator(250)
subplot1.xaxis.set_major_locator(majorLocator1)
subplot1.xaxis.set_major_formatter(majorFormatter1)
subplot1.xaxis.set_minor_locator(minorLocator1)
pyplot.xticks(fontsize = 16)
pyplot.xlim(-2500.0,2500.0)
# ----------
majorLocator2 = MultipleLocator(500)
majorFormatter2 = FormatStrFormatter('%d')
minorLocator2 = MultipleLocator(250)
subplot1.yaxis.set_major_locator(majorLocator2)
subplot1.yaxis.set_major_formatter(majorFormatter2)
subplot1.yaxis.set_minor_locator(minorLocator2)
pyplot.yticks(fontsize = 16)
pyplot.ylim(-2500.0,2500.0)
# ----------
subplot1.xaxis.grid()
subplot1.yaxis.grid()
# ----------
subplot1.axes.set_aspect('equal')
# ----------
pyplot.suptitle('Main Title', fontsize = 24, weight="bold")
# ----------
pyplot.xlabel('X [m]', fontsize=16, weight="bold")
pyplot.ylabel('Y [m]', fontsize=16, weight="bold")
# ----------
implot = subplot1.imshow( pyplot.imread('test.png') , interpolation='nearest', alpha=0.5)
# ----------
pyplot.show()
#pyplot.savefig("tmp.png", dpi=100)
pyplot.close()
...but I'm not getting the result I want... instead I just see the contour plot part. Something like:
What should I do in my code to get what I want?
You basically need to do two things, set the extent of the image you want in the background. If you dont, the coordinates are assumed to be pixel coordinates, in this case 0 till 600 for both x and y. So adjust you imshow command to:
implot = subplot1.imshow(pyplot.imread(r'test.png'), interpolation='nearest',
alpha=0.5, extent=[-2500.0,2500.0,-2500.0,2500.0])
If you want to stretch the image to the limits of the plot automatically, you can grab the extent with:
extent = subplot1.get_xlim()+ subplot1.get_ylim()
And pass it to imshow as extent=extent.
Since its the background image, setting the alpha to 0.5 makes it very faint, i would set it to 1.0.
Secondly, you set the alpha of the contour lines, but you probably also (or especially) want to set the alpha of the filled contours. And when you use alpha with filled contours, enabling anti-aliasing reduces artifacts. So change your contourf command to:
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap, alpha=.5, antialiased=True)
And since you already create the subplot object yourself, i would advice also using it to do the plotting instead of the pyplot interface, which operates on the currently active axes.
So:
subplot1.contourf()
etc
Instead of:
pyplot.contourf()
With the two changes mentioned above, my result looks like:
I personally used the multiple contour plot answer for a while with great results. However, I had to output my figures to PostScript, which does not support opacity (alpha option). I found this answer useful since it does not require the use of opacity.
The reason these lines show up is due to the edge color of the faces that make up the contour plot. The linked solution avoids this by changing the edge color to the face color.
cf = plt.contourf(x, y, z, levels=100)
# This is the fix for the white lines between contour levels
for c in cf.collections:
c.set_edgecolor("face")
Related
I have a boolean image, where the zeros are the background, and I want to plot the ellipse that encloses the major and minor axis of an object retrieved from skimage.measure.regionprops. The module skimage.draw.ellipse_perimeter generates the expected ellipse but also two undesired lines.
Code (the input image is here):
import skimage
import skimage.draw
from skimage.measure import label
from skimage.measure import regionprops
import matplotlib.pyplot as plt
# load example image
TP_mask = plt.imread('https://i.stack.imgur.com/UYLE0.png')
# connect region with same integer value
region = label(TP_mask)
# obtain RegionProperties
props = regionprops(region)
props = props[0]
# define centroid
y0,x0 = props.centroid
# draw ellipse perimeter
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
# plot
plt.plot(rr,cc, color = 'yellow')
plt.imshow(TP_mask, cmap = 'gray')
plt.show()
However, if I create a simplified example as follows, I obtain the expected ellipse. Could someone help me understand what am I doing wrong?
import numpy as np
img = np.zeros((1000,1000))
img[200:800,200:400] = 1
region = label(img)
props = regionprops(region)
props = props[0]
y0,x0 = props.centroid
rr,cc = skimage.draw.ellipse_perimeter(int(x0),int(y0),int(props.minor_axis_length*0.5),int(props.major_axis_length*0.5), orientation = props.orientation)
plt.plot(rr,cc, color = 'yellow')
plt.imshow(img, cmap = 'gray')
plt.show()
It turns out that the coordinates returned by the draw module are designed to index into an array, as shown in this example, rather than plot:
rr, cc = ellipse_perimeter(120, 400, 60, 20, orientation=math.pi / 4.)
img[rr, cc, :] = (1, 0, 1)
To use plt.plot and do a line plot, the coordinates need to be sorted as they go around the circle/ellipse. They are not properly sorted by default because the ellipse is actually drawn in four separate quadrants, which you can find by looking at the relevant part of the source code. (A clue: the lines hit exactly where the ellipse is vertical or horizontal.)
Since you have a convex surface, computing the angle between each point and the centre of the ellipse is enough to sort the points. Try the following:
fig, ax = plt.subplots()
_ = ax.imshow(TP_mask, cmap='gray')
angle = np.arctan2(rr - np.mean(rr), cc - np.mean(cc))
sorted_by_angle = np.argsort(angle)
rrs = rr[sorted_by_angle]
ccs = cc[sorted_by_angle]
_ = ax.plot(rrs, ccs, color='red')
Which gives:
I'm trying to produce a similar version of this image using Python:
I'm close but can't quite figure out how to modify a matplotlib colormap to make values <0.4 go to white. I tried masking those values and using set_bad but I ended up with a real blocky appearance, losing the nice smooth contours seen in the original image.
Result with continuous colormap (problem: no white):
Result with set_bad (problem: no smooth transition to white):
Code so far:
from netCDF4 import Dataset as NetCDFFile
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
nc = NetCDFFile('C:/myfile1.nc')
nc1 = NetCDFFile('C:/myfile2.nc')
lat = nc.variables['lat'][:]
lon = nc.variables['lon'][:]
time = nc.variables['time'][:]
uwnd = nc.variables['uwnd'][:]
vwnd = nc1.variables['vwnd'][:]
map = Basemap(llcrnrlon=180.,llcrnrlat=0.,urcrnrlon=340.,urcrnrlat=80.)
lons,lats = np.meshgrid(lon,lat)
x,y = map(lons,lats)
speed = np.sqrt(uwnd*uwnd+vwnd*vwnd)
#speed = np.ma.masked_where(speed < 0.4, speed)
#cmap = plt.cm.jet
#cmap.set_bad(color='white')
levels = np.arange(0.0,3.0,0.1)
ticks = np.arange(0.0,3.0,0.2)
cs = map.contourf(x,y,speed[0],levels, cmap='jet')
vw = plt.quiver(x,y,speed)
cbar = plt.colorbar(cs, orientation='horizontal', cmap='jet', spacing='proportional',ticks=ticks)
cbar.set_label('850 mb Vector Wind Anomalies (m/s)')
map.drawcoastlines()
map.drawparallels(np.arange(20,80,20),labels=[1,1,0,0], linewidth=0.5)
map.drawmeridians(np.arange(200,340,20),labels=[0,0,0,1], linewidth=0.5)
#plt.show()
plt.savefig('phase8_850wind_anom.png',dpi=600)
The answer to get the result smooth lies in constructing your own colormap. To do this one has to create an RGBA-matrix: a matrix with on each row the amount (between 0 and 1) of Red, Green, Blue, and Alpha (transparency; 0 means that the pixel does not have any coverage information and is transparent).
As an example the distance to some point is plotted in two dimensions. Then:
For any distance higher than some critical value, the colors will be taken from a standard colormap.
For any distance lower than some critical value, the colors will linearly go from white to the first color of the previously mentioned map.
The choices depend fully on what you want to show. The colormaps and their sizes depend on your problem. For example, you can choose different types of interpolation: linear, exponential, ...; single- or multi-color colormaps; etc..
The code:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
# create colormap
# ---------------
# create a colormap that consists of
# - 1/5 : custom colormap, ranging from white to the first color of the colormap
# - 4/5 : existing colormap
# set upper part: 4 * 256/4 entries
upper = mpl.cm.jet(np.arange(256))
# set lower part: 1 * 256/4 entries
# - initialize all entries to 1 to make sure that the alpha channel (4th column) is 1
lower = np.ones((int(256/4),4))
# - modify the first three columns (RGB):
# range linearly between white (1,1,1) and the first color of the upper colormap
for i in range(3):
lower[:,i] = np.linspace(1, upper[0,i], lower.shape[0])
# combine parts of colormap
cmap = np.vstack(( lower, upper ))
# convert to matplotlib colormap
cmap = mpl.colors.ListedColormap(cmap, name='myColorMap', N=cmap.shape[0])
# show some example
# -----------------
# open a new figure
fig, ax = plt.subplots()
# some data to plot: distance to point at (50,50)
x,y = np.meshgrid(np.linspace(0,99,100),np.linspace(0,99,100))
z = (x-50)**2. + (y-50)**2.
# plot data, apply colormap, set limit such that our interpretation is correct
im = ax.imshow(z, interpolation='nearest', cmap=cmap, clim=(0,5000))
# add a colorbar to the bottom of the image
div = make_axes_locatable(ax)
cax = div.append_axes('bottom', size='5%', pad=0.4)
cbar = plt.colorbar(im, cax=cax, orientation='horizontal')
# save/show the image
plt.savefig('so.png')
plt.show()
The result:
I am plotting a pie chart making background in the png image looks transparent. How can I make the center circle also looks transparent instead of the white color?
import matplotlib.pyplot as plt
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = 'Correct', 'Wrong'
sizes = [20, 80]
fig1, ax1 = plt.subplots()
ax1.pie(sizes,colors=['green','red'], labels=labels,autopct='%1.1f%%',
shadow=True, startangle=90)
centre_circle = plt.Circle((0,0),0.75,edgecolor='black',
facecolor='white',fill=True,linewidth=0.25)
fig1 = plt.gcf()
fig1.gca().add_artist(centre_circle)
ax1.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
fig1.savefig('foo.png', transparent=True)
The way you create the white middle part in the above code is by obfuscating the center of the pie by a circle. This can of course not procude a transparent interior.
A solution to this would also be found in the more sophisticated question Double donut chart in matplotlib. Let me go into detail:
In order to produce a true donut chart with a hole in the middle, one would need to cut the wedges such that they become partial rings. Fortunately, matplotlib provides the tools to do so. A pie chart consists of several wedges.
From the
matplotlib.patches.Wedge documentation we learn
class matplotlib.patches.Wedge(center, r, theta1, theta2, width=None, **kwargs)
Wedge shaped patch.
[...] If width is given, then a partial wedge is drawn from inner radius r - width to outer radius r.
In order to give set the width to all wedges, an easy method is to use plt.setp
wedges, _ = ax.pie([20,80], ...)
plt.setp( wedges, width=0.25)
Complete example:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
fig.set_facecolor("#fff9c9") # set yellow background color to see effect
wedges, text, autotext = ax.pie([25, 40], colors=['limegreen','crimson'],
labels=['Correct', 'Wrong'], autopct='%1.1f%%')
plt.setp( wedges, width=0.25)
ax.set_aspect("equal")
# the produced png will have a transparent background
plt.savefig(__file__+".png", transparent=True)
plt.show()
The following would be a way to tackle the problem if the Wedge did not have a width argument.
Since the pie chart is centered at (0,0), copying the outer path coordinates, reverting them and multiplying by some number smaller 1 (called r for radius in below code), gives the coordinates of the inner ring. Joining those two list of coordinates and taking care of the proper path codes allows to create a ring shape as desired.
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
import numpy as np
def cutwedge(wedge, r=0.8):
path = wedge.get_path()
verts = path.vertices[:-3]
codes = path.codes[:-3]
new_verts = np.vstack((verts , verts[::-1]*r, verts[0,:]))
new_codes = np.concatenate((codes , codes[::-1], np.array([79])) )
new_codes[len(codes)] = 2
new_path = mpath.Path(new_verts, new_codes)
new_patch = mpatches.PathPatch(new_path)
new_patch.update_from(wedge)
wedge.set_visible(False)
wedge.axes.add_patch(new_patch)
return new_patch
fig, ax = plt.subplots()
fig.set_facecolor("#fff9c9") # set yellow background color to see effect
wedges, text, autotext = ax.pie([25, 75], colors=['limegreen','indigo'],
labels=['Correct', 'Wrong'], autopct='%1.1f%%')
for w in wedges:
cutwedge(w)
# or try cutwedge(w, r=0.4)
ax.set_aspect("equal")
# the produced png will have a transparent background
plt.savefig(__file__+".png", transparent=True)
plt.show()
The problem is that you didnt really make a real donut chart. With this part of the code
centre_circle = plt.Circle((0,0),0.75,edgecolor='black',
facecolor='white',fill=True,linewidth=0.25)
you drew a circle in the middle of a pie chart. The problem is if you make this circle transparent you will once again see the middle of the pie chart. I recommend using a free photo editing program like pixlr to just make it transparent. Unless you can find a way to make a true donut chart which I unfortunantly do not know how to do it.
Similarly to the Double donut chart in matplotlib solution referenced by importanceofbeingearnest, you will need to use plt.setp(pie, width=width) to set the width of your pie chart, which will make it a true donut instead of a pie chart with a solid circle drawn on top.
import matplotlib.pyplot as plt
fig1, ax1 = plt.subplots()
ax1.axis('equal')
# Set the width of the pie slices;
# this is equivalent to (1.0-0.75), or
# (the radius of the pie chart - the radius of the inner circle)
width=0.25
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = ['Correct', 'Wrong']
sizes = [20., 80.]
# ax1.pie will return three values:
# 1. pie (the dimensions of each wedge of the pie),
# 2. labtext (the coordinates and text for the labels)
# 3. labpct (the coordinates and text of the "XX.X%"" labels)
pie, labtext, labpct = ax1.pie(x=sizes,
labels=labels,
colors=['green','red'],
startangle=90,
shadow=True,
autopct='%1.1f%%'
)
# apply "plt.setp" to set width property
plt.setp(pie, width=width)
# save the figure as transparent
fig1.savefig('foo.png', transparent=True)
I am using python 3.5.2
I would like to make a pie chart with an png image imbedded. I have pictures of certain bulk products that I would like to insert into the slices. For example strawberries in one slice and raspberries in another. Much like the picture http://www.python-course.eu/images/pie_chart_with_raspberries.png shows.
I can produce images and even plot images instead of points as demonstrated here Matplotlib: How to plot images instead of points?
However, I could not find any approach towards what I am proposing. I suppose it could be manually done in paint, but I was trying to avoid that.
That is sure possible. We can start with a normal pie chart. Then we would need to get the images into the plot. This is done using plt.imread and by using a matplotlib.offsetbox.OffsetImage. We would need to find good coordinates and zoom levels to place the image, such that it overlapps completely with respective pie wedge. Then the Path of the pie's wedge is used as a clip path of the image, such that only the part inside the wedge is left over. Setting the zorder of the unfilled wedge to a high number ensures the borders to be placed on top of the image. This way it looks like the wedges are filled with the image.
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
total = [5,7,4]
labels = ["Raspberries", "Blueberries", "Blackberries"]
plt.title('Berries')
plt.gca().axis("equal")
wedges, texts = plt.pie(total, startangle=90, labels=labels,
wedgeprops = { 'linewidth': 2, "edgecolor" :"k","fill":False, })
def img_to_pie( fn, wedge, xy, zoom=1, ax = None):
if ax==None: ax=plt.gca()
im = plt.imread(fn, format='png')
path = wedge.get_path()
patch = PathPatch(path, facecolor='none')
ax.add_patch(patch)
imagebox = OffsetImage(im, zoom=zoom, clip_path=patch, zorder=-10)
ab = AnnotationBbox(imagebox, xy, xycoords='data', pad=0, frameon=False)
ax.add_artist(ab)
positions = [(-1,0.3),(0,-0.5),(0.5,0.5)]
zooms = [0.4,0.4,0.4]
for i in range(3):
fn = "data/{}.png".format(labels[i].lower())
img_to_pie(fn, wedges[i], xy=positions[i], zoom=zooms[i] )
wedges[i].set_zorder(10)
plt.show()
I have two polygon patch plots with shading in grayscale, with each patch added to an axis. and I would like to add a colorbar underneath each subplot.
I'm using
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
poly=mpatches.Polygon(verts,color=rgb,ec='black') #create patch
ax.add_patch(poly) #add patch to axis
plt.set_cmap('gray') #grayscale colormap
#verts,rgb input created in above lines of code
The colorbars should be in grayscale and have range [0,1], with 0 being black, 0.5 marked, and 1 being white. im using subplot(121) and (122).
Thanks in advance.
To use colorbars you have to have some sort of ScalarMappable instance (this is used for imshow, scatter, etc.):
mappable = plt.cm.ScalarMappable(cmap='gray')
# the mappable usually contains an array of data, here we can
# use that to set the limits
mappable.set_array([0,1])
ax.colorbar(mappable)
I might be late here but now matplotlib has unique functionality to set colormap to gray as shown in this link.
plt.gray()
fig = plt.figure(figsize=(10, 10))
plt.subplots_adjust(left = 0, right = 1, top = 1, bottom = 0)
im = plt.imshow(output)
pos = fig.add_axes([0.93, 0.1, 0.02, 0.35]) # Set colorbar position in fig
fig.colorbar(im, cax=pos) # Create the colorbar
plt.savefig(os.path.join(args.output_path, image_name))
the line 1 will set your colormap to grayscale. the result is shown in the image.