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

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()

Related

Plotting a surface for a robot reach bubble in Python

I'm trying to simulate a robot reach bubble. The goal would be to export it into a CAD file and visualize the possible workspace. My approach was to plot all potential endpoints using forward kinematics for the robot, considering linkage lengths and joint limits. This may be a brute-force way to generate the endpoints (Rx, Ry, Rz), but it comes out to be very accurate (at least for 2D examples). [This image shows the provided workspace for an IRB robot and my results when plotting the points in 2D][1]
[1]: https://i.stack.imgur.com/g3cP7.jpg
I can display a three-dimensional figure of the bubble as a scatterplot; however, to export it into a CAD file, I need to mesh it first, which requires converting it into a surface, as I understand. This is the part I'm having trouble with.
Using matplotlib's ax.surface_plot(Rx, Ry, Rz) I receive an error stating that Rz must be a 2-dimensional value. I fiddled with np.meshgrid() and np.mgrid() functions but have been unable to create a simple surface of the bubble. What can I do to convert this scatterplot into a surface? Is there another approach that I'm missing?
Another thing that dawned on me is that I'd likely want to remove some of the intermediate points inside the reach bubble. Ideally, the surface would be composed of the outer ends and the hollow points from the center radius.
Below is a code that results in 1D arrays:
# Reach bubble 3D
import NumPy as np
import matplotlib.pyplot as plt
# Initialize figure and label axes
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
dr = np.pi/180 # Degree to radian multiple
pi = np.pi
# Define important robot dimensions and axis limits for IRB 1100
z0 = 0.327 # Fixed height from base to A1
link1 = 0.28
link2 = 0.3
a1 = np.linspace(0, 2*pi, 8) # Angle limits for axes
a2 = np.linspace(-115*dr, 113*dr, 12)
a3 = np.linspace(-205*dr, 55*dr, 12)
Rx = []
Ry = []
Rz = []
for i1 in a1:
for i2 in a2:
for i3 in a3:
r = link1*np.sin(i2) + link2*np.sin(pi/2+i2+i3)
Rx.append(r*np.cos(i1))
Ry.append(r*np.sin(i1))
Rz.append(z0 + link1*np.cos(i2) + link2*np.cos(pi/2+i2+i3))
# Plot reach points
ax.scatter(Rx, Ry, Rz, c='r', marker='o', alpha = 0.2)
plt.show()
Below is a code that results in 3D arrays but doesn't use for loops:
# 3D Reach bubble for robot
import numpy as np
import matplotlib.pyplot as plt
# Initialize figure and label axes
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
pi = np.pi
dr = pi/180 # Degree to radian multiple
# Define important robot dimensions and axis limits for GP8
z0 = 0.327 # Fixed height from base to A1
link1 = 0.28
link2 = 0.3
link3 = 0.064
a1, a2, a3 = np.mgrid[0:2*pi:15j, -115*dr:113*dr:13j, -205*dr:55*dr:17j]
r = link1*np.sin(a2) + link2*np.sin(pi/2+a2+a3)
Rx = r*np.cos(a1)
Ry = r*np.sin(a1)
Rz = z0 + link1*np.cos(a2) + link2*np.cos(pi/2+a2+a3)
# Plot reach points
ax.plot_surface(Rx, Ry, Rz, color = 'Red', alpha = 0.2)
ax.scatter(Rx, Ry, Rz, c='r', marker='o', alpha = 0.2)

Using a linear equation to create a radially interpolated circle

Given a linear equation, I want to use the slope to create a circle of values around a given point, defined by the slope of the linear equation if possible
Im currently a bit far away - can only make the radial plot but do not know how to connect this with an input equation. My first thought would be to change the opacity using import matplotlib.animation as animation and looping matplotlib's alpha argument to become gradually more and more opaque. However the alpha doesnt seem to change opacity.
Code:
# lenth of radius
distance = 200
# create radius
radialVals = np.linspace(0,distance)
# 2 pi radians = full circle
azm = np.linspace(0, 2 * np.pi)
r, th = np.meshgrid(radialVals, azm)
z = (r ** 2.0) / 4.0
# creates circle
plt.subplot(projection="polar")
# add color gradient
plt.pcolormesh(th, r, z)
plt.plot(azm, r,alpha=1, ls='', drawstyle = 'steps')
#gridlines
# plt.grid()
plt.show()
Here is one way to solve it, the idea is to create a mesh, calculate the colors with a function then use imshow to visualize the mesh.
from matplotlib import pyplot as plt
import numpy as np
def create_mesh(slope,center,radius,t_x,t_y,ax,xlim,ylim):
"""
slope: the slope of the linear function
center: the center of the circle
raadius: the radius of the circle
t_x: the number of grids in x direction
t_y: the number of grids in y direction
ax: the canvas
xlim,ylim: the lims of the ax
"""
def cart2pol(x,y):
rho = np.sqrt(x**2 + y**2)
phi = np.arctan2(y,x)
return rho,phi
def linear_func(slope):
# initialize a patch and grids
patch = np.empty((t_x,t_y))
patch[:,:] = np.nan
x = np.linspace(xlim[0],xlim[1],t_x)
y = np.linspace(ylim[0],ylim[1],t_y)
x_grid,y_grid = np.meshgrid(x, y)
# centered grid
xc = np.linspace(xlim[0]-center[0],xlim[1]-center[0],t_x)
yc = np.linspace(ylim[0]-center[1],ylim[1]-center[1],t_y)
xc_grid,yc_grid = np.meshgrid(xc, yc)
rho,phi = cart2pol(xc_grid,yc_grid)
linear_values = slope * rho
# threshold controls the size of the gaussian
circle_mask = (x_grid-center[0])**2 + (y_grid-center[1])**2 < radius
patch[circle_mask] = linear_values[circle_mask]
return patch
# modify the patch
patch = linear_func(slope)
extent = xlim[0],xlim[1],ylim[0],ylim[1]
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
fig,ax = plt.subplots(nrows=1,ncols=2,figsize=(12,6))
slopes = [40,30]
centroids = [[2,2],[4,3]]
radii = [1,4]
for item in ax:item.set_xlim(0,8);item.set_ylim(0,8)
v_max,v_min = max(slopes),0
create_mesh(slopes[0],centroids[0],radii[0],t_x=300,t_y=300,ax=ax[0],xlim=(0,8),ylim=(0,8))
create_mesh(slopes[1],centroids[1],radii[1],t_x=300,t_y=300,ax=ax[1],xlim=(0,8),ylim=(0,8))
plt.show()
The output of this code is
As you can see, the color gradient of the figure on the left is not as sharp as the figure on the right because of the different slopes ([40,30]).
Also note that, these two lines of code
v_max,v_min = max(slopes),0
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
are added in order to let the two subplots share the same colormap.

Changing aspect ratio for 3D plots on Matplotlib

I am trying to set up the aspect ratio for 3D plots using Matplotlib.
Following the answer for this question: Setting aspect ratio of 3D plot
I kind of applied the solution as:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
def get_proj(self):
"""
Create the projection matrix from the current viewing position.
elev stores the elevation angle in the z plane
azim stores the azimuth angle in the (x, y) plane
dist is the distance of the eye viewing point from the object point.
"""
# chosen for similarity with the initial view before gh-8896
relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
#EDITED TO HAVE SCALED AXIS
xmin, xmax = np.divide(self.get_xlim3d(), self.pbaspect[0])
ymin, ymax = np.divide(self.get_ylim3d(), self.pbaspect[1])
zmin, zmax = np.divide(self.get_zlim3d(), self.pbaspect[2])
# transform to uniform world coordinates 0-1, 0-1, 0-1
worldM = proj3d.world_transformation(xmin, xmax,
ymin, ymax,
zmin, zmax)
# look into the middle of the new coordinates
R = self.pbaspect / 2
xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
zp = R[2] + np.sin(relev) * self.dist
E = np.array((xp, yp, zp))
self.eye = E
self.vvec = R - E
self.vvec = self.vvec / np.linalg.norm(self.vvec)
if abs(relev) > np.pi/2:
# upside down
V = np.array((0, 0, -1))
else:
V = np.array((0, 0, 1))
zfront, zback = -self.dist, self.dist
viewM = proj3d.view_transformation(E, R, V)
projM = self._projection(zfront, zback)
M0 = np.dot(viewM, worldM)
M = np.dot(projM, M0)
return M
Axes3D.get_proj = get_proj
Then I'm creating a sample data and plotting as:
y,z,x = np.meshgrid(range(10),-np.arange(5)[::-1],range(20))
d = np.sin(x)+np.sin(y)+np.sin(z)
iy,ix = 0,-1
iz = -1
fig,ax = plt.subplots(figsize=(5,5),subplot_kw={'projection':'3d'})
ax.pbaspect = np.array([1, 1, 1])#np.array([2.0, 1.0, 0.5])
ax.contourf(x[iz], y[iz], d[iz],zdir='z',offset=0)
ax.contourf(x[:,iy,:],d[:,iy,:],z[:,iy,:],zdir='y',offset=y.min())
ax.contourf(d[:,:,ix],y[:,:,ix],z[:,:,ix],zdir='x',offset=x.max())
color = '0.3'
ax.plot(x[0,iy,:],y[0,iy,:],y[0,iy,:]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,iy,0]*0+x.max(),y[:,iy,0],z[:,iy,0],color,linewidth=1,zorder=1e4)
ax.plot(x[0,:,ix],y[0,:,ix],y[0,:,ix]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,0,ix],y[:,0,ix]*0+y.min(),z[:,0,ix],color,linewidth=1,zorder=1e4)
ax.set(xlim=[x.min(),x.max()],ylim=[y.min(),y.max()],zlim=[z.min(),z.max()])
fig.tight_layout()
If I set the pbaspect parameter as (1,1,1) I obtain:
But If I set it for (2,1,0.5) the axis seems to be correct, but it crops the data somehow:
Even if I let the xlim,ylim and zlim automatic. There is something strange with the aspect too.
Something tells me that the the axis as not orthogonal.
Does someone know how to correct the aspect ratio for this?
I would also like to know how to avoid the axis being cropped by the figure limits.
I searched on the web so long, but I could not find a better solution for this.
Update:
I tried using less than 1 values for pbaspect as suggested and it gets beter, but still crops the data:
You can try to change figsize and adjust subplots margins like below:
fig = plt.figure(figsize=(10,6))
fig.subplots_adjust(left=0.2, right=0.8, top=0.8, bottom=0.2)
ax = fig.gca(projection='3d')
ax.pbaspect = np.array([2, 1, 0.5])
ax.contourf(x[iz], y[iz], d[iz],zdir='z',offset=0)
ax.contourf(x[:,iy,:],d[:,iy,:],z[:,iy,:],zdir='y',offset=y.min())
ax.contourf(d[:,:,ix],y[:,:,ix],z[:,:,ix],zdir='x',offset=x.max())
ax.plot(x[0,iy,:],y[0,iy,:],y[0,iy,:]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,iy,0]*0+x.max(),y[:,iy,0],z[:,iy,0],color,linewidth=1,zorder=1e4)
ax.plot(x[0,:,ix],y[0,:,ix],y[0,:,ix]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,0,ix],y[:,0,ix]*0+y.min(),z[:,0,ix],color,linewidth=1,zorder=1e4)
plt.show()
Output for (2,1,0.5):
Regarding disappearing graphics there is Matplotlib issue:
The problem occurs due to the reduction of 3D data down to 2D + z-order scalar. A single value represents the 3rd dimension for all parts of 3D objects in a collection. Therefore, when the bounding boxes of two collections intersect, it becomes possible for this artifact to occur. Furthermore, the intersection of two 3D objects (such as polygons or patches) can not be rendered properly in matplotlib’s 2D rendering engine.

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)

Elevation distortion on sphere-projected image in 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:

Categories