Elevation distortion on sphere-projected image in Python - python

I'm trying to take two rectangular images, one of visible surface features and one representing elevation, and map them onto a 3D sphere. I know how to map features onto a sphere with Cartopy, and I know how to make relief surface maps, but I can't find a simple way to combine them to have exaggerated elevation on a spherical projection. For an example, here's it done in MATLAB:
Does anybody know if there's a simple way to do this in Python?

My solution does not meet all of your requirements. But it could be a good starter, to begin with.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from matplotlib.cbook import get_sample_data
from matplotlib._png import read_png
# Use world image with shape (360 rows, 720 columns)
pngfile = 'temperature_15-115.png'
fn = get_sample_data(pngfile, asfileobj=False)
img = read_png(fn) # get array of color
# Some needed functions / constant
r = 5
pi = np.pi
cos = np.cos
sin = np.sin
sqrt = np.sqrt
# Prep values to match the image shape (360 rows, 720 columns)
phi, theta = np.mgrid[0:pi:360j, 0:2*pi:720j]
# Parametric eq for a distorted globe (for demo purposes)
x = r * sin(phi) * cos(theta)
y = r * sin(phi) * sin(theta)
z = r * cos(phi) + 0.5* sin(sqrt(x**2 + y**2)) * cos(2*theta)
fig = plt.figure()
fig.set_size_inches(9, 9)
ax = fig.add_subplot(111, projection='3d', label='axes1')
# Drape the image (img) on the globe's surface
sp = ax.plot_surface(x, y, z, \
rstride=2, cstride=2, \
facecolors=img)
ax.set_aspect(1)
plt.show()
The resulting image:

Related

How can I rotate a graph that contains an imshow in matplotlib?

I have some microscopic scan data in a rectangular grid that was scanned by an X/Y scanner. The object I'm scanning contains markers in a nice, rectangular, orthogonal pattern on its surface.
Due to practicalities, we can only align the object to the X/Y scanner to within a few degrees when fixing it to the X/Y scanner table. We compensate for this by some calibration measurement, and then performing scans that are nicely aligned with the object we're trying to scan (by moving the X and Y axes simultaneously).
This works well, but now I want to display this data in a coordinate system that corresponds to the X/Y scanner's axes. So the scanned, rectangular dataset will be rotated a bit in that coordinate system. (See the second image below for what I mean).
For presentation purposes however, I would prefer to have the scanned image nicely horizontal, and the axes rotated. I am trying to accomplish this in Matplotlib but I'm failing miserably.
I can get a rotated Axes (see the third image), but I am unable to figure out what kind of transforms I should do to get the ax3.imshow() to show its data at its intended position.
Here's where I am so far:
The program to generate the test data and the image I have so far is shown below.
I'd appreciate any help I can get. I'm afraid to say the documentation of matplotlib leaves a lot to be desired, so I hope somebody who knows matplotlib inside out may chime in.
#! /usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
import mpl_toolkits.axisartist.floating_axes as floating_axes
import random
angle_deg = 5.0 # degrees CCW.
# Test data
NX = 5
NY = 3
x = np.zeros(shape=(NY, NX))
y = np.zeros(shape=(NY, NX))
c = np.zeros(shape=(NY, NX))
angle_rad = np.deg2rad(angle_deg)
for iy in range(NY):
for ix in range(NX):
icx = 0.5 * (NX - 1)
icy = 0.5 * (NY - 1)
# To centered coordinates, in physical units
xcp = (ix - icx) * 0.1
ycp = (iy - icy) * 0.1
# Rotate
xcpr = xcp * np.cos(angle_rad) - ycp * np.sin(angle_rad)
ycpr = xcp * np.sin(angle_rad) + ycp * np.cos(angle_rad)
# Rotate, and translated to the physical center
x[iy, ix] = xcpr + 40.0
y[iy, ix] = ycpr + 30.0
c[iy, ix] = 100 + ix + iy
fig = plt.figure()
fig.set_size_inches(15, 9)
# Add axis #1.
ax1 = fig.add_subplot(131)
ax1.set_title("imshow()")
ax1.imshow(c, origin='lower')
ax1.grid()
# Add axis #2.
ax2 = fig.add_subplot(132)
ax2.set_title("pcolormesh()")
ax2.pcolormesh(x, y, c, shading='nearest')
ax2.axis('equal')
ax2.grid()
# Add axis #3.
min_x = x.min()
max_x = x.max()
min_y = y.min()
max_y = y.max()
cx = 0.5 * (min_x + max_x)
cy = 0.5 * (min_y + max_y)
transform = Affine2D().rotate_deg(-angle_deg)
grid_helper = floating_axes.GridHelperCurveLinear(transform, extremes=(min_x, max_x, min_y, max_y))
ax3 = floating_axes.FloatingSubplot(fig, 133, grid_helper=grid_helper)
ax3.grid()
ax3.set_title("rotate the entire graph.\nHow do I do get the data to show up here,\nhorizontally like in the leftmost image!?\n")
ax3.imshow(c, origin='lower')
fig.add_subplot(ax3)
plt.show()

Make a Rotating Sphere in Python

I have made this code that applies the spherical harmonics in a spherical manner as I am trying to model stellar pulsation modes. Ideally, I'd like to be able to have an image that rotates that can be saved as a gif image. I have found a few examples of code for doing this but none of it seems to apply to my code or uses python packages that aren't available to me. I'm not sure if this is too far out of my range of skills in python as I'm very much a beginner.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm #import package to calculate spherical harmonics
theta = np.linspace(0, 2*np.pi, 100) #setting range for theta
phi = np.linspace(0, np.pi, 100) #setting range for phi
phi, theta = np.meshgrid(phi, theta) #setting the grid for phi and theta
#Setting the cartesian coordinates of the unit sphere
#Converting phi, theta, z to cartesian coordinates
x = np.sin(phi)*np.cos(theta)
y = np.sin(phi)*np.sin(theta)
z = np.cos(phi)
m, l = 4, 4 #m and l control the mode of pulsation and overall appearance of the figure
#Calculating the spherical harmonic Y(l,m) and normalizing it
figcolors = sph_harm(m, l, theta, phi).real
figmax, figmin = figcolors.max(), figcolors.min()
figcolors = (figcolors-figmin)/(figmax-figmin)
#Setting the aspect ratio to 1 which makes the sphere look spherical and not elongated
fig = plt.figure(figsize=plt.figaspect(1.)) #aspect ratio
axes = fig.add_subplot(111, projection='3d') #sets figure to 3d
#Sets the plot surface and colors of the figure where seismic is the color scheme
axes.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.autumn(figcolors))
#yellow zones are cooler and compressed, red zones are warmer and expanded
#Turn off the axis planes so only the sphere is visible
axes.set_axis_off()
fig.suptitle('m=4 l=4', fontsize=18, x=0.52, y=.85)
plt.savefig('m4_l4.png') #saves a .png file of my figure
plt.show() #Plots the figure
#figure saved for m=1, 2, 3, 4 and l=2, 3, 5, 6 respectively then all 6 were put together to form a single figure
I've also got an image showing what my code outputs currently. It's just a still sphere, of course. Thank you in advance! sphere4_4
Change the last part of your code to generate a set of figures (see below). In this case I create num = 10 frames, you can change this number if you want. Then open a terminal and type
convert m4_l4*.png m4_l4.gif
And this is the result
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm #import package to calculate spherical harmonics
theta = np.linspace(0, 2*np.pi, 100) #setting range for theta
phi = np.linspace(0, np.pi, 100) #setting range for phi
phi, theta = np.meshgrid(phi, theta) #setting the grid for phi and theta
#Setting the cartesian coordinates of the unit sphere
#Converting phi, theta, z to cartesian coordinates
x = np.sin(phi)*np.cos(theta)
y = np.sin(phi)*np.sin(theta)
z = np.cos(phi)
m, l = 4, 4 #m and l control the mode of pulsation and overall appearance of the figure
#Calculating the spherical harmonic Y(l,m) and normalizing it
figcolors = sph_harm(m, l, theta, phi).real
figmax, figmin = figcolors.max(), figcolors.min()
figcolors = (figcolors-figmin)/(figmax-figmin)
#Setting the aspect ratio to 1 which makes the sphere look spherical and not elongated
fig = plt.figure(figsize=plt.figaspect(1.)) #aspect ratio
axes = fig.add_subplot(111, projection='3d') #sets figure to 3d
#Sets the plot surface and colors of the figure where seismic is the color scheme
axes.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.autumn(figcolors))
#yellow zones are cooler and compressed, red zones are warmer and expanded
axes.set_axis_off()
fig.suptitle('m=4 l=4', fontsize=18, x=0.52, y=.85)
for idx, angle in enumerate(np.linspace(0, 360, 10)):
axes.view_init(30, angle)
plt.draw()
#Turn off the axis planes so only the sphere is visible
plt.savefig('m4_l4-%04d.png' % idx) #saves a .png file of my figure
plt.show()

Aligning maps made using basemap

Is there a way to align python basemaps like this figure below?
Here's some sample basemap code to produce a map:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 4.5))
plt.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.00)
m = Basemap(projection='robin',lon_0=0,resolution='c')
m.fillcontinents(color='gray',lake_color='white')
m.drawcoastlines()
plt.savefig('world.png',dpi=75)
I am not an expert with Matplotlib, but I found a way to get a similar result by using the data files included in the source folder of basemap. They can be combined into a meshgrid to plot some data, in the example below we plot the altitude at every point.
One of the tricks I used is to set matplotlib to an orthogonal projection so that there is no distortion in the vertical spacing of the maps.
I have put the parameters at the beginning of the code as you may find it useful to adjust.
One thing I couldn't get my head around is the shadow under the maps.
from mpl_toolkits.mplot3d import proj3d
from mpl_toolkits.basemap import Basemap
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import numpy as np
import matplotlib.pyplot as plt
# Parameters
n_maps = 5 # Number of maps
z_spacing = 4. # Spacing of maps along z
z_reduction = 1E-8 # Reduction factor for Z data, makes the map look flat
view_angles = (14., -100.) # Set view port angles
colbar_bottom = 0.2 # Space at the bottom of colorbar column
colbar_spacing = .132 # Space between colorbars
colbar_height = 0.1 # Height of colorbars
# Set orthogonal projection
def orthogonal_proj(zfront, zback):
a = (zfront+zback)/(zfront-zback)
b = -2*(zfront*zback)/(zfront-zback)
return np.array([[1,0,0,0],
[0,1,0,0],
[0,0,a,b],
[0,0,-0.0001,zback]])
proj3d.persp_transformation = orthogonal_proj
fig = plt.figure(figsize=[30, 10*n_maps])
ax = fig.gca(projection='3d')
etopo = np.loadtxt('etopo20data.gz')
lons = np.loadtxt('etopo20lons.gz')
lats = np.loadtxt('etopo20lats.gz')
# Create Basemap instance for Robinson projection.
m = Basemap(projection='robin', lon_0=0.5*(lons[0]+lons[-1]))
# Compute map projection coordinates for lat/lon grid.
X, Y = m(*np.meshgrid(lons,lats))
# Exclude the oceans
Z = etopo.clip(-1)
# Set the colormap
cmap = plt.cm.get_cmap("terrain")
cmap.set_under("grey")
for i in range(n_maps):
c = ax.contourf(X, Y, z_spacing*i + z_reduction*Z, 30, cmap=cmap, vmin=z_spacing*i, extend='neither')
cax = inset_axes(ax,
width="5%",
height="100%",
loc=3,
bbox_to_anchor=(.85, colbar_spacing*i+colbar_bottom, .2, colbar_height),
bbox_transform=ax.transAxes,
borderpad=0
)
cb = fig.colorbar(c, cax=cax)
cb.set_label("Altitude")
# Reset the ticks of the color bar to match initial data
cb.set_ticks([z_spacing * i + j/10. * z_reduction * Z.max() for j in range(11)])
cb.set_ticklabels([str(int(j/10. * Z.max())) for j in range(11)])
ax.set_axis_off()
ax.view_init(*view_angles)
ax.set_xlim3d(X.min(), X.max())
ax.set_ylim3d(Y.min(), Y.max())
ax.set_zlim3d(-1E-2, (n_maps-1)*z_spacing)
plt.savefig('world.png',dpi=75)
Edit:
If you want shadows and don't mind the extra compute time you can change the beginning of the for loop with something along the lines of:
shadow_Z = np.empty(Z.shape)
for i in range(n_maps):
c = ax.contourf(X, Y, z_spacing*i + z_reduction*Z, 30, cmap=cmaps[i], vmin=z_spacing*i, extend='neither')
for j in range(10):
shadow_Z.fill(z_spacing*i - 1E-2 * j)
s = ax.contourf((X - X.mean()) * (1 + 8E-3 * j) + X.mean() + 2E5,
(Y - Y.mean()) * (1 + 8E-3 * j) + Y.mean() - 2E5,
shadow_Z, colors='black', alpha=0.1 - j * 1E-2)

Rotation of a vector (Python)

I am rotating a vector in 3D via two 2D rotations using the following code:
NOTE: L is
np.array([11.231303753070549, 9.27144871768164, 18.085790226916288])
a predefined vector shown in blue in the plot below.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def angle_between(p1, p2):
ang1 = np.arctan2(*p1[::-1])
ang2 = np.arctan2(*p2[::-1])
return ((ang1 - ang2) % (2 * np.pi))
L = np.vstack([L,np.zeros(3)])
line_xy = [0.,1.]
line_L = [L[0,0],L[0,1]]
a = angle_between(line_xy, line_L)
def rotation(vector,theta):
v1_new = (vector[0]*np.cos(theta)) - (vector[1]*np.sin(theta))
v2_new = (vector[1]*np.cos(theta)) + (vector[0]*np.sin(theta))
z_trans = [v1_new,v2_new,vector[2]]
line_yz= [0.,1.]
theta2 = angle_between(line_yz, [z_trans[1],z_trans[2]])
v1_new = (z_trans[0]*np.cos(theta2)) - (z_trans[1]*np.sin(theta2))
v2_new = (z_trans[1]*np.cos(theta2)) + (z_trans[0]*np.sin(theta2))
y_trans = np.array([z_trans[0],v1_new,v2_new])
return z_trans,y_trans
L2,L3 = rotation(L[0,:],a)
L2 = np.vstack([L2,np.zeros(3)])
L3 = np.vstack([L3,np.zeros(3)])
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
#ax.scatter(x1*1000,y1*1000,z1*1000,c ='r',zorder=2)
ax.plot(L[:,0],L[:,1],L[:,2],color='b',zorder=1)
line = np.array([[0,0,0],[0,0,15]])
ax.plot(line[:,0],line[:,1],line[:,2],color = 'g')
ax.set_xlabel('X Kpc')
ax.set_ylabel('Y Kpc')
ax.set_zlabel('Z Kpc')
ax.plot(L2[:,0],L2[:,1],L2[:,2],color='g')
ax.plot(L3[:,0],L3[:,1],L3[:,2],color='y')
What I'm doing here is calculating the angle between x=0, y=1 (that's the line_xy part) and then rotating it around the z-axis using the first part of the rotation function:
v1_new = (vector[0]*np.cos(theta)) - (vector[1]*np.sin(theta))
v2_new = (vector[1]*np.cos(theta)) + (vector[0]*np.sin(theta))
z_trans = [v1_new,v2_new,vector[2]]
then repeat the process but this time rotating around the x axis using the second part of the rotation function:
line_yz= [0.,1.]
theta2 = angle_between(line_yz, [z_trans[1],z_trans[2]])
v1_new = (z_trans[0]*np.cos(theta2)) - (z_trans[1]*np.sin(theta2))
v2_new = (z_trans[1]*np.cos(theta2)) + (z_trans[0]*np.sin(theta2))
y_trans = np.array([z_trans[0],v1_new,v2_new])
Rotations are done via the standard 2D rotation equations:
x' = x cos(theta) - y sin(theta)
y' = y cos(theta) + x sin(theta)
But for some reason, after the second rotation, the line (in yellow) doesn't line up with the green line (the original target of rotating this vector).
I've tried checking the angles in both radians and degrees but it appears to only work with radians.
When checking the angle theta2, it comes out around 35 degrees which looks plausible.
I am not quite clear on your question, but hopefully this should help.
If you want to rotate a 3D vector around a particular axis, take advantage of matrix transformations instead of element wise (like you have written above).
Below is code to rotate a 3-D vector around any axis:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def unit_vector(vector):
""" Returns the unit vector of the vector."""
return vector / np.linalg.norm(vector)
def angle_between(v1, v2):
"""Finds angle between two vectors"""
v1_u = unit_vector(v1)
v2_u = unit_vector(v2)
return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
def x_rotation(vector,theta):
"""Rotates 3-D vector around x-axis"""
R = np.array([[1,0,0],[0,np.cos(theta),-np.sin(theta)],[0, np.sin(theta), np.cos(theta)]])
return np.dot(R,vector)
def y_rotation(vector,theta):
"""Rotates 3-D vector around y-axis"""
R = np.array([[np.cos(theta),0,np.sin(theta)],[0,1,0],[-np.sin(theta), 0, np.cos(theta)]])
return np.dot(R,vector)
def z_rotation(vector,theta):
"""Rotates 3-D vector around z-axis"""
R = np.array([[np.cos(theta), -np.sin(theta),0],[np.sin(theta), np.cos(theta),0],[0,0,1]])
return np.dot(R,vector)
Rotate Original Blue Vector 45 degrees (pi/2)
L_predef = np.array([11.231303753070549, 9.27144871768164, 18.085790226916288]) #blue vector
new_vect = z_rotation(L_predef, np.pi/2.0)
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot(np.linspace(0,L_predef[0]),np.linspace(0,L_predef[1]),np.linspace(0,L_predef[2]))
ax.plot(np.linspace(0,new_vect[0]),np.linspace(0,new_vect[1]),np.linspace(0,new_vect[2]))
plt.show()
There is a general solution to this problem. Given a vector, a rotation axis and an anticlockwise angle, I wrote a simple code, which works of course also for the cases already mentioned. What it does is:
projecting the vector onto the plane defined by the axis of rotation;
rotating the component of the vector in the plane;
finally reassembling all together to give the final result.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib
def rotve(v,erot,angle):
rotmeasure=np.linalg.norm(erot)
erot=erot/rotmeasure;
norme=np.dot(v,erot)
vplane=v-norme*erot
plnorm=np.linalg.norm(vplane)
ep=vplane/plnorm
eo=np.cross(erot,ep)
vrot=(np.cos(angle)*ep+np.sin(angle)*eo)*plnorm+norme*erot
return(vrot)
If you want, you can check with an example which plots the "umbrella" made by the rotations:
axrot=np.array([1,0,1]); v=np.array([1.,1.,1.])
fig3 = plt.figure(3)
ax3d = fig3.add_subplot(111, projection='3d')
ax3d.quiver(0,0,0,axrot[0],axrot[1],axrot[2],length=.5, normalize=True, color='black')
angles=np.linspace(0,2,10)*np.pi
for i in range(len(angles)):
vrot=rotve(v,axrot,angles[i]);
ax3d.quiver(0,0,0,vrot[0],vrot[1],vrot[2],length=.1, normalize=True, color='red')
ax3d.quiver(0,0,0,v[0],v[1],v[2],length=.1, normalize=True, color='blue')
ax3d.set_title('rotations')
fig3.show()
plt.show()

Python: How to revolve a surface around z axis and make a 3d plot?

I want to get 2d and 3d plots as shown below.
The equation of the curve is given.
How can we do so in python?
I know there may be duplicates but at the time of posting
I could not fine any useful posts.
My initial attempt is like this:
# Imports
import numpy as np
import matplotlib.pyplot as plt
# to plot the surface rho = b*cosh(z/b) with rho^2 = r^2 + b^2
z = np.arange(-3, 3, 0.01)
rho = np.cosh(z) # take constant b = 1
plt.plot(rho,z)
plt.show()
Some related links are following:
Rotate around z-axis only in plotly
The 3d-plot should look like this:
Ok so I think you are really asking to revolve a 2d curve around an axis to create a surface. I come from a CAD background so that is how i explain things.
and I am not the greatest at math so forgive any clunky terminology. Unfortunately you have to do the rest of the math to get all the points for the mesh.
Heres your code:
#import for 3d
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
change arange to linspace which captures the endpoint otherwise arange will be missing the 3.0 at the end of the array:
z = np.linspace(-3, 3, 600)
rho = np.cosh(z) # take constant b = 1
since rho is your radius at every z height we need to calculate x,y points around that radius. and before that we have to figure out at what positions on that radius to get x,y co-ordinates:
#steps around circle from 0 to 2*pi(360degrees)
#reshape at the end is to be able to use np.dot properly
revolve_steps = np.linspace(0, np.pi*2, 600).reshape(1,600)
the Trig way of getting points around a circle is:
x = r*cos(theta)
y = r*sin(theta)
for you r is your rho, and theta is revolve_steps
by using np.dot to do matrix multiplication you get a 2d array back where the rows of x's and y's will correspond to the z's
theta = revolve_steps
#convert rho to a column vector
rho_column = rho.reshape(600,1)
x = rho_column.dot(np.cos(theta))
y = rho_column.dot(np.sin(theta))
# expand z into a 2d array that matches dimensions of x and y arrays..
# i used np.meshgrid
zs, rs = np.meshgrid(z, rho)
#plotting
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
fig.tight_layout(pad = 0.0)
#transpose zs or you get a helix not a revolve.
# you could add rstride = int or cstride = int kwargs to control the mesh density
ax.plot_surface(x, y, zs.T, color = 'white', shade = False)
#view orientation
ax.elev = 30 #30 degrees for a typical isometric view
ax.azim = 30
#turn off the axes to closely mimic picture in original question
ax.set_axis_off()
plt.show()
#ps 600x600x600 pts takes a bit of time to render
I am not sure if it's been fixed in latest version of matplotlib but the setting the aspect ratio of 3d plots with:
ax.set_aspect('equal')
has not worked very well. you can find solutions at this stack overflow question
Only rotate the axis, in this case x
import numpy as np
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.axes3d as axes3d
np.seterr(divide='ignore', invalid='ignore')
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
x = np.linspace(-3, 3, 60)
rho = np.cosh(x)
v = np.linspace(0, 2*np.pi, 60)
X, V = np.meshgrid(x, v)
Y = np.cosh(X) * np.cos(V)
Z = np.cosh(X) * np.sin(V)
ax.set_xlabel('eje X')
ax.set_ylabel('eje Y')
ax.set_zlabel('eje Z')
ax.plot_surface(X, Y, Z, cmap='YlGnBu_r')
plt.plot(x, rho, 'or') #Muestra la curva que se va a rotar
plt.show()
The result:

Categories