Related
I have 4 arrays x, y, z and T of length n and I want to plot a 3D curve using matplotlib. The (x, y, z) are the points positions and T is the value of each point (which is plotted as color), like the temperature of each point. How can I do it?
Example code:
import numpy as np
from matplotlib import pyplot as plt
fig = plt.figure()
ax = fig.gca(projection='3d')
n = 100
cmap = plt.get_cmap("bwr")
theta = np.linspace(-4 * np.pi, 4 * np.pi, n)
z = np.linspace(-2, 2, n)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
T = (2*np.random.rand(n) - 1) # All the values are in [-1, 1]
What I found over the internet:
It's possible to use cmap with scatter like shown in the docs and in this stackoverflow question
ax = plt.gca()
ax.scatter(x, y, z, cmap=cmap, c=T)
The problem is that scatter is a set of points, not a curve.
In this stackoverflow question the solution was divide in n-1 intervals and each interval we use a different color like
t = (T - np.min(T))/(np.max(T)-np.min(T)) # Normalize
for i in range(n-1):
plt.plot(x[i:i+2], y[i:i+2], z[i:i+2], c=cmap(t[i])
The problem is that each segment has only one color, but it should be an gradient. The last value is not even used.
Useful links:
Matplotlib - Colormaps
Matplotlib - Tutorial 3D
This is a case where you probably need to use Line3DCollection. This is the recipe:
create segments from your array of coordinates.
create a Line3DCollection object.
add that collection to the axis.
set the axis limits.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize
def get_segments(x, y, z):
"""Convert lists of coordinates to a list of segments to be used
with Matplotlib's Line3DCollection.
"""
points = np.ma.array((x, y, z)).T.reshape(-1, 1, 3)
return np.ma.concatenate([points[:-1], points[1:]], axis=1)
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
n = 100
cmap = plt.get_cmap("bwr")
theta = np.linspace(-4 * np.pi, 4 * np.pi, n)
z = np.linspace(-2, 2, n)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
T = np.cos(theta)
segments = get_segments(x, y, z)
c = Line3DCollection(segments, cmap=cmap, array=T)
ax.add_collection(c)
fig.colorbar(c)
ax.set_xlim(x.min(), x.max())
ax.set_ylim(y.min(), y.max())
ax.set_zlim(z.min(), z.max())
plt.show()
I am not sure how to phrase my question in any way better. Basically, I have three lists of the same length x, y and z and I want to fill a 2D numpy array in the z/y plane with the average of the associated z values.
Here is how I can achieve what I wan to do:
import numpy as np
import matplotlib.pyplot as plt
x = [37.59390426045407, 38.00530354847739, 38.28412244348653, 38.74871247986305, 38.73175910429809, 38.869008864244016, 39.188234404976555, 39.92835838352555, 40.881394113153334, 41.686136269465884]
y = [0.1305391767832006, 0.13764519613447768, 0.14573326951792354, 0.15090729309032114, 0.16355823707239897, 0.17327106424274763, 0.17749746339532224, 0.17310384614773594, 0.16545780437882962, 0.1604752704890856]
z = [0.05738534353865021, 0.012572155256903583, -0.021709582561809437, -0.11191337750722108, -0.07931921785775153, -0.06241610118871843, 0.014216349927058225, 0.042002641153291886, -0.029354425271534645, 0.061894011359833856]
n = 5
image = np.zeros(shape=(n,n))
# Fill the 2D array
x0 = min(x)
y0 = min(y)
dx = (max(x) - min(x))/n
dy = (max(y) - min(y))/n
# Loop over each 2D cell
for index_x in range(n):
for index_y in range(n):
# find the limits of the cell
x1 = x0 + index_x * dx
x2 = x0 + (index_x+1) * dx
y1 = y0 + index_y * dy
y2 = y0 + (index_y+1) * dy
# find the points of z that lie within the range of the cell
vec_z = [z[idx] for idx in range(len(z)) if x[idx]>=x1 and x[idx]<x2 and y[idx]>=y1 and y[idx]<y2]
if vec_z:
image[index_x, index_y] = np.mean(vec_z)
# In the end, used to create a surface plot
fig, ax = plt.subplots()
ax.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
plt.show()
Is there a more easy way to achieve this? I can imagine there is a numpy method for that.
If I understand correctly what you want to do, maybe a 2D interpolation from scipy.interpolate.interp2d is what you are looking for.
You define the interpolation function of your points:
f = interp2d(x = x, y = y, z = z)
Then you define the X and Y meshgrid:
N = 50
x_axis = np.linspace(np.min(x), np.max(x), N)
y_axis = np.linspace(np.min(y), np.max(y), N)
X, Y = np.meshgrid(x_axis, y_axis)
Finally you can compute Z interpolated values on the meshgrid:
Z = np.zeros((N, N))
for i in range(N):
for j in range(N):
Z[i, j] = f(X[i, j], Y[i, j])
If you plot in 3D the interpolated surface, you get:
fig = plt.figure()
ax = fig.add_subplot(projection = '3d')
ax.plot_surface(X, Y, Z, cmap = 'jet', shade = False)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.show()
Interpolated surface compared to interpolation data points:
ax.scatter(x, y, z, color = 'black', s = 100, alpha = 1)
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:
I have this code, which I largely modified from Matplotlib: How to plot images instead of points?
See my output graph: Output
I would like to rotate each hamster picture with respect to the column "Rotation" in the dataframe. So that the pictures are orientated in the correct rotation.
How do I do this? I am struggling to understand the "offsetbox" guide for Matplotlib.
These are the first 10 rows on my dataframe.
import pandas as pd
df = pd.DataFrame([['21:21:00',0.1,0.0,10], ['21:21:01',0.1,0.0,20], ['21:21:02',0.1,0.0,28]\
,['21:21:03',0.1,0.0,12], ['21:21:03',0.1,0.0,12], ['21:21:04',0.5,0.6,12]\
,['21:21:05',3.7,4.4,10], ['21:21:06',6.8,8.1,10], ['21:21:07',9.9,11.9,20]\
,['21:21:08',13.0,15.7,29], ['21:21:09',16.1,19.5,33]]\
,columns=['Time', 'Northings', 'Eastings','Rotation'])
def main():
x = df['Eastings'][::2]
y = df['Northings'][::2]
image_path = get_sample_data(r'C:\Users\j.smith.EA.000\Desktop\PYTHON\hamster.jpg')
fig, ax = plt.subplots()
imscatter(x, y, image_path, zoom=0.03, ax=ax)
ax = df.plot(x = 'Eastings', y = "Northings", grid = True, figsize=(15,8), legend = False\
, xlim = (-30,30), ylim = (-30,30), kind = 'line', ax=ax)
plt.show()
def imscatter(x, y, image, ax=None, zoom=1):
image = plt.imread(image)
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), frameon=False,)
artists.append(ax.add_artist(ab))
return artists
main()
Going through your code, in your imscatter() function the for loop is assigning each image to each datapoint. You are passing the image to
ab = AnnotationBbox(im, (x0, y0), frameon=False,) where im is your image object.
Here, I would suggest passing the image after rotating it to whatever degree you want.
For ex:
im = rotate_image_by_angle(im, get_the_rotation_angle_from_colume)
ab = AnnotationBbox(im, (x0, y0), frameon=False,)
artists.append(ax.add_artist(ab))
This approach is implemented in the following code
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from matplotlib.cbook import get_sample_data
import cv2
import imutils
df = pd.DataFrame([['21:21:00',0.1,0.0,0], ['21:21:01',3.1,3.0,20], ['21:21:02',6.1,6.0,30]\
,['21:21:03',9.1,9.0,40], ['21:21:03',12.1,12.0,50], ['21:21:04',15.1,15.2,60]\
,['21:21:05',18.1,18.0,70], ['21:21:06',21.1,21.0,80], ['21:21:07',24.0,24.1,90]\
,['21:21:08',27.0,27.1,100], ['21:21:09',30.0,30.1,110]]\
,columns=['Time', 'Northings', 'Eastings','Rotation'])
def main():
x = df['Eastings'][::2]
y = df['Northings'][::2]
z = df['Rotation'][::2]
fig, ax = plt.subplots()
imscatter(x, y, z, zoom=0.03, ax=ax)
ax = df.plot(x = 'Eastings', y = "Northings", grid = True, figsize=(15,7), legend = False\
, xlim = (-5,30), ylim = (-5,30), kind = 'line', ax=ax)
plt.show()
def imscatter(x, y, z, ax=None, zoom=1):
image = cv2.imread('image.png')
im = OffsetImage(image, zoom=zoom)
x, y, z = np.atleast_1d(x, y, z)
artists = []
for x0, y0, z0 in zip(x, y, z):
rotated = rotate_bound(image, z0)
im = OffsetImage(rotated, zoom=zoom)
ab = AnnotationBbox(im, (x0, y0), frameon=False,)
artists.append(ax.add_artist(ab))
return artists
def rotate_bound(image, angle):
# grab the dimensions of the image and then determine the
# center
(h, w) = image.shape[:2]
(cX, cY) = (w // 2, h // 2)
# grab the rotation matrix (applying the negative of the
# angle to rotate clockwise), then grab the sine and cosine
# (i.e., the rotation components of the matrix)
M = cv2.getRotationMatrix2D((cX, cY), -angle, 1.0)
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
# compute the new bounding dimensions of the image
nW = int((h * sin) + (w * cos))
nH = int((h * cos) + (w * sin))
# adjust the rotation matrix to take into account translation
M[0, 2] += (nW / 2) - cX
M[1, 2] += (nH / 2) - cY
# perform the actual rotation and return the image
return cv2.warpAffine(image, M, (nW, nH), borderValue=(255,255,255))
main()
I have made minor changes throughout the code and added a function rotate_bound(image, angle) which will rotate the image by a given angle. More details on how it was done, can be found here.
The Output now looks like this...
I'm looking for help to draw a 3D cone using matplotlib.
My goal is to draw a HSL cone, then base on the vertex coordinats i will select the color.
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
theta1 = np.linspace(0, 2*np.pi, 100)
r1 = np.linspace(-2, 0, 100)
t1, R1 = np.meshgrid(theta1, r1)
X1 = R1*np.cos(t1)
Y1 = R1*np.sin(t1)
Z1 = 5+R1*2.5
theta2 = np.linspace(0, 2*np.pi, 100)
r2 = np.linspace(0, 2, 100)
t2, R2 = np.meshgrid(theta2, r2)
X2 = R2*np.cos(t2)
Y2 = R2*np.sin(t2)
Z2 = -5+R2*2.5
ax.set_xlabel('x axis')
ax.set_ylabel('y axis')
ax.set_zlabel('z axis')
# ax.set_xlim(-2.5, 2.5)
# ax.set_ylim(-2.5, 2.5)
# ax.set_zlim(0, 5)
ax.set_aspect('equal')
ax.plot_surface(X1, Y1, Z1, alpha=0.8, color="blue")
ax.plot_surface(X2, Y2, Z2, alpha=0.8, color="blue")
# ax.plot_surface(X, Y, Z, alpha=0.8)
#fig. savefig ("Cone.png", dpi=100, transparent = False)
plt.show()
HSL CONE
My cone
So my question now is how to define color of each element.
i have found a solution, maybe it will be usefull for others.
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np
import colorsys
from matplotlib.tri import Triangulation
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
n_angles = 80
n_radii = 20
# An array of radii
# Does not include radius r=0, this is to eliminate duplicate points
radii = np.linspace(0.0, 0.5, n_radii)
# An array of angles
angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)
# Repeat all angles for each radius
angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
# Convert polar (radii, angles) coords to cartesian (x, y) coords
# (0, 0) is added here. There are no duplicate points in the (x, y) plane
x = np.append(0, (radii*np.cos(angles)).flatten())
y = np.append(0, (radii*np.sin(angles)).flatten())
# Pringle surface
z = 1+-np.sqrt(x**2+y**2)*2
print(x.shape, y.shape, angles.shape, radii.shape, z.shape)
# NOTE: This assumes that there is a nice projection of the surface into the x/y-plane!
tri = Triangulation(x, y)
triangle_vertices = np.array([np.array([[x[T[0]], y[T[0]], z[T[0]]],
[x[T[1]], y[T[1]], z[T[1]]],
[x[T[2]], y[T[2]], z[T[2]]]]) for T in tri.triangles])
x2 = np.append(0, (radii*np.cos(angles)).flatten())
y2 = np.append(0, (radii*np.sin(angles)).flatten())
# Pringle surface
z2 = -1+np.sqrt(x**2+y**2)*2
# NOTE: This assumes that there is a nice projection of the surface into the x/y-plane!
tri2 = Triangulation(x2, y2)
triangle_vertices2 = np.array([np.array([[x2[T[0]], y2[T[0]], z2[T[0]]],
[x2[T[1]], y2[T[1]], z2[T[1]]],
[x2[T[2]], y2[T[2]], z2[T[2]]]]) for T in tri2.triangles])
triangle_vertices = np.concatenate([triangle_vertices, triangle_vertices2])
midpoints = np.average(triangle_vertices, axis=1)
def find_color_for_point(pt):
c_x, c_y, c_z = pt
angle = np.arctan2(c_x, c_y)*180/np.pi
if (angle < 0):
angle = angle + 360
if c_z < 0:
l = 0.5 - abs(c_z)/2
#l=0
if c_z == 0:
l = 0.5
if c_z > 0:
l = (1 - (1-c_z)/2)
if c_z > 0.97:
l = (1 - (1-c_z)/2)
col = colorsys.hls_to_rgb(angle/360, l, 1)
return col
facecolors = [find_color_for_point(pt) for pt in midpoints] # smooth gradient
# facecolors = [np.random.random(3) for pt in midpoints] # random colors
coll = Poly3DCollection(
triangle_vertices, facecolors=facecolors, edgecolors=None)
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.add_collection(coll)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
ax.elev = 50
plt.show()
Inspired from Jake Vanderplas with Python Data Science Handbook, when you are drawing some 3-D plot whose base is a circle, it is likely that you would try:
# Actually not sure about the math here though:
u, v = np.mgrid[0:2*np.pi:100j, 0:np.pi:20j]
x = np.cos(u)*np.sin(v)
y = np.sin(u)*np.sin(v)
and then think about the z-axis. Since viewing from the z-axis the cone is just a circle, so the relationships between z and x and y is clear, which is simply: z = np.sqrt(x ** 2 + y ** 2). Then you can draw the cone based on the codes below:
from mpl_toolkits import mplot3d
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
def f(x, y):
return np.sqrt(x ** 2 + y ** 2)
fig = plt.figure()
ax = plt.axes(projection='3d')
# Can manipulate with 100j and 80j values to make your cone looks different
u, v = np.mgrid[0:2*np.pi:100j, 0:np.pi:80j]
x = np.cos(u)*np.sin(v)
y = np.sin(u)*np.sin(v)
z = f(x, y)
ax.plot_surface(x, y, z, cmap=cm.coolwarm)
# Some other effects you may want to try based on your needs:
# ax.plot_surface(x, y, -z, cmap=cm.coolwarm)
# ax.scatter3D(x, y, z, color="b")
# ax.plot_wireframe(x, y, z, color="b")
# ax.plot_wireframe(x, y, -z, color="r")
# Can set your view from different angles.
ax.view_init(azim=15, elev=15)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
plt.show()
ax.set_ylabel("y")
ax.set_zlabel("z")
plt.show()
And from my side, the cone looks like:
and hope it helps.