Related
I want to add flag images such as below to my bar chart:
I have tried AnnotationBbox but that shows with a square outline. Can anyone tell how to achieve this exactly as above image?
Edit:
Below is my code
ax.barh(y = y, width = values, color = r, height = 0.8)
height = 0.8
for i, (value, url) in enumerate(zip(values, image_urls)):
response = requests.get(url)
img = Image.open(BytesIO(response.content))
width, height = img.size
left = 10
top = 10
right = width-10
bottom = height-10
im1 = img.crop((left, top, right, bottom))
print(im1.size)
im1
ax.imshow(im1, extent = [value - 6, value, i - height / 2, i + height / 2], aspect = 'auto', zorder = 2)
Edit 2:
height = 0.8
for j, (value, url) in enumerate(zip(ww, image_urls)):
response = requests.get(url)
img = Image.open(BytesIO(response.content))
ax.imshow(img, extent = [value - 6, value - 2, j - height / 2, j + height / 2], aspect = 'auto', zorder = 2)
ax.set_xlim(0, max(ww)*1.05)
ax.set_ylim(-0.5, len(yy) - 0.5)
plt.tight_layout()
You need the images in a .png format with a transparent background. (Software such as Gimp or ImageMagick could help in case the images don't already have the desired background.)
With such an image, plt.imshow() can place it in the plot. The location is given via extent=[x0, x1, y0, y1]. To prevent imshow to force an equal aspect ratio, add aspect='auto'. zorder=2 helps to get the image on top of the bars. Afterwards, the plt.xlim and plt.ylim need to be set explicitly (also because imshow messes with them.)
The example code below used 'ada.png' as that comes standard with matplotlib, so the code can be tested standalone. Now it is loading flags from countryflags.io, following this post.
Note that the image gets placed into a box in data coordinates (6 wide and 0.9 high in this case). This box will get stretched, for example when the plot gets resized. You might want to change the 6 to another value, depending on the x-scale and on the figure size.
import numpy as np
import matplotlib.pyplot as plt
# import matplotlib.cbook as cbook
import requests
from io import BytesIO
labels = ['CW', 'CV', 'GW', 'SX', 'DO']
colors = ['crimson', 'dodgerblue', 'teal', 'limegreen', 'gold']
values = 30 + np.random.randint(5, 20, len(labels)).cumsum()
height = 0.9
plt.barh(y=labels, width=values, height=height, color=colors, align='center')
for i, (label, value) in enumerate(zip(labels, values)):
# load the image corresponding to label into img
# with cbook.get_sample_data('ada.png') as image_file:
# img = plt.imread(image_file)
response = requests.get(f'https://www.countryflags.io/{label}/flat/64.png')
img = plt.imread(BytesIO(response.content))
plt.imshow(img, extent=[value - 8, value - 2, i - height / 2, i + height / 2], aspect='auto', zorder=2)
plt.xlim(0, max(values) * 1.05)
plt.ylim(-0.5, len(labels) - 0.5)
plt.tight_layout()
plt.show()
PS: As explained by Ernest in the comments and in this post, using OffsetImage the aspect ratio of the image stays intact. (Also, the xlim and ylim stay intact.) The image will not shrink when there are more bars, so you might need to experiment with the factor in OffsetImage(img, zoom=0.65) and the x-offset in AnnotationBbox(..., xybox=(-25, 0)).
An extra option could place the flags outside the bar for bars that are too short. Or at the left of the y-axis.
The code adapted for horizontal bars could look like:
import numpy as np
import requests
from io import BytesIO
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
def offset_image(x, y, label, bar_is_too_short, ax):
response = requests.get(f'https://www.countryflags.io/{label}/flat/64.png')
img = plt.imread(BytesIO(response.content))
im = OffsetImage(img, zoom=0.65)
im.image.axes = ax
x_offset = -25
if bar_is_too_short:
x = 0
ab = AnnotationBbox(im, (x, y), xybox=(x_offset, 0), frameon=False,
xycoords='data', boxcoords="offset points", pad=0)
ax.add_artist(ab)
labels = ['CW', 'CV', 'GW', 'SX', 'DO']
colors = ['crimson', 'dodgerblue', 'teal', 'limegreen', 'gold']
values = 2 ** np.random.randint(2, 10, len(labels))
height = 0.9
plt.barh(y=labels, width=values, height=height, color=colors, align='center', alpha=0.8)
max_value = values.max()
for i, (label, value) in enumerate(zip(labels, values)):
offset_image(value, i, label, bar_is_too_short=value < max_value / 10, ax=plt.gca())
plt.subplots_adjust(left=0.15)
plt.show()
To complete #johanC answer, it's possible to use flags from iso-flags-png under GNU/linux and the iso3166 python package:
import matplotlib.pyplot as plt
from iso3166 import countries
import matplotlib.image as mpimg
def pos_image(x, y, pays, haut):
pays = countries.get(pays).alpha2.lower()
fichier = "/usr/share/iso-flags-png-320x240"
fichier += f"/{pays}.png"
im = mpimg.imread(fichier)
ratio = 4 / 3
w = ratio * haut
ax.imshow(im,
extent=(x - w, x, y, y + haut),
zorder=2)
plt.style.use('seaborn')
fig, ax = plt.subplots()
liste_pays = [('France', 10), ('USA', 9), ('Spain', 5), ('Italy', 5)]
X = [p[1] for p in liste_pays]
Y = [p[0] for p in liste_pays]
haut = .8
r = ax.barh(y=Y, width=X, height=haut, zorder=1)
y_bar = [rectangle.get_y() for rectangle in r]
for pays, y in zip(liste_pays, y_bar):
pos_image(pays[1], y, pays[0], haut)
plt.show()
which gives:
I am trying to plot multiple images in a figure using matplotlib.
Basically, I read the images using PIl library, convert it to numpy array and do some operation on it (setting the elements in a row to zero). Everything works fine till this point. But when I try to save the results using matplotlib, I get inconsistent results.
Please have a look at my code.
Importing the libraries
import numpy as np
import matplotlib.pyplot as plt
import PIL.Image as PI
Loading the file
fileName = 'n01978287_43.jpg'
img = PI.open(fileName)
size = 224
img = img.resize((size, size))
img = np.asarray(img, dtype=np.uint8).astype(np.float32)
img = img/255
Result 1
temp_img = np.copy(img)
temp_img[51, :, :] = 0*temp_img[51, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 6, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 6, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_516.png')
plt.close(fig)
Result 2
temp_img = np.copy(img)
temp_img[52, :, :] = 0*temp_img[52, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 6, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 6, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_526.png')
plt.close(fig)
Result 3
temp_img = np.copy(img)
temp_img[51, :, :] = 0*temp_img[51, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 2, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 2, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_512.png')
plt.close(fig)
Result 4
temp_img = np.copy(img)
temp_img[56, :, :] = 0*temp_img[56, :, :]
fig = plt.figure()
ax1 = plt.subplot(1, 2, 1)
ax1.imshow(img, interpolation='none')
ax2 = plt.subplot(1, 2, 2)
ax2.imshow(temp_img, interpolation='none')
plt.savefig('test_562.png')
plt.close(fig)
Now, if you look at the results, you would notice the inconsistency.
Firstly, for first two images (figure with 6 axes), you see the black line only in one of the image. (There is a pattern to this if you zero out all the rows (one at a time) and then try to save the results).
In the last two images, black line gets thicker. (I didn't find any pattern in this case).
System Setup - Python3, Matplotlib3, PIL, Numpy
Update:
After looking for ways to save a figure with the desired resolution (224*224 in this case), I wrote the following code (using multiple resources from web).
Importing libraries and loading the image file
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image, ImageDraw, ImageFont
fileName = 'n01978287_43.jpg'
img = Image.open(fileName)
size = 224
img = img.resize((size, size))
img = np.asarray(img, dtype=np.uint8).astype(np.float32)
img = img/255
Function to plot the grid of images
def plt_save(grid, idx, name):
nRows = len(grid)
nCols = len(grid[0])
print('Clearing figure')
plt.rcParams.update({'font.size': 8})
wFig = (nCols+2) # Figure width (two more than nCols because I want to add ylabels on the very left and very right of figure)
hFig = (nRows+1) # Figure height (one more than nRows becasue I want to add xlabels to the top of figure)
fig = plt.figure(figsize=( wFig, hFig ))
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
fig.patch.set_facecolor('grey')
for r in range(nRows):
for c in range(nCols):
ax = plt.subplot2grid( shape=[hFig, wFig], loc=[r+1, c+1] )
im= ax.imshow(grid[r][c], interpolation='none')
ax.spines['bottom'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
#fig.colorbar(im, ax=ax)
#ax.set_aspect('auto')
if not r:
ax.set_title('Image',
rotation=22.5,
horizontalalignment='left',
verticalalignment='bottom')
if not c:
ax.set_ylabel('leftLabel',
rotation=0,
horizontalalignment='right',
verticalalignment='center')
if c == wFig-3:
ax2 = ax.twinx()
#ax2.axis('off')
ax2.set_xticks([])
ax2.set_yticks([])
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['bottom'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax2.set_ylabel( 'rightLabel',
rotation=0,
verticalalignment='center',
horizontalalignment='left' )
print('Saving file')
plt.savefig( ( str(idx) + '_' + name + '_' + fileName.split('.')[0] + '.png'),
orientation='landscape',
#bbox_inches='tight',
facecolor = fig.get_facecolor(),
dpi=224, # DPI is 224 becasue the axis size is 1x1 inch and I want 224x224 pixels in each axis
transparent=True,
frameon=False )
plt.close(fig)
Loop to zero out the rows of the image (one at a time)
for i in range(0, 224):
temp_img = np.copy(img)
temp_img[i, :, :] = 0*temp_img[i, :, :]
# 1*4 Grid of images (can vary based on the requirement)
grid = [img, temp_img, img, temp_img]
grid = [grid, grid] #2*4 grid of images
plt_save(grid, i, 'PLT_')
Here is how one of the 224 images looks like.
The thing is that it works perfectly as long as I stick with this kind of plot. But the moment I try to make some changes (like adding a colorbar, having some spaces between each axis etc), the image resolution changes. If I use bbox_inches = 'tight' while saving my figure, it adjusts everything but changes the original resolution while keeping the figure size constant.
Is there any other way similar to bbox_inches='tight' such that it can keep the axis resolution fixed while adjusting the figure size accordingly. Or if there is no such thing in matplotlib, could you suggest me any other way to incorporate colorbar (small spaces between axis, ylabel for each axis etc) while keeping the image resolution fixed.
The image you start with has 224 pixels in height.
In the first two cases you distribute those over 72 pixels in the resulting image. This means any row of the image has a 72/224=32% chance of showing up in the final plot. In row number 52 you are lucky and hit this one third chance.
In the second two cases the resulting image is 226 pixels in height (i.e. just slightly larger than the original). Here you have a 2/224=0.9% chance that one row will occupy two pixels. In the case of row no. 56 you hit that unlucky chance.
I'm rather fond of The Logistic Map' Period Doubling Bifurcation and would like to print it on a canvas.
I can create the plot in python, but need some help preparing figure properties so that it has suitable resolution to be printed. My code right ow produces some jagged lines.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
# overall image properties
width, height, dpi = 2560, 1440, 96
picture_background = 'white'
aspect_ratio = width / height
plt.close('all')
R = np.linspace(3.5,4,5001)
fig = plt.figure(figsize=(width / dpi, height / dpi), frameon=False)
ylim = -0.1,1.1
ax = plt.Axes(fig, [0, 0, 1, 1], xlim = (3.4,4))
ax.set_axis_off()
fig.add_axes(ax)
for r in R:
x = np.zeros(5001)
x[0] = 0.1
for i in range(1,len(x)):
x[i] = r*x[i-1]*(1-x[i-1])
ax.plot(r*np.ones(2500),x[-2500:],marker = '.', markersize= 0.01,color = 'grey', linestyle = 'none')
plt.show()
plt.savefig('figure.eps', dpi=dpi, bbox_inches=0, pad_inches=0, facecolor=picture_background)
Here is what the code produces:
As you can see, some of the lines to the far left of the plot are rather jagged.
How can I create this figure so that the resolution is suitable to be printed on a variety of frame dimensions?
I think the source of the jaggies is underlying pixel size + that you are drawing this using very small 'point' markers. The pixels that the line are going through are getting fully saturated so you get the 'jaggy'.
A somewhat better way to plot this data is to do the binning ahead of time and then have mpl plot a heat map:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
plt.ion()
width, height, dpi = 2560 / 2, 1440 / 2, 96 # cut to so SO will let me upload result
aspect_ratio = width / height
fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), frameon=False,
tight_layout=True)
ylim = -0.1, 1.1
ax.axis('off')
# make heatmap at double resolution
accumulator = np.zeros((height, width), dtype=np.uint64)
burn_in_count = 25000
N = 25000
R = np.linspace(3.5, 4, width)
x = 0.1 * np.ones_like(R)
row_indx = np.arange(len(R), dtype='uint')
# do all of the r values in parallel
for j in range(burn_in_count):
x = R * x * (1 - x)
for j in range(N):
x = R * x * (1 - x)
col_indx = (height * x).astype('int')
accumulator[col_indx, row_indx] += 1
im = ax.imshow(accumulator, cmap='gray_r',
norm=mcolors.LogNorm(), interpolation='none')
Note that this is log-scaled, if you just want to see what pixels are hit
use
im = ax.imshow(accumulator>0, cmap='gray_r', interpolation='nearest')
but these still have issues of the jaggies and (possibly worse) sometimes the narrow lines get aliased out.
This is the sort of problem that datashader or rasterized scatter is intended to solve by re-binning the data at draw time in an intelligent way (see this PR for a prototype datashader/mpl integartion). Both of those are still prototype/proof-of-concept, but usable.
http://matplotlib.org/examples/event_handling/viewlims.html which re-compute the Mandelbrot set on zoom might also be of interest to you.
I found a similar quesion on How to plot confusion matrix with string axis rather than integer in python. But the answer is not exact what I want. Because it doesn't contain gridding (e.g., the numbers are not in little squares) and there is background color to show the number which is not what I want.
import numpy as np
import matplotlib.pyplot as plt
conf_arr = [[33,2,0,0,0,0,0,0,0,1,3],
[3,31,0,0,0,0,0,0,0,0,0],
[0,4,41,0,0,0,0,0,0,0,1],
[0,1,0,30,0,6,0,0,0,0,1],
[0,0,0,0,38,10,0,0,0,0,0],
[0,0,0,3,1,39,0,0,0,0,4],
[0,2,2,0,4,1,31,0,0,0,2],
[0,1,0,0,0,0,0,36,0,2,0],
[0,0,0,0,0,0,1,5,37,5,1],
[3,0,0,0,0,0,0,0,0,39,0],
[0,0,0,0,0,0,0,0,0,0,38]]
norm_conf = []
for i in conf_arr:
a = 0
tmp_arr = []
a = sum(i, 0)
for j in i:
tmp_arr.append(float(j)/float(a))
norm_conf.append(tmp_arr)
fig = plt.figure()
plt.clf()
ax = fig.add_subplot(111)
ax.set_aspect(1)
res = ax.imshow(np.array(norm_conf), cmap=plt.cm.jet,
interpolation='nearest')
width, height = conf_arr.shape
for x in xrange(width):
for y in xrange(height):
ax.annotate(str(conf_arr[x][y]), xy=(y, x),
horizontalalignment='center',
verticalalignment='center')
cb = fig.colorbar(res)
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plt.xticks(range(width), alphabet[:width])
plt.yticks(range(height), alphabet[:height])
plt.savefig('confusion_matrix.png', format='png')
By making only a few changes to that rather excellent code proposal (I upvoted it, consider doing that too), you can get the figure you're describing.
You'll get gridding by calling the hlines and vlines methods of the ax object, which will add horizontal and vertical lines respectively.
When you then also remove the call to imshow, the colors are gone. Like this:
import numpy as np
import matplotlib.pyplot as plt
conf_arr = np.array([[33,2,0,0,0,0,0,0,0,1,3],
[3,31,0,0,0,0,0,0,0,0,0],
[0,4,41,0,0,0,0,0,0,0,1],
[0,1,0,30,0,6,0,0,0,0,1],
[0,0,0,0,38,10,0,0,0,0,0],
[0,0,0,3,1,39,0,0,0,0,4],
[0,2,2,0,4,1,31,0,0,0,2],
[0,1,0,0,0,0,0,36,0,2,0],
[0,0,0,0,0,0,1,5,37,5,1],
[3,0,0,0,0,0,0,0,0,39,0],
[0,0,0,0,0,0,0,0,0,0,38]])
height, width = conf_arr.shape
fig = plt.figure('confusion matrix')
ax = fig.add_subplot(111, aspect='equal')
for x in range(width):
for y in range(height):
ax.annotate(str(conf_arr[x][y]), xy=(y, x), ha='center', va='center')
offset = .5
ax.set_xlim(-offset, width - offset)
ax.set_ylim(-offset, height - offset)
ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plt.xticks(range(width), alphabet[:width])
plt.yticks(range(height), alphabet[:height])
plt.savefig('confusion_matrix.png', format='png')
Remark that when you remove the call to imshow, you'll need to set the x- and y-limits explicitly, as shown above, otherwise you'll only see the lower left region (imshow updates the limits automatically depending on what you pass to it).
I would like to have a window that is divided in 4 sectors: in the (0,0) a imshow image (ax1); (1,0) a subplot image that uses twinx() image that divides the window(ax2 & ax3); (1,1) a regular plot image (ax4); and an iterative section (0,1) of plots that should give "number_of_subplots" plots one above the other (ax5). Hopefully with no xticklabels but the last one.
This is how the frame should look like before the iterative subplot creation.
My problem: when iterating to create the subplots on the top right space of the window, the subplots span away from that space and eliminate the ax4
This is how the window looks after the "for" cyle for the subplot creation
Below you'll find a simplification of the code I am using, just so you can see it better. I have replaced my experimental data with random numbers so you can replicate this easily.
Could you give me a hint on what am I doing wrong? I still do not dominate all the handlers in python. I used to do similar things in matlab a few years ago.
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import numpy as np
import pdb
pos = [1,2,3,4,5]
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
xx = np.linspace(0, 20, 1000)
fig1 = plt.figure()
number_of_subplots = len(pos) #number between 1-7
ax1 = plt.subplot2grid((number_of_subplots+1,2),(0,0),rowspan = number_of_subplots-1) # Here the idea is to "dinamically" create the division of the grid, making space at the bottom of it for the image in the bottom left.
ax1.scatter(x,y)
ax2 = plt.subplot2grid((number_of_subplots+1,2),(number_of_subplots-1,0), rowspan = 2)
ax2.plot(xx,np.sin(xx),label = 'sin(x)',color = 'b')
ax3 = ax2.twinx()
ax3.plot(xx,np.cos(xx), label = 'cos(x)', color = 'r')
ax4 = plt.subplot2grid((number_of_subplots+1,2),(number_of_subplots-1,1), rowspan = 2)
ax4.plot(xx,np.tan(xx), label = 'tan(x)', color = 'g')
for i,v in enumerate(xrange(number_of_subplots)):
v = v+1
ax5 = plt.subplot2grid((number_of_subplots+1,2),(v-1,1))
ax5.plot(np.sin(xx+3.1416*v/2)) # Grafica los perfiles, asociandoles el mismo color que para los cortes en la imagen 2D
if (i % 2 == 0): #Even
ax5.yaxis.tick_left()
else:
ax5.yaxis.tick_right()
plt.draw()
plt.show()
Solved the issue by using GridSpec as it is supposed to be used. Below is the implementation of the code that gives the following solution.
This is the correct way the image should look like and the implementation is below on the code.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import pdb
pos = [1,2,3,4,5]
N = 50
x = np.random.rand(N)
y = np.random.rand(N)
xx = np.linspace(0, 20, 1000)
number_of_subplots = len(pos) #number between 1-7
fig1 = plt.figure()
gs0 = gridspec.GridSpec(2,2,height_ratios=[3,1],hspace=0.1)
ax1 = plt.subplot(gs0[0,0])
ax2 = plt.subplot(gs0[-1,0])
ax4 = plt.subplot(gs0[-1,-1])
gs2 = gridspec.GridSpecFromSubplotSpec(number_of_subplots, 1, subplot_spec=gs0[1],wspace=0.0, hspace=0.0)
ax1.scatter(x,y)
ax2.plot(xx,np.sin(xx),label = 'sin(x)',color = 'b')
ax3 = ax2.twinx()
ax3.plot(xx,np.cos(xx), label = 'cos(x)', color = 'r')
ax4.plot(xx,np.tan(xx), label = 'tan(x)', color = 'g')
for i in enumerate(xrange(number_of_subplots)):
ax5 = plt.subplot(gs2[i,:])
ax5.plot(np.sin(xx+3.1416*i/2))
if (i % 2 == 0): #Even
ax5.yaxis.tick_left()
else:
ax5.yaxis.tick_right()
plt.draw()
plt.show()