Way to contour outer edge of selected grid region in Python - python

I have the following code:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-np.pi/2, np.pi/2, 30)
y = np.linspace(-np.pi/2, np.pi/2, 30)
x,y = np.meshgrid(x,y)
z = np.sin(x**2+y**2)[:-1,:-1]
fig,ax = plt.subplots()
ax.pcolormesh(x,y,z)
Which gives this image:
Now lets say I want to highlight the edge certain grid boxes:
highlight = (z > 0.9)
I could use the contour function, but this would result in a "smoothed" contour. I just want to highlight the edge of a region, following the edge of the grid boxes.
The closest I've come is adding something like this:
highlight = np.ma.masked_less(highlight, 1)
ax.pcolormesh(x, y, highlight, facecolor = 'None', edgecolors = 'w')
Which gives this plot:
Which is close, but what I really want is for only the outer and inner edges of that "donut" to be highlighted.
So essentially I am looking for some hybrid of the contour and pcolormesh functions - something that follows the contour of some value, but follows grid bins in "steps" rather than connecting point-to-point. Does that make sense?
Side note: In the pcolormesh arguments, I have edgecolors = 'w', but the edges still come out to be blue. Whats going on there?
EDIT:
JohanC's initial answer using add_iso_line() works for the question as posed. However, the actual data I'm using is a very irregular x,y grid, which cannot be converted to 1D (as is required for add_iso_line().
I am using data which has been converted from polar coordinates (rho, phi) to cartesian (x,y). The 2D solution posed by JohanC does not appear to work for the following case:
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
def pol2cart(rho, phi):
x = rho * np.cos(phi)
y = rho * np.sin(phi)
return(x, y)
phi = np.linspace(0,2*np.pi,30)
rho = np.linspace(0,2,30)
pp, rr = np.meshgrid(phi,rho)
xx,yy = pol2cart(rr, pp)
z = np.sin(xx**2 + yy**2)
scale = 5
zz = ndimage.zoom(z, scale, order=0)
fig,ax = plt.subplots()
ax.pcolormesh(xx,yy,z[:-1, :-1])
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xmin, xmax = xx.min(), xx.max()
ymin, ymax = yy.min(), yy.max()
ax.contour(np.linspace(xmin,xmax, zz.shape[1]) + (xmax-xmin)/z.shape[1]/2,
np.linspace(ymin,ymax, zz.shape[0]) + (ymax-ymin)/z.shape[0]/2,
np.where(zz < 0.9, 0, 1), levels=[0.5], colors='red')
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)

This post shows a way to draw such lines. As it is not straightforward to adapt to the current pcolormesh, the following code demonstrates a possible adaption.
Note that the 2d versions of x and y have been renamed, as the 1d versions are needed for the line segments.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
x = np.linspace(-np.pi / 2, np.pi / 2, 30)
y = np.linspace(-np.pi / 2, np.pi / 2, 30)
xx, yy = np.meshgrid(x, y)
z = np.sin(xx ** 2 + yy ** 2)[:-1, :-1]
fig, ax = plt.subplots()
ax.pcolormesh(x, y, z)
def add_iso_line(ax, value, color):
v = np.diff(z > value, axis=1)
h = np.diff(z > value, axis=0)
l = np.argwhere(v.T)
vlines = np.array(list(zip(np.stack((x[l[:, 0] + 1], y[l[:, 1]])).T,
np.stack((x[l[:, 0] + 1], y[l[:, 1] + 1])).T)))
l = np.argwhere(h.T)
hlines = np.array(list(zip(np.stack((x[l[:, 0]], y[l[:, 1] + 1])).T,
np.stack((x[l[:, 0] + 1], y[l[:, 1] + 1])).T)))
lines = np.vstack((vlines, hlines))
ax.add_collection(LineCollection(lines, lw=1, colors=color))
add_iso_line(ax, 0.9, 'r')
plt.show()
Here is an adaption of the second answer, which can work with only 2d arrays:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from scipy import ndimage
x = np.linspace(-np.pi / 2, np.pi / 2, 30)
y = np.linspace(-np.pi / 2, np.pi / 2, 30)
x, y = np.meshgrid(x, y)
z = np.sin(x ** 2 + y ** 2)
scale = 5
zz = ndimage.zoom(z, scale, order=0)
fig, ax = plt.subplots()
ax.pcolormesh(x, y, z[:-1, :-1] )
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xmin, xmax = x.min(), x.max()
ymin, ymax = y.min(), y.max()
ax.contour(np.linspace(xmin,xmax, zz.shape[1]) + (xmax-xmin)/z.shape[1]/2,
np.linspace(ymin,ymax, zz.shape[0]) + (ymax-ymin)/z.shape[0]/2,
np.where(zz < 0.9, 0, 1), levels=[0.5], colors='red')
ax.set_xlim(*xlim)
ax.set_ylim(*ylim)
plt.show()

I'll try to refactor add_iso_line method in order to make it more clear an open for optimisations. So, at first, there comes a must-do part:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
x = np.linspace(-np.pi/2, np.pi/2, 30)
y = np.linspace(-np.pi/2, np.pi/2, 30)
x, y = np.meshgrid(x,y)
z = np.sin(x**2+y**2)[:-1,:-1]
fig, ax = plt.subplots()
ax.pcolormesh(x,y,z)
xlim, ylim = ax.get_xlim(), ax.get_ylim()
highlight = (z > 0.9)
Now highlight is a binary array that looks like this:
After that we can extract indexes of True cells, look for False neighbourhoods and identify positions of 'red' lines. I'm not comfortable enough with doing it in a vectorised manner (like here in add_iso_line method) so just using simple loop:
lines = []
cells = zip(*np.where(highlight))
for x, y in cells:
if x == 0 or highlight[x - 1, y] == 0: lines.append(([x, y], [x, y + 1]))
if x == highlight.shape[0] or highlight[x + 1, y] == 0: lines.append(([x + 1, y], [x + 1, y + 1]))
if y == 0 or highlight[x, y - 1] == 0: lines.append(([x, y], [x + 1, y]))
if y == highlight.shape[1] or highlight[x, y + 1] == 0: lines.append(([x, y + 1], [x + 1, y + 1]))
And, finally, I resize and center coordinates of lines in order to fit with pcolormesh:
lines = (np.array(lines) / highlight.shape - [0.5, 0.5]) * [xlim[1] - xlim[0], ylim[1] - ylim[0]]
ax.add_collection(LineCollection(lines, colors='r'))
plt.show()
In conclusion, this is very similar to JohanC solution and, in general, slower. Fortunately, we can reduce amount of cells significantly, extracting contours only using python-opencv package:
import cv2
highlight = highlight.astype(np.uint8)
contours, hierarchy = cv2.findContours(highlight, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
cells = np.vstack(contours).squeeze()
This is an illustration of cells being checked:

Related

colormap scatter plot dependant on cluster membership

Im conducting soft clustering on a data set and I wanted to create a cool graphic that looks similar to the image posted. I want to show a data points membership between two (or more clusters) in graphical form. Im not really sure how to go about this however. Ive used criteria to assign colours to a data point, but am unsure how to create a more dynamic sort of graphic seen below. Any help appreciated.
I think markers are just the thing your looking for:
x1 = y1 = 1
x2 = y2 = 2
dx = np.random.rand(10)
dy = np.random.rand(10)
x = np.array([x1 + dx, x2 + dx]).ravel()
y = np.array([y1 + dy, y2 + dy]).ravel()
threshold = 4
markers = np.array(["o" if xy > threshold else "h" for xy in x + y])
fig, ax = plt.subplots()
for marker in np.unique(markers):
index = markers == marker
ax.scatter(x[index], y[index], marker=marker)
Adding someaditional code to control color and transparency (alpha)
import numpy as np
import matplotlib.pyplot as plt
x1 = y1 = 1
x2 = y2 = 2
dx = np.random.rand(10)
dy = np.random.rand(10)
x = np.array([x1 + dx, x2 + dx]).ravel()
y = np.array([y1 + dy, y2 + dy]).ravel()
threshold = 4
markers = np.array(["o" if xy > threshold else "h" for xy in x + y])
blue_color = "midnightblue" # predefined
pink_color = "orchid"
colors = [blue_color if marker == "o" else pink_color for marker in markers]
alphas = np.array([abs(xy - threshold) for xy in x + y])
alphas = 1 - alphas/np.max(alphas)
fig, ax = plt.subplots()
for i in range(len(x)):
ax.scatter(x[i], y[i], marker=markers[i], color=colors[i], alpha=alphas[i])
The GaussianMixture in scikit-learn does something close to what the question asks.
Specifically, predict_proba(X) returns an array with the probability of each point in X belonging to the component. In the example below we fit two mixture components, so the last two plots should be opposites of each other:
from sklearn.mixture import GaussianMixture
from sklearn.datasets import make_moons
import matplotlib.pyplot as plt
X, _ = make_moons(noise=0.05)
mix = GaussianMixture(n_components=2).fit(X)
probs = mix.predict_proba(X)
fig, ax = plt.subplots(1, 3, sharey=True)
ax[0].scatter(X[:, 0], X[:, 1])
ax[1].scatter(X[:, 0], X[:, 1], c=probs[:, 0])
ax[2].scatter(X[:, 0], X[:, 1], c=probs[:, 1])
plt.show()

Matplotlib contour hatching not working if only two levels was used

I am trying to plot hatches over contours lines that
statisfy certian criteria folliwng the example found here. Yet, I got regular contours (the yellow lines) instead of the hatches. Any ideas how to resolve that. Thanks
import matplotlib.pyplot as plt
import numpy as np
# invent some numbers, turning the x and y arrays into simple
# 2d arrays, which make combining them together easier.
x = np.linspace(-3, 5, 150).reshape(1, -1)
y = np.linspace(-3, 5, 120).reshape(-1, 1)
z = np.cos(x) + np.sin(y)
# we no longer need x and y to be 2 dimensional, so flatten them.
x, y = x.flatten(), y.flatten()
fig2, ax2 = plt.subplots()
n_levels = 6
a=ax2.contourf(x, y, z, n_levels)
fig2.colorbar(a)
[m,n]=np.where(z > 0.5)
z1=np.zeros(z.shape)
z1[m,n]=99
cs = ax2.contour(x, y, z1,2,hatches=['','.'])
plt.show()enter code here
Use contourf() with proper parameters to get useful plot with hatching. See important comment within the working code below:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-3, 5, 150).reshape(1, -1)
y = np.linspace(-3, 5, 120).reshape(-1, 1)
z = np.cos(x) + np.sin(y)
x, y = x.flatten(), y.flatten()
fig2, ax2 = plt.subplots()
n_levels = 6
a = ax2.contourf(x, y, z, n_levels)
fig2.colorbar(a)
[m,n] = np.where(z > 0.5)
z1=np.zeros(z.shape)
z1[m, n] = 99
# use contourf() with proper hatch pattern and alpha value
cs = ax2.contourf(x, y, z1 ,3 , hatches=['', '..'], alpha=0.25)
plt.show()
The output plot:

Color Gradient on line plot based on y value [duplicate]

I want to start the curve with one color and progressively blend into another color until the end. The following function in my MCVE works, but surely, there has to be a better way I haven't found out about, yet?!
import numpy as np
import matplotlib.pyplot as plt
def colorlist(color1, color2, num):
"""Generate list of num colors blending from color1 to color2"""
result = [np.array(color1), np.array(color2)]
while len(result) < num:
temp = [result[0]]
for i in range(len(result)-1):
temp.append(np.sqrt((result[i]**2+result[i+1]**2)/2))
temp.append(result[i+1])
result = temp
indices = np.linspace(0, len(result)-1, num).round().astype(int)
return [result[i] for i in indices]
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
colors = colorlist((1, 0, 0), (0, 0, 1), len(x))
for i in range(len(x)-1):
xi = x[i:i+1+1]
yi = y[i:i+1+1]
ci = colors[i]
plt.plot(xi, yi, color=ci, linestyle='solid', linewidth='10')
plt.show()
Not sure what "better way" refers to. A solution with less code, which would draw faster is the use of a LineCollection together with a colormap.
A colormap can be defined by two colors and any colors in between are automatically interpolated.
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("", [(1, 0, 0), (0, 0, 1)])
A LineCollection can be used to plot a lot of lines at once. Being a ScalarMappable it can use a colormap to colorize each line differently according to some array - in this case one may just use the x values for that purpose.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.colors import LinearSegmentedColormap
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
cmap = LinearSegmentedColormap.from_list("", [(1, 0, 0), (0, 0, 1)])
points = np.array([x, y]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-1],points[1:]], axis=1)
lc = LineCollection(segments, cmap=cmap, linewidth=10)
lc.set_array(x)
plt.gca().add_collection(lc)
plt.gca().autoscale()
plt.show()
The drawback of this solution as can be see in the picture is that the individual lines are not well connected.
So to circumvent this, one may plot those points overlapping, using
segments = np.concatenate([points[:-2],points[1:-1], points[2:]], axis=1)
In the above the color is linearly interpolated between the two given colors. The plot therefore looks different than the one from the question using some custom interpolation.
To obtain the same colors as in the question, you may use the same function to create the colors used in the colormap for the LineCollection. If the aim is to simplify this function you may directly calculate the values as the square root of the color difference in the channels.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.colors import LinearSegmentedColormap
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
def colorlist2(c1, c2, num):
l = np.linspace(0,1,num)
a = np.abs(np.array(c1)-np.array(c2))
m = np.min([c1,c2], axis=0)
s = np.sign(np.array(c2)-np.array(c1)).astype(int)
s[s==0] =1
r = np.sqrt(np.c_[(l*a[0]+m[0])[::s[0]],(l*a[1]+m[1])[::s[1]],(l*a[2]+m[2])[::s[2]]])
return r
cmap = LinearSegmentedColormap.from_list("", colorlist2((1, 0, 0), (0, 0, 1),100))
points = np.array([x, y]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-2],points[1:-1], points[2:]], axis=1)
lc = LineCollection(segments, cmap=cmap, linewidth=10)
lc.set_array(x)
plt.gca().add_collection(lc)
plt.gca().autoscale()
plt.show()
In response to a comment above: If you want to change the color depending on the y value, you can use the following code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.colors import LinearSegmentedColormap
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
ynorm = (y - y.min()) / (y.max() - y.min())
def colorlist2(c1, c2, num):
l = np.linspace(0, 1, num)
a = np.abs(np.array(c1) - np.array(c2))
m = np.min([c1, c2], axis=0)
s = np.sign(np.array(c2) - np.array(c1)).astype(int)
s[s == 0] = 1
r = np.sqrt(np.c_[(l * a[0] + m[0])[::s[0]],
(l * a[1] + m[1])[::s[1]], (l * a[2] + m[2])[::s[2]]])
return r
cmap = LinearSegmentedColormap.from_list(
"", colorlist2((1, 0, 0), (0, 0, 1), 100))
colors = [cmap(k) for k in ynorm[:-1]]
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-2], points[1:-1], points[2:]], axis=1)
lc = LineCollection(segments, colors=colors, linewidth=10)
lc.set_array(x)
plt.gca().add_collection(lc)
plt.gca().autoscale()
plt.show()
This will output this graph:
Graph with color depending on y value

How to add colors to each individual face of a cylinder using matplotlib

I am trying to color each individual face of a cylinder, however I am not sure how to go about it, I have tried the following:
for i in range(10):
col.append([])
for i in range(10):
for j in range(20):
col[i].append(plt.cm.Blues(0.4))
ax.plot_surface(X, Y, Z,facecolors = col,edgecolor = "red")
I want each face to be assigned its own color, so I would think I would supply an array of colors for each of the faces in a 2d array.
But this gives an error:
in plot_surface
colset.append(fcolors[rs][cs])
IndexError: list index out of range
Here is the full code:
import numpy as np
from matplotlib import cm
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.linalg import norm
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
origin = np.array([0, 0, 0])
#axis and radius
p0 = np.array([1, 3, 2])
p1 = np.array([8, 5, 9])
R = 5
#vector in direction of axis
v = p1 - p0
#find magnitude of vector
mag = norm(v)
#unit vector in direction of axis
v = v / mag
#make some vector not in the same direction as v
not_v = np.array([1, 0, 0])
if (v == not_v).all():
not_v = np.array([0, 1, 0])
#make vector perpendicular to v
n1 = np.cross(v, not_v)
#normalize n1
n1 /= norm(n1)
#make unit vector perpendicular to v and n1
n2 = np.cross(v, n1)
#surface ranges over t from 0 to length of axis and 0 to 2*pi
t = np.linspace(0, mag, 200)
theta = np.linspace(0, 2 * np.pi, 100)
#use meshgrid to make 2d arrays
t, theta = np.meshgrid(t, theta)
#generate coordinates for surface
X, Y, Z = [p0[i] + v[i] * t + R * np.sin(theta) * n1[i] + R * np.cos(theta) * n2[i] for i in [0, 1, 2]]
col = []
for i in range(10):
col.append([])
for i in range(10):
for j in range(20):
col[i].append(plt.cm.Blues(0.4))
ax.plot_surface(X, Y, Z,facecolors = col,edgecolor = "red")
#plot axis
ax.plot(*zip(p0, p1), color = 'red')
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.set_zlim(0, 10)
plt.axis('off')
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
plt.show()
Your Z array is of size 100x200, yet you are only specifying 10x20 colors. A quicker way to make col (with the right dimensions) might be something like:
col1 = plt.cm.Blues(np.linspace(0,1,200)) # linear gradient along the t-axis
col1 = np.repeat(col1[np.newaxis,:, :], 100, axis=0) # expand over the theta-axis
col2 = plt.cm.Blues(np.linspace(0,1,100)) # linear gradient along the theta-axis
col2 = np.repeat(col2[:, np.newaxis, :], 200, axis=1) # expand over the t-axis
ax=plt.subplot(121, projection='3d')
ax.plot_surface(X, Y, Z, facecolors=col1)
ax=plt.subplot(122, projection='3d')
ax.plot_surface(X, Y, Z, facecolors=col2)
Which produces:

How can I fix polar RGB imshow's xticks?

This code:
def complex_to_rgb(complex_data, invert=False):
from numpy import angle, max, pi, sin, zeros
phase = angle(complex_data)
amplitude = abs(complex_data)
amplitude = amplitude/max(max(amplitude))
A = zeros((complex_data.shape[0], complex_data.shape[1], 3))
A[:,:,0] = .5*(sin(phase)+1)*amplitude
A[:,:,1] = .5*(sin(phase+pi/2)+1)*amplitude
A[:,:,2] = .5*(-sin(phase)+1)*amplitude
if(invert):
return 1-A
else:
return A
import numpy as np
from matplotlib.pyplot import figure
N = 1024
x = np.linspace(-1, 1, N)
y = np.linspace(-1, 1, N)
X,Y = np.meshgrid(x,y)
R = np.sqrt(X*X + Y*Y)
PHI = np.arctan2(Y, X)
fig = figure()
ax = fig.add_subplot(212, polar=True)
ax.imshow(complex_to_rgb(R*np.exp(1j*PHI) * (R<1), invert=True))
ax.set_xticks([-.5, 0, np.pi/2, np.pi, 3*np.pi/2])
ax.set_yticks([0, N/3, 2*N/3, N])
ax.set_xticklabels(['', '$0$', r'$\pi/2$', r'$\pi$', r'$3\pi/2$'])
ax.set_yticklabels([])
fig.show()
Generates a nice HSV legend plot. Now I'd like to remove the -.5 xtick, but that seems to mess everything up. Anyone know how to fix this? I already reported it as a bug
As described in the bug report, I can place the radial axis anywhere I want by specifying an explicit extent to imshow. Additionally, rgrids can be used to fix the angle of the tick labels.
def complex_to_rgb(complex_data, invert=False):
from numpy import angle, max, pi, sin, zeros
phase = angle(complex_data)
amplitude = abs(complex_data)
amplitude = amplitude/max(max(amplitude))
A = zeros((complex_data.shape[0], complex_data.shape[1], 3))
A[:,:,0] = .5*(sin(phase)+1)*amplitude
A[:,:,1] = .5*(sin(phase+pi/2)+1)*amplitude
A[:,:,2] = .5*(-sin(phase)+1)*amplitude
if(invert):
return 1-A
else:
return A
import numpy as np
from matplotlib.pyplot import figure
N = 1024
x = np.linspace(-1, 1, N)
y = np.linspace(-1, 1, N)
X,Y = np.meshgrid(x,y)
R = np.sqrt(X*X + Y*Y)
PHI = np.arctan2(Y, X)
fig = figure()
ax = fig.add_subplot(111, polar=True)
ax.imshow(complex_to_rgb(R*np.exp(1j*PHI) * (R<1), invert=True), extent=[0,2*np.pi, 0,1024])
ax.set_rgrids([1,N/3,2*N/3], angle=45)
ax.set_xticks([0, np.pi/2, np.pi, 3*np.pi/2])
ax.set_yticks([0, N/3, 2*N/3, N])
ax.set_xticklabels([r'$0$', r'$\pi/2$', r'$\pi$', r'$3\pi/2$'])
ax.set_yticklabels([r'0', r'$1/3$', r'$2/3$', '1'])
fig.show()
Which results in:

Categories