matplotlib render image with specs (size, alligment etc.) in pixels - python

I have specs for a mockup, for example, Photoshop/Illustrator/Sketch file with each elements specifications like size, position, and coordinates in pixels.
An image looks like:
I can draw similar image just using standard matplotlib technic without any problems.
The question is, how to render an image exactly with its specs? All sizes, font sizes, alignments, should be the same as in the specs (as it's drawn in Illustrator).
I've researched over matplotlib docs, but even transformers tutorial doesn't help.
Update with an exact example.
I have a mock at Zeplin which shows coordinated of each plot (Image is also a plot here). So I know, that the image has margins 25x25px from a border and its size is 80x80 pixels.
this is mock image (again not allowed to embed the image).
How would you do that?
The code I use for drawing
fig, ax = plt.subplots(1, 2, figsize=(20, 10), sharey=True)
recs = ax[0].barh(y_pos, widths, align='edge');
img = mpimg.imread('/Users/iwitaly/Downloads/2017-11-12 17.40.46.jpg')
ax[0].spines['left'].set_visible(False);
ax[0].spines['right'].set_visible(False);
ax[0].spines['bottom'].set_visible(False);
ax[0].spines['top'].set_visible(False);
ax[0].get_xaxis().set_visible(True);
ax[0].get_yaxis().set_visible(True);
obj = ax[0].text(x=0, y=3.9, s=r'Name: Vitaly Davydov',
fontsize=30, fontname="Courier New", weight='bold');
ax[0].axhline(y=3.85, xmin=0, xmax=0.75, color='black');
# I'd like to place axicon exactly with 25x25 marging from top left corner
axicon = fig.add_axes([0.08, 1, 0.2, 0.2], transform=None)
axicon.axis('off');
axicon.imshow(img, interpolation='none');
for i, r in enumerate(recs):
r.set_color(index_to_color[i]);
r.set_height(col_wight);
ax[0].text(x=0, y=text_positions[i], s=index_to_text[i], fontsize=30, fontname="Courier New");
ax[1].spines['left'].set_visible(False);
ax[1].spines['right'].set_visible(False);
ax[1].spines['bottom'].set_visible(False);
ax[1].spines['top'].set_visible(False);
ax[1].get_xaxis().set_visible(False);
ax[1].get_yaxis().set_visible(False);
ax[1].text(x=0, y=3.9, s='Increment:', fontsize=30,
fontname="Courier New", weight='bold');
ax[1].axhline(y=3.85, xmin=0, xmax=0.4, color='black');
for i, r in enumerate(recs):
text_x, text_y = r.xy
increment_pos_y = text_y + col_wight / 2
if increment_values[i] > 0:
increment_text = '+{}'.format(increment_values[i])
elif increment_values[i] < 0:
increment_text = '-{}'.format(increment_values[i])
else:
increment_text = '{}'.format(increment_values[i])
ax[1].text(x=0, y=increment_pos_y, s=increment_text,
fontsize=30, color=index_to_color[i],
fontname="Courier New", weight='bold');
In this example I'd like to place axicon axes that is an image with 25x25 margin and 80x80 size (all in pixels).

To place an axes to the figure, you can use fig.add_axes([left, bottom, width, height]), where left, bottom, width, height are fractions of the figure size.
To convert from pixels to fraction of figure size, you need to divide the pixels by the figure dpi and the figure size. E.g. for the left edge
left = 25/fig.dpi/fig.get_size_inches()[0]
Complete example:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(1, 2, figsize=(10, 7))
y_pos, widths = np.linspace(0,3,4), np.random.rand(4)
recs = ax[0].barh(y_pos, widths, align='edge');
#img = plt.imread('/Users/iwitaly/Downloads/2017-11-12 17.40.46.jpg')
img = np.random.rand(80,80)
# I'd like to place axicon exactly with 25x25 marging from top left corner
x,y= 25,25 #pixels
dx,dy = 80,80
w,h = fig.get_size_inches()
axicon = fig.add_axes([x/float(fig.dpi)/w, 1.-(y+dy)/float(fig.dpi)/h,
dx/float(fig.dpi)/w, dy/float(fig.dpi)/h])
axicon.axis('off');
axicon.imshow(img, interpolation='none');
plt.show()

Related

Add external margins with constrained layout?

When generating a figure to save to a pdf file, I'd like to adjust the positioning of the figure relative to the edges of the page, for example to add an inch margin along all sides. As far as I can tell, the solutions to do this (for example, in this question) either:
don't work with constrained_layout mode -- applying plt.subplots_adjust() after creating the figure but prior to fig.savefig() messes up the constrained layout
don't actually quantitatively adjust the positioning of the figure -- adding bbox_inches="tight" or pad=-1 don't seem to do anything meaningful
Is there a straightforward way to adjust external margins of a constrained layout figure?
For example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
Yields the following (I'd like to see more coral around the edges!):
Have you tried using the constrained layout padding option?
fig.set_constrained_layout_pads(w_pad=4./72., h_pad=4./72.,
hspace=0./72., wspace=0./72.)
While this might help with spacing, the constrained layout will restrict the amount of object that you can add to the defined space.
''' Here is the modified code '''
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec
import numpy as np
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.set_constrained_layout_pads(w_pad=2./12., h_pad=4./12.,
hspace=0., wspace=0.)
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large", y=0.98)
# this doesn't appear to do anything with constrained_layout=True
page_grid.update(left=0.2, right=0.8, bottom=0.2, top=0.8)
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
# this ruins the constrained layout
# plt.subplots_adjust(left=0.2,right=0.8, bottom=0.2, top=0.8)
fig.savefig("temp.png", facecolor="coral")
You can set the rectangle that the layout engine operates within. See the rect parameter for each engine at https://matplotlib.org/stable/api/layout_engine_api.html.
It's unfortunately not a very friendly part of the API, especially because TightLayoutEngine and ConstrainedLayoutEngine have different semantics for rect: TightLayoutEngine uses rect = (left, bottom, right, top) and ConstrainedLayoutEngine uses rect = (left, bottom, width, height).
def set_margins(fig, margins):
"""Set figure margins as [left, right, top, bottom] in inches
from the edges of the figure."""
left,right,top,bottom = margins
width, height = fig.get_size_inches()
#convert to figure coordinates:
left, right = left/width, 1-right/width
bottom, top = bottom/height, 1-top/height
#get the layout engine and convert to its desired format
engine = fig.get_layout_engine()
if isinstance(engine, matplotlib.layout_engine.TightLayoutEngine):
rect = (left, bottom, right, top)
elif isinstance(engine, matplotlib.layout_engine.ConstrainedLayoutEngine):
rect = (left, bottom, right-left, top-bottom)
else:
raise RuntimeError('Cannot adjust margins of unsupported layout engine')
#set and recompute the layout
engine.set(rect=rect)
engine.execute(fig)
With your example:
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
page_grid = gridspec.GridSpec(nrows=2, ncols=1, figure=fig)
#your margins were [0.2, 0.8, 0.2, 0.8] in figure coordinates
#which are 0.2*11 and 0.2*8.5 in inches from the edge
set_margins(fig,[0.2*11, 0.2*11, 0.2*8.5, 0.2*8.5])
top_row_grid = gridspec.GridSpecFromSubplotSpec(1, 3, subplot_spec=page_grid[0])
for i in range(3):
ax = fig.add_subplot(top_row_grid[:, i], aspect="equal")
n_bottom_row_plots = 10
qc_grid = gridspec.GridSpecFromSubplotSpec(1, n_bottom_row_plots, subplot_spec=page_grid[1])
for i, metric in enumerate(range(n_bottom_row_plots)):
ax = fig.add_subplot(qc_grid[:, i])
plt.plot(np.arange(5), np.arange(5))
fig.suptitle("my big label", fontweight="bold", fontsize="x-large", y=0.9)
fig.savefig("temp.png", facecolor="coral")
Note: fig.suptitle text is apparently not handled by the layout engine, so it doesn't move.
If what you want to do is add a margin around the finished figure, and you are not concerned with how it looks on screen, another option is to set pad_inches with bbox_inches='tight'.
import matplotlib.pyplot as plt
fig = plt.figure(constrained_layout=True, figsize=(11, 8.5))
fig.suptitle("My BIG Label", fontweight="bold", fontsize="x-large")
sfigs = fig.subfigures(2, 1)
# top subfigure
ax = sfigs[0].subplots(1, 3)
# bottom subfigure
n_bottom_row_plots = 10
axs = sfigs[1].subplots(1, n_bottom_row_plots)
fig.savefig("temp.png", facecolor="coral", bbox_inches='tight', pad_inches=1)
There are few things in the above comment and answers to note - first constrained layout indeed deals with subtitles, however, it doesn't if you manually set y because it assumes if you placed it manually that is where you would like it.
Second, there is a rect argument for the layout, but the suptitle doesn't get constrained by that, because rect is not really meant to be a constraint on decorations, but on where the axes get placed. I think this could be considered a bug, or at least unexpected behaviour and constrained layout should work just inside its rect. However, given the ease of adding padding in saved output it's probably not urgent, but if someone wants this to work that way, they are encouraged to open a bug report.
Finally, I modernized the above to use subfigures which are meant to be easier to work with than nested subplotspecs. Note each subfigure can also have a suptitle, etc
If you want to achieve more flexibility, I personally dont recommend using constrained layout and specifying [left, right, bottom, top] together. Either specify the margin by yourself, or let matplotlib contrained layout do the rearangement for you. For more space between axes for placing texts and labels, just use hspace and wspace to adjust.
If I want more margin from both sides, and have enough space for axes labels, tick labels, and some texts. I do the following way.
fig = plt.figure(figsize=(11, 8.5), facecolor='coral')
# you code already has this
left, right, bottom, top = [0.1, 0.95, 0.1, 0.5]
# You can specify wspace and hspace to hold axes labels and some other texts.
wspace = 0.25
hspace = 0.1
nrows=1
ncols=3
gs1 = fig.add_gridspec(nrows=1, ncols=3, left=left, right=right, bottom=0.6,
top=0.9, wspace=wspace, hspace=hspace)
axes1 = [fig.add_subplot(gs1[row, col]) for row in range(nrows) for col in
range(ncols)]
nrows=1
ncols=10
# this grid have larger wspace than gs1
gs2 = fig.add_gridspec(nrows=1, ncols=ncols, left=left, right=right,
bottom=0.1, top=top, wspace=0.6, hspace=hspace)
axes2 = [fig.add_subplot(gs2[row, col]) for row in range(nrows) for col in
range(ncols)]

adding custom images to matplotlib plot

I want to create a plot with two custom images below the plot, but I haven't figured out how to do it.
I'm using python 3.7 and matplotlib
this is what I have so far
df = pd.DataFrame({
'some data':[2,0,0,3,2,1,4],
'other data':[5,1,0,5,2,2,3]
})
logo = plt.imread('A.jpg')
title = 'images'
ax = df.plot(kind='bar',x='some data',y='other data')
ax.set_title(title, fontsize=20)
ax.figure.figimage(logo, 40, 40, alpha=.15, zorder=1)
This is the result
and this is what I'm trying to get.
This is the image in the plot
In order to get the images how you have them displayed in your expected output (one aligned left, one aligned right, both below plot) you can do something like this
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame({
'some data':[2,0,0,3,2,1,4],
'other data':[5,1,0,5,2,2,3]
})
logo = plt.imread('A.jpg')
title = 'images'
fig, ax = plt.subplots(1)
df.plot(kind='bar',x='some data',y='other data', ax=ax)
ax.set_title(title, fontsize=20)
h = logo.shape[1]/fig.bbox.ymax
fig.subplots_adjust(0.05, h, 0.97, 0.93)
ax.figure.figimage(logo, 0, 0, alpha=.15, zorder=1)
ax.figure.figimage(logo, fig.bbox.xmax - logo.shape[0], 0, alpha=.15, zorder=1)
plt.show()
This will resize the figure to accommodate the height of the logos: logo.shape[0] is the width of the logo in pixels logo.shape[1] is the height of the logo in pixels, fig.bbox.ymax is the height of the figure in pixels. h is the fractional height of the logo which can then be used in fig.subplots_adjust to adjust the height accordingly.
It then positions one logo with the bottom left corner at offset (0, 0) and another with the bottom left corner at (fig.bbox.xmax - logo.shape[0], 0) where fig.bbox.xmax is the width of the figure in pixels.1
This will give you something that looks like this:
you can export the plot as a svg file. Open this with inkscape and add the picture.

Matplotlib automate placement of watermark

I'm trying to write a function to automate the placement of watermark in the lower right of my figures. Here is my function so far.
from PIL import Image
import matplotlib.pyplot as plt
def watermark(fig, ax):
""" Place watermark in bottom right of figure. """
# Get the pixel dimensions of the figure
width, height = fig.get_size_inches()*fig.dpi
# Import logo and scale accordingly
img = Image.open('logo.png')
wm_width = int(width/4) # make the watermark 1/4 of the figure size
scaling = (wm_width / float(img.size[0]))
wm_height = int(float(img.size[1])*float(scaling))
img = img.resize((wm_width, wm_height), Image.ANTIALIAS)
# Place the watermark in the lower right of the figure
xpos = ax.transAxes.transform((0.7,0))[0]
ypos = ax.transAxes.transform((0.7,0))[1]
plt.figimage(img, xpos, ypos, alpha=.25, zorder=1)
The problem is that when I add a label to either axis the position of the water mark changes. E.g. adding ax.set_xlabel('x-label', rotation=45) changes the watermark position significantly. This seems to be the case because the placement of the watermark is relative to the whole figure (e.g. the plotting and axis area), however the function get_size_inches() only calculates the plotting area (e.g. not including the axis area).
Is there anyway to get the pixel dimensions of the entire figure (e.g. including axis area) or another easy workaround.
Thanks in advance.
You may want to use an AnchoredOffsetbox in which you place an OffsetImage. The advantage would be that you can use the loc=4 argument to place the Offsetbox in the lower right corner of the axes (just like in the case of a legend).
from PIL import Image
import matplotlib.pyplot as plt
from matplotlib.offsetbox import ( OffsetImage,AnchoredOffsetbox)
def watermark2(ax):
img = Image.open('house.png')
width, height = ax.figure.get_size_inches()*fig.dpi
wm_width = int(width/4) # make the watermark 1/4 of the figure size
scaling = (wm_width / float(img.size[0]))
wm_height = int(float(img.size[1])*float(scaling))
img = img.resize((wm_width, wm_height), Image.ANTIALIAS)
imagebox = OffsetImage(img, zoom=1, alpha=0.2)
imagebox.image.axes = ax
ao = AnchoredOffsetbox(4, pad=0.01, borderpad=0, child=imagebox)
ao.patch.set_alpha(0)
ax.add_artist(ao)
fig, ax = plt.subplots()
ax.plot([1,2,3,4], [1,3,4.5,5])
watermark2(ax)
ax.set_xlabel("some xlabel")
plt.show()

Extent and aspect; square pixels in an image with shared axis in matplotlib

I am stuck in a rather complicated situation. I am plotting some data as an image with imshow(). Unfortunately my script is long and a little messy, so it is difficult to make a working example, but I am showing the key steps. This is how I get the data for my image from a bigger array, written in a file:
data = np.tril(np.loadtxt('IC-heatmap-20K.mtx'), 1)
#
#Here goes lot's of other stuff, where I define start and end
#
chrdata = data[start:end, start:end]
chrdata = ndimage.rotate(chrdata, 45, order=0, reshape=True,
prefilter=False, cval=0)
ax1 = host_subplot(111)
#I don't really need host_subplot() in this case, I could use something more common;
#It is just divider.append_axes("bottom", ...) is really convenient.
plt.imshow(chrdata, origin='lower', interpolation='none',
extent=[0, length*resolution, 0, length*resolution]) #resolution=20000
So the values I am interested in are all in a triangle with the top angle in the middle of the top side of a square. At the same time I plot some data (lot's of coloured lines in this case) along with the image near it's bottom.
So at first this looks OK, but is actually is not: all pixels in the image are not square, but elongated with their height being bigger, than their width. This is how they look if I zoom in:
This doesn't happen, If I don't set extent when calling imshow(), but I need it so that coordinates in the image and other plots (coloured lines at the bottom in this case), where identical (see Converting coordinates of a picture in matplotlib?).
I tried to fix it using aspect. I tried to do that and it fixed the pixels' shape, but I got a really weird picture:
The thing is, later in the code I explicitly set this:
ax1.set_ylim(0*resolution, length*resolution) #resolution=20000
But after setting aspect I get absolutely different y limits. And the worst thing: ax1 is now wider, than axes of another plot at the bottom, so that their coordinates do not match anymore! I add it in this way:
axPlotx = divider.append_axes("bottom", size=0.1, pad=0, sharex=ax1)
I would really appreciate help with getting it fixed: square pixels, identical coordinates in two (or more, in other cases) plots. As I see it, the axes of the image need to become wider (as aspect does), the ylims should apply and the width of the second axes should be identical to the image's.
Thanks for reading this probably unclear explanation, please, let me know, if I should clarify anything.
UPDATE
As suggested in the comments, I tried to use
ax1.set(adjustable='box-forced')
And it did help with the image itself, but it caused two axes to get separated by white space. Is there any way to keep them close to each other?
Re-edited my entire answer as I found the solution to your problem. I solved it using the set_adjustable("box_forced") option as suggested by the comment of tcaswell.
import numpy
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import host_subplot, make_axes_locatable
#Calculate aspect ratio
def determine_aspect(shape, extent):
dx = (extent[1] - extent[0]) / float(shape[1])
dy = (extent[3] - extent[2]) / float(shape[0])
return dx / dy
data = numpy.random.random((30,60))
shape = data.shape
extent = [-10, 10, -20, 20]
x_size, y_size = 6, 6
fig = plt.figure(figsize = (x_size, y_size))
ax = host_subplot(1, 1, 1)
ax.imshow(data, extent = extent, interpolation = "None", aspect = determine_aspect(shape, extent))
#Determine width and height of the subplot frame
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
#Calculate distance, the second plot needs to be elevated by
padding = (y_size - (height - width)) / float(1 / (2. * determine_aspect(shape, extent)))
#Create second image in subplot with shared x-axis
divider = make_axes_locatable(ax)
axPlotx = divider.append_axes("bottom", size = 0.1, pad = -padding, sharex = ax)
#Turn off yticks for axPlotx and xticks for ax
axPlotx.set_yticks([])
plt.setp(ax.get_xticklabels(), visible=False)
#Make the plot obey the frame
ax.set_adjustable("box-forced")
fig.savefig("test.png", dpi=300, bbox_inches = "tight")
plt.show()
This results in the following image where the x-axis is shared:
Hope that helps!

overlay matplotlib imshow with line plots that are arranged in a grid

I would like to plot a number of curves over an image
Using this code I am reasonably close:
G=plt.matplotlib.gridspec.GridSpec(64,1)
fig = plt.figure()
plt.imshow(img.data[:,:],cmap='gray')
plt.axis('off')
plt.axis([0,128,0,64])
for i in arange(64):
fig.add_subplot(G[i,0])
plt.axis('off')
# note that vtc.data.shape = (64, 128*400=51200)
# so every trace for each image pixel is 400 points long
plt.plot(vtc.data[i,:])
plt.axis([0, 51200, 0, 5])
The result that I am getting looks like this:
The problem is that while I seem to be able to get rid of all the padding in the horizontal (x) direction, there is different amount of padding in the image and the stacked plots in the vertical direction.
I tried using
ax = plt.gca()
ax.autoscale_view('tight')
but that didn't reduce the margin either.
How can I get a grid of m-by-n line plots to line up precisely with a blown up (by factor f) version of an image with dimensions (fm)-by-(fn)?
UPDATE and Solution:
The answer by #RutgerKassies works quite well. I achieved it using his code like so:
fig, axs = plt.subplots(1,1,figsize=(8,4))
axs.imshow(img.data[:,:],cmap='gray', interpolation='none')
nplots = 64
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
# make sure to get image orientation right and
tmpax.plot(vtc.data[nplots-i-1,:],alpha=.3)
tmpax.set_ylim(0,5)
tmpax.set_xlim(0, 51200)
I think the easiest way is to use the boundaries from your 'imshow axes' to manually calculate the boundaries of all your 'lineplot axes':
import matplotlib.pyplot as plt
import numpy as np
fig, axs = plt.subplots(1,1,figsize=(15,10))
axs.imshow(np.random.rand(50,100) ,cmap='gray', interpolation='none', alpha=0.3)
nplots = 50
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
tmpax.plot(np.sin(np.linspace(0,np.random.randint(20,1000),1000))*0.4)
tmpax.set_ylim(-1,1)
The above code seems nice, but i do have some issues with the autoscale chopping off part of the plot. Try removing the last line to see the effect, im not sure why thats happening.

Categories

Resources