I have two 3-D arrays of ground penetrating radar data. Each array is basically a collection of time-lapse 2-D images, where time is increasing along the third dimension. I want to create a 3-D plot which intersects a 2-D image from each array.
I'm essentially trying to create a fence plot. Some examples of this type of plot are found on these sites:
http://www.geogiga.com/images/products/seismapper_3d_seismic_color.gif
http://www.usna.edu/Users/oceano/pguth/website/so461web/seismic_refl/fence.png
I typically use imshow to individually display the 2-D images for analysis. However, my research into the functionality of imshow suggests it doesn't work with the 3D axes. Is there some way around this? Or is there another plotting function which could replicate imshow functionality but can be combined with 3D axes?
There might be better ways, but at least you can always make a planar mesh and color it:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
# create a 21 x 21 vertex mesh
xx, yy = np.meshgrid(np.linspace(0,1,21), np.linspace(0,1,21))
# create some dummy data (20 x 20) for the image
data = np.random.random((20, 20))
# create vertices for a rotated mesh (3D rotation matrix)
X = np.sqrt(1./3) * xx + np.sqrt(1./3) * yy
Y = -np.sqrt(1./3) * xx + np.sqrt(1./3) * yy
Z = np.sqrt(1./3) * xx - np.sqrt(1./3) * yy
# create the figure
fig = plt.figure()
# show the reference image
ax1 = fig.add_subplot(121)
ax1.imshow(data, cmap=plt.cm.BrBG, interpolation='nearest', origin='lower', extent=[0,1,0,1])
# show the 3D rotated projection
ax2 = fig.add_subplot(122, projection='3d')
ax2.plot_surface(X, Y, Z, rstride=1, cstride=1, facecolors=plt.cm.BrBG(data), shade=False)
This creates:
(Please note, I was not very careful with the rotation matrix, you will have to create your own projection. It might really be a good idea to use a real rotation matrix.)
Just note that there is a slight problem with the fence poles and fences, i.e. the grid has one more vertex compared to the number of patches.
The approach above is not very efficient if you have high-resolution images. It may not even be useful with them. Then the other possibility is to use a backend which supports affine image transforms. Unfortunately, you will then have to calculate the transforms yourself. It is not hideously difficult, but still a bit clumsy, and then you do not get a real 3D image which could be rotated around, etc.
For this approach, see http://matplotlib.org/examples/api/demo_affine_image.html
Alternateively, you can use OpenCV and its cv2.warpAffine function to warp your image before showing it with imshow. If you fill the surroundings with transparent color, you can then layer images to get a result which looks like your example iamge.
Just to give you an idea of the possibilities of plot_surface, I tried to warp Lena around a semi-cylinder:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
# create a 513 x 513 vertex mesh
xx, yy = np.meshgrid(np.linspace(0,1,513), np.linspace(0,1,513))
# create vertices for a rotated mesh (3D rotation matrix)
theta = np.pi*xx
X = np.cos(theta)
Y = np.sin(theta)
Z = yy
# create the figure
fig = plt.figure()
# show the 3D rotated projection
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, facecolors=plt.imread('/tmp/lena.jpg')/255., shade=False)
She indeed bends well, but all operations on the image are quite slow:
If you're happy to contemplate using a different plotting library (ie not matplotlib) then it might be worth considering mayavi / tvtk (although the learning curve is a little steep). The closest I've seen to what you want is the scalar cut planes in
http://wiki.scipy.org/Cookbook/MayaVi/Examples
The bulk of the documentation is at:
http://docs.enthought.com/mayavi/mayavi/index.html
There is no way of doing this with matplotlib. #DrV's answer is an approximation. Matplotlib does not actually show each individual pixel of the original image but some rescaled image. rstride and cstride allow you to help specify how the image gets scaled, however, the output will not be the exact image.
Related
I have a data-set of 3D points (x,y,z) projected onto a plane and i'd like to transform them into a simple 2D plot by looking at the points from an orthogonal direction to that plane. Any python explanation are much appreciated!
You can use this :
import pylab
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
fig = pylab.figure()
ax = fig.add_subplot(111, projection = '3d')
x = y = z = [1, 2, 3]
sc = ax.scatter(x,y,z)
#####################
x2, y2, _ = proj3d.proj_transform(1, 1, 1, ax.get_proj())
print x2, y2 # project 3d data space to 2d data space
print ax.transData.transform((x2, y2)) # convert 2d space to screen space
#####################
def on_motion(e):
# move your mouse to (1,1,1), and e.xdata, e.ydata will be the same as x2, y2
print e.x, e.y, e.xdata, e.ydata
fig.canvas.mpl_connect('motion_notify_event', on_motion)
pylab.show()
Depending on how the data were projected, and how perfectly planar they are, one way could be to use a PCA
Example on a fabricated dataset (please, next time, provide such a fabricated dataset. It is better, because I did mine by surmising how yours may look).
import numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.decomposition import PCA
# Fabrication of the dataset : a bunch of 10000 random points distributed in a ring (like Saturn's rings)
radius=np.random.uniform(100,200,(10000,))
angles=np.random.uniform(0,2*np.pi,(10000,))
x1=radius*np.cos(angles)
y1=radius*np.sin(angles)
# Just to see how the original data look like
plt.figure()
plt.scatter(x1,y1)
plt.show()
# Note that those are "secret" data.
# We are not supposed to know how the 2D data are
# What we will work on are 3D data fabricated as follows:
# generate 3D data, that are those, on a plane
# with some noise
# we just use vectors (1,1,1) and (1,-1,1) as generator of the plane.
# So a planar point (x,y) will be sent do a
# 3d point x(1,1,1)+y(1,-1,1).
# Plus some noise
xyz=x1.reshape(-1,1)#[[1,1,1]] + y1.reshape(-1,1)#[[1,-1,1]] + np.random.normal(0,2,(10000,3))
fig=plt.figure()
ax = fig.add_subplot(111, projection = '3d')
ax.scatter(xyz[:,0],xyz[:,1],xyz[:,2])
plt.show()
So, that is the data we will work on. 3D data that are mainly on a plane. We want the 2D dataset of that plane. But, of course, we can't access to x1, y1, since we pretend to know only of xyz
pca=PCA(n_components=2)
xy=pca.fit_transform(xyz)
# xy are the projection on the best possible plane
# of xyz data.
plt.figure()
plt.scatter(xy[:,0], xy[:,1])
plt.show()
You may also know what are the axis of this plane
pca.components_
#array([[-0.70692992, 0.02117576, -0.70696653],
# [ 0.01489184, 0.99977576, 0.01505521]])
So, roughly (-√2/2,0,-√2/2) and (0,1,0).
Not the same axis we've used (1,1,1) and (1,-1,1).
But see that one basis generate the other : (1,1,1) is -√2(-√2/2,0,-√2/2)+(0,1,0). And (1,-1,1) is -√2(-√2/2,0,-√2/2)-(0,1,0).
Or, the other way round : (-√2/2,0,-√2/2) = -√2/4(1,1,1)-√2/4(1,-1,1); (0,1,0)=½(1,1,1)-½(1,-1,1)
So, it is the same plane. Just not the same axis in that plane, but that is normal: nothing in a 3D data of planar points can tell how the 3D data were built.
Note that this method is well suited if 3D data are a little bit noisy. If not, you could achieve the same result with simple Gram-Schmidt method. Choosing extreme points
Starting from another xyz without the noise
# same xyz but without the noise
xyzClean=x1.reshape(-1,1)#[[1,1,1]] + y1.reshape(-1,1)#[[1,-1,1]]
# One (randomly chosen. So why not the 1st) point
# of the dataset
m0=xyzClean[0]
# Choose m1 so that it is further from m0 as possible
dm0=((xyzClean-m0)**2).sum(axis=1)
idx1=np.argmax(dm0)
m1=xyzClean[idx1]
# Choose m2 as far as both m0 and m1 as possible
dm1=((xyzClean-m1)**2).sum(axis=1)
idx2=np.argmax(np.minimum(dm0,dm1))
m2=xyzClean[idx2]
# Gram-Schmidt process to get two orthogonal
# vectors from origin m0
v1=m1-m0
v1=v1/np.sqrt(v1#v1) # normalization
v2=(m2-m0) - ((m2-m0)#v1)*v1
v2=v2/np.sqrt(v2#v2)
# v1=[ 0.70700705, -0.01679433, 0.70700705]
# v2=[0.01187538, 0.99985897, 0.01187538]
# Note that 1.39721978*v1+1.02360973*v2
= (1,1,1)
# And 1.43080844*v1-0.9761082*v2 = (1,-1,1)
# So, again, same plane
# The advantage of having orthogonal basis, is that
# projection on this basis is now easy
projDataV1 = xyzClean#v1
projDataV2 = xyzClean#v2
plt.figure()
plt.scatter(projDataV1, projDataV2)
plt.show()
That second method is well suited if you have no noise, and your 3d data are exactly planar.
Not that the 1st one wouldn't work (it would work anytime). But this one is faster. And could be even faster, if instead of selecting m0, m1 and m2 as far as possible from each other, I had just selected them "far enough" from each other.
Consider the following data, which is defined in polar space in theta, r, and is plotted twice; once in the the orthogonal theta-r phase space, and one in cartesian space after an inverse transformation from polar coordinates to x-y (i.e. what matplotlib's projection='polar' does):
import matplotlib.pyplot as plt
import numpy as np
theta = np.linspace(0, 2*np.pi, 50)
r = np.linspace(0, 1, 50)
THETA, R = np.meshgrid(theta, r)
Z = np.sin(R*np.pi) * np.sin(THETA+np.pi/2)
fig = plt.figure()
axpol = fig.add_subplot(121)
axcart = fig.add_subplot(122, projection='polar')
axcart.contourf(THETA, R, Z, levels=10)
axpol.contourf(THETA, R, Z, levels=10)
axcart.set_title('cartesian space')
axpol.set_title('polar space')
axpol.set_xlim([0, 2*np.pi])
axpol.set_xlabel('r')
axpol.set_ylabel('theta')
plt.show()
This produces:
(NOTE: Oops, the axis labels in the polar plots (left side) should be swapped in each of the images below)
Now, if we shift the theta array by pi:
theta = np.linspace(np.pi, 3*np.pi, 50)
and rerun the above, we see
Notice that the data plotted in the projected polar space successfully wraps the data at theta > 2*np.pi back to the beginning of the angular domain (since this is defined in the projections inverse transformation), such that it appears unchanged. In polar space, this does not happen.
Of course, this is expected; this axis has no associated transformation, and thus does know know how to wrap the data, or that it even should.
My question is, how can I enable this behavior, without having to shift the coordinates and data manually? That is, is there a way to have the axis on the left of the figure above inherit the polar transformation, but not the projection?
I would prefer to do this without defining my own transformation or projection objects. I thought there should be a way to inherit this small piece of the polar transformation, without doing the "full" transformation to Cartesian x,y.
I have a 3-D surface plot that shows x and y coordinates and depths. I also have a 2-D contourf plot with x and y coordinates and the filled contours at the different locations. If I know the depths at the coordinates in the contourf plot, is there a way I can show the contours on the 3-D surface plot?
I have created a 3-D surface plot using plotly with the code below:
import plotly.graph_objects as go
import oceansdb
import numpy as np
import matplotlib.pyplot as plt
Xa = np.linspace(29.005,29.405,200)
Ya = np.linspace(-93.6683,-93.2683,200)
db = oceansdb.ETOPO()
dcont = db['topography'].extract(lat=Xa, lon=Ya)
depth = dcont['height']
fig = go.Figure(data=[go.Surface(z=depth, x=Xa, y=Ya)])
fig.show()
Say my contourf plot can be created with the code below:
X = np.array([29.1,29.15,29.2,29.25])
Y = np.array([-93.5,-93.45,-93.4,-93.35])
r = np.array([0,0,0,2,3,0,0,6,7,8,9,1,9,0,0,0])
plt.figure()
plt.contourf(X,Y,r.reshape(len(X),len(Y)))
plt.show()
Assuming that the depth at each location can be determined using the oceansdb module, can I overlay the contour plot on the surface plot at the correct depth?
Using matplotlib the short answer is "yes", but there are two buts you have face:
Visualizing 3d data is difficult, and overlapping multiple datasets is more often than not confusing beyond the simplest cases
Matplotlib has a 2d renderer, so even though you can plot multiple objects in the same 3d figure, there will often be rendering artifacts (in particular, two objects can typically be either fully in front of or behind one another).
The key methods you need are Axes3D.contour or Axes3D.contourf. Here are these in action with your example data:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # this enables 3d axes
X = np.array([29.1,29.15,29.2,29.25])
Y = np.array([-93.5,-93.45,-93.4,-93.35])
r = np.array([0,0,0,2,3,0,0,6,7,8,9,1,9,0,0,0]).reshape(X.size, Y.size)
# plot your 2d contourf for reference
fig,ax = plt.subplots()
ax.contourf(X, Y, r)
# plot in 3d using contourf
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.contourf(X, Y, r)
# plot in 3d using contour
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.contour(X, Y, r)
plt.show()
Here's your 2d contourf plot:
Here's the 3d contourf version:
And here's the 3d contour version:
As you can see the difference between the latter two is that contourf also plots horizontal planes for each level (just like terraces), whereas contour only plots the level lines themselves.
Since repeated plots using the same axes will accumulate plots there's nothing stopping you from superimposing either of these 3d contour plots on your original surface. However, in line with my earlier warnings you'll have to watch if the contours are rendered correctly over the surface (under all view angles), and even if so the result might not be all that transparent for conveying information. I personally tend to find contourf much easier to comprehend than contour on a 3d plot, but I suspect that if we put these on top of full surface plots the latter will fare better.
I have 3 different parameters X,Y and Z over a range of values, and for each combination of these a certain value of V. To make it clearer, the data would look something like this.
X Y Z V
1 1 2 10
1 2 3 15
etc...
I'd like to visualize the data with a surface/contour plot, using V as a colour to see its value at that point, but I do not see how to add my custom colouring scheme into the mix using Python. Any idea on how to do this (or is this visualization outright silly)?
Thanks a lot!
Matplotlib allows one to pass the facecolors as an argument to e.g.
ax.plot_surface.
That would imply then that you would have to perform 2D interpolation on your
current array of colors, because you currently only have the colors in the
corners of the rectangular faces (you did mention that you have a rectilinear
grid).
You could use
scipy.interpolate.interp2d
for that, but as you see from the documentation, it is suggested to use
scipy.interpolate.RectBivariateSpline.
To give you a simple example:
import numpy as np
y,x = np.mgrid[1:10:10j, 1:10:10j] # returns 2D arrays
# You have 1D arrays that would make a rectangular grid if properly reshaped.
y,x = y.ravel(), x.ravel() # so let's convert to 1D arrays
z = x*(x-y)
colors = np.cos(x**2) - np.sin(y)**2
Now I have a similar dataset as you (one-dimensional arrays for x, y, z and
colors). Remark that the colors are defined for
each point (x,y). But when you want to plot with plot_surface, you'll
generate rectangular patches, of which the corners are given by those points.
So, on to interpolation then:
from scipy.interpolate import RectBivariateSpline
# from scipy.interpolate import interp2d # could 've used this too, but docs suggest the faster RectBivariateSpline
# Define the points at the centers of the faces:
y_coords, x_coords = np.unique(y), np.unique(x)
y_centers, x_centers = [ arr[:-1] + np.diff(arr)/2 for arr in (y_coords, x_coords)]
# Convert back to a 2D grid, required for plot_surface:
Y = y.reshape(y_coords.size, -1)
X = x.reshape(-1, x_coords.size)
Z = z.reshape(X.shape)
C = colors.reshape(X.shape)
#Normalize the colors to fit in the range 0-1, ready for using in the colormap:
C -= C.min()
C /= C.max()
interp_func = RectBivariateSpline(x_coords, y_coords, C.T, kx=1, ky=1) # the kx, ky define the order of interpolation. Keep it simple, use linear interpolation.
In this last step, you could also have used interp2d (with kind='linear'
replacing the kx=1, ky=1). But since the docs suggest to use the faster
RectBivariateSpline...
Now you're ready to plot it:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.cm as cm
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
r = ax.plot_surface(X,Y,Z,
facecolors=cm.hot(interp_func(x_centers, y_centers).T),
rstride=1, cstride=1) # only added because of this very limited dataset
As you can see, the colors on the faces have nothing to do anymore with the height of the dataset.
Note that you could have thought simply passing the 2D array C to facecolors would work, and matplotlib would not have complained. However, the result isn't accurate then, because matplotlib will use only a subset of C for the facecolors (it seems to ignore the last column and last row of C). It is equivalent to using only the color defined by one coordinate (e.g. the top-left) over the entire patch.
An easier method would have been to let matplotlib do the interpolation and obtain the facecolors and then pass those in to the real plot:
r = ax.plot_surface(X,Y,C, cmap='hot') # first plot the 2nd dataset, i.e. the colors
fc = r.get_facecolors()
ax.clear()
ax.plot_surface(X, Y, Z, facecolors=fc)
However, that won't work in releases <= 1.4.1 due to this recently submitted bug.
It really depends on how you plan on plotting this data. I like to plot graphs with gnuplot: it's easy, free and intuitive. To plot your example with gnuplot you'd have to print those line into a file (with only those four columns) and plot using a code like the following
reset
set terminal png
set output "out.png"
splot "file.txt" using 1:2:3:4 with lines palette
Assuming that you save your data into the file file.txt. splot stands for surface plot. Of course, this is a minimum example.
Alternatively you can use matplotlib, but that is not, in my opinion, as intuitive. Although it has the advantage of centering all the processing in python.
I'm making a surface plot on matplotlib. My axes are x, y, and depth. I have a two dimensional array which has RGB values, and the index corresponds to the (x,y) coordinate. How can I make the colormap from this 2D array? Thanks.
Code that makes numpy array:
import Image
import numpy as np
def makeImageArray(filename):
img = Image.open(filename)
a = np.array(img).astype("float32")
return a
Image is in greyscale.
From what I gather for every point (x,y) you have two pieces of information, the height and the color. You want to have a surface plot using the height, and colored according to the color at each location.
While you can easily specify custom color maps I don't think this will help you.
What you are thinking of is not that the same as a colormap which maps the height at (x,y) to a color.
The result is most evident in the Surface plots example here
I believe what you want is beyond the scope of matplotlib and can only be done with some kind of hack which I doubt you will wish to use.
Still here is my suggestion:
import pylab as py
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
X = np.arange(-5, 5, 0.1)
Y = np.arange(-5, 5, 0.1)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
colorise = [((5.0 + X[i][i])/10.0, 0.5, 0.0) for i in xrange((len(X)))]
ax = py.subplot(111, projection='3d')
for i in xrange(len(X)):
ax.plot(X[i], Y[i], Z[i], "o", color=colorise[i])
py.show()
This produces the following:
Importantly this displayed a 3D surface with the colouring not dependant on the height (it is a gradient in on direction). The most obvious issue is that coloring individual points looses matplotlibs surfaces making it painfully clear why the 3d plotting is called a projection!
Sorry this isn't very helpful, hopefully better software exists or I am unaware of matplotlibs full features.