Specifying and saving a figure with exact size in pixels
Asked Answered
A

9

251

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.

Adornment answered 5/12, 2012 at 0:34 Comment(0)
H
269

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 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.

Hypochondria answered 5/12, 2012 at 1:4 Comment(12)
It is handy to remember that monitor sizes (and therefore standard browser and ui window sizes) are normally in terms of 96 dpi - multiples of 96. Suddenly numbers like 1440 pixels are meaningful (15 inches) when thought of like this.Glottal
Couldn't get this to work passing figsize to plt.figure. The solution was to do as the other answers suggest and after calling it without figsize, then call fig.set_size_inches(w,h)Mourn
Docs for figure and savefig.Comprise
The link doesn't show the correct value for Apple Thunderbolt Display.Linlithgow
I like this solution, but I have one warning. The text size scales inversely as dpi. (My system is MacBook Pro, OS X), so for interactive printing making the dpi to large (like 10*my_dpi) shrinks the text to near invisibility.Dividend
What if you are running the code on a headless server or another headless environment, and others will be running the code in different environments?Cespitose
@MaxCandocia If you are running on a headless server there is no restriction of the figure fitting in the screen. The only limit is that it can't have more than 2^16 pixels in each dimension. So you need to make sure that the sizes times DPI are less than 2^16. If others run the code in different environments, just make sure they all have the same DPI and the figures should be consistent.Hypochondria
Why not using figsize directly with the resolution needed? And dpi=1? Is there a particular reason?Sometimes
@AgustinBarrachina that would work for images, but if you have any other plot elements or anything with text, it will be severely affected, because DPI also controls font sizes and the like.Hypochondria
"mpl doesn't work with pixels" - except when setting the marker to "," when it then does use actual pixels.Crimmer
For me, this works fine when using "dimensions+1": plt.figure(figsize=((800+1)/my_dpi, (800+1)/my_dpi), dpi=my_dpi)Heid
"So you basically just divide the dimensions in inches by your DPI." → "So you basically just divide the dimensions in pixels by your DPI."Caterer
S
31

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!

Stephen answered 5/12, 2012 at 1:26 Comment(1)
Works for me if aspect='normal' is changed to aspect='equal' or aspect=1 (see docs).Ecphonesis
S
19

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.

Serialize answered 25/7, 2020 at 18:3 Comment(2)
Thanks. It's been a few years since I asked this Q, but from what I remember and see in your answer, this would work well when you are trying to save an array of data as an image, but what if you want to save is an actual figure itself (regardless of its contents) and still control exactly the pixel dimensions of the resulting image file?Adornment
I really like this method, but for some specific image sizes, there seems to be a bug, where one pixel row is missing after saving. Since I also work in research, all hell broke loose, when I lost the pixels! To avoid this I used dpi=1 in matplotlib.image.imsave(output_path, img, dpi=1). Apparently, the bug has been known for some time (see here: github.com/matplotlib/matplotlib/issues/4280).Mikimikihisa
M
10

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.

Misogamy answered 20/7, 2018 at 8:58 Comment(0)
B
4

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.

Bigamy answered 27/12, 2020 at 19:16 Comment(1)
With matplotlib version 3.4.1, this is the only answer on this page that correctly outputs a figure containing an image with exact pixel size and no extraneous whitespace.Bach
C
2

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
Clearway answered 16/9, 2020 at 12:33 Comment(0)
H
2

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#

Hematite answered 16/6, 2022 at 7:42 Comment(0)
P
-1

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)
Probability answered 30/4, 2020 at 21:35 Comment(1)
Please add sample code showing exactly which parameter you're setting and recommended values for the original post's use case.St
S
-3

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

Stalingrad answered 28/10, 2021 at 15:17 Comment(3)
This works well for photo image data, however many people want to add matplotlib specific annotations and plot features, such as plots and text. Additionally, there are several libraries other than PIL for Image creation: e.g. tifffile, imageio, rasterio, etc. Each has their own strengths and purpose.Roughish
Yes you're right Luke. However, I'm only replying to what op wants in this post: he want those raw pixels saved, no other plot contents.Stalingrad
pyplot also comes with a handy function imsave (which I think uses PIL under the hood).Jorum

© 2022 - 2024 — McMap. All rights reserved.