Keep original image data when saving to pdf - python

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.

Related

buffer_rgba() mysteriously adds whitespace to matplotlib figure

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):

Why can't I add an image on top of my Matplotlib/Cartopy map?

I am working on a project that involves plotting data on a map with Cartopy.
Everything has been working so far, but I have been refactoring the code to make different functions callable by other parts of the program. So to that end, I have one function which adds my background to the map, and another which adds a placemarker at a specified lat/lon. Obviously, I want the placemarker above the background, but I can't seem to make it work.
For the background, I want to be able to use Cartopy stock images or web map tiles. The problem is the same either way, so I am using the Cartopy background for current testing purpose. Here is that function:
def custom_background(self, source_point):
cartmap = self.plot
source_point = source_point.split(" ")
source_point = (float(source_point[0]), float(source_point[1]))
dx = 2.5
dy = 5
pad = 0.5
lon_min, lon_max = source_point[0]-dx, source_point[0]+dx
lat_min, lat_max = source_point[1]-dy, source_point[1]+dy
area = 4*dx*dy
zoom = self.get_zoom(area) ##only relevant when using a map tile
cartmap.set_extent([lat_min-pad, lat_max+pad, lon_min-pad, lon_max+pad])
#~ cartmap.add_image(self.tile, zoom)
cartmap.add_feature(cartopy.feature.LAND, zorder=1)
return cartmap
Here is the placemark function:
def add_point_icon(self, x, y, cartmap):
src_point = np.array(Image.open('icons/icon63.png'))
im = OffsetImage(src_point, zoom=1, alpha=1.0, zorder=3)
ab = AnnotationBbox(im, (x,y), xycoords='data', frameon=False)
cartmap.add_artist(ab)
Both of these are called one after the other like so:
cartmap = self.custom_background(mysrc)
#~ cartmap=self.plot
self.add_point_icon(x1, y1, cartmap)
Results:
If I run the code as it is, this is how the map looks:
If I change it to (i.e. bypassing the function which draws the background):
#~ cartmap = self.custom_background(mysrc)
cartmap=self.plot
self.add_point_icon(x1, y1, cartmap)
Then I get:
Why can't I get the red "plus" sign to show up on top of the map? I've tried setting the "zorder" parameter of the different objects and it doesn't seem to do anything. I'm at a complete loss right now. Any help would be hugely appreciated, thanks.
Edit: perhaps I should also include the lines which create the subplot:
def __init__(self, mylevs):
self.fig, self.header, self.footer, self.plot, self.legend =
self.create_spec()
def create_spec(self):
"""Define layout of figure"""
#left column: header, footer, plt
fig = plt.figure(figsize=(12,10))
layout = 1
if layout == 1: #Default
widths = [8,1]
heights = [2, 10, 3]
column_border = 0.75
pad = 0.1
colorbar_width = 0.05
spec = gridspec.GridSpec(ncols=1, nrows=3, width_ratios = [1], height_ratios=heights, left=0.1, right = column_border)
#right column: colorbar
spec2 = gridspec.GridSpec(ncols=1, nrows=1, width_ratios = [1], height_ratios=[1], left=column_border+pad, right=column_border+pad+colorbar_width)
header = plt.subplot(spec[0,0])
footer = plt.subplot(spec[2,0])
plot = plt.subplot(spec[1,0], projection=cimgt.OSM().crs)
legend = plt.subplot(spec2[0,0])
return fig, header, footer, plot, legend
The problem was that in switching back to the Cartopy stock background from the map tiles, I forgot to switch the projection from ccrs.OSM() back to ccrs.PlateCarree(). In the map tile projection, the placemark was being plotted outside the viewing window.

matplotlib hatched contourf visibility depends on pdf reader

I'm struggling with the pdf backend of matplotlib and the contourf function. I try to plot forbidden areas on a 2D colored map. The forbidden areas are represented by hatched contourf with transparent (alpha=0.4) black color. the used code is given below, with two classes written to generate a user defined legend:
import matplotlib
print matplotlib.__version__
import matplotlib.patches as mpatches
class ConstrainedArea(object):
def __init__(self,axe,_xdata,_ydata,_zdata,boundaries,fc='none',ec='none',lw=None,alpha=None,hatch='//',ls='-',fill=False,label=None):
self.bnd = boundaries
self.fc = fc
self.ec = ec
self.lw = lw
self.ls = ls
self.al = alpha
self.hh = hatch
self.fl = fill
self.lb = label
self.ctr = axe.contour(_xdata,_ydata,_zdata,boundaries,linewidths=lw,colors=ec,linestyles=ls)
#self.ctf = axe.contourf(_xdata,_ydata,_zdata,boundaries,hatches=hatch,colors=fc,facecolors=fc,alpha=alpha)
self.ctf = axe.contourf(_xdata,_ydata,_zdata,boundaries,hatches=hatch,colors=fc,alpha=alpha,antialiased=False)
pass
class ConstrainedAreaHandler(object):
def legend_artist(self,legend,orig_handle,fontsize,handlebox):
x0,y0 = handlebox.xdescent,handlebox.ydescent
wi,he = handlebox.width,handlebox.height
patch = mpatches.Rectangle([x0,y0],wi,he,facecolor=orig_handle.fc,edgecolor=orig_handle.ec,hatch=orig_handle.hh,lw=orig_handle.lw,ls=orig_handle.ls,fill=orig_handle.fl,transform=handlebox.get_transform(),label=orig_handle.lb)
handlebox.add_artist(patch)
if __name__ == "__main__":
matplotlib.rcParams['backend'] = 'PDF'
import numpy,matplotlib.pyplot as plt
xs, ys = numpy.mgrid[0:30, 0:40]
zs = (xs - 15) ** 2 + (ys - 20) ** 2 + (numpy.sin(ys) + 10) ** 2
fig = plt.figure('test',figsize=(16.0,11.8875))
axe = fig.add_subplot(111)
pcm = axe.pcolormesh(xs,ys,zs,shading='gouraud')
cas = []
for bnd,hch,ls,lb in zip([[zs.min(),200],[400,zs.max()]],['/','\\'],['-','--'],[r'$f<200$',r'$f>400$']):
cas.append(ConstrainedArea(axe,xs,ys,zs,bnd,hatch=hch,fc='k',ec='k',lw=3,ls=ls,alpha=0.2,fill=False,label=lb))
cbr = fig.colorbar(pcm)
legframe = axe.legend(cas,[c.lb for c in cas],loc=3,handler_map={ConstrainedArea:ConstrainedAreaHandler()},ncol=3,fontsize=matplotlib.rcParams['font.size']*1.2**4,numpoints=1,framealpha=0.8)
#fig.savefig('test.pdf',bbox_inches='tight',facecolor='none',edgecolor='none',transparent=True)
fig.savefig('test.pdf',bbox_inches='tight',transparent=True)
After reading the tracks on matplotlib issues
GitHub matplotlib issue 3023, and
GitHub matplotlib issue 7421, I installed matplotlib 2.0.0 thinking it would solve my problem, but it didn't.
PROBLEM DEFINITION
Using the pdf backend I save the result as pdf, but reading the same file with evince, okular, or Acrobat Reader gives different screenshots, as illustrated on the figures below:
INFORMATION
The expected output is the one given by evince (visible hatches). As already mentioned in other tracks, the rasterization of the contourf object does give the expected result but I need vectorial images. Furthermore, if rasterized hatches are used with high dpi (>300), the hatch width tends to 0 yielding wrong output. Finally I found this track matplotlib generated PDF cannot be viewed with acrobat reader issue which yielded this workaround solution :
open the matplotlib output pdf file with evince
print it to pdf
vizualise the evince-printed output with okular
which gives the screenshot below:
Thanks a lot in advance for any explanation or solution for this problem. Don't hesitate if orther details/information are needed,
Tariq

Get text bounding box, independent of backend

I would like to get the bounding box (dimensions) around some text in a matplotlib figure. The post here, helped me realize that I can use the method text.get_window_extent(renderer) to get the bounding box, but I have to supply the correct renderer. Some backends do not have the method figure.canvas.get_renderer(), so I tried matplotlib.backend_bases.RendererBase() to get the renderer and it did not produce satisfactory results. Here is a simple example
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
fig = plt.figure()
ax = plt.subplot()
txt = fig.text(0.15,0.5,'afdjsklhvvhwd', fontsize = 36)
renderer1 = fig.canvas.get_renderer()
renderer2 = mpl.backend_bases.RendererBase()
bbox1 = txt.get_window_extent(renderer1)
bbox2 = txt.get_window_extent(renderer2)
rect1 = Rectangle([bbox1.x0, bbox1.y0], bbox1.width, bbox1.height, \
color = [0,0,0], fill = False)
rect2 = Rectangle([bbox2.x0, bbox2.y0], bbox2.width, bbox2.height, \
color = [1,0,0], fill = False)
fig.patches.append(rect1)
fig.patches.append(rect2)
plt.draw()
This produces the following plot:
Clearly the red box is too small. I think a Paul's answer here found the same issue. The black box looks great, but I cannot use the MacOSX backend, or any others that do not have the method figure.canvas.get_renderer().
In case it matters, I am on Mac OS X 10.8.5, Matplotlib 1.3.0, and Python 2.7.5
Here is my solution/hack. #tcaswell suggested I look at how matplotlib handles saving figures with tight bounding boxes. I found the code for backend_bases.py on Github, where it saves the figure to a temporary file object simply in order to get the renderer from the cache. I turned this trick into a little function that uses the built-in method get_renderer() if it exists in the backend, but uses the save method otherwise.
def find_renderer(fig):
if hasattr(fig.canvas, "get_renderer"):
#Some backends, such as TkAgg, have the get_renderer method, which
#makes this easy.
renderer = fig.canvas.get_renderer()
else:
#Other backends do not have the get_renderer method, so we have a work
#around to find the renderer. Print the figure to a temporary file
#object, and then grab the renderer that was used.
#(I stole this trick from the matplotlib backend_bases.py
#print_figure() method.)
import io
fig.canvas.print_pdf(io.BytesIO())
renderer = fig._cachedRenderer
return(renderer)
Here are the results using find_renderer() with a slightly modified version of the code in my original example. With the TkAgg backend, which has the get_renderer() method, I get:
With the MacOSX backend, which does not have the get_renderer() method, I get:
Obviously, the bounding box using MacOSX backend is not perfect, but it is much better than the red box in my original question.
If you would like to get the tight bounding box of a rotated text region, here is a possible solution.
# generate text layer
def text_on_canvas(text, myf, ro, margin = 1):
axis_lim = 1
fig = plt.figure(figsize = (5,5), dpi=100)
plt.axis([0, axis_lim, 0, axis_lim])
# place the left bottom corner at (axis_lim/20,axis_lim/20) to avoid clip during rotation
aa = plt.text(axis_lim/20.,axis_lim/20., text, ha='left', va = 'top', fontproperties = myf, rotation = ro, wrap=True)
plt.axis('off')
text_layer = fig2img(fig) # convert to image
plt.close()
we = aa.get_window_extent()
min_x, min_y, max_x, max_y = we.xmin, 500 - we.ymax, we.xmax, 500 - we.ymin
box = (min_x-margin, min_y-margin, max_x+margin, max_y+margin)
# return coordinates to further calculate the bbox of rotated text
return text_layer, min_x, min_y, max_x, max_y
def geneText(text, font_family, font_size, style):
myf = font_manager.FontProperties(fname=font_family, size=font_size)
ro = 0
if style < 8: # rotated text
# no rotation, just to get the minimum bbox
htext_layer, min_x, min_y, max_x, max_y = text_on_canvas(text, myf, 0)
# actual rotated text
ro = random.randint(0, 90)
M = cv2.getRotationMatrix2D((min_x,min_y),ro,1)
# pts is 4x3 matrix
pts = np.array([[min_x, min_y, 1],[max_x, min_y, 1],[max_x, max_y, 1],[min_x, max_y,1]]) # clockwise
affine_pts = np.dot(M, pts.T).T
#print affine_pts
text_layer, _, _, _, _ = text_on_canvas(text, myf, ro)
visualize_points(htext_layer, pts)
visualize_points(text_layer, affine_pts)
return text_layer
else:
raise NotImplementedError
fonts = glob.glob(fonts_path + '/*.ttf')
ret = geneText('aaaaaa', fonts[0], 80, 1)
The result looks like this: The first one is un-rotated, and the second one is rotated text region. The full code snippet is here.

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")

Categories