Reputation: 96274
Say I have an image of size 3841 x 7195 pixels. I would like to save the contents of the figure to disk, resulting in an image of the exact size I specify in pixels.
No axis, no titles. Just the image. I don't personally care about DPIs, as I only want to specify the size the image takes in the screen in disk in pixels.
I have read other threads, and they all seem to do conversions to inches and then specify the dimensions of the figure in inches and adjust dpi's in some way. I would like to avoid dealing with the potential loss of accuracy that could result from pixel-to-inches conversions.
I have tried with:
w = 7195
h = 3841
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig(some_path, dpi=1)
with no luck (Python complains that width and height must each be below 32768 (?))
From everything I have seen, matplotlib
requires the figure size to be specified in inches
and dpi
, but I am only interested in the pixels the figure takes in disk. How can I do this?
To clarify: I am looking for a way to do this with matplotlib
, and not with other image-saving libraries.
Upvotes: 261
Views: 343788
Reputation: 1325
The consensus here is that one should use figsize=(width_px / dpi, height_px / dpi), dpi=dpi
for creating a figure with a well defined size, and figure.savefig(target, dpi=dpi)
to produce an output size of exactly width_px
x height_px
. However, it seems like matplotlib uses floor
instead of round
when determining the output size.
Example:
Using width_px = 853
and dpi = 100.0
, Matplotlib produces an output of size 852
(not 853
), probably because (853 / 100.0) * 100.0 = 852.9999999999999
.
So it might be necessary to add a small epsilon to the pixel size:
figsize=((width_px + 1e-6) / dpi, (height_px + 1e-6) / dpi), dpi=dpi
Upvotes: 0
Reputation: 311
Say you have a 2D data with size (w,h) that plt.imshow()
can display its RGB image. now you want to save an image file that has dimension of (w,h,3) with 3 being RGB channels. One method is to display data by plt.imshow()
and then save resulted figure by adjusting dpi and other display settings. The alternative and more straight-forward way (in my opinion) is to change your data into RGB channels yourself and then save it using PIL library as an image. The hard part might be configuring your data into RGB channels. If you don't change vmax
, vmin
, and norm
parameters in plt.imshow()
, then basically, plt.imshow()
is normalizing your data into RGB of a colormap. Let's consider the easiest way, in which your data is divided into 3 classes (RGB) between the minimum and maximum of your data:
# normalizing arr_RGB to have values between 0-255
def normRGB(a):
mynorm = Normalize(vmin=np.min(a), vmax=np.max(a))
return mynorm(a)
# channelizing data between 3 equally divided classes
def arrRGB(arr, original_minmax = False): # use original_minmax = True if you prefer original array's minimum and maximum
# producing RGB variants by slicing intensity of the data into 3 equal sections
R = np.percentile(arr,100 - 100/3)
B = np.percentile(arr,100/3)
arr_R = np.where(arr >= R, arr, 0)
arr_B = np.where(arr <= B, arr, 0)
arr_G = np.where((arr > B) & (arr < R), arr, 0)
if original_minmax == True:
arr = normRGB(abs(arr)) # absolute values help acknowledging negative values' intensity, exclude them if you mean otherwise
# # otherwise run below if you prefer RGB channels' minimums and maximums
elif original_minmax == False:
arr_R = normRGB(abs(arr_R)) # absolute values help acknowledging negative values' intensity, exclude them if you mean otherwise
arr_G = normRGB(abs(arr_G))
arr_B = normRGB(abs(arr_B))
# stacking RGB into one ndarray
arr_RGB = np.dstack((arr_R,arr_G,arr_B))
return arr_RGB
Now, you can simply apply channelizing and normalizing functions to your data (here a random data with a very high size):
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
from PIL import Image
import numpy as np
w = 7195
h = 3841
arr = numpy.random.rand(h, w)
arr_RGB = arrRGB(arr)
im = Image.fromarray((arr_RGB * 255).astype(np.uint8))
im.save("arr_RGB.png") # > 16 MB file size
Below small screenshot snippet shows how the resulted .png file has original data dimension as pixel sizes:
Upvotes: 0
Reputation: 23492
Matplotlib doesn't work with pixels directly, but rather physical sizes and DPI. If you want to display a figure with a certain pixel size, you need to know the DPI of your monitor. For example this link will detect that for you.
If you have an image of 3841x7195 pixels it is unlikely that you monitor will be that large, so you won't be able to show a figure of that size (matplotlib requires the figure to fit in the screen, if you ask for a size too large it will shrink to the screen size). Let's imagine you want an 800x800 pixel image just for an example. Here's how to show an 800x800 pixel image in my monitor (my_dpi=96
):
plt.figure(figsize=(800/my_dpi, 800/my_dpi), dpi=my_dpi)
So you basically just divide the dimensions in pixels by your DPI.
If you want to save a figure of a specific size, then it is a different matter. Screen DPIs are not so important anymore (unless you ask for a figure that won't fit in the screen). Using the same example of the 800x800 pixel figure, we can save it in different resolutions using the dpi
keyword of savefig
. To save it in the same resolution as the screen just use the same dpi:
plt.savefig('my_fig.png', dpi=my_dpi)
To save it as an 8000x8000 pixel image, use a dpi 10 times larger:
plt.savefig('my_fig.png', dpi=my_dpi * 10)
Note that the setting of the DPI is not supported by all backends. Here, the PNG backend is used, but the pdf and ps backends will implement the size differently. Also, changing the DPI and sizes will also affect things like fontsize. A larger DPI will keep the same relative sizes of fonts and elements, but if you want smaller fonts for a larger figure you need to increase the physical size instead of the DPI.
Getting back to your example, if you want to save a image with 3841 x 7195 pixels, you could do the following:
plt.figure(figsize=(3.841, 7.195), dpi=100)
( your code ...)
plt.savefig('myfig.png', dpi=1000)
Note that I used the figure dpi of 100 to fit in most screens, but saved with dpi=1000
to achieve the required resolution. In my system this produces a png with 3840x7190 pixels -- it seems that the DPI saved is always 0.02 pixels/inch smaller than the selected value, which will have a (small) effect on large image sizes. Some more discussion of this here.
Upvotes: 276
Reputation: 2161
The matplotlib reference has examples about how to set the figure size in different units. For pixels:
px = 1/plt.rcParams['figure.dpi'] # pixel in inches
plt.subplots(figsize=(600*px, 200*px))
plt.text(0.5, 0.5, '600px x 200px', **text_kwargs)
plt.show()
https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_size_units.html#
Upvotes: 4
Reputation: 809
Why everyone keep using matplotlib?
If your image is an numpy array with shape (3841, 7195, 3), its data type is numpy.uint8 and rgb value ranges from 0 to 255, you can simply save this array as an image without using matplotlib:
from PIL import Image
im = Image.fromarray(A)
im.save("your_file.jpeg")
I found this code from another post
Upvotes: -2
Reputation: 398
This solution works for matplotlib versions 3.0.1, 3.0.3 and 3.2.1.
def save_inp_as_output(_img, c_name, dpi=100):
h, w, _ = _img.shape
fig, axes = plt.subplots(figsize=(h/dpi, w/dpi))
fig.subplots_adjust(top=1.0, bottom=0, right=1.0, left=0, hspace=0, wspace=0)
axes.imshow(_img)
axes.axis('off')
plt.savefig(c_name, dpi=dpi, format='jpeg')
Because the subplots_adjust setting makes the axis fill the figure, you don't want to specify a bbox_inches='tight', as it actually creates whitespace padding in this case. This solution works when you have more than 1 subplot also.
Upvotes: 5
Reputation: 2368
I had same issue. I used PIL Image to load the images and converted to a numpy array then patched a rectangle using matplotlib. It was a jpg image, so there was no way for me to get the dpi from PIL img.info['dpi'], so the accepted solution did not work for me. But after some tinkering I figured out way to save the figure with the same size as the original.
I am adding the following solution here thinking that it will help somebody who had the same issue as mine.
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
img = Image.open('my_image.jpg') #loading the image
image = np.array(img) #converting it to ndarray
dpi = plt.rcParams['figure.dpi'] #get the default dpi value
fig_size = (img.size[0]/dpi, img.size[1]/dpi) #saving the figure size
fig, ax = plt.subplots(1, figsize=fig_size) #applying figure size
#do whatver you want to do with the figure
fig.tight_layout() #just to be sure
fig.savefig('my_updated_image.jpg') #saving the image
This saved the image with the same resolution as the original image.
In case you are not working with a jupyter notebook. you can get the dpi in the following manner.
figure = plt.figure()
dpi = figure.dpi
Upvotes: 3
Reputation: 213
The OP wants to preserve 1:1 pixel data. As an astronomer working with science images I cannot allow any interpolation of image data as it would introduce unknown and unpredictable noise or errors. For example, here is a snippet from a 480x480 image saved via pyplot.savefig(): Detail of pixels which matplotlib resampled to be roughly 2x2, but notice the column of 1x2 pixels
You can see that most pixels were simply doubled (so a 1x1 pixel becomes 2x2) but some columns and rows became 1x2 or 2x1 per pixel which means the the original science data has been altered.
As hinted at by Alka, plt.imsave() which will achieve what the OP is asking for. Say you have image data stored in image array im, then one can do something like
plt.imsave(fname='my_image.png', arr=im, cmap='gray_r', format='png')
where the filename has the "png" extension in this example (but you must still specify the format with format='png' anyway as far as I can tell), the image array is arr, and we chose the inverted grayscale "gray_r" as the colormap. I usually add vmin and vmax to specify the dynamic range but these are optional.
The end result is a png file of exactly the same pixel dimensions as the im array.
Note: the OP specified no axes, etc. which is what this solution does exactly. If one wants to add axes, ticks, etc. my preferred approach is to do that on a separate plot, saving with transparent=True (PNG or PDF) then overlay the latter on the image. This guarantees you have kept the original pixels intact.
Upvotes: 21
Reputation: 11
plt.imsave worked for me. You can find the documentation here: https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.imsave.html
#file_path = directory address where the image will be stored along with file name and extension
#array = variable where the image is stored. I think for the original post this variable is im_np
plt.imsave(file_path, array)
Upvotes: 0
Reputation: 111
Based on the accepted response by tiago, here is a small generic function that exports a numpy array to an image having the same resolution as the array:
import matplotlib.pyplot as plt
import numpy as np
def export_figure_matplotlib(arr, f_name, dpi=200, resize_fact=1, plt_show=False):
"""
Export array as figure in original resolution
:param arr: array of image to save in original resolution
:param f_name: name of file where to save figure
:param resize_fact: resize facter wrt shape of arr, in (0, np.infty)
:param dpi: dpi of your screen
:param plt_show: show plot or not
"""
fig = plt.figure(frameon=False)
fig.set_size_inches(arr.shape[1]/dpi, arr.shape[0]/dpi)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(arr)
plt.savefig(f_name, dpi=(dpi * resize_fact))
if plt_show:
plt.show()
else:
plt.close()
As said in the previous reply by tiago, the screen DPI needs to be found first, which can be done here for instance: http://dpi.lv
I've added an additional argument resize_fact
in the function which which you can export the image to 50% (0.5) of the original resolution, for instance.
Upvotes: 11
Reputation: 27575
This worked for me, based on your code, generating a 93Mb png image with color noise and the desired dimensions:
import matplotlib.pyplot as plt
import numpy
w = 7195
h = 3841
im_np = numpy.random.rand(h, w)
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig('figure.png', dpi=1)
I am using the last PIP versions of the Python 2.7 libraries in Linux Mint 13.
Hope that helps!
Upvotes: 31