Matplotlib canvas as numpy array artefacts - python

I want to convert a matplotlib figure into a numpy array. I have been able to do this by accessing the contents of the renderer directly. However, when I call imshow on the numpy array it has what looks like aliasing artefacts along the edges which aren't present in the original figure.
I've tried playing around with various parameters but can't figure out how to fix the artefacts from imshow. The differences in the images remain if I save the figures to an image file.
Note that what I want to achieve is a way to confirm that the content of the array is the same as the figure I viewed before. I think probably these artefacts are not present in the numpy array but are created during the imshow call. Perhaps approriate configuration of imshow can resolve the problem.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
import math
fig = plt.figure(frameon=False)
ax = plt.gca()
ax.add_patch(Rectangle((0,0), 1, 1, angle=45, color="red"))
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
ax.set_aspect(1)
plt.axis("off")
fig.canvas.draw()
plt.savefig("rec1.png")
plt.show()
X = np.array(fig.canvas.renderer._renderer)
fig = plt.figure(frameon=False)
ax = plt.gca()
plt.axis("off")
plt.imshow(X)
plt.savefig("rec2.png")
plt.show()

These are clearly resampling artefacts, which can be avoided by using plt.figimage which specifically adds a non-resampled image to the figure.
plt.figimage(X)
plt.show()
Note that this will not work with the %matplotlib inline in Jupyter Notebook, but it does work fine with %matplotlib notebook and with GUI backends.

By adding the fig.tight_layout with padding of -1.08, I was able to get the exact image as the real image.
X = np.array(fig.canvas.renderer._renderer)
fig = plt.figure(frameon=False)
ax = plt.gca()
plt.axis("off")
plt.imshow(X)
fig.tight_layout(pad=-1.08)
plt.savefig("rec2.png")
plt.show()
Real Image
From numpy array
I hope that solves your problem, atleast till you find a better way. Cheers.

The best one I can think of is by using cv2 (openCV-python) library. My solution does require saving the image and in the case of color images, the decoded images will have the channels stored in B G R order.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
import math
import cv2 #import openCV
fig = plt.figure(frameon=False)
ax = plt.gca()
ax.add_patch(Rectangle((0,0), 1, 1, angle=45, color="red"))
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
ax.set_aspect(1)
plt.axis("off")
fig.canvas.draw()
plt.savefig("rec1.png")
plt.show()`
im = cv2.imread("rec1.png")
print(type(im)) #prints numpy.ndarray
cv2.imshow("pic",im) #creates a window named pic, loads im
cv2.waitKey(0) #has no time limit, window destroyed on any key press
cv2.destroyAllWindows()
Final result looks like
Since it is a numpy array, you can call methods on it for your comparison.
print(im.shape) #prints (288, 432, 3)

The image that is shown in the second plot is plotted smaller than the first image; the reason is that the complete first figure's image is squeezed into a newly created smaller axes -- this would be obvious when not turning the axes off.
In order to make sure the second figure only shows the image itself, you may adjust the margins, such that there is no spacing between the figure edge and the axes, using subplots_adjust.
fig = plt.figure(frameon=False)
fig.subplots_adjust(0,0,1,1)
ax = plt.gca()
plt.axis("off")
plt.imshow(X)
This produces the desired plot.
Note however that the array is not exactly the same due to antialiasing being applied when saving the png file. You may find out via
X = np.array(fig.canvas.renderer._renderer)/255.
Y = plt.imread("rec1.png")
print(np.all(X==Y))
## This prints False
Inversely speaking, if you want to have the same numpy array as the one that is saved, you should make sure to use the saved image itself.
plt.savefig("rec1.png")
X = plt.imread("rec1.png")
# use X from here onwards

Thanks to the comments who pointed out interpolation as the cause. I found the following code (adapted for Python 3) which displays the image in the way I want; identical to the first image but via the numpy array.
import PIL.Image
from io import BytesIO
import IPython.display
import numpy as np
def showarray(a, fmt='png'):
a = np.uint8(a)
f = BytesIO()
PIL.Image.fromarray(a).save(f, fmt)
IPython.display.display(IPython.display.Image(data=f.getvalue()))
source: https://gist.github.com/kylemcdonald/2f1b9a255993bf9b2629

Related

plt.savefig() partially crops subscript character from colorbar label, despite using bbox_inches="tight"

I have a figure which contains a labelled colourbar below the x axis of the main plot. When I attempt to save this using plt.savefig(), the very bottom of the subscript character in the label is cropped from the saved image, like this, despite using bbox_inches="tight". However, if I simply save the figure manually in the pop-up window, the subscript character is not cropped, like this.
Although the latter image could be manually cropped, or cropped using additional lines in the code, I would be grateful for any advice on how to resolve this issue without the need for this additional work.
I have tried to add a line break to the colourbar label like so:
label="$U/U_{"+(u"\u221e")+"}$\n"
But this simply adds white space below the label; the bottom of the subscript character is still cropped.
I have also tried to add the line:
cb.set_label(label,labelpad=5)
But this simply offsets the label from the bottom of the colourbar; no additional padding is provided below the label to fully display the subscript character.
The code is below:
import numpy
import random
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import matplotlib.colors as mcolors
import matplotlib.colorbar as cbar
from matplotlib import cm
##########################################################
# Centre colourmap so 0=white
class MidpointNormalize(mpl.colors.Normalize):
def __init__(self,vmin=None,vmax=None,midpoint=None,clip=False):
self.midpoint=midpoint
mpl.colors.Normalize.__init__(self,vmin,vmax,clip)
def __call__(self,value,clip=None):
x,y=[self.vmin,self.midpoint,self.vmax],[0,0.5,1]
return numpy.ma.masked_array(numpy.interp(value,x,y),numpy.isnan(value))
##########################################################
# Set min and max values
xymin=0
xymax=10
valmin=-5
valmax=5
val=numpy.zeros((xymax,xymax),dtype=float)
# Configure plot
fig,ax=plt.subplots()
ax.set_xlim([xymin,xymax])
ax.set_ylim([xymin,xymax])
# Configure colour bar
colours=plt.cm.RdBu(numpy.linspace(0,1,256))
colourmap=mcolors.LinearSegmentedColormap.from_list('colourmap',colours)
normalisecolors=mpl.colors.Normalize(vmin=valmin,vmax=valmax)
scalecolors=cm.ScalarMappable(norm=normalisecolors,cmap=colourmap)
label="$U/U_{"+(u"\u221e")+"}$"
for ix in range(xymin,xymax):
for iy in range(xymin,xymax):
xlow=ix*+1 # Calculate vertices of patch
xhigh=(ix*1)+1
ylow=iy*1
yhigh=(iy*1)+1
val[ix][iy]=random.randint(valmin,valmax) # Patch value
rgbacolor=scalecolors.to_rgba(val[ix][iy]) # Calculate RGBA colour for value
ax.add_patch(patches.Polygon([(xlow,ylow),(xlow,yhigh),(xhigh,yhigh),(xhigh,ylow)],fill=True,facecolor=rgbacolor)) # Add value as polygon patch
cax,_=cbar.make_axes(ax,orientation="horizontal")
cb=cbar.ColorbarBase(cax,cmap=colourmap,norm=MidpointNormalize(midpoint=0,vmin=valmin,vmax=valmax),orientation="horizontal",label=label)
plt.savefig("C:/Users/Christopher/Desktop/test.png",dpi=1200,bbox_inches="tight")
plt.clf
plt.close()
I'm afraid I don't really have a good answer for you. This appears to be related to this bug https://github.com/matplotlib/matplotlib/issues/15313
The good news is that it is being worked on, the bad news is that there is no fix as of yet.
Two points to consider anyway (based on reading the thread on github):
the higher the dpi, the worst it is. So you may want to save at a lower dpi (300 works fine for me)
the problem is not present on the pdf backend, so you could save your plot in pdf (and eventually convert to png if needed)
BTW (this is unrelated to the bug in question): I'm confused by the complexity of your code. It seems to me the following code produces the same output:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
N=10
valmin=-5
valmax=5
valmid=0
val=np.random.randint(low=valmin, high=valmax, size=(N,N))
cmap = 'RdBu'
norm = TwoSlopeNorm(vcenter=valmid, vmin=valmin, vmax=valmax)
label="$U/U_{"+(u"\u221e")+"}$"
# Configure plot
fig, ax=plt.subplots()
im = ax.imshow(val, cmap=cmap, norm=norm, aspect='auto', origin='lower')
cbar = fig.colorbar(im, orientation='horizontal', label=label)
fig.savefig('./test-1200.png',dpi=1200,bbox_inches="tight") # subscript is cut
fig.savefig('./test-300.png',dpi=300,bbox_inches="tight") # subscript is not cut
fig.savefig('./test-pdf.pdf',dpi=1200,bbox_inches="tight") # subscript is not cut
1200 dpi:
300 dpi:
pdf:

Display a colorbar without associated image map

I have matplotlib figure that displays an image, histogram and a colobar under the histogram, generated by the following code:
import numpy as np
import imageio
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
image = imageio.imread('imageio:camera.png')
fig, axes = plt.subplots(2,1,figsize=(8,4))
divider = make_axes_locatable(axes[1])
cbar_ax = divider.append_axes("bottom", size='15%', pad=0.02)
im = axes[0].imshow(image,cmap='gray')
values, bins, patches = axes[1].hist(image.ravel(), 255, color='#cccccc', density=True)
cbar = plt.colorbar(im, cax=cbar_ax, orientation='horizontal')
fig.savefig('test.png', dpi=300)
plt.show()
This code generates exactly this image below
Since it's a recurrent image (I'm studying the histogram), I would like to create a figure just displaying the histogram and the associated colorbar below it.
But to display a colorbar is mandatory provides a image_map as argument and I need to call ax.imshow() to have a image_map. The exact output I want is something like
And I don't know how to achieve that.
Of course I could edit my images in some editor (as I did), but this isn't acceptable, since every update takes me a huge effort editing many image.

Cannot save annotated image in matplotlib

I have read every SO thread I could find on this topic, and have read the documentation rather extensively, and even copied and pasted code from these questions/tutorials. But I am still unable to load a jpg, annotate it, and save the figure with matplotlib. I really could use some advice on this topic.
Here is an example of one of my many attempts:
import cv2
import matplotlib.pyplot as plt
image = cv2.imread(...filepath-to-img...)
fig, ax = plt.subplots()
ax.imshow(image)
ax.add_patch(plt.Rectangle(...params...)
plt.savefig(...filepath...)
The image loads correctly, and when I work interactively and run the plt.subplots( and ax.imshow(image) commands, I see a plot with the image pop up, and I get a note saying that it is an AxesImage object.
But when I got to save, it says 'Figure size 432x288 with 0 Axes,' and the resulting image saved to disk is blank.
I've also tried the following for saving to no avail.
my_fig = plt.cgf()
my_fig.savefig(...filepath...)
Basically, it seems that creating a figure and axes, and calling ax.imshow() is not adding the image to my axes, nor are the ax.add_patch() calls doing anything to the axes.
I've also tried it without creating separate axes, as with:
plt.figure()
plt.imshow(image)
my_axes = plt.gca()
my_axes.add_patch(plt.Rectangle(...params...)
plt.savefig(...filepath...)
Again, the resulting figure is blank and has 0 axes.
I know I'm probably missing an obvious step, but I can't figure out what it is, and even copying and pasting code has been no help.
Edit: Adding complete code in response to comment
import cv2
from matplotlib import pyplot as plt
img = './1.png' # 364x364
image = cv2.imread(img)
fig, ax = plt.subplots()
ax.imshow(image)
color = (1, 0, 0, 1)
ax.add_patch(plt.Rectangle((139, 25), 85, 336,
color = color,
fill = False,
linewidth = 2))
plt.savefig('./annotated.png')
I also faced the same problem and here is what worked for me.
from matplotlib.patches import Rectangle
fig,ax = plt.subplots(figsize=(15,12))
ax.imshow(frames[0])
x,y,w,h = bboxes[0]
ax.add_patch(Rectangle((x,y),w,h, linewidth=3, edgecolor='r', facecolor='none'))
plt.axis('off')
plt.savefig("out.png",bbox_inches='tight',pad_inches=0)
plt.show()
I was also getting a blank image on disk when plt.show() was written before plt.savefig()

How to set labels with MATPLOTLIB?

I have my image
Then I try this
#!/usr/bin/python
import os,sys
import Image
import matplotlib.pyplot as plt
jpgfile = Image.open("t002.jpg")
fig = plt.imshow(jpgfile)
ax = fig.add_subplot(111)
ax.set_xlabel('normlized resistivities')
ay.set_ylabel('normlized velocities')
fig.savefig("fig.jpg")
But then I have
AttributeError: 'AxesImage' object has no attribute 'add_subplot'
How to setup xlabel and ylabel and then save new image as a file?
It should be enough to simply do
plt.figure()
plt.imshow(jpgfile)
plt.xlabel('normlized resistivities')
plt.ylabel('normlized velocities')
plt.savefig('out.jpg')
Your current error is because imshow does not return a Figure.
I guess the best way is to actually plot the image, and only show the axis labels (hide axis and ticks)
This reply may be the way to go
You could just use plt.xlabel('xlabel')
Hannes's answer works fine, but sometimes you need the ax object, so I tend to do this sort of thing:
import os,sys
import Image
import matplotlib.pyplot as plt
jpgfile = Image.open("t002.jpg")
# Set up the figure and axes.
fig = plt.figure(figsize=(12,8)) # ...or whatever size you want.
ax = fig.add_subplot(111)
# Draw things.
plt.imshow(jpgfile)
ax.set_xlabel('normalized resistivities')
ax.set_ylabel('normalized velocities')
# Save and show.
plt.savefig("fig.jpg")
plt.show()
By the way, I recommend saving as PNG for figures containing text and other elements with fine detail. And setting a high dpi for the saved figure.

Creating a rotatable 3D earth

I know we can create simple 3-Dimensional spheres using matplotlib, an example of such a sphere is included in the documentation.
Now, we also have a warp method as part of the matplotlib module, an example of it's usage is here .
To warp a cylindrical image to the sphere. Is it possible to combine these methods to create a 3D rotatable earth? Unless my way of thinking about this problem is way off it seems that to be able to do this you would have to take the pixel data of the image and then plot every pixel using the sin and cosine expressions along the surface of the 3D sphere being created in the first example. Some examples of these cylindrical maps can be found here
I know alternative ways to do this are through maya and blender, but I am attempting to stay within matplotlib to do this, as I want to create this plot and then be able to plot geospatial data to the surface using an array of data.
Interesting question. I tried to basically follow the thinking outlined by #Skeletor, and map the image so that it can be shown with plot_surface:
import PIL
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
# load bluemarble with PIL
bm = PIL.Image.open('bluemarble.jpg')
# it's big, so I'll rescale it, convert to array, and divide by 256 to get RGB values that matplotlib accept
bm = np.array(bm.resize([d/5 for d in bm.size]))/256.
# coordinates of the image - don't know if this is entirely accurate, but probably close
lons = np.linspace(-180, 180, bm.shape[1]) * np.pi/180
lats = np.linspace(-90, 90, bm.shape[0])[::-1] * np.pi/180
# repeat code from one of the examples linked to in the question, except for specifying facecolors:
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
x = np.outer(np.cos(lons), np.cos(lats)).T
y = np.outer(np.sin(lons), np.cos(lats)).T
z = np.outer(np.ones(np.size(lons)), np.sin(lats)).T
ax.plot_surface(x, y, z, rstride=4, cstride=4, facecolors = bm)
plt.show()
Result:
Here what I made some hours ago:
First we import the needed libraries:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import imageio
Secondly, we make the figures and stored them as png in our directory:
Note that I wrote range(0,330,20)
for i in range(0,330,20):
my_map = Basemap(projection='ortho', lat_0=0, lon_0=i, resolution='l', area_thresh=1000.0)
my_map.bluemarble()
my_map.etopo()
name=str(i)
path='/path/to/your/directory/'+name
plt.savefig(path+'.png')
plt.show()
plt.clf()
plt.cla()
plt.close()
And finally we can join all the images in an animated GIF:
images = []
for f in range(0,330,20):
images.append(imageio.imread("/path/to/your/directory/"+str(f)+".png"))
imageio.mimsave('movie.gif', images, duration=0.5)
and then enjoy the result:
I could imagine the following solution:
Using numpy.roll you could shift your array by one column (ore more) with each call. So you could load your image of the earth surface into a numpy array as a template and export the rotated image into a jpg. This you plot as shown in the warp example.

Categories