display image with maximum width keeping an equal ratio - matplotlib

How to display an image with imshow on all the width of an axe (viewport) but keeping the ratio 'equal' ?
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(nrows=2, figsize=(8,8))
img = ax[0].imshow(np.random.random((10,10)), aspect='equal')
line = ax[1].plot([12,34],[45,78])
I would like that the image width be aligned on the line plot keeping an equal ratio even if that implies having blank around. My application has a zooming feature coded on the image in fact so I would to offer all the width possible for its display.
So in few words, I would like the same width as with aspect='auto' but with square pixels.
Is it possible ? Thanks.

Found with a reading of Axes class - set explicitly size (width/height) of axes in given units
and the use of
set_adjustable('datalim')
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(nrows=2, figsize=(8,8))
img = ax[0].imshow(np.random.random((10,10)), aspect='equal')
ax[0].set_adjustable('datalim')
line = ax[1].plot([12,34],[45,78])

Related

Consider sign in ytick label adjustment

When adding a colorbar to images plotting with matplotlib.pyplot.imshow, the colorbar's tick labels are normally placed to the right. If the value range spans positive and negative values, this leads to a misalignment between positive and negative values (positive values do not account for the '-' sign), which, IMHO, is very ugly.
How can I adjust this so that the tick labels are aligned properly?
import numpy as np
import matplotlib.pyplot as plt
data = np.random.rand(100, 100) - 0.5
plt.figure(figsize=(2,1.5))
img = plt.imshow(data)
cbar = plt.colorbar()
In order to have the positive and negative labels on the colorbar aligned, you may align the text to the right and increase the padding between axis and labels. The amount of padding to add will depend on the length of the ticklabels.
import numpy as np
import matplotlib.pyplot as plt
data = np.random.rand(100, 100) - 0.5
plt.figure()
img = plt.imshow(data)
cbar = plt.colorbar()
plt.setp(cbar.ax.get_yticklabels(), ha="right")
cbar.ax.tick_params(pad=30)
plt.show()
I'm not convinced that this looks better though.

Draw grid lines over an image

I am following this answer but I do not get grid lines every 10 steps:
import matplotlib.pyplot as plt
plt.figure()
img=ims[0].copy()
dx, dy = 10,10
# Custom (rgb) grid color
grid_color = -1500
# Modify the image to include the grid
img[:,::dy] = grid_color
img[::dx,:] = grid_color
plt.imshow(img,'gray',interpolation='none',vmin=-1500,vmax=2258)
In order to make sure every pixel in the image is actually shown you need to make sure to draw the image such that one pixel in the image is larger or equal one pixel on screen.
Example: If the figure has a dpi of 100 and is 4.5 inch heigh and you take 10% margin on each side, an image with 350 pixels will be shown correctly,
import numpy as np
import matplotlib.pyplot as plt
plt.figure(figsize=(6,4.5))
plt.subplots_adjust(top=0.9, bottom=0.1)
img=np.random.rand(350,350)
dx, dy = 10,10
grid_color = -1
img[:,::dy] = grid_color
img[::dx,:] = grid_color
plt.imshow(img,'gray',vmin=-1,vmax=1)
plt.show()
If the figure has a dpi of 100 and is 3.2 inch heigh and you take 10% margin on each side, an image with 350 pixels will not show every pixel and hence you get the following output,
import numpy as np
import matplotlib.pyplot as plt
plt.figure(figsize=(6,3.2))
plt.subplots_adjust(top=0.9, bottom=0.1)
img=np.random.rand(350,350)
dx, dy = 10,10
grid_color = -1
img[:,::dy] = grid_color
img[::dx,:] = grid_color
plt.imshow(img,'gray',vmin=-1,vmax=1)
plt.show()
So in order to obtain a grid even for the latter case, this answer is a better approach. You can create a grid and set the linewidth of the grid, such that is always 0.72 points (=1pixel # 100dpi).
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker
plt.figure(figsize=(6,3.2))
plt.subplots_adjust(top=0.9, bottom=0.1)
img=np.random.rand(350,350)
plt.imshow(img,'gray',vmin=-1,vmax=1)
plt.minorticks_on()
plt.gca().xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(10))
plt.gca().yaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(10))
plt.grid(which="both", linewidth=0.72,color="k")
plt.tick_params(which="minor", length=0)
plt.show()

Matplotlib plot over imshow while keeping axis size

Whenever I use imshow() to plot an image, plotting 1D data over it in a twinned bottom x-axis changes the size and aspect ratio of the initial x-axis created with for imshow(). How do I avoid this behavior? Here is how to reproduce the issue:
import numpy as np
import matplotlib
matplotlib.use('macosx')
import matplotlib.pyplot as plt
im = np.random.rand(2856, 4290)
light_curve = im[1000, :]
fig = plt.figure(1, figsize=(10,10))
ax1 = plt.subplot(2,1,1)
ax1.imshow(im, cmap='gray', origin='lower')
ax2 = plt.subplot(2,1,2)
ax2.imshow(im, cmap='gray', origin='lower')
# Setting aspect ratio to equal does not help
ax2.set_aspect('equal')
ax21 = ax2.twinx()
ax21.plot(light_curve, alpha=0.7)
# Setting axis limits does not help
ax1.axis([0, im.shape[1], 0, im.shape[0]])
ax21.set_xlim([0, im.shape[1]])
And here is what it looks like with my graphical backend (macosx, if that is of any relevance)
Isn't it the purpose of twinx() used above to help with this in the first place?
So how may I keep the initial imshow() x-axis fixed and have the subsequent axis of the 1D plot simply fit, without resizing or messing with the aspect ratio, without going completely manual with building my axes?
It is indeed a bit unfortunate that the aspect does not propagate to the twin axes in the sense that it would have the same box around it.
I think the only way to overcome this is to calculate the aspect manually and set it for the twin axes as well.
import numpy as np
import matplotlib.pyplot as plt
im = np.random.rand(285, 429)
light_curve = im[100, :]
fig = plt.figure(1, figsize=(8,8))
ax1 = plt.subplot(2,1,1)
ax1.imshow(im, cmap='gray', origin='lower')
ax2 = plt.subplot(2,1,2)
ax2.imshow(im, cmap='gray', origin='lower')
ax2.set_aspect("equal", "box-forced")
ax21 = ax2.twinx()
ax21.plot(light_curve, alpha=0.7)
# Setting axis limits does not help
ax21.set_xlim(ax1.get_xlim())
a = np.diff(ax21.get_ylim())[0]/np.diff(ax1.get_xlim())*im.shape[1]/im.shape[0]
ax21.set_aspect(1./a, "box-forced")
plt.show()

Matplotlib colorbar and WCS projection

I'm trying to write a function to display astronomical images with a colorbar on the top (automaticly with the same length of the x-axis).
I'm having problem because when I try to put the tick on the top it doesn't do anything...it keeps the tick on the bottom of the colorbar (and also the tick on the y-axis of the colobar).
I think that could be a problem with the WCS coordinate of the x-axis, because when i try to do it without the projection it work well!
import numpy as np
import matplotlib.pyplot as plt
from astropy import wcs
from matplotlib.colors import PowerNorm
from mpl_toolkits.axes_grid1 import make_axes_locatable
from matplotlib import cm
#WCS coordinate system
w = wcs.WCS(naxis=2)
w.wcs.crpix = [23.5, 23.5]
w.wcs.cdelt = np.array([-0.0035, 0.0035])
w.wcs.crval = [266.8451, -28.151658]
w.wcs.ctype = ["RA---TAN", "DEC--TAN"]
w.wcs.set_pv([(2, 1, 45.0)])
#generate an array as image test
data = (np.arange(10000).reshape((100,100)))
#display image
fig = plt.figure()
ax = plt.gca(projection=w)
graf = ax.imshow(data, origin='lower', cmap=cm.viridis, norm=PowerNorm(1))
#colorbar
divider = make_axes_locatable(ax)
cax = divider.append_axes("top", size="5%")
cbar = fig.colorbar(graf, cax=cax, orientation='horizontal')
cax.xaxis.set_ticks_position('top')
fig.show()
Thanks!
You can fix this issue using matplotlib's axes class.
...
import matplotlib.axes as maxes
cax = divider.append_axes("top", size="5%", axes_class=maxes.Axes)
...
You need to use the internal machinery of the WCSAxes to handle the ticks in the WCS projection. It looks like WCSAxes handles the colorbar ticks through a coordinate map container (you can find it in cbar.ax.coords) instead of the xaxis/yaxis attributes (that don't seem to be used much).
So, after running your code, the following trick worked for me and the xticks moved up:
c_x = cbar.ax.coords['x']
c_x.set_ticklabel_position('t')
cbar.update_normal(cax)
To get something like this to work, I needed a few additional parameters:
from mpl_toolkits.axes_grid1 import make_axes_locatable
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
cax.coords[0].grid(False)
cax.coords[1].grid(False)
cax.tick_params(direction='in')
cax.coords[0].set_ticks(alpha=0, color='w', size=0, values=[]*u.dimensionless_unscaled)
cax.coords[1].set_ticklabel_position('r')
cax.coords[1].set_axislabel_position('r')
because the default axis gad the grid on, the labels to the left, and x-axis labels enabled. I'm not sure why the original post didn't have issues with this.

matplotlib: Stretch image to cover the whole figure

I am quite used to working with matlab and now trying to make the shift matplotlib and numpy. Is there a way in matplotlib that an image you are plotting occupies the whole figure window.
import numpy as np
import matplotlib.pyplot as plt
# get image im as nparray
# ........
plt.figure()
plt.imshow(im)
plt.set_cmap('hot')
plt.savefig("frame.png")
I want the image to maintain its aspect ratio and scale to the size of the figure ... so when I do savefig it exactly the same size as the input figure, and it is completely covered by the image.
Thanks.
I did this using the following snippet.
#!/usr/bin/env python
import numpy as np
import matplotlib.cm as cm
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
from pylab import *
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
Z = Z2-Z1 # difference of Gaussians
ax = Axes(plt.gcf(),[0,0,1,1],yticks=[],xticks=[],frame_on=False)
plt.gcf().delaxes(plt.gca())
plt.gcf().add_axes(ax)
im = plt.imshow(Z, cmap=cm.gray)
plt.show()
Note the grey border on the sides is related to the aspect rario of the Axes which is altered by setting aspect='equal', or aspect='auto' or your ratio.
Also as mentioned by Zhenya in the comments Similar StackOverflow Question
mentions the parameters to savefig of bbox_inches='tight' and pad_inches=-1 or pad_inches=0
You can use a function like the one below.
It calculates the needed size for the figure (in inches) according to the resolution in dpi you want.
import numpy as np
import matplotlib.pyplot as plt
def plot_im(image, dpi=80):
px,py = im.shape # depending of your matplotlib.rc you may
have to use py,px instead
#px,py = im[:,:,0].shape # if image has a (x,y,z) shape
size = (py/np.float(dpi), px/np.float(dpi)) # note the np.float()
fig = plt.figure(figsize=size, dpi=dpi)
ax = fig.add_axes([0, 0, 1, 1])
# Customize the axis
# remove top and right spines
ax.spines['right'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
# turn off ticks
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.imshow(im)
plt.show()
Here's a minimal object-oriented solution:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, xticks=[], yticks=[])
Testing it out with
ax.imshow([[0]])
fig.savefig('test.png')
saves out a uniform purple block.
edit: As #duhaime points out below, this requires the figure to have the same aspect as the axes.
If you'd like the axes to resize to the figure, add aspect='auto' to imshow.
If you'd like the figure to resize to be resized to the axes, add
from matplotlib import tight_bbox
bbox = fig.get_tightbbox(fig.canvas.get_renderer())
tight_bbox.adjust_bbox(fig, bbox, fig.canvas.fixed_dpi)
after the imshow call. This is the important bit of matplotlib's tight_layout functionality which is implicitly called by things like Jupyter's renderer.