Matplotlib: Plot images instead of points without overlapping images - python

Using this SO post, I can plot images instead of points:
def _implot(self, x, y, image, ax, zoom=1):
im = OffsetImage(image, zoom=zoom, cmap='gray_r')
ab = AnnotationBbox(im, (x, y), xycoords='data', frameon=False)
ax.add_artist(ab)
ax.update_datalim(np.column_stack([x, y]))
ax.autoscale()
fig, ax = plt.subplots(1, 1)
fig.set_size_inches(10, 10)
for img, x in zip(Y, X):
img = img.reshape(28, 28)
self._implot(x[0], x[1], img, ax=ax, zoom=0.5)
Above, Y is an Nx(28*28) matrix of flattened images and X is an Nx2 matrix of locations. My issue is that many of these images overlap, making the figure hard to read:
I'd like to skip plotting any image if it would overlap with an existing image. How can I do this? I could manually track which (x, y) coordinates have been plotted and also account for the image width and height, but that seems brittle, and I'm hoping Matplotlib has a built-in way to check for overlapping artists.

Related

Make a heatmap whit 2d points and 2 images

Good day to all,
I am trying to create a heatmap, given a set of x, y coordinate pairs extracted from a CSV file. I am using numpy.histogram2d to plot it, the heatmap gives me correct, but I need to overlay it on another image. I share my code
Here is the CSV
csv = pd.read_csv("../test2.csv")
#Gets the centroid of the coords
csv["x"] = (combined_csv["x_min"]+(combined_csv["x_max"]-combined_csv["x_min"])/2).astype(int)
csv["y"] = (combined_csv["y_min"]+(combined_csv["y_max"]-combined_csv["y_min"])/2).astype(int)
am = csv[(csv["tracking_id"] != 7) &(csv["tracking_id"] != 28)] #Some filters
#Taken from another question
def myplot(x, y, s, bins=1000):
heatmap, xedges, yedges = np.histogram2d(x, y, bins=bins, range=[[0, 960], [0, 480]])
heatmap = gaussian_filter(heatmap, sigma=s)
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
return heatmap.T, extent
img, extent = myplot(am["x"], am["y_max"], 16,200)
plt.imshow(img, extent=extent, origin='upper', cmap=cm.jet)
The previous code gives me this image
I put the range between 0 and 480 for x and 0 and 960 for y, because the target image has that size, but doing this, histogram2d returns me the histogram matrix h, which is 200x200, then I use matplotlib to plot it and This graphs me correctly in the range 0-480 for x and 0-960 for y, what I need is to save said image, preserving as much information as possible in the 480x960 pixel size, and then add it with the target image using this tutorial how to superimpose heatmap on a base image?
final_img = cv2.addWeighted (heatmap_img, 0.5, image, 0.5, 0)
For creating something like this
But the image and the histogram doesnt match in the sizes that i wanted.
This is the target image
Ok, so i fix the error by the next code, printing the two images at the same plot
base_image = image
sigma = 8
fig, ax = plt.subplots(1, 1, figsize = (20, 16))
heatmap, xedges, yedges = np.histogram2d(am["x"], am["y"], bins=200, range=[[0,960],[0,480]])
heatmap = gaussian_filter(heatmap, sigma=sigma)
extent = [xedges[0], xedges[-1], yedges[-1], yedges[0]]
img = heatmap.T
ax.imshow(img, extent=extent, cmap=cm.jet)
ax.imshow(base_image, alpha = 0.5)
ax.set_xlim(0, 960)
ax.set_ylim(480,0)
ax.axes.get_yaxis().set_visible(False)
ax.axes.get_xaxis().set_visible(False)
plt.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
plt.savefig("heatmap_final.png", bbox_inches='tight')
plt.show()
plt.close(fig)

Over-plot an equation curve over a png image

enter image description hereI'm having trouble overplotting a relation between radial velocity and offset(position). I've looked at various solutions, but it doesn't seem to work. I've converted the equation into numbers, with only one variable.It also doesn't display the picture to the required dimensions.
x = np.linspace(-0.8 ,0.8 , 1000)
y = 0.5*((1.334e+20/x)**0.5)
img = plt.imread('Pictures/PVdiagram1casaviewer.png')
fig, ax = plt.subplots(figsize=(16, 16), tight_layout=True)
ax.set_xlabel('Offset(arcsec)', fontsize=14)
ax.set_ylabel('Radial Velocity (Km/S)', fontsize=14)
ax.imshow(img, extent=[-0.8, 0.8, -5, 15])
ax.plot(x, y, linewidth=5, color='white')
plt.title('PV Diagram')
plt.show()
enter image description here
If I plot your image, you can see that the axis of the image and matplotlib don't match, because the image contains space between the plot and border of the pictures (axis titles, and so on...)
So, first you need to crop the image, so that it contains just the plot area.
Next, you can plot the image with the argument aspect=auto to scale it to your figsize:
ax.imshow(img, extent=[-0.8,0.8,-5,15], aspect='auto')
If you try to plot your y function over the image, you will see that the values of y are much larger, so the curve is above the image (notice the tiny image is at the bottom).
I don't know what the physical background of y is, but if you divide it by 10e9 it fits inside the image-range.
Full code:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-0.8 ,0.8 , 1000)
y = 0.5*((1.334e+20/x)**0.5)/10e9 # Scale it here... but how?
img = plt.imread('hNMw82.png')
fig, ax = plt.subplots(figsize=(16, 16), tight_layout=True)
ax.set_xlabel('Offset(arcsec)', fontsize=14)
ax.set_ylabel('Radial Velocity (Km/S)', fontsize=14)
ax.imshow(img, extent=[-0.8,0.8,-5,15], aspect='auto')
ax.plot(x, y, linewidth=5, color='white')
ax.set_ylim([-5,15])
ax.set_xlim([-0.8,0.8])
plt.title('PV Diagram')
plt.show()
Result:
(I also set the axis limits.)

how to set the grid when using pcolormesh

I am using pcolormesh to create a grid that overlaps a 2dhistogram.
import matplotlib.pyplot as plt
import numpy as np
import random
x = [random.randrange(1,161,1) for _ in range (10)]
y = [random.randrange(1,121,1) for _ in range (10)]
fig, ax = plt.subplots()
ax.set_xlim(0,160)
ax.set_ylim(0,120)
zi, yi, xi = np.histogram2d(y, x, bins=(50,120))
zi = np.ma.masked_equal(zi, 0)
ax.pcolormesh(xi, yi, zi, edgecolors='black')
scat = ax.scatter(x, y, s=2)
Although, this code only produces a grid that covers the outermost xy data points.
I'd like the grid to be constant with the set axes limits (x = 0,160), (y = 0,120). So The grid is constantly covering the plotted area. From 0,0 to 160,120.
I have tried to use the vmin, vmax function in pcolormesh. But this just produces a blank figure. I don't get an error code though?
ax.pcolormesh(xi, yi, zi, edgecolors='black', vmin = (0,0), vmax = (120,160))
Is there another way to extend the grid to the desired axes limits?
One problem is that the histogram2d function determines the bins itself if you use it like you do.
This means that both the offset and the width of your bins is unclear until runtime because they depend on your random points rather than on your axis limits. Now once the bins are found you could read back their shape and set an axis grid accordingly. But it's easier to create your own bins so you get a grid that spans the whole axis ranges.
Then you can set the edges of your bins as minor ticks and enable a grid on them.
Using the lines created by pcolormesh would work too but when using it you will get some lines that are thicker than others (this has to do with line positions falling between pixels). With axis grid this doesn't happen but some lines appear to cut through your bins. In the end it's a matter of taste which one you prefer. You can always play around with edgecolor and linewidth until pcolormesh shows a decent result.
import matplotlib.pyplot as plt
import numpy as np
import random
x = [random.randrange(1,161,1) for _ in range (10)]
y = [random.randrange(1,121,1) for _ in range (10)]
fig, ax = plt.subplots()
ax.set_xlim(0,160)
ax.set_ylim(0,120)
bins = [
np.linspace(*ax.get_xlim(), 120),
np.linspace(*ax.get_ylim(), 50)
]
# Note that I switched back to x, y and used zi.T later which I find
# more readable
zi, xi, yi = np.histogram2d(x, y, bins=bins)
zi = np.ma.masked_equal(zi, 0)
# Either use the next four lines for axis grid
ax.pcolormesh(xi, yi, zi.T)
ax.set_xticks(bins[0], minor=True)
ax.set_yticks(bins[1], minor=True)
ax.grid(True, which='minor')
# or use the next line to stick with edges drawn by pcolormesh
# ax.pcolormesh(xi, yi, zi.T, edgecolor='black')
scat = ax.scatter(x, y, s=2)

matplotlib - autosize of text according to shape size

I'm adding a text inside a shape by:
ax.text(x,y,'text', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10) (ax is AxesSubplot)
The problem is that I couldn't make the circle size constant while changing the string length. I want the text size adjust to the circle size and not the other way around.
The circle is even completely gone if the string is an empty one.
The only bypass to the problem I had found is dynamically to set the fontsize param according to the len of the string, but that's too ugly and not still the circle size is not completely constant.
EDIT (adding a MVCE):
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.text(0.5,0.5,'long_text', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10)
ax.text(0.3,0.7,'short', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10)
plt.show()
Trying to make both circles the same size although the string len is different. Currently looks like this:
I have a very dirty and hard-core solution which requires quite deep knowledge of matplotlib. It is not perfect but might give you some ideas how to start.
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
plt.close('all')
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
t1 = ax.text(0.5,0.5,'long_text', ha='center', va='center',fontsize=10)
t2 = ax.text(0.3,0.7,'short', ha='center', va='center', fontsize=10)
t3 = ax.text(0.1,0.7,'super-long-text-that-is-long', ha='center', va='center', fontsize=10)
fig.show()
def text_with_circle(text_obj, axis, color, border=1.5):
# Get the box containing the text
box1 = text_obj.get_window_extent()
# It turned out that what you get from the box is
# in screen pixels, so we need to transform them
# to "data"-coordinates. This is done with the
# transformer-function below
transformer = axis.transData.inverted().transform
# Now transform the corner coordinates of the box
# to data-coordinates
[x0, y0] = transformer([box1.x0, box1.y0])
[x1, y1] = transformer([box1.x1, box1.y1])
# Find the x and y center coordinate
x_center = (x0+x1)/2.
y_center = (y0+y1)/2.
# Find the radius, add some extra to make a nice border around it
r = np.max([(x1-x0)/2., (y1-y0)/2.])*border
# Plot the a circle at the center of the text, with radius r.
circle = Circle((x_center, y_center), r, color=color)
# Add the circle to the axis.
# Redraw the canvas.
return circle
circle1 = text_with_circle(t1, ax, 'g')
ax.add_artist(circle1)
circle2 = text_with_circle(t2, ax, 'r', 5)
ax.add_artist(circle2)
circle3 = text_with_circle(t3, ax, 'y', 1.1)
ax.add_artist(circle3)
fig.canvas.draw()
At the moment you have to run this in ipython, because the figure has to be drawn BEFORE you get_window_extent(). Therefore the fig.show() has to be called AFTER the text is added, but BEFORE the circle can be drawn! Then we can get the coordinates of the text, figures out where the middle is and add a circle around the text with a certain radius. When this is done we redraw the canvas to update with the new circle. Ofcourse you can customize the circle a lot more (edge color, face color, line width, etc), look into the Circle class.
Example of output plot:

Matplotlib: How to plot images instead of points?

I want to read a list of images into Python/Matplotlib and then plot this images instead of other markers (like points) in a graph. I have tried with imshow but I didn't succeed, because I cannot shift the image to another position and scale it appropriately. Maybe somebody has a good idea : )
There are two ways to do this.
Plot the image using imshow with the extent kwarg set based on the location you want the image at.
Use an OffsetImage inside an AnnotationBbox.
The first way is the easiest to understand, but the second has a large advantage. The annotation box approach will allow the image to stay at a constant size as you zoom in. Using imshow will tie the size of the image to the data coordinates of the plot.
Here's an example of the second option:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from matplotlib.cbook import get_sample_data
def main():
x = np.linspace(0, 10, 20)
y = np.cos(x)
image_path = get_sample_data('ada.png')
fig, ax = plt.subplots()
imscatter(x, y, image_path, zoom=0.1, ax=ax)
ax.plot(x, y)
plt.show()
def imscatter(x, y, image, ax=None, zoom=1):
if ax is None:
ax = plt.gca()
try:
image = plt.imread(image)
except TypeError:
# Likely already an array...
pass
im = OffsetImage(image, zoom=zoom)
x, y = np.atleast_1d(x, y)
artists = []
for x0, y0 in zip(x, y):
ab = AnnotationBbox(im, (x0, y0), xycoords='data', frameon=False)
artists.append(ax.add_artist(ab))
ax.update_datalim(np.column_stack([x, y]))
ax.autoscale()
return artists
main()
If you want different images:
This is now the first reply when googling "matplotlib scatter with images". If you're like me and actually need to plot different images on each image, try this minimalied example instead. Just be sure to input your own images.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
def getImage(path, zoom=1):
return OffsetImage(plt.imread(path), zoom=zoom)
paths = [
'a.jpg',
'b.jpg',
'c.jpg',
'd.jpg',
'e.jpg']
x = [0,1,2,3,4]
y = [0,1,2,3,4]
fig, ax = plt.subplots()
ax.scatter(x, y)
for x0, y0, path in zip(x, y,paths):
ab = AnnotationBbox(getImage(path), (x0, y0), frameon=False)
ax.add_artist(ab)

Categories

Resources