I am using the MayaVi Python library to plot 3d points, using the points3d class. The documentation specifies that the colour of each point is specified through a fourth argument, s:
In addition, you can pass a fourth array s of the same shape as x, y,
and z giving an associated scalar value for each point, or a function
f(x, y, z) returning the scalar value. This scalar value can be used
to modulate the color and the size of the points.
This specifies a scalar value for each point, which maps the point to a colourmap, such as copper, jet or hsv. E.g. from their documentation:
import numpy
from mayavi.mlab import *
def test_points3d():
t = numpy.linspace(0, 4*numpy.pi, 20)
cos = numpy.cos
sin = numpy.sin
x = sin(2*t)
y = cos(t)
z = cos(2*t)
s = 2+sin(t)
return points3d(x, y, z, s, colormap="copper", scale_factor=.25)
Gives:
Instead, I would like to specify the actual value for each point as an (r, g, b) tuple. Is this possible in MayaVi? I have tried replacing the s with an array of tuples, but an error is thrown.
After struggling with this for most of today, I found a relatively simple way to do exactly what the question asks -- specify an RGB tuple for each point. The trick is just to define a color map with exactly the same number of entries as there are points to plot, and then set the argument to be a list of indices:
# Imports
import numpy as np
from mayavi.mlab import quiver3d, draw
# Primitives
N = 200 # Number of points
ones = np.ones(N)
scalars = np.arange(N) # Key point: set an integer for each point
# Define color table (including alpha), which must be uint8 and [0,255]
colors = (np.random.random((N, 4))*255).astype(np.uint8)
colors[:,-1] = 255 # No transparency
# Define coordinates and points
x, y, z = colors[:,0], colors[:,1], colors[:,2] # Assign x, y, z values to match color
pts = quiver3d(x, y, z, ones, ones, ones, scalars=scalars, mode='sphere') # Create points
pts.glyph.color_mode = 'color_by_scalar' # Color by scalar
# Set look-up table and redraw
pts.module_manager.scalar_lut_manager.lut.table = colors
draw()
I've found a better way to set the colors directly.
You can create your own direct LUT pretty easily. Let's say we want 256**3 granularity:
#create direct grid as 256**3 x 4 array
def create_8bit_rgb_lut():
xl = numpy.mgrid[0:256, 0:256, 0:256]
lut = numpy.vstack((xl[0].reshape(1, 256**3),
xl[1].reshape(1, 256**3),
xl[2].reshape(1, 256**3),
255 * numpy.ones((1, 256**3)))).T
return lut.astype('int32')
# indexing function to above grid
def rgb_2_scalar_idx(r, g, b):
return 256**2 *r + 256 * g + b
#N x 3 colors. <This is where you are storing your custom colors in RGB>
colors = numpy.array([_.color for _ in points])
#N scalars
scalars = numpy.zeros((colors.shape[0],))
for (kp_idx, kp_c) in enumerate(colors):
scalars[kp_idx] = rgb_2_scalar_idx(kp_c[0], kp_c[1], kp_c[2])
rgb_lut = create_8bit_rgb_lut()
points_mlab = mayavi.mlab.points3d(x, y, z, scalars, mode='point')
#magic to modify lookup table
points_mlab.module_manager.scalar_lut_manager.lut._vtk_obj.SetTableRange(0, rgb_lut.shape[0])
points_mlab.module_manager.scalar_lut_manager.lut.number_of_colors = rgb_lut.shape[0]
points_mlab.module_manager.scalar_lut_manager.lut.table = rgb_lut
You can use a rgb look up table and map your rgb values to it using whatever logic you want. Here's a simple example:
import numpy, random
from mayavi.mlab import *
def cMap(x,y,z):
#whatever logic you want for colors
return [random.random() for i in x]
def test_points3d():
t = numpy.linspace(0, 4*numpy.pi, 20)
cos = numpy.cos
sin = numpy.sin
x = sin(2*t)
y = cos(t)
z = cos(2*t)
s = cMap(x,y,z)
return points3d(x, y, z, s, colormap="spectral", scale_factor=0.25)
test_points3d()
I have no idea what color scheme you want, but you can evaluate the positions of x,y,z and return whatever scalar corresponds to the rgb value you are seeking.
This can now simply be done with the color argument
from mayavi import mlab
import numpy as np
c = np.random.rand(200, 3)
r = np.random.rand(200) / 10.
mlab.points3d(c[:, 0], c[:, 1], c[:, 2], r, color=(0.2, 0.4, 0.5))
mlab.show()
Related
I have three 3D mesh matrices (X, Y, Z) corresponding to the xyz coordinate space.
I also have a 3D Numpy matrix A where A[i,j,k] contains a float that is associated with the point (x,y,z) where x=X[i,j,k], y=Y[i,j,k], and z=Z[i,j,k]. The float values are continuous within A (i.e. the change in value between adjacent elements of A are typically small).
Is there a way to plot the surface that corresponds to a given float value in A using Matplotlib or any other Python-based graphics package? For example, if given a value 2.34, I am interested in getting a plotted contour surface of the matrix A wherever 2.34 (plus or minus some tolerance) shows up?
So far, I have been able to recover the xyz coordinates of all values in A that are within some tolerance of the target value and then make a 3D scatter plot using this (code below). Perhaps there is also a way of plotting a surface from these points?
def clean (A, t, dt):
# function for making A binary for t+-dt
# t is the target value I want in the matrix A with tolerance dt
new_A = np.copy(A)
new_A[np.logical_and(new_A > t-dt, new_A < t+dt)] = -1
new_A[new_A != -1] = 0
new_A[new_A == -1] = 1
return (new_A)
def get_surface (X, Y, Z, new_A):
x_vals = []
y_vals = []
z_vals = []
# Retrieve (x,y,z) coordinates of surface
for i in range(new_A.shape[0]):
for j in range(new_A.shape[1]):
for k in range(new_A.shape[2]):
if new_A[i,j,k] == 1.0:
x_vals.append(X[i,j,k])
y_vals.append(Y[i,j,k])
z_vals.append(Z[i,j,k])
return (np.array(x_vals), np.array(y_vals), np.array(z_vals))
cleaned_A = clean (A, t=2.5, dt=0.001)
x_f, y_f, z_f = get_surface (X, Y, Z, cleaned_A )
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d', aspect='equal')
ax.scatter(x_f, y_f, z_f, color='g', s=1)
I have also tried ax.plot_trisurf(x_f,y_f,z_f), but this gives me an poorly connected plot. I'm guessing that the ordering of values in my arrays might be affecting this, in which case is there a package that can do some kind of 3D interpolated surface plot with random ordering of points (e.g. through minimizing the surface area or something like that?)
The object that I am interested in is roughly spherical (i.e. two z's per (x,y)). I can't seem to find any working examples of someone triangulating over a closed 3D surface, but maybe I'm not looking in the right places.
After a lot of digging around, I think I've arrived at a solution that works (for a sphere at least--will update my answer when I try out deformations of a sphere). Many thanks to the comments which helped me think down the right path. I am basically using a ConvexHull for the triangulation from scipy.spatial:
from matplotlib.tri import Triangulation
from scipy.spatial import ConvexHull
def clean (A, t, dt):
# function for making A binary for t+-dt
# t is the target value I want in the matrix A with tolerance dt
new_A = np.copy(A)
new_A[np.logical_and(new_A > t-dt, new_A < t+dt)] = -1
new_A[new_A != -1] = 0
new_A[new_A == -1] = 1
return (new_A)
def get_surface (X, Y, Z, new_A):
x_vals = []
y_vals = []
z_vals = []
# Retrieve (x,y,z) coordinates of surface
for i in range(new_A.shape[0]):
for j in range(new_A.shape[1]):
for k in range(new_A.shape[2]):
if new_A[i,j,k] == 1.0:
x_vals.append(X[i,j,k])
y_vals.append(Y[i,j,k])
z_vals.append(Z[i,j,k])
return (np.array(x_vals), np.array(y_vals), np.array(z_vals))
cleaned_A = clean (A, t=2.5, dt=0.001)
x_f, y_f, z_f = get_surface (X, Y, Z, cleaned_A )
Xs = np.vstack((x_f, y_f, z_f)).T
hull = ConvexHull(Xs)
x, y, z = Xs.T
tri = Triangulation(x, y, triangles=hull.simplices)
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d', aspect='equal')
ax.plot_trisurf(tri, z, color='g', alpha=0.1)
When you want to plot a numpy array with imshow, this is what you normally do:
import numpy as np
import matplotlib.pyplot as plt
A=np.array([[3,2,5],[8,1,2],[6,6,7],[3,5,1]]) #The array to plot
im=plt.imshow(A,origin="upper",interpolation="nearest",cmap=plt.cm.gray_r)
plt.colorbar(im)
Which gives us this simple image:
In this image, the x and y coordinates are simply extracted from the position of each value in the array. Now, let's say that A is an array of values that refer to some specific coordinates:
real_x=np.array([[15,16,17],[15,16,17],[15,16,17],[15,16,17]])
real_y=np.array([[20,21,22,23],[20,21,22,23],[20,21,22,23]])
These values are made-up to just make my case. Is there a way to force imshow to assign each value in A the corresponding pair of coordinates (real_x,real_y)?
PS: I am not looking for adding or subtracting something to the array-based x and y to make them match real_x and real_y, but for something that reads these values from the real_x and real_y arrays. The intended outcome is then an image with the real_x values on the x-axis and the real_y values on the y-axis.
Setting the extent
Assuming you have
real_x=np.array([15,16,17])
real_y=np.array([20,21,22,23])
you would set the image extent as
dx = (real_x[1]-real_x[0])/2.
dy = (real_y[1]-real_y[0])/2.
extent = [real_x[0]-dx, real_x[-1]+dx, real_y[0]-dy, real_y[-1]+dy]
plt.imshow(data, extent=extent)
Changing ticklabels
An alternative would be to just change the ticklabels
real_x=np.array([15,16,17])
real_y=np.array([20,21,22,23])
plt.imshow(data)
plt.gca().set_xticks(range(len(real_x)))
plt.gca().set_yticks(range(len(real_x)))
plt.gca().set_xticklabels(real_x)
plt.gca().set_yticklabels(real_y)
If I understand correctly, this is about producing a raster for imshow, that is, given X - image coordinates and y - values, produce input matrix for imshow. I am not aware of a standard function for that, so implemented it
import numpy as np
def to_raster(X, y):
"""
:param X: 2D image coordinates for values y
:param y: vector of scalar or vector values
:return: A, extent
"""
def deduce_raster_params():
"""
Computes raster dimensions based on min/max coordinates in X
sample step computed from 2nd - smallest coordinate values
"""
unique_sorted = np.vstack((np.unique(v) for v in X.T)).T
d_min = unique_sorted[0] # x min, y min
d_max = unique_sorted[-1] # x max, y max
d_step = unique_sorted[1]-unique_sorted[0] # x, y step
nsamples = (np.round((d_max - d_min) / d_step) + 1).astype(int)
return d_min, d_max, d_step, nsamples
d_min, d_max, d_step, nsamples = deduce_raster_params()
# Allocate matrix / tensor for raster. Allow y to be vector (e.g. RGB triplets)
A = np.full((*nsamples, 1 if y.ndim==1 else y.shape[-1]), np.NaN)
# Compute index for each point in X
ind = np.round((X - d_min) / d_step).T.astype(int)
# Scalar/vector values assigned over outer dimension
A[list(ind)] = y # cell id
# Prepare extent in imshow format
extent = np.vstack((d_min, d_max)).T.ravel()
return A, extent
This can then be used with imshow as:
import matplotlib.pyplot as plt
A, extent = to_raster(X, y)
plt.imshow(A, extent=extent)
Note that deduce_raster_params() works in O(n*log(n)) instead of O(n) because of the sort in np.unique() - this simplifies the code and probably shouldn't be a problem with things sent to imshow
Here is a minimal example how to re-scale the y axes to another range:
import matplotlib.pyplot as plt
import numpy as np
def yaxes_rerange(row_count, new_y_range):
scale = (new_y_range[1] - new_y_range[0]) / row_count
y_range = np.array([1, row_count - 1]) * scale
dy = (y_range[1] - y_range[0]) / 2 - (new_y_range[1] - new_y_range[0])
ext_y_range = y_range + new_y_range[0] + np.array([-dy, dy])
extent = [-0.5, data.shape[1] - 0.5, ext_y_range[0], ext_y_range[1]]
aspect = 1 / scale
return extent, aspect
data = np.array([[1, 5, 3], [8, 2, 3], [1, 3, 5], [1, 2, 4]])
row_count = data.shape[0]
new_range = [8, 16]
extent, aspect = yaxes_rerange(row_count, new_range)
img = plt.imshow(data, extent=extent, aspect=aspect)
img.axes.set_xticks(range(data.shape[1]))
img.axes.set_xticklabels(["water", "wine", "stone"])
For the extent method, to make it work, the argument aspect of imshow() needs to be "auto".
Usually I use Scipy.optimize.curve_fit to fit custom functions to data.
Data in this case was always a 1 dimensional array.
Is there a similiar function for a two dimensional array?
So, for example, I have a 10x10 numpy array. Then I have a function that does some stuff and creates a 10x10 numpy array, and I want to fit the function, so that the resulting 10x10 array has the best fit to the input array.
Maybe an example is better :)
data = pyfits.getdata('data.fits') #fits is an image format, this gives me a NxM numpy array
mod1 = pyfits.getdata('mod1.fits')
mod2 = pyfits.getdata('mod2.fits')
mod3 = pyfits.getdata('mod3.fits')
mod1_1D = numpy.ravel(mod1)
mod2_1D = numpy.ravel(mod2)
mod3_1D = numpy.ravel(mod3)
def dostuff(a,b): #originaly this is a function for 2D arrays
newdata = (mod1_1D*12)+(mod2_1D)**a - mod3_1D/b
return newdata
Now a and b should be fitted, so that newdata is as close as possible to data.
What I got so far:
data1D = numpy.ravel(data)
data_X = numpy.arange(data1D.size)
fit = curve_fit(dostuff,data_X,data1D)
But print fit only gives me
(array([ 1.]), inf)
I do have some nans in the arrays, maybe thats a problem?
The goal is to express the 2D function as a 1D function: g(x, y, ...) --> f(xy, ...)
Converting the coordinate pair (x, y) into a single number xy may seem tricky at first. But it's actually quite simple. Just enumerate all data points and you have a single number that uniquely defines each coordinate pair. The fitted function simply has to reconstruct the original coordinates, do it's calculations and return the result.
Example that fits a 2D linear gradient in a 20x10 image:
import scipy as sp
import numpy as np
import matplotlib.pyplot as plt
n, m = 10, 20
# noisy example data
x = np.arange(m).reshape(1, m)
y = np.arange(n).reshape(n, 1)
z = x + y * 2 + np.random.randn(n, m) * 3
def f(xy, a, b):
i = xy // m # reconstruct y coordinates
j = xy % m # reconstruct x coordinates
out = i * a + j * b
return out
xy = np.arange(z.size) # 0 is the top left pixel and 199 is the top right pixel
res = sp.optimize.curve_fit(f, xy, np.ravel(z))
z_est = f(xy, *res[0])
z_est2d = z_est.reshape(n, m)
plt.subplot(2, 1, 1)
plt.plot(np.ravel(z), label='original')
plt.plot(z_est, label='fitted')
plt.legend()
plt.subplot(2, 2, 3)
plt.imshow(z)
plt.xlabel('original')
plt.subplot(2, 2, 4)
plt.imshow(z_est2d)
plt.xlabel('fitted')
I would recommend using symfit for this, I wrote that to take care of all of the magic for you automatically.
In symfit you would just write the equation pretty much as you would on paper, and then you can run the fit.
I would do something like this:
from symfit import parameters, variables, Fit
# Assuming all this data is in the form of NxM arrays
data = pyfits.getdata('data.fits')
mod1 = pyfits.getdata('mod1.fits')
mod2 = pyfits.getdata('mod2.fits')
mod3 = pyfits.getdata('mod3.fits')
a, b = parameters('a, b')
x, y, z, u = variables('x, y, z, u')
model = {u: (x * 12) + y**a - z / b}
fit = Fit(model, x=mod1, y=mod2, z=mod3, u=data)
fit_result = fit.execute()
print(fit_result)
Unfortunatelly I have not yet included examples of the kind you need in the docs yet, but if you just look at the docs I think you can figure it out in case this doesn't work out of the box.
From a complex 3D shape, I have obtained by tricontourf the equivalent top view of my shape.
I wish now to export this result on a 2D array.
I have tried this :
import numpy as np
from shapely.geometry import Polygon
import skimage.draw as skdraw
import matplotlib.pyplot as plt
x = [...]
y = [...]
z = [...]
levels = [....]
cs = plt.tricontourf(x, y, triangles, z, levels=levels)
image = np.zeros((100,100))
for i in range(len(cs.collections)):
p = cs.collections[i].get_paths()[0]
v = p.vertices
x = v[:,0]
y = v[:,1]
z = cs.levels[i]
# to see polygon at level i
poly = Polygon([(i[0], i[1]) for i in zip(x,y)])
x1, y1 = poly.exterior.xy
plt.plot(x1,y1)
plt.show()
rr, cc = skdraw.polygon(x, y)
image[rr, cc] = z
plt.imshow(image)
plt.show()
but unfortunately, from contours vertices only one polygon is created by level (I think), generated at the end an incorrect projection of my contourf in my 2D array.
Do you have an idea to correctly represent contourf in a 2D array ?
Considering a inner loop with for path in ...get_paths() as suggested by Andreas, things are better ... but not completely fixed.
My code is now :
import numpy as np
import matplotlib.pyplot as plt
import cv2
x = [...]
y = [...]
z = [...]
levels = [....]
...
cs = plt.tricontourf(x, y, triangles, z, levels=levels)
nbpixels = 1024
image = np.zeros((nbpixels,nbpixels))
pixel_size = 0.15 # relation between a pixel and its physical size
for i,collection in enumerate(cs.collections):
z = cs.levels[i]
for path in collection.get_paths():
verts = path.to_polygons()
for v in verts:
v = v/pixel_size+0.5*nbpixels # to centered and convert vertices in physical space to image pixels
poly = np.array([v], dtype=np.int32) # dtype integer is necessary for the next instruction
cv2.fillPoly( image, poly, z )
The final image is not so far from the original one (retunred by plt.contourf).
Unfortunately, some empty little spaces still remains in the final image.(see contourf and final image)
Is path.to_polygons() responsible for that ? (considering only array with size > 2 to build polygons, ignoring 'crossed' polygons and passing through isolated single pixels ??).
I am working on using the forward difference scheme for numerically solving the diffusion function in one dimension. My final plot of the solution should be a surface where the solution u(x,t) is plotted over a grid of x and t values. I have the problem solved, but I can't get the data to be plotted with the grid representation.
I can think of 2 ways to fix this:
1.) My x and t arrays should be one dimensional, but my u array should be a 2D array. Ultimately, I want a square matrix for u, but I am having a hard time coding that. Currently I have a 1D array for u. Here is the code where u is populated.
u = zeros(Nx+1) # unknown u at new time level
u_1 = zeros(Nx+1) # u at the previous time level
# Set initial condition u(x,0) = I(x)
for i in range(0, Nx+1):
#set initial u's to I(xi)
u_1[i] = 25-x[i]**2
for n in range(0, Nt):
# Compute u at inner mesh points
for i in range(1, Nx):
u[i] = u_1[i] + F*(u_1[i-1] - 2*u_1[i] + u_1[i+1])
2.) The above code returns a 1D array for u, is there a way to plot a 3D surface with 3 1D arrays for x,y,z?
Well..., there is a lot of information you haven't provided. For instance you said you wanted a x,y,z plot but haven't said what the x, y and z should be in the context of your plot. Also z is typically z(x,y).
The following recipe assumes a t and x, and u(t,x) as variables to be put into a surface. I imagine is not exactly your idea but it should be adaptable to your exercise:
EDIT: Also your code (which is in the function computeU in this recipe) had a loop for Nt that does not seem to do anything. I've removed it for the purpose of this example.
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np
def computeU(Nx,x,F,Nt):
u = np.zeros(Nx+1) # unknown u at new time level
u_1 = np.zeros(Nx+1) # u at the previous time level
# Set initial condition u(x,0) = I(x)
for i in range(0, Nx+1):
#set initial u's to I(xi)
u_1[i] = 25-x[i]**2
#for n in range(0, Nt): # I'm not sure what this is doing. It has no effect.
# Compute u at inner mesh points
for i in range(1, Nx):
u[i] = u_1[i] + F*(u_1[i-1] - 2*u_1[i] + u_1[i+1])
return np.hstack((u[:,np.newaxis],u_1[:,np.newaxis]))
Nx = 10
F = 3
Nt = 5
x = np.arange(11)
t = np.arange(2)
X,Y = np.meshgrid(t,x)
Z = computeU(Nx,x,F,Nt)
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm,linewidth=0, antialiased=False)
plt.show()
Notice how I've used meshgrid to build new t,x (from 1D arrays) to be mapped against your stack of U arrays (which will have the same shape of X,Y - the new t,x). The result is this: