how to save this Matplotlib drawing as a Numpy array? - python

I have a function that takes an image stored as a Numpy array, draws a few rectangles on it, labels them, then displays the result.
The shape of the source Numpy array is (480, 640, 3) - it's an RGB image from a camera. This probably doesn't matter a lot, but I'm just showing you an example of the data I'm working with.
This is the function:
def draw_boxes(imdata, v_boxes, v_labels, v_scores):
fig = pyplot.imshow(imdata)
# get the context for drawing boxes
ax = pyplot.gca()
# plot each box
for i in range(len(v_boxes)):
box = v_boxes[i]
# get coordinates
y1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax
# calculate width and height of the box
width, height = x2 - x1, y2 - y1
# create the shape
rect = Rectangle((x1, y1), width, height, fill=False, color='white')
# draw the box
ax.add_patch(rect)
# draw text and score in top left corner
label = "%s (%.3f)" % (v_labels[i], v_scores[i])
ax.text(x1, y1, label, color='white')
pyplot.show()
I would like to take the annotated image (the image with the rectangles and labels drawn on it) and extract all that as a Numpy array. Basically, return an annotated Numpy array.
I've spent a couple hours trying various solution found on Google, but nothing works. For example, I cannot do this...
fig.canvas.draw()
X = np.array(fig.canvas.renderer.buffer_rgba())
...because fig.canvas.draw() fails with:
AttributeError: 'AxesImage' object has no attribute 'canvas'

The problem is that your fig variable is not a figure but an AxesImage as the error is stating. Thus change the first line of your code with :
fig, ax = plt.subplots()
ax = plt.imshow(imdata)
The complete function is then :
def draw_boxes(imdata, v_boxes, v_labels, v_scores):
fig, ax = plt.subplots()
ax = plt.imshow(imdata)
# get the context for drawing boxes
ax = pyplot.gca()
# plot each box
for i in range(len(v_boxes)):
box = v_boxes[i]
# get coordinates
y1, x1, y2, x2 = box.ymin, box.xmin, box.ymax, box.xmax
# calculate width and height of the box
width, height = x2 - x1, y2 - y1
# create the shape
rect = Rectangle((x1, y1), width, height, fill=False, color='white')
# draw the box
ax.add_patch(rect)
# draw text and score in top left corner
label = "%s (%.3f)" % (v_labels[i], v_scores[i])
ax.text(x1, y1, label, color='white')
fig.canvas.draw()
X = np.array(fig.canvas.renderer.buffer_rgba(), dtype=float)
return X

Related

Line and text don't align in matplotlib [duplicate]

Have a plot with several diagonal lines with different slopes. I would like to annotate these lines with a text label that matches the slope of the lines.
Something like this:
Is there a robust way to do this?
I've tried both text's and annotate's rotation parameters, but those are in screen coordinates, not data coordinates (i.e. it's always x degrees on the screen no matter the xy ranges). My x and y ranges differ by orders of magnitude, and obviously the apparent slope is affected by viewport size among other variables, so a fixed-degree rotation doesn't do the trick. Any other ideas?
This is the exact same process and basic code as given by #Adam --- it's just restructured to be (hopefully) a little more convenient.
def label_line(line, label, x, y, color='0.5', size=12):
"""Add a label to a line, at the proper angle.
Arguments
---------
line : matplotlib.lines.Line2D object,
label : str
x : float
x-position to place center of text (in data coordinated
y : float
y-position to place center of text (in data coordinates)
color : str
size : float
"""
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
ax = line.get_axes()
text = ax.annotate(label, xy=(x, y), xytext=(-10, 0),
textcoords='offset points',
size=size, color=color,
horizontalalignment='left',
verticalalignment='bottom')
sp1 = ax.transData.transform_point((x1, y1))
sp2 = ax.transData.transform_point((x2, y2))
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = np.degrees(np.arctan2(rise, run))
text.set_rotation(slope_degrees)
return text
Used like:
import numpy as np
import matplotlib.pyplot as plt
...
fig, axes = plt.subplots()
color = 'blue'
line, = axes.plot(xdata, ydata, '--', color=color)
...
label_line(line, "Some Label", x, y, color=color)
Edit: note that this method still needs to be called after the figure layout is finalized, otherwise things will be altered.
See: https://gist.github.com/lzkelley/0de9e8bf2a4fe96d2018f1b1bd5a0d3c
I came up with something that works for me. Note the grey dashed lines:
The rotation must be set manually, but this must be done AFTER draw() or layout. So my solution is to associate lines with annotations, then iterate through them and do this:
get line's data transform (i.e. goes from data coordinates to display coordinates)
transform two points along the line to display coordinates
find slope of displayed line
set text rotation to match this slope
This isn't perfect, because matplotlib's handling of rotated text is all wrong. It aligns by the bounding box and not by the text baseline.
Some font basics if you're interested about text rendering: http://docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html
This example shows what matplotlib does: http://matplotlib.org/examples/pylab_examples/text_rotation.html
The only way I found to have a label properly next to the line is to align by center in both vertical and horizontal. I then offset the label by 10 points to the left to make it not overlap. Good enough for my application.
Here is my code. I draw the line however I want, then draw the annotation, then bind them with a helper function:
line, = fig.plot(xdata, ydata, '--', color=color)
# x,y appear on the midpoint of the line
t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color)
text_slope_match_line(t, x, y, line)
Then call another helper function after layout but before savefig (For interactive images I think you'll have to register for draw events and call update_text_slopes in the handler)
plt.tight_layout()
update_text_slopes()
The helpers:
rotated_labels = []
def text_slope_match_line(text, x, y, line):
global rotated_labels
# find the slope
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})
def update_text_slopes():
global rotated_labels
for label in rotated_labels:
# slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates
text, line = label["text"], label["line"]
p1, p2 = label["p1"], label["p2"]
# get the line's data transform
ax = line.get_axes()
sp1 = ax.transData.transform_point(p1)
sp2 = ax.transData.transform_point(p2)
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = math.degrees(math.atan(rise/run))
text.set_rotation(slope_degrees)
Even though this question is old, I keep coming across it and get frustrated, that it does not quite work. I reworked it into a class LineAnnotation and helper line_annotate such that it
uses the slope at a specific point x,
works with re-layouting and resizing, and
accepts a relative offset perpendicular to the slope.
x = np.linspace(np.pi, 2*np.pi)
line, = plt.plot(x, np.sin(x))
for x in [3.5, 4.0, 4.5, 5.0, 5.5, 6.0]:
line_annotate(str(x), line, x)
I originally put it into a public gist, but #Adam asked me to include it here.
import numpy as np
from matplotlib.text import Annotation
from matplotlib.transforms import Affine2D
class LineAnnotation(Annotation):
"""A sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
fig, ax = subplots()
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
ax.add_artist(LineAnnotation("text", line, 1.5))
"""
def __init__(
self, text, line, x, xytext=(0, 5), textcoords="offset points", **kwargs
):
"""Annotate the point at *x* of the graph *line* with text *text*.
By default, the text is displayed with the same rotation as the slope of the
graph at a relative position *xytext* above it (perpendicularly above).
An arrow pointing from the text to the annotated point *xy* can
be added by defining *arrowprops*.
Parameters
----------
text : str
The text of the annotation.
line : Line2D
Matplotlib line object to annotate
x : float
The point *x* to annotate. y is calculated from the points on the line.
xytext : (float, float), default: (0, 5)
The position *(x, y)* relative to the point *x* on the *line* to place the
text at. The coordinate system is determined by *textcoords*.
**kwargs
Additional keyword arguments are passed on to `Annotation`.
See also
--------
`Annotation`
`line_annotate`
"""
assert textcoords.startswith(
"offset "
), "*textcoords* must be 'offset points' or 'offset pixels'"
self.line = line
self.xytext = xytext
# Determine points of line immediately to the left and right of x
xs, ys = line.get_data()
def neighbours(x, xs, ys, try_invert=True):
inds, = np.where((xs <= x)[:-1] & (xs > x)[1:])
if len(inds) == 0:
assert try_invert, "line must cross x"
return neighbours(x, xs[::-1], ys[::-1], try_invert=False)
i = inds[0]
return np.asarray([(xs[i], ys[i]), (xs[i+1], ys[i+1])])
self.neighbours = n1, n2 = neighbours(x, xs, ys)
# Calculate y by interpolating neighbouring points
y = n1[1] + ((x - n1[0]) * (n2[1] - n1[1]) / (n2[0] - n1[0]))
kwargs = {
"horizontalalignment": "center",
"rotation_mode": "anchor",
**kwargs,
}
super().__init__(text, (x, y), xytext=xytext, textcoords=textcoords, **kwargs)
def get_rotation(self):
"""Determines angle of the slope of the neighbours in display coordinate system
"""
transData = self.line.get_transform()
dx, dy = np.diff(transData.transform(self.neighbours), axis=0).squeeze()
return np.rad2deg(np.arctan2(dy, dx))
def update_positions(self, renderer):
"""Updates relative position of annotation text
Note
----
Called during annotation `draw` call
"""
xytext = Affine2D().rotate_deg(self.get_rotation()).transform(self.xytext)
self.set_position(xytext)
super().update_positions(renderer)
def line_annotate(text, line, x, *args, **kwargs):
"""Add a sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
line_annotate("sin(x)", line, 1.5)
See also
--------
`LineAnnotation`
`plt.annotate`
"""
ax = line.axes
a = LineAnnotation(text, line, x, *args, **kwargs)
if "clip_on" in kwargs:
a.set_clip_path(ax.patch)
ax.add_artist(a)
return a
New in matplotlib 3.4.0
There is now a built-in parameter transform_rotates_text for rotating text relative to a line:
To rotate text with respect to a line, the correct angle won't be the angle of that line in the plot coordinate system, but the angle that the line appears in the screen coordinate system. This angle can be determined automatically by setting the new parameter transform_rotates_text.
So now we can just pass the raw data angle to plt.text and let matplotlib automatically transform it to the correct visual angle by setting transform_rotates_text=True:
# plot line from (1, 4) to (6, 10)
x = [1, 6]
y = [4, 10]
plt.plot(x, y, 'r.-')
# compute angle in raw data coordinates (no manual transforms)
dy = y[1] - y[0]
dx = x[1] - x[0]
angle = np.rad2deg(np.arctan2(dy, dx))
# annotate with transform_rotates_text to align text and line
plt.text(x[0], y[0], f'rotation={angle:.2f}', ha='left', va='bottom',
transform_rotates_text=True, rotation=angle, rotation_mode='anchor')
This approach is robust against the figure and axes scales. Even if we modify the figsize or xlim after placing the text, the rotation stays properly aligned:
# resizing the figure won't mess up the rotation
plt.gcf().set_size_inches(9, 4)
# rescaling the axes won't mess up the rotation
plt.xlim(0, 12)

How to add a fixed-width border to subplot

I want to add a coloured border to some subplots with a fixed width specified in pixels. I wrote the following function to do so by adding a Rectangle patch to the figure behind the axes.
def add_subplot_border(ax, width=0, color=None):
fig = ax.get_figure()
# Convert bottom-left and top-right to display coordinates
x0, y0 = ax.transAxes.transform((0, 0))
x1, y1 = ax.transAxes.transform((1, 1))
# Adjust margins
x0 -= width
x1 += width
y0 -= width
y1 += width
# Convert back to Axes coordinates
x0, y0 = ax.transAxes.inverted().transform((x0, y0))
x1, y1 = ax.transAxes.inverted().transform((x1, y1))
rect = plt.Rectangle((x0, y0), x1-x0, y1-y0,
color=color,
transform=ax.transAxes,
zorder=-1)
fig.patches.append(rect)
This appears to be a good starting point, but when the figure is resized the relative thickness of the border changes too. How can I specify a transform to scale and translate the patch to appear as a fixed-width border regardless of window scaling? Or, is there a better way to approach this?
Original figure
Scaled figure - uneven border
Instead of calculating a margin and drawing a Rectangle with that extra width (which then get's overlayed by the Axis, you can give the Rectangle a line width (in points) that is preserved upon re-scaling. Note though, that the line is always centred on the border of the Rectangle, so if you want, say, a 5 point frame around your axis, you should request a line width of 10 (or possibly 11).
I adjusted your function slightly and added a use case example:
from matplotlib import pyplot as plt
def add_subplot_border(ax, width=1, color=None ):
fig = ax.get_figure()
# Convert bottom-left and top-right to display coordinates
x0, y0 = ax.transAxes.transform((0, 0))
x1, y1 = ax.transAxes.transform((1, 1))
# Convert back to Axes coordinates
x0, y0 = ax.transAxes.inverted().transform((x0, y0))
x1, y1 = ax.transAxes.inverted().transform((x1, y1))
rect = plt.Rectangle(
(x0, y0), x1-x0, y1-y0,
color=color,
transform=ax.transAxes,
zorder=-1,
lw=2*width+1,
fill=None,
)
fig.patches.append(rect)
if __name__ == '__main__':
fig,axes = plt.subplots(ncols=2,nrows=2,figsize=(8,8))
colors = 'brgy'
widths = [1,2,4,8]
for ax,col,w in zip(axes.reshape(-1),colors, widths):
add_subplot_border(ax,w,col)
plt.show()
This is the original figure:
and this is the scaled figure (the lines look thinner because I increased the figure size):

Python show image upon hovering over a point

I have a 2-d scatter plot of points, that correspond to images. I was wondering if there's an easy way to display the corresponding image (as a popup or tooltip) when you hover your mouse over each point? I tried plotly but found out you need to manually edit javascript to get the hover event to work. Is there a simple solution just with matplotlib or some other common package?
Find here a complete solution on how to display an image on hover events. It uses a 'motion_notify_event' to detect when the mouse is over a scatter point (hovering). If this is the case, it displays an image annotation with a corresponding image next to the hovered scatter point.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)
# Generate data x, y for scatter and an array of images.
x = np.arange(20)
y = np.random.rand(len(x))
arr = np.empty((len(x),10,10))
for i in range(len(x)):
f = np.random.rand(5,5)
arr[i, 0:5,0:5] = f
arr[i, 5:,0:5] =np.flipud(f)
arr[i, 5:,5:] =np.fliplr(np.flipud(f))
arr[i, 0:5:,5:] = np.fliplr(f)
# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
line, = ax.plot(x,y, ls="", marker="o")
# create the annotations box
im = OffsetImage(arr[0,:,:], zoom=5)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)
def hover(event):
# if the mouse is over the scatter points
if line.contains(event)[0]:
# find out the index within the array from the event
ind, = line.contains(event)[1]["ind"]
# get the figure size
w,h = fig.get_size_inches()*fig.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
# if event occurs in the top or right quadrant of the figure,
# change the annotation box position relative to mouse.
ab.xybox = (xybox[0]*ws, xybox[1]*hs)
# make annotation box visible
ab.set_visible(True)
# place it at the position of the hovered scatter point
ab.xy =(x[ind], y[ind])
# set the image corresponding to that point
im.set_data(arr[ind,:,:])
else:
#if the mouse is not over a scatter point
ab.set_visible(False)
fig.canvas.draw_idle()
# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)
plt.show()
If you want your images to be displayed in RGB, you have to slightly adjust the code. For this example the images need to be at your disk. Instead of using an 3darray for the images where the first dimension only represents the index you need to read the image with the plt.imread and then set_data to the corresponding position in your array containing the image names.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)
import os
os.chdir('Path/to/your/images')
# Generate data x, y for scatter and an array of images.
x = np.arange(3)
y = np.random.rand(len(x))
jpg_name_np = np.array(['904646.jpg', '903825.jpg', '905722.jpg']).astype('<U12') # names of your images files
cmap = plt.cm.RdYlGn
# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
#line, = ax.plot(x,y, ls="", marker="o")
line = plt.scatter(x,y,c=heat, s=10, cmap=cmap)
image_path = np.asarray(jpg_name_np)
# create the annotations box
image = plt.imread(image_path[0])
im = OffsetImage(image, zoom=0.1)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)
def hover(event):
# if the mouse is over the scatter points
if line.contains(event)[0]:
# find out the index within the array from the event
ind, = line.contains(event)[1]["ind"]
# get the figure size
w,h = fig.get_size_inches()*fig.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
# if event occurs in the top or right quadrant of the figure,
# change the annotation box position relative to mouse.
ab.xybox = (xybox[0]*ws, xybox[1]*hs)
# make annotation box visible
ab.set_visible(True)
# place it at the position of the hovered scatter point
ab.xy =(x[ind], y[ind])
# set the image corresponding to that point
im.set_data(plt.imread(image_path[ind]))
else:
#if the mouse is not over a scatter point
ab.set_visible(False)
fig.canvas.draw_idle()
# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)
fig = plt.gcf()
fig.set_size_inches(10.5, 9.5)
plt.show()
Plot when hovering over scatterpoint

How can I get the coordinates of a Matplotlib patch and use it to add a new axis?

I created a figure and axis using fig = plt.figure() and ax = fig.add_subplot(my_arguments). Then I added a few patches using matplotlib.patches. I transformed each patch by using matplotlib.transforms.Affine2D() to translate and rotate in data coordinates and then convert the transformed coordinates in display coordinates by adding ax.transData() to the end of my Affine2D transformations.
This is a simplified version of the code:
import matplotlib as mpl
import matplotlib.patches as patches
from matplotlib.transforms import Bbox
fig = plt.figure()
ax = fig.add_subplot(111)
# plot anything here
ax.plot(range(10), 'ro')
my_patches = []
# in my code there many patches and therefore the line
# below is actually a list comprehension for each one
my_patches.append(
patches.Rectangle( (1, 2), 10, 20, transform=mpl.transforms.Affine2D() \
.translate(1, 1) \
.rotate_deg_around(1, 2, 35)
+ ax.transData, fill=False, color='blue')
)
# now add a new axis using the coordinates of the patch
patch = my_patches[0]
# get the coords of the lower left corner of the patch
left, bottom = patch.get_xy()
# get its width and height
width, height = patch.get_width(), patch.get_height()
# create a Bbox instance using the coords of the patch
bbox = Bbox.from_bounds(left, bottom, width, height)
# transform from data coords to display coords
disp_coords = ax.transData.transform(bbox)
# transform from display coords to figure coords
fig_coords = fig.transFigure.inverted().transform(disp_coords)
# new axis
ax2 = fig.add_axes(Bbox(fig_coords))
# plot anything else here
ax2.plot(range(10), 'bo')
However, the additional axis is not added to the figure at the same position as the transformed coordinates of the patch (they're close, though). Am I missing something?
I'm uncertain about what the purpose of this code is, so this might not be what you want. But in order for the axes box to appear at coordinates (1,2), you should probably draw the canvas first before working with coordinates obtained from patches.
...
fig.canvas.draw()
left, bottom = patch.get_xy()
...

How to make Matplotlib scatterplots transparent as a group?

I'm making some scatterplots using Matplotlib (python 3.4.0, matplotlib 1.4.3, running on Linux Mint 17). It's easy enough to set alpha transparency for each point individually; is there any way to set them as a group, so that two overlapping points from the same group don't change the color?
Example code:
import matplotlib.pyplot as plt
import numpy as np
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
ax.scatter(x1, y1, s=100, color="blue", alpha=0.5)
ax.scatter(x2, y2, s=100, color="red", alpha=0.5)
fig.savefig("test_scatter.png")
Results in this output:
but I want something more like this one:
I can workaround by saving as SVG and manually grouping then in Inkscape, then setting transparency, but I'd really prefer something I can code. Any suggestions?
Yes, interesting question. You can get this scatterplot with Shapely. Here is the code :
import matplotlib.pyplot as plt
import matplotlib.patches as ptc
import numpy as np
from shapely.geometry import Point
from shapely.ops import cascaded_union
n = 100
size = 0.02
alpha = 0.5
def points():
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
polygons1 = [Point(x1[i], y1[i]).buffer(size) for i in range(n)]
polygons2 = [Point(x2[i], y2[i]).buffer(size) for i in range(n)]
polygons1 = cascaded_union(polygons1)
polygons2 = cascaded_union(polygons2)
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
for polygon1 in polygons1:
polygon1 = ptc.Polygon(np.array(polygon1.exterior), facecolor="red", lw=0, alpha=alpha)
ax.add_patch(polygon1)
for polygon2 in polygons2:
polygon2 = ptc.Polygon(np.array(polygon2.exterior), facecolor="blue", lw=0, alpha=alpha)
ax.add_patch(polygon2)
ax.axis([-0.2, 1.2, -0.2, 1.2])
fig.savefig("test_scatter.png")
and the result is :
Interesting question, I think any use of transparency will result in the stacking effect you want to avoid. You could manually set a transparency type colour to get closer to the results you want,
import matplotlib.pyplot as plt
import numpy as np
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
alpha = 0.5
ax.scatter(x1, y1, s=100, lw = 0, color=[1., alpha, alpha])
ax.scatter(x2, y2, s=100, lw = 0, color=[alpha, alpha, 1.])
plt.show()
The overlap between the different colours are not included in this way but you get,
This is a terrible, terrible hack, but it works.
You see while Matplotlib plots data points as separate objects that can overlap, it plots the line between them as a single object - even if that line is broken into several pieces by NaNs in the data.
With that in mind, you can do this:
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['lines.solid_capstyle'] = 'round'
def expand(x, y, gap=1e-4):
add = np.tile([0, gap, np.nan], len(x))
x1 = np.repeat(x, 3) + add
y1 = np.repeat(y, 3) + add
return x1, y1
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
ax.plot(*expand(x1, y1), lw=20, color="blue", alpha=0.5)
ax.plot(*expand(x2, y2), lw=20, color="red", alpha=0.5)
fig.savefig("test_scatter.png")
plt.show()
And each color will overlap with the other color but not with itself.
One caveat is that you have to be careful with the spacing between the two points you use to make each circle. If they're two far apart then the separation will be visible on your plot, but if they're too close together, matplotlib doesn't plot the line at all. That means that the separation needs to be chosen based on the range of your data, and if you plan to make an interactive plot then there's a risk of all the data points suddenly vanishing if you zoom out too much, and stretching if you zoom in too much.
As you can see, I found 1e-5 to be a good separation for data with a range of [0,1].
Just pass an argument saying edgecolors='none' to plt.scatter()
Here's a hack if you have more than just a few points to plot. I had to plot >500000 points, and the shapely solution does not scale well. I also wanted to plot a different shape other than a circle. I opted to instead plot each layer separately with alpha=1 and then read in the resulting image with np.frombuffer (as described here), then add the alpha to the whole image and plot overlays using plt.imshow. Note this solution forfeits access to the original fig object and attributes, so any other modifications to figure should be made before it's drawn.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
def arr_from_fig(fig):
canvas = FigureCanvas(fig)
canvas.draw()
img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,))
return img
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
imgs = list()
figsize = (4, 4)
dpi = 200
for x, y, c in zip([x1, x2], [y1, y2], ['blue', 'red']):
fig = plt.figure(figsize=figsize, dpi=dpi, tight_layout={'pad':0})
ax = fig.add_subplot(111)
ax.scatter(x, y, s=100, color=c, alpha=1)
ax.axis([-0.2, 1.2, -0.2, 1.2])
ax.axis('off')
imgs.append(arr_from_fig(fig))
plt.close()
fig = plt.figure(figsize=figsize)
alpha = 0.5
alpha_scaled = 255*alpha
for img in imgs:
img_alpha = np.where((img == 255).all(-1), 0, alpha_scaled).reshape([*img.shape[:2], 1])
img_show = np.concatenate([img, img_alpha], axis=-1).astype(int)
plt.imshow(img_show, origin='lower')
ticklabels = ['{:03.1f}'.format(i) for i in np.linspace(-0.2, 1.2, 8, dtype=np.float16)]
plt.xticks(ticks=np.linspace(0, dpi*figsize[0], 8), labels=ticklabels)
plt.yticks(ticks=np.linspace(0, dpi*figsize[1], 8), labels=ticklabels);
plt.title('Test scatter');
I encountered the save issue recently, my case is there are too many points very close to each other, like 100 points of alpha 0.3 on top of each other, the alpha of the color in the generated image is almost 1. So instead of setting the alpha value in the cmap or scatter. I save it to a Pillow image and set the alpha channel there. My code:
import io
import os
import numpy as np
import numpy.ma as ma
import matplotlib.pyplot as plt
from matplotlib import colors
from PIL import Image
from dhi_base import DHIBase
class HeatMapPlot(DHIBase):
def __init__(self) -> None:
super().__init__()
# these 4 values are precalculated
top=75
left=95
width=1314
height=924
self.crop_box = (left, top, left+width, top+height)
# alpha 0.5, [0-255]
self.alpha = 128
def get_cmap(self):
v = [
...
]
return colors.LinearSegmentedColormap.from_list(
'water_level', v, 512)
def png3857(self):
"""Generate flooding images
"""
muids = np.load(os.path.join(self.npy_dir, 'myfilename.npy'))
cmap = self.get_cmap()
i = 0
for npyf in os.listdir(self.npy_dir):
if not npyf.startswith('flooding'):
continue
flooding_num = np.load(os.path.join(self.npy_dir, npyf))
image_file = os.path.join(self.img_dir, npyf.replace('npy', 'png'))
# if os.path.isfile(image_file):
# continue
# filter the water level value that is less than 0.001
masked_arr = ma.masked_where(flooding_num > 0.001, flooding_num)
flooding_masked = flooding_num[masked_arr.mask]
muids_masked = muids[masked_arr.mask, :]
plt.figure(figsize=(self.grid2D['numJ'] / 500, self.grid2D['numK'] / 500))
plt.axis('off')
plt.tight_layout()
plt.scatter(muids_masked[:, 0], muids_masked[:, 1], s=0.1, c=flooding_masked,
alpha=1, edgecolors='none', linewidths=0,
cmap=cmap,
vmin=0, vmax=1.5)
img_buf = io.BytesIO()
plt.savefig(img_buf, transparent=True, dpi=200, format='png')#, pad_inches=0)
plt.clf()
plt.close()
img_buf.seek(0)
img = Image.open(img_buf)
# Cropped image of above dimension
# (It will not change original image)
img = img.crop(self.crop_box)
alpha_channel = img.getchannel('A')
# Make all opaque pixels into semi-opaque
alpha_channel = alpha_channel.point(lambda i: self.alpha if i>0 else 0)
img.putalpha(alpha_channel)
img.save(image_file)
self.logger.info("PNG saved to {}".format(image_file))
i += 1
# if i > 15:
# break
if __name__ == "__main__":
hp = HeatMapPlot()
hp.png3857()

Categories