Fill path with gradient [duplicate] - python

I have to draw a triangle in Python using mathplotlib.
This is how it should eventually look like:
My objective is, once drawn the triangle, to plot some points on it.
At the moment I can draw the triangle just fine:
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
ax.add_patch(Polygon([[0,0],[0,1],[1,0]], closed=True,fill=True))
ax.set_xlim((0,1))
ax.set_ylim((0,1))
plt.show()
But I can only fill it with a solid color. How do I add a gradient like shown in the picture?
Can some one help me?

There is an example on the matplotlib page showing how to use a clip path for an image.
Adapting this to your case would give this:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.path import Path
from matplotlib.patches import PathPatch
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
path = Path([[0,0],[0,1],[1,0],[0,0]])
patch = PathPatch(path, facecolor='none')
ax.add_patch(patch)
Z, Z2 = np.meshgrid(np.linspace(0,1), np.linspace(0,1))
im = plt.imshow(Z-Z2, interpolation='bilinear', cmap=plt.cm.RdYlGn,
origin='lower', extent=[0, 1, 0, 1],
clip_path=patch, clip_on=True)
im.set_clip_path(patch)
ax.set_xlim((0,1))
ax.set_ylim((0,1))
plt.show()

In response to the comment by Stücke, here is an example of a rotation (in degrees) of a 2-colour pattern for an arbitrary closed geometry:
MWE
This is an example for a geometry with a 2 colour pattern rotated 10 degrees counter clock wise (ccw).
def create_gradient_rectangle():
"""Creates a gradient in arbitrary direction in the shape of a
rectangle."""
fig = plt.figure()
ax = fig.add_subplot(111, aspect="equal")
path = Path([[1, 1], [3, 1], [3, 5], [1, 6], [1, 1]])
patch = PathPatch(path, facecolor="none")
ax.add_patch(patch)
# Create a grid that specifies the grid pattern from 0 to 1 for
# red to blue. Resolution = 50 pixels
resolution = 400
arr = np.zeros([resolution, resolution])
for row in range(resolution):
for col in range(resolution):
arr[row][col] = row / resolution
# TODO: verify the entries start at 0 for the first rows
# Ensure the matrix can be plotted at once.
np.set_printoptions(threshold=np.inf)
np.set_printoptions(linewidth=2000) # default = 75
# Rotate the colour gradient matrix.
angle_ccw_deg = -10 # degrees
arr = rotate(arr, angle=angle_ccw_deg)
if angle_ccw_deg > 90 or angle_ccw_deg < -90:
raise Exception(
"Rotation error too large, swap the colour pattern instead please."
)
# Trim the rotated matrix to remove blank triangles that are generated.
colour_resolution = 4 # 10^5=10.000 different colours.
rounded_flipped_arr = np.flip(np.around(arr, colour_resolution), axis=1)
arr = trim_rotated_square(rounded_flipped_arr, resolution, angle_ccw_deg)
im = plt.imshow(
arr,
interpolation="bilinear",
origin="lower",
cmap=plt.cm.RdYlGn,
extent=[1, 3, 1, 6],
clip_path=patch,
clip_on=True,
)
im.set_clip_path(patch)
ax.set_xlim((0, 10))
ax.set_ylim((0, 10))
plt.show()
plt.cla()
plt.clf()
plt.close()
def trim_rotated_square(arr, resolution, angle_ccw_deg):
"""Removes the right and left sides of the colour gradient matrix because
it contains triangles on which the pattern is not extended due to the
rotation.
:param arr: The original rotated and rounded array.
:param resolution: The resolution of the colour gradient pattern/original
unrotated matrix size.
:param angle_ccw_deg: The angle at which the pattern is rotated.
"""
# Assumes the rotated matrix is a square matrix.
width = arr.shape[0]
# If the rotation is to the ccw, then the top right triangle will move up
# into the edge of the larger matrix that encapsulates the rotated matrix.
if angle_ccw_deg < 0:
# Get the most right column on which the pattern is uninterrupted.
max_col = get_max_col(arr, resolution)
# Get the most left column on which the pattern is uninterrupted.
min_col = width - max_col
# If the rotation is to the cw, then the top left triangle will move up
# into the edge of the larger matrix that encapsulates the rotated matrix.
elif angle_ccw_deg > 0:
# Get the most left column on which the pattern is uninterrupted.
min_col = get_max_col(arr, resolution)
# Get the most right column on which the pattern is uninterrupted.
max_col = width - min_col
cut = arr[:, min_col:max_col]
return cut
def get_max_col(arr, resolution):
"""Returns the maximum column number for which the rotated matrix shows an
uninterrupted pattern.
:param arr: The original rotated and rounded array.
:param resolution: The resolution of the colour gradient pattern/original
unrotated matrix size.
"""
# Loop through the rows from top to bottom until the rotated left or right
# edge is encountered.
for row in range(resolution):
# Scan the columns horizontally until an edge is encountered. Assumes
# the matrix stars with zeros on top for the first colour pattern.
for col in range(resolution):
# Since the matrix is rounded to some digits, the first 0.000x will
# be rounded down to 0, and when the colour value becomes larger,
# it will exceed 0, this indicates the first top edge is found.
# Print the arr to see how this happens.
if arr[row][col] > 0:
# Return the column for which the rotated edge is found.
print(f"row={row},col={col},arr[row][col]={arr[row][col]}")
return col
raise Exception("Did not find rotated corner.")
It yields:
whereas rotating it -10 degrees ccw yields:
Inefficiency
It is quite in-efficient as it first rounds the gradient pattern to some number of variables, then rotates the square matrix by putting it into a larger square matrix, and then I start looping through the larger rotated matrix to find the first top edge position. Then I trim the sides of the larger matrix again to ensure a matrix is returned in which the pattern is propagated completely, instead of with missing triangles.
Recommendation
I have not yet tried multi colour patterns. And it currently only supports a single line as colour pattern. However, if one computes the cut off position for the rotation using sinesoids, the rotation angle and the lenght of the original matrix, then one could do the cut off regardless of the colour pattern.

Related

Can we draw rectangle using matplotlib.Patches by just specifying its corner?

I want to conceal few points on a plot, I am using patches to draw a rectangle, so is there any way of plotting a rectangle with just specifying the corners?
I only know how to draw by height and width parameters.
patch= ax1.add_patch(patches.Rectangle((x, y), 0.3, 0.5)
how can i modify the code to draw rectangle by just using say coordinates like these (x1,y1),(x2,y2)(x3,y3)(x4,y4).
I assume that the coordinates to be ordered in the following way:
top_left = [2,2]
bottom_left = [2, 1]
top_right = [4,2]
bottm_right = [4, 1]
So you can easily calculate the width and height and input them to patches
w = top_left[0]-top_right[0]
h = top_left[1]-bottom_left[1]
NOTE
If they are not ordered the logic is simple, you find to points where the x position is identical and calculate in absolute value the the difference and obtain the width (and symmetrically the height)
The selected answer still just calculates the length and width (and ignores any angle if one was desired). It could be made to work by calculating the angle and adding that too, but it's still hacking around your intention if you've already calculated all of the vertices.
Another option you have is to just use the patches.Polygon class.
points = [(x1,y1),(x2,y2)(x3,y3)(x4,y4)]
rect = patches.Polygon(points, linewidth=1, edgecolor='r', facecolor='none')
ax.add_patch(rect)
will end up just drawing a rectangle if that's what those points specify. Note, the order of the points matters, but that isn't a big deal. Here is an image of where I just did this. The green boxes + are my calculated points, and the red rectangles are my polygons
sample of where I did this

changing facecolor "alpha" value gives unwanted edges (matplotlib pcolormesh)

I need to introduce a non-constant alpha value using pcolormesh (imshow is a priori not a possible substitue because I need to use log scale for the axes -- hence non-regular spacing along each coordinate).
Following this post, I tried to change a posteriori the alpha value of the faces. However, in the results, I can't get rid of edges that appear.
Here is a minimal example, where I plot a 2D gaussian bump (with very few points), with alpha increasing from the lower left to the upper right corner:
from matplotlib import pyplot as plt
import numpy as np
# start with coordinates, corresponding meshgrid to compute the "shading" value and
# extended coordinate array for pcolormesh (center mesh)
xx = np.linspace(-4,4,7)
xmesh, ymesh = np.meshgrid(xx,xx)
xplot = np.pad(0.5*(xx[1:]+xx[:-1]),1,'reflect',reflect_type="odd") # center & extend
yy = np.exp(-xx[None,:]**2-xx[:,None]**2) # data to plot
# plot the data
fig = plt.figure()
hpc = plt.pcolormesh(xplot, xplot, yy, shading="flat", edgecolor=None)
plt.gca().set_aspect(1)
# change alpha of the faces: lower-left to upper-right gradient
fig.canvas.draw() # this generate face color array
colors = hpc.get_facecolor()
grad = ( (xmesh.ravel()+ymesh.ravel())/2. - xx.min() ) / ( xx.max()-xx.min() )
colors[:,3] = grad.ravel() # change alpha
hpc.set_facecolor(colors) # update face colors
fig.canvas.draw() # make the modification appears
The result looks like this: 2D gaussian bump (with very few points), with alpha increasing from the lower left to the upper right corner:
Is it possible to get rid of these edges ? My problem is that I don't even know where it comes from... I tried adding hpc.set_antialiased(True), hpc.set_rasterized(True), explicitely adding edges with hpc.set_facecolor(face), tuning the linewidth to very small values -- none of these worked.
Thanks a lot for your help
The problem is that the squares overlap a tiny bit, and they are somewhat transparent (you're setting their alpha values != 1) -- so at the overlaps, they're less transparent than they should be, and it looks like a line.
You can fix it by making the squares opaque, but with a colour as if they had the stated transparency, with a white background:
def alpha_to_white(color):
white = np.array([1,1,1])
alpha = color[-1]
color = color[:-1]
return alpha*color + (1 - alpha)*white
colors = np.array([alpha_to_white(color) for color in colors])

matplotlib markers / mask on image pixels

So I have an image and I have a pixel mask for that image, where the mask is the same size as the image and contains values of 0 and 1, where if it is 0 I don't want to modify the image, and if it is 1 I want to add a transparent color over that pixel of the image.
Basically I want to highlight certain segments of the image but still see what is underneath.
Now I have searched high and low but haven't found a simple way to do this. I used np.where with the mask to get the pixel locations of the 1's to use with the plot functions. I first tried scatter plots with a small marker size and no edge color (small scatter plot markers in matplotlib are always black), but the markers are not one image pixel in size, they seem to be an absolute size and so depending on the size of the figure the transparency is affected and weird patterns are created from the overlapping markers.
Just the regular pyplot plot function created the exact look I desired (where the coloring was smooth and invariant to figure size) but it also colored horizontal connections between disjoint segments in the mask (since it is drawing lines I guess), so I couldn't use that.
What worked the best was patches, which I came across in this question: (How to set a fixed/static size of circle marker on a scatter plot?). I found that rectangular patches with width and height of 1 gave me the exact desired effect, where I could put a transparent color over certain pixels of the image. However this proved to produce a ton (tens of thousands) of rectangles for certain images, and so it was quite slow. Even when using a PatchCollection instead of calling addPatch every time it was still slow.
Now I can probably just join adjacent rectangles to reduce the number of things needing to be drawn, but I was just wondering if there was an easier way to do this?
Thanks.
You can do a semitransparent overlay either using masked arrays or by setting the alpha values in an RGBA image. Here are both worked through (using the example of three semitransparent red squares placed over a circular pattern), and they give similar images (so I'll only show one):
from pylab import *
from numpy import ma
x = y = linspace(-6, 6, 100)
X, Y = meshgrid(x, y)
z3 = X*X + Y*Y # circular pattern
# first, do this with a masked array
figure()
# z4 = 3 diagonal square
# zm = a uniform image (ones), with a mask of squares (~z4)
z4 = np.repeat(np.repeat(eye(3, dtype=bool), 40, axis=0), 40, axis=1)
zm = ma.masked_where(~z4, ones((120,120)))
imshow(z3, cmap=cm.jet)
imshow(zm, cmap=cm.bwr, alpha=.3, vmin=0, vmax=1) #cm.bwr is an easy way to get red
# do this by changing alpha for each pixel
figure()
z5 = zeros((120, 120, 4), dtype=float)
z5[..., 0] = 1
z5[..., 3] = .4*z4.astype(float)
imshow(z3, cmap=cm.jet)
imshow(z5)
show()
I think both approaches can produce the same results for all cases, but:
1. the masked arrays can be a more direct approach if the mask or composition becomes complicated, and masking gives you more flexibility in drawing your overlay image since, for example, you can use colormaps rather than specifying the full RGBA for every pixel, but,
2. the masked array approach doesn't give full pixel-by-pixel control over the alpha value like RGBA does.
z1 = sin(X*Y)
z1 = cos(2*X)
z2 = cos(5*(X+Y))
zm = ma.masked_where( (z2<.5) & (Y>0), z1)
figure()
imshow(z3)
imshow(zm, cmap=cm.gray, alpha=.4, vmin=-2, vmax=2)
show()
It's a bit crazy, but here's what's going on: The primary image is a circular pattern that goes from blue to red (z3). Then there are vertical bars that faintly shade this (z1) but only in half of the figure and in narrow alternate diagonal bands on the other half (due to the mask). Here's a more complicated image using masked arrays:
Just to add on to what tom10 has posted, the masked arrays do work great with colormaps, but I also wrote a small function in the meantime that should work with any RGB color tuple.
def overlayImage(im, mask, col, alpha):
maskRGB = np.tile(mask[..., np.newaxis], 3)
untocuhed = (maskRGB == False) * im
overlayComponent = alpha * np.array(col) * maskRGB
origImageComponent = (1 - alpha) * maskRGB * im
return untocuhed + overlayComponent + origImageComponent
im is the rgb image
mask is a boolean mask of the image, such that mask.shape + (3,) = im.shape
col is just the 3-tuple rgb value you want to mask the image with
alpha is just the alpha value / transparency for the mask
I also needed a clear contour on my areas. Thus, you can easily add a contour plot on top: e.g., create a dummy numpy array and set a different value in each area of interest.
Here's an example build on top of tom10's answer with a different condition:
x = y = linspace(-6, 6, 100)
X, Y = meshgrid(x, y)
z3 = X*X + Y*Y # circular pattern
# first, do this with a masked array
figure()
imshow(z3, cmap=cm.jet, extent = (-6,6,-6,6));
zm = ma.masked_where((z3>=0.7) & (z3<=1.5), ones(np.shape(z3)));
imshow(zm, cmap=cm.bwr, alpha=.4, vmin=0, vmax=1, extent = (-6,6,-6,6)) #cm.bwr is an easy way to get red
# Build dummy array of 1s and 0s (you can play with different values to obtain different contours for different regions):
temp_vector = ones(np.shape(z3));
temp_vector[(z3>=0.7) & (z3<=1.5)] = 0.0;
temp_vector[(z3>8.2)] = 2.0; # etc.
# Create contour. I found only one contour necessary:
contour(X, Y, temp_vector, 1, colors=['r','g']);
show()
Which yields:

skimage slic: getting neighbouring segments

There is a nice implementation of super resolution segment generation (SLIC) in skimage.segmentation package in the python sklearn package.
The slic() method returns the integer sets of labels. My question is how can I get the segments that are spatial neighbors of each other? What I would like to do is build a graph using these segments and the edges would connect the immediate neighbors. However, I cannot figure out how to get the immediate neighbors of a segment.
The python code to perform the SLIC is as follows:
from skimage import io
from skimage.segmentation import slic
from skimage.segmentation import find_boundaries
# An image of dimensions 300, 300
image = img_as_float(io.imread("image.png"))
# call slic. This returns an numpy array which assigns to every
# pixel in the image an integer label
# So segments is a numpy array of shape (300, 300)
segments = slic(image, 100, sigma = 5)
# Now I want to know the neighbourhood segment for each super-pixel
# There is a method called find_boundaries which returns a boolean
# for every pixel to show if it is a boundary pixel or not.
b = find_boundaries(segments)
Here, I am stuck. I would like to know how to parse this boundary indices and find out for a given label index (say 0), which label indexes share a boundary with label of index 0. Is there a way to do this efficiently without looping through the boundary array for every label index?
The way I do it is to build a graph containing an edge from each pixel to its left and bottom pixel (so a 4 neighborhood), label them with their superpixel number and remove duplicates.
You can find code and details in my blog post.
You can find some related functions here, thought they are not very well documented (yet).
A simple method using just np.unique posing each segment-image pixel vs. the one to the right as well as below:
from skimage.data import astronaut
from skimage.segmentation import slic
from scipy.spatial import Delaunay
from skimage.segmentation import mark_boundaries
from matplotlib.lines import Line2D
img = astronaut().astype(np.float32) / 255.
# SLIC
segments = slic(img, n_segments=500, compactness=20)
segments_ids = np.unique(segments)
# centers
centers = np.array([np.mean(np.nonzero(segments==i),axis=1) for i in segments_ids])
vs_right = np.vstack([segments[:,:-1].ravel(), segments[:,1:].ravel()])
vs_below = np.vstack([segments[:-1,:].ravel(), segments[1:,:].ravel()])
bneighbors = np.unique(np.hstack([vs_right, vs_below]), axis=1)
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
plt.imshow(mark_boundaries(img, segments))
plt.scatter(centers[:,1],centers[:,0], c='y')
for i in range(bneighbors.shape[1]):
y0,x0 = centers[bneighbors[0,i]]
y1,x1 = centers[bneighbors[1,i]]
l = Line2D([x0,x1],[y0,y1], alpha=0.5)
ax.add_line(l)
An alternative (and somewhat incomplete) method, using Delaunay tessellation:
# neighbors via Delaunay tesselation
tri = Delaunay(centers)
# draw centers and neighbors
fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)
plt.imshow(mark_boundaries(img, segments))
plt.scatter(centers[:,1],centers[:,0], c='y')
# this contains the neighbors list: tri.vertex_neighbor_vertices
indptr,indices = tri.vertex_neighbor_vertices
# draw lines from each center to its neighbors
for i in range(len(indptr)-1):
N = indices[indptr[i]:indptr[i+1]] # list of neighbor superpixels
centerA = np.repeat([centers[i]], len(N), axis=0)
centerB = centers[N]
for y0,x0,y1,x1 in np.hstack([centerA,centerB]):
l = Line2D([x0,x1],[y0,y1], alpha=0.5)
ax.add_line(l)
Incomplete because some boundary neighbors will not arise from the tessellation.

Area of overlapping circles

I have the following Python code to generate random circles in order to simulate Brownian motion. I need to find the total area of the small red circles so that I can compare it to the total area of a larger blue circle. Since the circles are generated randomly, many of them overlap making it difficult to find the area. I have read many other responses related to this question about pixel painting, etc. What is the best way to find the area of these circles? I do not want to modify the generation of the circles, I just need to find the total area of the red circles on the plot.
The code to generate the circles I need is as follows (Python v. 2.7.6):
import matplotlib.pyplot as plt
import numpy as np
new_line = []
new_angle = []
x_c = [0]
y_c = [0]
x_real = []
y_real = []
xy_dist = []
circ = []
range_value = 101
for x in range(0,range_value):
mu, sigma = 0, 1
new_line = np.random.normal(mu, sigma, 1)
new_angle = np.random.uniform(0, 360)*np.pi/180
x_c.append(new_line*np.cos(new_angle))
y_c.append(new_line*np.sin(new_angle))
x_real = np.cumsum(x_c)
y_real = np.cumsum(y_c)
a = np.mean(x_real)
b = np.mean(y_real)
i = 0
while i<=range_value:
xy_dist.append(np.sqrt((x_real[i]-a)**2+(y_real[i]-b)**2))
i += 1
circ_rad = max(xy_dist)
small_rad = 0.2
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
circ1 = plt.Circle((a,b), radius=circ_rad+small_rad, color='b')
ax.add_patch(circ1)
j = 0
while j<=range_value:
circ = plt.Circle((x_real[j], y_real[j]), radius=small_rad, color='r', fill=True)
ax.add_patch(circ)
j += 1
plt.axis('auto')
plt.show()
The package Shapely might be of some use:
https://gis.stackexchange.com/questions/11987/polygon-overlay-with-shapely
http://toblerity.org/shapely/manual.html#geometric-objects
I can think of an easy way to do it thought the result will have inaccuracies:
With Python draw all your circles on a white image, filling the circles as you draw them. At the end each "pixel" of your image will have one of 2 colors: white color is the background and the other color (let's say red) means that pixel is occupied by a circle.
You then need to sum the number of red pixels and multiply them by the scale with which you draw them. You will have then the area.
This is inaccurate as there is no way of drawing a circle using square pixels, so in the mapping you lose accuracy. Keep in mind that the bigger you draw the circles, the smaller the inaccuracy becomes.

Categories

Resources