I'm trying to produce an image from an array using imshow, and export it to file without having any whitespace added.
In the case in which the data has equal width and height I managed to achieve this by following this answer:
import numpy as np
import matplotlib.pyplot as plt
def borderless_imshow_save(data, outputname, size=(1, 1), dpi=80):
fig = plt.figure()
fig.set_size_inches(size)
ax = plt.Axes(fig, [0, 0, 1, 1])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(data);
plt.savefig(outputname, dpi=dpi)
data = np.random.randn(40, 40)
borderless_imshow_save(data, 'test.png', dpi=100)
This works perfectly.
However, I actually need to do this for data that is rectangular, that is, something like np.random.randn(40, 100).
In this case, the code above does not work, as again whitespace is produced in the final image.
I tried playing with the size parameter and the arguments of plt.Axes but without success.
What's the best way to achieve this?
Note that imsave actually works here with something like
plt.imsave('test.png', np.random.randn(40, 100))
the problem with this is that with imsave I do not have access to same amount of options I have with imshow.
The problem is you are specifying a square figure size with size=(1,1) and then plotting a rectangular image. I have modified your code to eliminate the white space around the figure by automatically setting the figure size to match the dimensions of the input data. The size parameter now specifies the width of the image, and the height is scaled from that:
import numpy as np
import matplotlib.pyplot as plt
def borderless_imshow_save(data, outputname, size = 1, dpi=80):
width = 1*size
height = data.shape[0] / data.shape[1] * size
size=(width, height)
fig = plt.figure(figsize=size, dpi=dpi)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_axis_off()
ax.imshow(data);
fig.savefig(outputname, dpi=dpi)
data = np.random.randn(40, 100)
borderless_imshow_save(data, 'test.png', size=5, dpi=100)
Saved image:
An easy option is to not care about the actual size of the figure and just crop the image automatically while saving.
import numpy as np
import matplotlib.pyplot as plt
data = np.random.randn(40, 100)
fig, ax = plt.subplots()
ax.imshow(data)
ax.set_axis_off()
fig.savefig("data.png", bbox_inches="tight", pad_inches=0)
Related
There are a lot of posts on plotting pre-saved png images as scatter plot markers but I'm wondering if there's a way to take an array of 2D arrays (lets naively call them images) and, given x and y coordinates, use them as scatter markers without having to save out as pngs and then read in.
For example, say you have these rather dull 'images':
import numpy as np
images = np.random.uniform(0,1,(5,10,10))
... that is, we have 5 lots of 10 by 10, 2D images.
If we want to plot these 'images' as markers at the following 5 locations specified by x and y coordinates:
x, y, = np.array([0, 2, -3, 6, 6.5]), np.array([10, 3, -2, -1, 0.2])
... what is the best way to go about doing this?
Closest example I have tried but failed to make work:
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from matplotlib.offsetbox import AnnotationBbox, OffsetImage
# Convert the images into PIL images
# How? Using: https://stackoverflow.com/a/62144323/8188120
# Why? Maybe PIL images are nice and easy for plotting as markers.. I'm grasping at straws really
pil_images = []
for i in range(images.shape[0]):
pil_images.append(Image.fromarray(np.uint8(images[i])).convert('RGB'))
# Try plotting the images as markers
# Why this method? Saw it in this thread and continued grasping for more straws: https://stackoverflow.com/a/53851017/8188120
fig, ax = plt.subplots()
for i in range(len(pil_images)):
ab = AnnotationBbox(OffsetImage(pil_images[i]), x[i], y[i], frameon=False)
ax.add_artist(ab)
fig.savefig(image_path + 'funny_markers.png')
plt.close('all')
You need to set the limits of the axis accordingly, or they would default to just (0,1):
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
fig, ax = plt.subplots()
for x0, y0, img in zip(x, y, images):
ab = AnnotationBbox(OffsetImage(img, zoom=5, cmap='gray'), (x0, y0), frameon=False)
ax.add_artist(ab)
plt.xlim(x.min(), x.max()+1)
plt.ylim(y.min(), y.max()+1)
plt.show()
Output:
This code:
import matplotlib.pyplot as plt
fig = plt.figure(facecolor=(1, 0, 0, .1))
ax = fig.add_subplot(1,1,1)
plt.show()
Outputs a pink frame for the figure, with a white frame within it for the axes-object. Now, is it possible to change the size of the axes-object within the figure, without changing the figure size?
Try this code and see if this is what you want. You could change the fixed size values given inside the function.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import Divider, Size
from mpl_toolkits.axes_grid1.mpl_axes import Axes
def demo_fixed_size_axes():
fig = plt.figure(facecolor=(1, 0, 0, .1))
# The first items are for padding and the second items are for the axes.
# sizes are in inch.
h = [Size.Fixed(1.0), Size.Fixed(4.5)]
v = [Size.Fixed(0.7), Size.Fixed(5.)]
divider = Divider(fig, (0.0, 0.0, 1., 1.), h, v, aspect=False)
# the width and height of the rectangle is ignored.
ax = Axes(fig, divider.get_position())
ax.set_axes_locator(divider.new_locator(nx=1, ny=1))
fig.add_axes(ax)
# ax.plot([1, 2, 3])
plt.show()
demo_fixed_size_axes()
I am plotting a collection of rectangles with matplotlib.patches. My code is:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig = plt.figure(figsize=(14, 10))
for i in rectangles_list:
ax1 = fig.add_subplot(111, aspect='equal')
ax1.add_patch(patches.Rectangle(
(x[i], y[i]),
width[i],
height[i],
alpha = 1.0,
facecolor = colors_list[i]
)
)
plt.show()
The rectangles may be overlapping, therefore some of them may be completely hidden. Do you know if it is possible to get the colors of the visible rectangles? I mean the colors of the rectangles that are not completely hidden and therefore that can be actually viewed by the user. I was thinking to some function that returns the color of the pixels, but more intelligent ideas are welcome. If possible, I'd prefer to not use PIL. Unfortunately I cannot find any solution on the internet.
Following Vlass Sokolov comment and this Stackoverflow post by Joe Kington, here is how you could get a numpy array containing all the unique colors that are visible on a matplotlib figure:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
plt.close('all')
# Generate some data :
N = 1000
x, y = np.random.rand(N), np.random.rand(N)
w, h = np.random.rand(N)/10 + 0.05, np.random.rand(N)/10 + 0.05
colors = np.vstack([np.random.random_integers(0, 255, N),
np.random.random_integers(0, 255, N),
np.random.random_integers(0, 255, N)]).T
# Plot and draw the data :
fig = plt.figure(figsize=(7, 7), facecolor='white')
ax = fig.add_subplot(111, aspect='equal')
for i in range(N):
ax.add_patch(Rectangle((x[i], y[i]), w[i], h[i], fc=colors[i]/255., ec='none'))
ax.axis([0, 1, 0, 1])
ax.axis('off')
fig.canvas.draw()
# Save data in a rgb string and convert to numpy array :
rgb_data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
rgb_data = rgb_data.reshape((int(len(rgb_data)/3), 3))
# Keep only unique colors :
rgb_data = np.vstack({tuple(row) for row in rgb_data})
# Show and save figure :
fig.savefig('rectangle_colors.png')
plt.show()
So in python I have the following code, taken from this answer:
import matplotlib.pyplot as plt
import sympy
x = sympy.symbols('x')
y = 1 + sympy.sin(sympy.sqrt(x**2 + 20))
lat = sympy.latex(y)
#add text
plt.text(0, 0.6, r"$%s$" % lat, fontsize = 50)
#hide axes
fig = plt.gca()
fig.axes.get_xaxis().set_visible(False)
fig.axes.get_yaxis().set_visible(False)
plt.savefig('out.png', bbox_inches='tight', pad_inches=0)
plt.show()
This opens the text and exports it to a png file just fine:
But this includes whitespace beyond the whitespace outside of the frame. How would you go about cropping the image to export only the text, like a bounding box, like so?
The following is not a perfect solution, but it will hopefully give you some ideas on how to progress:
import matplotlib.pyplot as plt
import sympy
x = sympy.symbols('x')
y = 1 + sympy.sin(sympy.sqrt(x**2 + 2))
lat = sympy.latex(y)
fig = plt.figure()
renderer = fig.canvas.get_renderer()
t = plt.text(0.001, 0.001, f"${lat}$", fontsize=50)
wext = t.get_window_extent(renderer=renderer)
fig.set_size_inches(wext.width / 65, wext.height / 40, forward=True)
fig.patch.set_facecolor('white')
plt.axis('off')
plt.tight_layout()
plt.savefig('out.png', bbox_inches='tight', pad_inches=0)
plt.show()
The idea being that you can determine the size of your text by getting the window extent using the current renderer. It is then also possible to manually specify a figure size. I am though not sure on the correct approach to convert between the two. Note, I added a border to the image so you can see that amount of remaining padding:
As a workaround to this problem, the following approach simply makes use of Python's PIL library to automatically crop the image before saving it:
import io
from PIL import Image, ImageOps
import matplotlib.pyplot as plt
import sympy
x = sympy.symbols('x')
y = 5 /sympy.sqrt(1 + sympy.sin(sympy.sqrt(x**2 + 2)))
lat = sympy.latex(y)
fig = plt.figure()
t = plt.text(0.001, 0.001, f"${lat}$", fontsize=50)
fig.patch.set_facecolor('white')
plt.axis('off')
plt.tight_layout()
with io.BytesIO() as png_buf:
plt.savefig(png_buf, bbox_inches='tight', pad_inches=0)
png_buf.seek(0)
image = Image.open(png_buf)
image.load()
inverted_image = ImageOps.invert(image.convert("RGB"))
cropped = image.crop(inverted_image.getbbox())
cropped.save('out.png')
The cropped version looks like:
I'm currently working in a plot in which I show to datas combined.
I plot them with the following code:
plt.figure()
# Data 1
data = plt.cm.binary(data1)
data[..., 3] = 1.0 * (data1 > 0.0)
fig = plt.imshow(data, interpolation='nearest', cmap='binary', vmin=0, vmax=1, extent=(-4, 4, -4, 4))
# Plotting just the nonzero values of data2
x = numpy.linspace(-4, 4, 11)
y = numpy.linspace(-4, 4, 11)
data2_x = numpy.nonzero(data2)[0]
data2_y = numpy.nonzero(data2)[1]
pts = plt.scatter(x[data2_x], y[data2_y], marker='s', c=data2[data2_x, data2_y])
And this gives me this plot:
As can be seen in the image, my background and foreground squares are not aligned.
Both of then have the same dimension (20 x 20). I would like to have a way, if its possible, to align center with center, or corner with corner, but to have some kind of alignment.
In some grid cells it seems that I have right bottom corner alignment, in others left bottom corner alignment and in others no alignment at all, with degrades the visualization.
Any help would be appreciated.
Thank you.
As tcaswell says, your problem may be easiest to solve by defining the extent keyword for imshow.
If you give the extent keyword, the outermost pixel edges will be at the extents. For example:
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(111)
ax.imshow(np.random.random((8, 10)), extent=(2, 6, -1, 1), interpolation='nearest', aspect='auto')
Now it is easy to calculate the center of each pixel. In X direction:
interpixel distance is (6-2) / 10 = 0.4 pixels
center of the leftmost pixel is half a pixel away from the left edge, 2 + .4/2 = 2.2
Similarly, the Y centers are at -.875 + n * 0.25.
So, by tuning the extent you can get your pixel centers wherever you want them.
An example with 20x20 data:
import matplotlib.pyplot as plt
import numpy
# create the data to be shown with "scatter"
yvec, xvec = np.meshgrid(np.linspace(-4.75, 4.75, 20), np.linspace(-4.75, 4.75, 20))
sc_data = random.random((20,20))
# create the data to be shown with "imshow" (20 pixels)
im_data = random.random((20,20))
fig = plt.figure()
ax = fig.add_subplot(111)
ax.imshow(im_data, extent=[-5,5,-5,5], interpolation='nearest', cmap=plt.cm.gray)
ax.scatter(xvec, yvec, 100*sc_data)
Notice that here the inter-pixel distance is the same for both scatter (if you have a look at xvec, all pixels are 0.5 units apart) and imshow (as the image is stretched from -5 to +5 and has 20 pixels, the pixels are .5 units apart).
here is a code where there is no alignment problem.
import matplotlib.pyplot as plt
import numpy
data1 = numpy.random.rand(10, 10)
data2 = numpy.random.rand(10, 10)
data2[data2 < 0.4] = 0.0
plt.figure()
# Plotting data1
fig = plt.imshow(data1, interpolation='nearest', cmap='binary', vmin=0.0, vmax=1.0)
# Plotting data2
data2_x = numpy.nonzero(data2)[0]
data2_y = numpy.nonzero(data2)[1]
pts = plt.scatter(data2_x, data2_y, marker='s', c=data2[data2_x, data2_y])
plt.show()
which gives a perfectly aligned combined plots:
Thus the use of additional options in your code might be the reason of the non-alignment of the combined plots.