Reputation: 4005
I have a bit of a situation. What I need is a plot with a black background with several white circles drawn on top of that black background.
I managed to do this using the following code:
import numpy
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, aspect = "equal", axisbg = "black")
ax.add_artist(plt.Circle((0., 0., .5), color = "white"))
plt.xlim(-5, 5)
plt.ylim(-5, 5)
fig.savefig("test.png", dpi = 300)
plt.show()
This produces the following result:
Now, what I would like to do is make this image transparent. So what this means is that only the white circle should become transparent. You might already be able to see the problem arising because if I would set transparent = True
. The black background automatically becomes transparent and I lose the colour black from my figure.
Another thing I tried is to not set transparent = True
in savefig
but to actually set the option alpha = 0.
in plt.Circle
. This makes the white circle actually transparent which is the end goal. However, because it is transparent I am left with an entire black background. Any ideas to solve this problem?
To summarize my goal:
I want to save a transparent version of the figure in which the white circle is transparent while the black parts are not.
I know I can use different programs such as inkscape
and gimp
to create what I want. However, I really need to do it within python as well due to other operations I need to perform.
Thank you!
Upvotes: 6
Views: 4208
Reputation: 1779
Edit 3:
It has been clarified that the underlying question is:
how to put a 'black & transparent' mask in front of a matplotlib image produced by imshow
?
The mask shall result from a matplotlib previously drawn black & white figure.
The following code demonstrate this feature by accessing and mixing the figure rgba bitmaps:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.mlab as mlab
def get_rgba_bitmap(fig):
fig.canvas.draw()
tab = fig.canvas.copy_from_bbox(fig.bbox).to_string_argb()
ncols, nrows = fig.canvas.get_width_height()
return np.fromstring(tab, dtype=np.uint8).reshape(nrows, ncols, 4)
def black_white_to_black_transpa(rgba):
rgba[:, :, 3] = 255 - rgba[:, :, 0]
rgba[:, :, 0:3] = 0
def over(rgba1, rgba2):
if rgba1.shape != rgba2.shape:
raise ValueError("rgba1 and rgba2 shall have same size")
alpha = np.expand_dims(rgba1[:, :, 3] / 255., 3)
rgba = np.array(rgba1 * alpha + rgba2 * (1.-alpha), dtype = np.uint8)
return rgba[:, :, 0:3]
# fig 1)
fig1 = plt.figure(facecolor = "white")
fig1.set_dpi(300)
ax1 = fig1.add_subplot(1, 1, 1, aspect = "equal", axisbg = "black")
ax1.add_artist(plt.Circle((0., 0., .5), color = "white"))
ax1.set_xlim(-5, 5)
ax1.set_ylim(-5, 5)
bitmap_rgba1 = get_rgba_bitmap(fig1)
black_white_to_black_transpa(bitmap_rgba1)
# fig 2
fig2 = plt.figure(facecolor = "white")
fig2.set_dpi(300)
delta = 0.025
ax2 = fig2.add_subplot(1, 1, 1, aspect = "equal", axisbg = "black")
ax2.set_xlim(-5, 5)
ax2.set_ylim(-5, 5)
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
im = ax2.imshow(Z, interpolation='bilinear', cmap=cm.jet,
origin='lower', extent=[-5, 5, -5, 5],
vmax=abs(Z).max(), vmin=-abs(Z).max())
bitmap_rgba2 = get_rgba_bitmap(fig2)
# now saving the composed figure
fig = plt.figure()
fig.patch.set_alpha(0.0)
ax = fig.add_axes([0., 0., 1., 1.])
ax.patch.set_alpha(0.0)
ax.imshow(over(bitmap_rgba1, bitmap_rgba2))
plt.axis('off')
fig.savefig("test_transpa.png", dpi=300)
plt.show()
Giving:
I tested with your initial photonic test case and the pic quality seems OK
Now if you want the figure background transparent too:
fig1 = plt.figure(facecolor='white')
, as white will become transparent when passed to black_white_to_black_transpa
fig2.patch.set_alpha(0.0)
as it will be stored with no modification into bitmap_rgba2
bitmap_rgba1
and bitmap_rgba2
inside over
function (see below a possible modification)def over(rgba1, rgba2): if rgba1.shape != rgba2.shape: raise ValueError("rgba1 and rgba2 shall have same size") alpha1 = np.expand_dims(rgba1[:, :, 3] / 255., axis=3) alpha2 = np.expand_dims(rgba2[:, :, 3] / 255., axis=3) alpha = 1. - (1.-alpha1) * (1.-alpha2) C1 = rgba1[:, :, 0:3] C2 = rgba2[:, :, 0:3] C = (alpha1 * C1 + (1-alpha1) * alpha2 * C2) / alpha rgba = np.empty_like(rgba1, dtype = np.uint8) rgba[:, :, 0:3] = C rgba[:, :, 3] = 255 * alpha[:, :, 0] return rgba
last (?) edit:
It seems there is an inconsistence between the array returned byto_string_argb
and the one expected by imshow
(order of the rgb channels). A possible solution is to change ax.imshow(over(bitmap_rgba1, bitmap_rgba2))
to:
over_tab = over(bitmap_rgba1, bitmap_rgba2)
over_tab[:, :, 0:3] = over_tab[:, :, ::-1][:, :, 1:4]
ax.imshow(over_tab)
Upvotes: 5
Reputation: 16269
Colormaps can have an alpha channel, so if your data is on a mesh with high vs low values showing circle vs not-circle, one set of those values can be transparent.
This only works for me when saving the figure programmatically, with the transparent
keyword; not from the Python image window.
Starting from one of the matplotlib gallery examples (in a gimp-alike, I can cut & paste segments and the transparency is right):
# plot transparent circles with a black background
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
from matplotlib.cm import Greys
dark_low = ((0., 1., 1.),
(.3, 1., 0.),
(1., 0., 0.))
cdict = {'red': dark_low,
'green': dark_low,
'blue': dark_low}
cdict3 = {'red': dark_low,
'green': dark_low,
'blue': dark_low,
'alpha': ((0.0, 0.0, 0.0),
(0.3, 0.0, 1.0),
(1.0, 1.0, 1.0))
}
greys = LinearSegmentedColormap('Greys', cdict)
plt.register_cmap(cmap=greys)
dropout_high = LinearSegmentedColormap('Dropout', cdict3)
plt.register_cmap(cmap = dropout_high)
# Make some illustrative fake data:
x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2*np.pi, 0.1)
X, Y = np.meshgrid(x,y)
Z = np.cos(X) * np.sin(Y) * 10
# Make the figure:
plt.figure()
plt.subplot(1,3,1)
plt.imshow(Z, cmap=Greys)
plt.title('Smooth\ncolorbar')
plt.colorbar()
plt.subplot(1,3,2)
plt.imshow(Z, cmap=greys)
plt.title('Linear\ncolorbar')
plt.colorbar()
plt.subplot(1,3,3)
plt.imshow(Z, cmap = dropout_high)
plt.title('Alpha crops\n colorbar')
plt.colorbar()
plt.savefig('dropout_cmap', transparent=True)
And as a layer over another image. Interesting, the colorbar with alpha channel doesn't have transparency. That seems like a bug.
Upvotes: 1
Reputation: 509
This might not be the answer you are looking for but it gives the picture you wanted! I think you want to fill the areas outside the circle!(s) with black and leave the background transparent, rather than the other way around. It's trivial to calculate the boundaries of a single circle and use fill_between
. Doing it for multiple circles might be trickier!
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, aspect = "equal", )
# A Circle
xy=(1,1); r=3
# more points is smoother
xdata=np.linspace(-5,5,1001)
ydata=np.linspace(-5,5,1001)
# circle edges (top and bottom)
c1=np.sqrt((xy[0]**2-xdata**2)+r**2)+xy[1]
c2=-np.sqrt((xy[0]**2-xdata**2)+r**2)+xy[1]
c1=np.where(np.isnan(c1),xy[0],c1)
c2=np.where(np.isnan(c2),xy[0],c2)
ax.fill_between(xdata,5,c1,color='black')
ax.fill_between(xdata,-5,c2,color='black')
plt.xlim(-5, 5)
plt.ylim(-5, 5)
fig.savefig("test.png", dpi = 300, transparent=True)
Transparent circle with center 1,1 and radius 3
Upvotes: 0