buffer_rgba() mysteriously adds whitespace to matplotlib figure - python

I have some simple code in a notebook to visualize an image with matplotlib
f = plt.figure()
plt.imshow(rgb_img)
# f.tight_layout(pad=0) doesn't fix the issue
f.canvas.draw()
# save figure as a np array for easy visualization w/ imshow later
fig_as_np_array = np.array(f.canvas.renderer.buffer_rgba())
At this point everything looks fine:
I then try to view the saved np array (plt.imshow(fig_as_np_array)) which I expect to display the same thing but instead I get odd whitespace plus a new sets of axis:
I can't for the life of me figure out what is adding the extra whitespace/axis, the shapes are slightly different as well:
print(f'rgb shape: {rgb_img.shape}') # prints: rgb shape: (480, 640, 3)
print(f'saved fig shape: {fig_as_np_array.shape}') # prints: saved fig shape: (288, 432, 4)
Any idea what is going on (fwiw I am visualizing this in a notebook). Thanks for your time

If I understood your question correctly, you'll have to ensure to create the figure with the correct dimensions and then remove the axes (via ax.set_axis_off()) and the frame of the figure around the image (via frameon=False) before writing to buffer, see the comments below:
import matplotlib as mpl
mpl.use("tkagg") # <— you may not need this,
# but I had to specify an agg backend manually
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
## image taken from
# "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Empty_road_at_night.jpg/1024px-Empty_road_at_night.jpg"
filename = "1024px-Empty_road_at_night.jpg"
im = mpimg.imread(filename)
## create the figure with the correct dpi & resolution
# and make sure that you specify to show "no frame" around the image
figure_dpi = 72
fig = plt.figure(figsize=(1024/figure_dpi,768/figure_dpi),dpi=figure_dpi,frameon=False,facecolor="w")
ax = fig.add_subplot()
## turn of axes, make imshow use the whole frame
ax.set_axis_off()
plt.subplots_adjust(top = 1, bottom = 0, right = 1, left = 0, hspace = 0, wspace = 0)
plt.margins(0,0)
## show image
ax.imshow(im,zorder=0,alpha=1.0,origin="upper")
## add some text label
ax.text(300,600,"this is the middle lane",fontsize=30,color="w")
def fig2rgb_array(fig):
"""adapted from: https://stackoverflow.com/questions/21939658/"""
fig.canvas.draw()
buf = fig.canvas.tostring_rgb()
ncols, nrows = fig.canvas.get_width_height()
print("to verify, our resolution is: ",ncols,nrows)
return np.frombuffer(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
## make a new figure and read from buffer
fig2,ax2 = plt.subplots()
ax2.imshow(fig2rgb_array(fig))
plt.show()
yields (note there is now only one set of axes around the image, not two):

Related

Keep original image data when saving to pdf

I have plots that I annotate using images:
def add_image(axe, filename, position, zoom):
img = plt.imread(filename)
off_img = matplotlib.offsetbox.OffsetImage(img, zoom = zoom, resample = False)
art = matplotlib.offsetbox.AnnotationBbox(off_img, position, xybox = (0, 0),
xycoords = axe.transAxes, boxcoords = "offset points", frameon = False)
axe.add_artist(art)
Then I save the figure to some pdf file, say fig.pdf. I expect the exact original image to be embedded in the resulting pdf, without resampling. However, the image is resampled according to the dpi parameter of savefig().
How can I force matplotlib to NOT resample the image (there is no point in doing that for a vector output anyway) ?
For more details, here is a simple example, using this image as image.png:
import numpy as np
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
def add_image(axe, filename, position, zoom):
img = plt.imread(filename)
off_img = matplotlib.offsetbox.OffsetImage(img, zoom = zoom, resample = False)
art = matplotlib.offsetbox.AnnotationBbox(off_img, position, xybox = (0, 0),
xycoords = axe.transAxes, boxcoords = "offset points", frameon = False)
axe.add_artist(art)
# ==========
fig = plt.figure()
axe = plt.axes()
fig.set_size_inches(3, 1.5)
axe.plot(np.arange(10), np.arange(10))
add_image(axe, "image.png", position = (0.2, 0.7), zoom = 0.07)
fig.savefig("temp.pdf", bbox_inches = "tight", pad_inches = 0)
Expected result:
Actual result:
EDIT: There is a bug/feature issue for this question
Just a quick summary of the discussion in https://github.com/matplotlib/matplotlib/issues/16268:
Passing the image through without resampling is indeed a desireable feature, mostly because for vector output, it should really be up to the renderer (e.g. pdf viewer, printer etc.) to determine the resolution.
The fact that matplotlib currently does not allow for this is mostly an oversight.
A workaround solution (a quick hack) is to add the following code before producing the figure:
from matplotlib.backends.backend_mixed import MixedModeRenderer
def _check_unsampled_image(self, renderer):
if isinstance(renderer, MixedModeRenderer):
return True
else:
return False
matplotlib.image.BboxImage._check_unsampled_image = _check_unsampled_image
This is not meant to be used in production code though, and a more robust solution needs to be implemented in a future matplotlib version. Contributions are welcome.

Pyplot saving blank figures

I need to save graphics in very high quality, like eps. Basically I need to save 4 images of a hyperspectral data. Showing the graphics is not a problem, so I know my figures are ok, but I can't save them.
I have already tried other formats, like jpg,png or pdf, and none of them worked. I also already tried to save 4 figures instead of one figure with 4 subplots, but the problem persisted. I changed also matplotlib's backend a lot of times, and none of them worked.
Here is my code:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(0)
RGB = np.random.randint(255, size=(3518,117,3))
var = RGB[:,:,0]
cmap = plt.cm.get_cmap('cividis')
col = 3
forma = "eps"
fig, ax = plt.subplots(1,col,figsize = (1.5,45))
plt.subplots_adjust(left = 2, right = 4)
im = ax[0].imshow(RGB.astype(np.uint8), cmap = cmap)
ax[1].pcolormesh(var, cmap = cmap)
ax[2].plot(np.mean(var,axis = 1),np.arange(var.shape[0]))
plt.colorbar(im)
fig.savefig("runnable" + "." + forma, format = forma,dpi=1200 )
plt.show()
I get a warning that I don't understand:
RunTimeWarning:"Mean of empty slice"
I've done some research and it seems like this is common when there is NaN in the data. However, I looked for it and didn't find any.
edit: I changed the code so it can be runnable.

Matplotlib transparent overlay & pdf transparency

Let's assume I have two numpy arrays (The ones I present are just examples):
import numpy as np
A = np.arange(144).reshape((12, 12))
np.random.shuffle(A)
B = np.ones((12,12))
B[0:10:4,:] = None
I want to plot A using imshow:
import matplotlib.pyplot as mplt
mplt.imshow(A, cmap = mplt.gray())
and overlay B so that the None areas are fully transparent and the one areas have an alpha of (e.g. alpha = 0.3.).
I already tried using something along the lines of:
mplt.imshow(B, cmap = mplt.get_cmap('Reds), alpha = 0.3)
but that does not work. Also tried to use masked arrays to create B, but cannot get my head around it. Any suggestions?
Thanks
EDIT:
I ended up using
my_red_cmap = mplt.cm.Reds
my_red_cmap.set_under(color="white", alpha="0")
which works like a charm (I tested Bill's solution as well, which also works perfectly).
If instead of None you use 0's for the transparent colors, you can take your favorite matplotlib colormap and add a transparent color at the beginning of it:
my_red_cmap = mplt.cm.Reds
my_red_cmap.set_under(color="white", alpha="0")
then you can just plot the array B with a global alpha of 0.3 whatever you want, using your custom color map, which will use a transparent white as its first value.
You can do the following:
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
x = np.arange(100).reshape(10, 10)
y = np.arange(-50, 150, 2).reshape(10, 10)
y[y<x] = -100 # Set bad values
cmap1 = cm.gray
cmap2 = cm.Reds
cmap2.set_under((1, 1, 1, 0))
params = {'interpolation': 'nearest'}
plt.imshow(x, cmap=cmap1, **params)
plt.show()
plt.imshow(y, cmap=cmap2, **params)
plt.show()
plt.imshow(x, cmap=cmap1, **params)
plt.imshow(y, cmap=cmap2, vmin=0, **params) # vmin > -100
plt.show()

Matplotlib.animation: how to remove white margin

I try to generate a movie using the matplotlib movie writer. If I do that, I always get a white margin around the video. Has anyone an idea how to remove that margin?
Adjusted example from http://matplotlib.org/examples/animation/moviewriter.html
# This example uses a MovieWriter directly to grab individual frames and
# write them to a file. This avoids any event loop integration, but has
# the advantage of working with even the Agg backend. This is not recommended
# for use in an interactive setting.
# -*- noplot -*-
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.animation as manimation
FFMpegWriter = manimation.writers['ffmpeg']
metadata = dict(title='Movie Test', artist='Matplotlib',
comment='Movie support!')
writer = FFMpegWriter(fps=15, metadata=metadata, extra_args=['-vcodec', 'libx264'])
fig = plt.figure()
ax = plt.subplot(111)
plt.axis('off')
fig.subplots_adjust(left=None, bottom=None, right=None, wspace=None, hspace=None)
ax.set_frame_on(False)
ax.set_xticks([])
ax.set_yticks([])
plt.axis('off')
with writer.saving(fig, "writer_test.mp4", 100):
for i in range(100):
mat = np.random.random((100,100))
ax.imshow(mat,interpolation='nearest')
writer.grab_frame()
Passing None as an arguement to subplots_adjust does not do what you think it does (doc). It means 'use the deault value'. To do what you want use the following instead:
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)
You can also make your code much more efficent if you re-use your ImageAxes object
mat = np.random.random((100,100))
im = ax.imshow(mat,interpolation='nearest')
with writer.saving(fig, "writer_test.mp4", 100):
for i in range(100):
mat = np.random.random((100,100))
im.set_data(mat)
writer.grab_frame()
By default imshow fixes the aspect ratio to be equal, that is so your pixels are square. You either need to re-size your figure to be the same aspect ratio as your images:
fig.set_size_inches(w, h, forward=True)
or tell imshow to use an arbitrary aspect ratio
im = ax.imshow(..., aspect='auto')
I searched all day for this and ended up using this solution from #matehat when creating each image.
import matplotlib.pyplot as plt
import matplotlib.animation as animation
To make a figure without the frame :
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
To make the content fill the whole figure
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
Draw the first frame, assuming your movie is stored in 'imageStack':
movieImage = ax.imshow(imageStack[0], aspect='auto')
I then wrote an animation function:
def animate(i):
movieImage.set_array(imageStack[i])
return movieImage
anim = animation.FuncAnimation(fig,animate,frames=len(imageStack),interval=100)
anim.save('myMovie.mp4',fps=20,extra_args=['-vcodec','libx264']
It worked beautifully!
Here is the link to the whitespace removal solution:
1: remove whitespace from image
In a recent build of matplotlib, it looks like you can pass arguments to the writer:
def grab_frame(self, **savefig_kwargs):
'''
Grab the image information from the figure and save as a movie frame.
All keyword arguments in savefig_kwargs are passed on to the 'savefig'
command that saves the figure.
'''
verbose.report('MovieWriter.grab_frame: Grabbing frame.',
level='debug')
try:
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
self.fig.savefig(self._frame_sink(), format=self.frame_format,
dpi=self.dpi, **savefig_kwargs)
except RuntimeError:
out, err = self._proc.communicate()
verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
err), level='helpful')
raise
If this was the case, you could pass bbox_inches="tight" and pad_inches=0 to grab_frame -> savefig and this should remove most of the border. The most up to date version on Ubuntu however, still has this code:
def grab_frame(self):
'''
Grab the image information from the figure and save as a movie frame.
'''
verbose.report('MovieWriter.grab_frame: Grabbing frame.',
level='debug')
try:
# Tell the figure to save its data to the sink, using the
# frame format and dpi.
self.fig.savefig(self._frame_sink(), format=self.frame_format,
dpi=self.dpi)
except RuntimeError:
out, err = self._proc.communicate()
verbose.report('MovieWriter -- Error running proc:\n%s\n%s' % (out,
err), level='helpful')
raise
So it looks like the functionality is being put in. Grab this version and give it a shot!
If you "just" want to save a matshow/imshow rendering of a matrix without axis annotation then newest developer version of scikit-video (skvideo) may also be relevant, - if you have avconv installed. An example in the distribution shows a dynamic image constructed from numpy function: https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py
Here is my modification of the example:
# Based on https://github.com/aizvorski/scikit-video/blob/master/skvideo/examples/test_writer.py
from __future__ import print_function
from skvideo.io import VideoWriter
import numpy as np
w, h = 640, 480
checkerboard = np.tile(np.kron(np.array([[0, 1], [1, 0]]), np.ones((30, 30))), (30, 30))
checkerboard = checkerboard[:h, :w]
filename = 'checkerboard.mp4'
wr = VideoWriter(filename, frameSize=(w, h), fps=8)
wr.open()
for frame_num in range(300):
checkerboard = 1 - checkerboard
image = np.tile(checkerboard[:, :, np.newaxis] * 255, (1, 1, 3))
wr.write(image)
print("frame %d" % (frame_num))
wr.release()
print("done")

What can I do about the overlapping labels in these subplots?

Below is a figure I created with matplotlib. The problem is pretty obvious -- the labels overlap and the whole thing is an unreadable mess.
I tried calling tight_layout for each subplot, but this crashes my ipython-notebook kernel.
What can I do to fix the layout? Acceptable approaches include fixing the xlabel, ylabel, and title for each subplot, but another (and perhaps better) approach would be to have a single xlabel, ylabel and title for the entire figure.
Here's the loop I used to generate the above subplots:
for i, sub in enumerate(datalist):
subnum = i + start_with
subplot(3, 4, i)
# format data (sub is a PANDAS dataframe)
xdat = sub['x'][(sub['in_trl'] == True) & (sub['x'].notnull()) & (sub['y'].notnull())]
ydat = sub['y'][(sub['in_trl'] == True) & (sub['x'].notnull()) & (sub['y'].notnull())]
# plot
hist2d(xdat, ydat, bins=1000)
plot(0, 0, 'ro') # origin
title('Subject {0} in-Trial Gaze'.format(subnum))
xlabel('Horizontal Offset (degrees visual angle)')
ylabel('Vertical Offset (degrees visual angle)')
xlim([-.005, .005])
ylim([-.005, .005])
# tight_layout # crashes ipython-notebook kernel
show()
Update:
Okay, so ImageGrid seems to be the way to go, but my figure is still looking a bit wonky:
Here's the code I used:
fig = figure(dpi=300)
grid = ImageGrid(fig, 111, nrows_ncols=(3, 4), axes_pad=0.1)
for gridax, (i, sub) in zip(grid, enumerate(eyelink_data)):
subnum = i + start_with
# format data
xdat = sub['x'][(sub['in_trl'] == True) & (sub['x'].notnull()) & (sub['y'].notnull())]
ydat = sub['y'][(sub['in_trl'] == True) & (sub['x'].notnull()) & (sub['y'].notnull())]
# plot
gridax.hist2d(xdat, ydat, bins=1000)
plot(0, 0, 'ro') # origin
title('Subject {0} in-Trial Gaze'.format(subnum))
xlabel('Horizontal Offset\n(degrees visual angle)')
ylabel('Vertical Offset\n(degrees visual angle)')
xlim([-.005, .005])
ylim([-.005, .005])
show()
You want ImageGrid (tutorial).
First example lifted directly from that link (and lightly modified):
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
import numpy as np
im = np.arange(100)
im.shape = 10, 10
fig = plt.figure(1, (4., 4.))
grid = ImageGrid(fig, 111, # similar to subplot(111)
nrows_ncols = (2, 2), # creates 2x2 grid of axes
axes_pad=0.1, # pad between axes in inch.
aspect=False, # do not force aspect='equal'
)
for i in range(4):
grid[i].imshow(im) # The AxesGrid object work as a list of axes.
plt.show()

Categories