[python] Specifying and saving a figure with exact size in pixels

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.

This question is related to python matplotlib scipy

The answer is


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)

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

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!


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.


Comparison of different approaches

Here is a quick comparison of some of the approaches I've tried with images showing what the give.

Baseline example without trying to set the image dimensions

Just to have a comparison point:

base.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

fig, ax = plt.subplots()
print('fig.dpi = {}'.format(fig.dpi))
print('fig.get_size_inches() = ' + str(fig.get_size_inches())
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig('base.png', format='png')

run:

./base.py
identify base.png

outputs:

fig.dpi = 100.0
fig.get_size_inches() = [6.4 4.8]
base.png PNG 640x480 640x480+0+0 8-bit sRGB 13064B 0.000u 0:00.000

enter image description here

My best approach so far: plt.savefig(dpi=h/fig.get_size_inches()[1] height-only control

I think this is what I'll go with most of the time, as it is simple and scales:

get_size.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

height = int(sys.argv[1])
fig, ax = plt.subplots()
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
    'get_size.png',
    format='png',
    dpi=height/fig.get_size_inches()[1]
)

run:

./get_size.py 431

outputs:

get_size.png PNG 574x431 574x431+0+0 8-bit sRGB 10058B 0.000u 0:00.000

enter image description here

and

./get_size.py 1293

outputs:

main.png PNG 1724x1293 1724x1293+0+0 8-bit sRGB 46709B 0.000u 0:00.000

enter image description here

I tend to set just the height because I'm usually most concerned about how much vertical space the image is going to take up in the middle of my text.

plt.savefig(bbox_inches='tight' changes image size

I always feel that there is too much white space around images, and tended to add bbox_inches='tight' from: Removing white space around a saved image in matplotlib

However, that works by cropping the image, and you won't get the desired sizes with it.

Instead, this other approach proposed in the same question seems to work well:

plt.tight_layout(pad=1)
plt.savefig(...

which gives the exact desired height for height equals 431:

enter image description here

Fixed height, set_aspect, automatically sized width and small margins

Ermmm, set_aspect messes things up again and prevents plt.tight_layout from actually removing the margins...

Asked at: How to obtain a fixed height in pixels, fixed data x/y aspect ratio and automatically remove remove horizontal whitespace margin in Matplotlib?

plt.savefig(dpi=h/fig.get_size_inches()[1] + width control

If you really need a specific width in addition to height, this seems to work OK:

width.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

h = int(sys.argv[1])
w = int(sys.argv[2])
fig, ax = plt.subplots()
wi, hi = fig.get_size_inches()
fig.set_size_inches(hi*(w/h), hi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
    'width.png',
    format='png',
    dpi=h/hi
)

run:

./width.py 431 869

output:

width.png PNG 869x431 869x431+0+0 8-bit sRGB 10965B 0.000u 0:00.000

enter image description here

and for a small width:

./width.py 431 869

output:

width.png PNG 211x431 211x431+0+0 8-bit sRGB 6949B 0.000u 0:00.000

enter image description here

So it does seem that fonts are scaling correctly, we just get some trouble for very small widths with labels getting cut off, e.g. the 100 on the top left.

I managed to work around those with Removing white space around a saved image in matplotlib

plt.tight_layout(pad=1)

which gives:

width.png PNG 211x431 211x431+0+0 8-bit sRGB 7134B 0.000u 0:00.000

enter image description here

From this, we also see that tight_layout removes a lot of the empty space at the top of the image, so I just generally always use it.

Fixed magic base height, dpi on fig.set_size_inches and plt.savefig(dpi= scaling

I believe that this is equivalent to the approach mentioned at: https://stackoverflow.com/a/13714720/895245

magic.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

magic_height = 300
w = int(sys.argv[1])
h = int(sys.argv[2])
dpi = 80
fig, ax = plt.subplots(dpi=dpi)
fig.set_size_inches(magic_height*w/(h*dpi), magic_height/dpi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
    'magic.png',
    format='png',
    dpi=h/magic_height*dpi,
)

run:

./magic.py 431 231

outputs:

magic.png PNG 431x231 431x231+0+0 8-bit sRGB 7923B 0.000u 0:00.000

enter image description here

And to see if it scales nicely:

./magic.py 1291 693

outputs:

magic.png PNG 1291x693 1291x693+0+0 8-bit sRGB 25013B 0.000u 0:00.000

enter image description here

So we see that this approach also does work well. The only problem I have with it is that you have to set that magic_height parameter or equivalent.

Fixed DPI + set_size_inches

This approach gave a slightly wrong pixel size, and it makes it is hard to scale everything seamlessly.

set_size_inches.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

w = int(sys.argv[1])
h = int(sys.argv[2])
fig, ax = plt.subplots()
fig.set_size_inches(w/fig.dpi, h/fig.dpi)
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(
    0,
    60.,
    'Hello',
    # Keep font size fixed independently of DPI.
    # https://stackoverflow.com/questions/39395616/matplotlib-change-figsize-but-keep-fontsize-constant
    fontdict=dict(size=10*h/fig.dpi),
)
plt.savefig(
    'set_size_inches.png',
    format='png',
)

run:

./set_size_inches.py 431 231

outputs:

set_size_inches.png PNG 430x231 430x231+0+0 8-bit sRGB 8078B 0.000u 0:00.000

so the height is slightly off, and the image:

enter image description here

The pixel sizes are also correct if I make it 3 times larger:

./set_size_inches.py 1291 693

outputs:

set_size_inches.png PNG 1291x693 1291x693+0+0 8-bit sRGB 19798B 0.000u 0:00.000

enter image description here

We understand from this however that for this approach to scale nicely, you need to make every DPI-dependant setting proportional to the size in inches.

In the previous example, we only made the "Hello" text proportional, and it did retain its height between 60 and 80 as we'd expect. But everything for which we didn't do that, looks tiny, including:

  • line width of axes
  • tick labels
  • point markers

SVG

I could not find how to set it for SVG images, my approaches only worked for PNG e.g.:

get_size_svg.py

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

height = int(sys.argv[1])
fig, ax = plt.subplots()
t = np.arange(-10., 10., 1.)
plt.plot(t, t, '.')
plt.plot(t, t**2, '.')
ax.text(0., 60., 'Hello', fontdict=dict(size=25))
plt.savefig(
    'get_size_svg.svg',
    format='svg',
    dpi=height/fig.get_size_inches()[1]
)

run:

./get_size_svg.py 431

and the generated output contains:

<svg height="345.6pt" version="1.1" viewBox="0 0 460.8 345.6" width="460.8pt"

and identify says:

get_size_svg.svg SVG 614x461 614x461+0+0 8-bit sRGB 17094B 0.000u 0:00.000

and if I open it in Chromium 86 the browser debug tools mouse image hover confirm that height as 460.79.

But of course, since SVG is a vector format, everything should in theory scale, so you can just convert to any fixed sized format without loss of resolution, e.g.:

inkscape -h 431 get_size_svg.svg -b FFF -e get_size_svg.png

gives the exact height:

TODO regenerate image, messed up the upload somehow.

I use Inkscape instead of Imagemagick's convert here because you need to mess with -density as well to get sharp SVG resizes with ImageMagick:

And setting <img height="" on the HTML should also just work for the browser.

Tested on matplotlib==3.2.2.


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.


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.


Examples related to python

programming a servo thru a barometer Is there a way to view two blocks of code from the same file simultaneously in Sublime Text? python variable NameError Why my regexp for hyphenated words doesn't work? Comparing a variable with a string python not working when redirecting from bash script is it possible to add colors to python output? Get Public URL for File - Google Cloud Storage - App Engine (Python) Real time face detection OpenCV, Python xlrd.biffh.XLRDError: Excel xlsx file; not supported Could not load dynamic library 'cudart64_101.dll' on tensorflow CPU-only installation

Examples related to matplotlib

"UserWarning: Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure." when plotting figure with pyplot on Pycharm How to increase image size of pandas.DataFrame.plot in jupyter notebook? How to create a stacked bar chart for my DataFrame using seaborn? How to display multiple images in one figure correctly? Edit seaborn legend How to hide axes and gridlines in Matplotlib (python) How to set x axis values in matplotlib python? How to specify legend position in matplotlib in graph coordinates Python "TypeError: unhashable type: 'slice'" for encoding categorical data Seaborn Barplot - Displaying Values

Examples related to scipy

Reading images in python Numpy Resize/Rescale Image How to get the indices list of all NaN value in numpy array? ImportError: cannot import name NUMPY_MKL numpy.where() detailed, step-by-step explanation / examples Scikit-learn train_test_split with indices Matplotlib: Specify format of floats for tick labels Installing NumPy and SciPy on 64-bit Windows (with Pip) Can't install Scipy through pip Plotting a fast Fourier transform in Python