Cannot save annotated image in matplotlib - python

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

Related

Add my own picture as marker in animation in Python [duplicate]

I would like to utilize customer markers in both scatter and line charts. How can I make custom marker out of a PNG file?
I don't believe matplotlib can customize markers like that. See here for the level of customization, which falls way short of what you need.
As an alternative, I've coded up this kludge which uses matplotlib.image to place images at the line point locations.
import matplotlib.pyplot as plt
from matplotlib import image
# constant
dpi = 72
path = 'smile.png'
# read in our png file
im = image.imread(path)
image_size = im.shape[1], im.shape[0]
fig = plt.figure(dpi=dpi)
ax = fig.add_subplot(111)
# plot our line with transparent markers, and markersize the size of our image
line, = ax.plot((1,2,3,4),(1,2,3,4),"bo",mfc="None",mec="None",markersize=image_size[0] * (dpi/ 96))
# we need to make the frame transparent so the image can be seen
# only in trunk can you put the image on top of the plot, see this link:
# http://www.mail-archive.com/matplotlib-users#lists.sourceforge.net/msg14534.html
ax.patch.set_alpha(0)
ax.set_xlim((0,5))
ax.set_ylim((0,5))
# translate point positions to pixel positions
# figimage needs pixels not points
line._transform_path()
path, affine = line._transformed_path.get_transformed_points_and_affine()
path = affine.transform_path(path)
for pixelPoint in path.vertices:
# place image at point, centering it
fig.figimage(im,pixelPoint[0]-image_size[0]/2,pixelPoint[1]-image_size[1]/2,origin="upper")
plt.show()
Produces:
Following on from Mark's answer. I just thought I would add to this a bit because I tried to run this and it does what I want with the exception of actually displaying the icons on the graph. Maybe something has changed with matplotlib. It has been 4 years.
The line of code that reads:
ax.get_frame().set_alpha(0)
does not seem to work, however
ax.patch.set_alpha(0)
does work.
The other answer may lead to problems when resizing the figure. Here is a different approach, positionning the images inside annotation boxes, which are anchored in data coordinates.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
path = "https://upload.wikimedia.org/wikipedia/commons/b/b5/Tango-example_icons.png"
image = plt.imread(path)[116:116+30, 236:236+30]
x = np.arange(10)
y = np.random.rand(10)
fig, ax = plt.subplots()
ax.plot(x,y)
def plot_images(x, y, image, ax=None):
ax = ax or plt.gca()
for xi, yi in zip(x,y):
im = OffsetImage(image, zoom=72/ax.figure.dpi)
im.image.axes = ax
ab = AnnotationBbox(im, (xi,yi), frameon=False, pad=0.0,)
ax.add_artist(ab)
plot_images(x, y, image, ax=ax)
plt.show()

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.

Matplotlib canvas as numpy array artefacts

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

Matplotlib - unable to save image in same resolution as original image

I am unable to save the image without the white borders and at the initial resolution (1037x627)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import pyplot, lines
import matplotlib.image as mpimg
from matplotlib.patches import Ellipse
x=[0,0,0,0,0]
y=[0,0,0,0,0]
a=10**1.3*15
inc=25
b=np.cos(np.radians(inc))*a
x[0],y[0]=516.667,313.021
x[1],y[1]=x[0]-a,y[0]
x[2],y[2]=x[0]+a,y[0]
x[3],y[3]=x[0],y[0]+b
x[4],y[4]=x[0],y[0]-b
for pa in range(0,10,5):
fig, ax = plt.subplots()
img=mpimg.imread('IC342.png')
imgplot = plt.imshow(img)
x[1],y[1]=x[0]-a/2*np.cos(np.radians(pa)),y[0]-a/2*np.sin(np.radians(pa))
x[2],y[2]=x[0]+a/2*np.cos(np.radians(pa)),y[0]+a/2*np.sin(np.radians(pa))
x[3],y[3]=x[0]+b/2*np.cos(np.radians(pa+90)),y[0]+b/2*np.sin(np.radians(pa+90))
x[4],y[4]=x[0]-b/2*np.cos(np.radians(pa+90)),y[0]-b/2*np.sin(np.radians(pa+90))
ell = Ellipse(xy=[516.667,313.021], width=a, height=b, angle=pa, edgecolor='b',lw=4, alpha=0.5, facecolor='none')
name='plt'+str(pa)+'.png'
leg='PA='+str(pa)
#ax.text(10, 10, leg, fontsize=15,color='white')
ax.add_artist(ell)
xn=[x[1],x[2],x[0]]
yn=[y[1],y[2],y[0]]
xnw=[x[3],x[4],x[0]]
ynw=[y[3],y[4],y[0]]
line = lines.Line2D(xn, yn, linestyle='-.',lw=5., color='r', alpha=0.4)
line1 = lines.Line2D(xnw, ynw, linestyle='-.',lw=5., color='g', alpha=0.4)
ax.add_line(line)
ax.add_line(line1)
plt.axis('off')
fig.savefig(name, transparent=True, bbox_inches='tight', pad_inches=0,dpi=150 )
initial image
Result
Also I need the white text PA=something to be on the image without changing the resolution. From what I understand adding another figure like text might automatically change the resolution.
Thank you for your time!
There are two factors at play here:
An Axes doesn't take up the entire Figure by default
In matplotlib, the Figure's size is fixed, and the contents are stretched/squeezed/interpolated to fit the figure. You want the Figure's size to be defined by its contents.
To do what you want to do, there are three steps:
Create a figure based on the size of the image and a set DPI
Add a subplot/axes that takes up the entire figure
Save the figure with the DPI you used to calculate figure's size
Let's use a random Hubble image from Nasa http://www.nasa.gov/sites/default/files/thumbnails/image/hubble_friday_12102015.jpg. It's a 1280x1216 pixel image.
Here's a heavily commented example to walk you through it:
import matplotlib.pyplot as plt
# On-screen, things will be displayed at 80dpi regardless of what we set here
# This is effectively the dpi for the saved figure. We need to specify it,
# otherwise `savefig` will pick a default dpi based on your local configuration
dpi = 80
im_data = plt.imread('hubble_friday_12102015.jpg')
height, width, nbands = im_data.shape
# What size does the figure need to be in inches to fit the image?
figsize = width / float(dpi), height / float(dpi)
# Create a figure of the right size with one axes that takes up the full figure
fig = plt.figure(figsize=figsize)
ax = fig.add_axes([0, 0, 1, 1])
# Hide spines, ticks, etc.
ax.axis('off')
# Display the image.
ax.imshow(im_data, interpolation='nearest')
# Add something...
ax.annotate('Look at This!', xy=(590, 650), xytext=(500, 500),
color='cyan', size=24, ha='right',
arrowprops=dict(arrowstyle='fancy', fc='cyan', ec='none'))
# Ensure we're displaying with square pixels and the right extent.
# This is optional if you haven't called `plot` or anything else that might
# change the limits/aspect. We don't need this step in this case.
ax.set(xlim=[-0.5, width - 0.5], ylim=[height - 0.5, -0.5], aspect=1)
fig.savefig('test.jpg', dpi=dpi, transparent=True)
plt.show()
The saved test.jpg will be exactly 1280x1216 pixels. Of course, because we're using a lossy compressed format for both input and output, you won't get a perfect pixel match due to compression artifacts. If you used lossless input and output formats you should, though.

Matplotlib text transparency

I was wondering if it is possible to change the transparency of a text in Matplotlib. set_alpha does not function and in the documentation I couldn't find anything relevant. Are there may be any workarounds?
I want to connect it to a pick_event.
EDIT:
I was actually trying to change the transparency of a legend-text. Although I tried to solve the issue with set_alpha, I have overseen that I was trying to modify the transparency of a list and hence I couldn't succeed. To sum up, as can be seen from answers, the transparency can be modified with set_alpha
You can set alpha when using annotate to add the text to your figure.
Before:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.annotate("TESTING", xy=(.5, .5), xytext=(.5, .5))
plt.show()
After:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
text = ax.annotate("TESTING", xy=(.5, .5), xytext=(.5, .5))
text.set_alpha(.4)
plt.show()
If you want to set alpha on the legend text, you should have said so:
ax.plot([1,2,3], [4,5,6], label='Null')
leg = ax.legend()
# print dir(leg) # inspection
for _txt in leg.texts:
_txt.set_alpha(0.3)
Side note: Because I can never remember where exactly to find things in the mpl docs, I inspected the legend object. Attribute texts sounded useful.

Categories