0

I am having some trouble using Pyplot imshow to plot an image from a numpy ndarray called data keeping both its aspect ratio and square-shaped pixels. The shape of the ndarray is (112, 2182). Here´s the code I´m using:

import matplotlib as mpl
from mpl_toolkits.axes_grid1 import make_axes_locatable
import numpy as np

mpl.use('Agg')

import matplotlib.pyplot as plt

data = np.random.random_sample((112, 2182))
plt.figure(figsize=(25, 3.5))
plt.subplots_adjust(left=0.1, right=0.9)

ax = plt.subplot(111)
plt.axis('off')
plt.title('Title', fontweight='bold', y=1.15)

im = ax.imshow(data)

# Add same-width colorbar to the bottom of the image
divider = make_axes_locatable(ax)
cax = divider.append_axes("bottom", size="25%", pad=0.3)
cbar = plt.colorbar(im, orientation="horizontal", cax=cax)

# Save image
plt.savefig('image.pdf', dpi=1000, format=pdf)

I thought that by setting the aspect using the shape of the ndarray the image would show square pixels. However, the image seems to keep the aspect ratio but pixels are not squares, as you can see in zoomed image (I still can´t post images, sorry):

zoomed plot
whole plot

These outputs are the same I got when I used im = ax.imshow(data) withouth the aspect argument. I manually counted pixels in the y-axis of the whole plot and there are 112 of them, so it is not a matter of pixel grouping. Any ideas on how to solve this?

EDIT: the image I am plotting will also have a colorbar and a title. Besides, I need to be able to save it to a pdf file. The zoomed plot I uploaded is a screen capture of the plot zoomed in a pdf viewer.

Jorge
  • 45
  • 1
  • 7

4 Answers4

2

If you use ax.imshow(data) you will automatically get square pixels. If instead you set the aspect to something other than 1/ "equal", then you will not get square pixels. This is however independent of the effect observed in the question.

The problem in the "zoomed image" is that the size of the data pixels is of the order of actual screen pixels. Hence the size of an image pixels might vary by as much as a screen pixel. The solution to this is to save on a figure such that one image pixels is exactly one screen pixel, or any multiple of that.

In this case this would be as simple as

data = np.random.random_sample((2182, 112))
plt.imsave("filename.png", data)
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • I didn't mention that the plot will have a title and a colorbar and needs to be saved to a pdf file. I guess this answer won't work then, am I right? Please see my edit. – Jorge Jan 16 '19 at 13:38
  • Ok, could you then provide the actual code you're interested in? In the question you have a 25 inch wide figure, but the array has much more rows than columns. Also `plt.axis("off")` doesn't indicate that you want a title. – ImportanceOfBeingErnest Jan 16 '19 at 13:55
  • I corrected the shape of random data, as rows and columns were switched. I read [link](https://stackoverflow.com/questions/26475642/matplotlib-imshow-grid-uneven-when-grid-size-is-large?rq=1) and then your answer here [link](https://stackoverflow.com/questions/47633546/relationship-between-dpi-and-figure-size) but i don't fully understand how `figsize`, `dpi` and `ppi` work in `imshow` and with pdf. Setting `dpi=1000` made pixels much more square-shaped, but still are not perfect squares. Any idea on how to calculate `figsize` and `dpi` to solve the problem? – Jorge Jan 17 '19 at 09:26
  • The colorbar present makes this really hard. So you need leftmargin+axessize+space+colorbar+rightmargin = figuresize*dpi and axessize = n*arrayshape, where n needs to be a whole number. The same for the vertical direction. The problem is that the margins are in different units than the figure size. So it will be cumbersome. Not sure if I find time and motivation to write this down in code. – ImportanceOfBeingErnest Jan 17 '19 at 12:51
0

The documentation says that the aspect parameter may be a float or

'equal': Ensures an aspect ratio of 1. Pixels will be square (unless pixel sizes are explicitly made non-square in data coordinates using extent).

'auto': The axes is kept fixed and the aspect is adjusted so that the data fit in the axes. In general, this will result in non-square pixels.

So an aspect ratio of 1.0 is what you are looking for. It is not expected that you get the output you show above given the value you calculated from the shape of the array. As @ImportanceOfBeingErnest points out aspect='equal' is the default, so it as easy as

im = ax.imshow(data)
Christian K.
  • 2,785
  • 18
  • 40
0

You need to rescale pixels with use of imshow options: shape and extent:

import numpy as np
import matplotlib.pyplot as plt

shape = (112, 2182)
extent = [0, 112, 0, 2182]

data = np.random.random_sample(shape)

plt.figure(figsize=(5, 3.5))

ax = plt.subplot(111)
plt.axis('off')

dx = (extent[1] - extent[0]) / shape[1]
dy = (extent[3] - extent[2]) / shape[0]
dx_dy = dx/dy

im = ax.imshow(data, extent=extent, aspect=dx_dy)
plt.show()
  • 1
    If I understand your code well, in the end `dx_dy == 1`, which is the default value for `aspect`. So we could say that you are 'only' adding the `extent` argument to `imshow`. I tried your code but had no effect on output, it remained as it was. – Jorge Jan 16 '19 at 13:44
  • In you case dx_dy = 0.002634671980817975. If you zoom created image you get nice shaped squares. –  Jan 16 '19 at 15:13
0

I had the same problem, or at least a very similar one, where some pixels in an exported PDF file were rectangular. It seemed to me that I shouldn't have to determine a pixel-perfect DPI value for my exported PDF files.

What caught my eye was that in the Matplotlib imshow documentation they specifically call out one interpolation parameter value that acts differently on the PDF and other vector-graphics backends: interpolation='none'. The warning here is that in the case of downsampled images, the PDF viewer will be the one doing the downsampling, resulting in potentially variable behavior depending on the PDF viewer. For upsampling however, it seems to work well for me.

On the left, below, I show a screenshot of the image in the PDF with default interpolation, and on the right I show the same screenshot with interpolation='none'.