draping texture over a 3D surface in python - python

my goal is to drap a texture (i.e. an image) over a surface and visualize it in 3D with python. The application is viewing an orthophotography over a DEM, I thence use gdal for importing my data (both image and DEM). I tried to use plot_surface from matplotlib but it seems that I can't add texture to the surface.
here is the current code:
from osgeo import gdal
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import numpy as np
ds = gdal.Open('MyDEM.cub')
dem = ds.ReadAsArray()
do = gdal.Open('MyOrtho.cub')
or = do.ReadAsArray()
xres = gt[1]
yres = gt[5]
X = np.arange(gt[0], gt[0] + dem.shape[1]*xres, xres)
Y = np.linspace(gt[3], gt[3] + dem.shape[0]*yres, ds.RasterYSize)
X, Y = np.meshgrid(X, Y)
fig, ax = plt.subplots(figsize=(16,8), subplot_kw={'projection': '3d'})
surf = ax.plot_surface(X,Y,dem,rstride=1, cstride=1,linewidth=0, antialiased=True,cmap=plt.cm.RdYlBu_r)
fig.colorbar(surf, shrink=0.4, aspect=20)
plt.show()
How can I use or array as a texture (e.g., or can have a different resolution/size than dem but I'll manage this later), I want first to have a stupid 3D surface with a texture. This is easy in Matlab, but how to do it with Python? Any idea ?

Yeah Mayavi will do this. You can open the DEM file in gdal and then pull the image into TVTK as a texture. Finally you can wrap it over the surface with the mlab.surf() commands. Here is a link to a good example of this.
Example

From the matplotlib docs:
Axes3D.plot_surface(X, Y, Z, *args, **kwargs)
Create a surface plot.
By default it will be colored in shades of a solid color, but it also supports color mapping by supplying the cmap argument.
It seems, therefore, that matplotlib does not support the use of an arbitrary image on a surface plot. You will need to identify another library that provides this feature (a quick search suggests that mayavi may do what you want).

Related

Interactive Satellite Map using Python

I'm trying to overlay a Lambert Conformal Conical satellite image onto a Holoviews interactive map. I can map the satellite image just fine, but I can't figure out how to translate this map onto a Holoviews map properly. Below is reproducible code where I grab data using the Unidata Siphon library.
Import Packages
from datetime import datetime
import matplotlib.pyplot as plt
from netCDF4 import Dataset
from siphon.catalog import TDSCatalog
import holoviews as hv
import geoviews as gv
import geoviews.feature as gf
from cartopy import crs
from cartopy import feature as cf
hv.extension('bokeh')
Grab data and create figure
date=datetime.utcnow()
idx=-2
regionstr = 'CONUS'
channelnum = 13
datestr = str(date.year) + "%02d"%date.month + "%02d"%date.day
channelstr = 'Channel' + "%02d"%channelnum
cat = TDSCatalog('http://thredds-test.unidata.ucar.edu/thredds/catalog/satellite/goes16/GOES16/' + regionstr +
'/' + channelstr + '/' + datestr + '/catalog.xml')
ds = cat.datasets[idx].remote_access(service='OPENDAP')
x = ds.variables['x'][:]
y = ds.variables['y'][:]
z = ds.variables['Sectorized_CMI'][:]
proj_var = ds.variables[ds.variables['Sectorized_CMI'].grid_mapping]
# Create a Globe specifying a spherical earth with the correct radius
globe = ccrs.Globe(ellipse='sphere', semimajor_axis=proj_var.semi_major,
semiminor_axis=proj_var.semi_minor)
proj = ccrs.LambertConformal(central_longitude=proj_var.longitude_of_central_meridian,
central_latitude=proj_var.latitude_of_projection_origin,
standard_parallels=[proj_var.standard_parallel],
globe=globe)
fig = plt.figure(figsize=(14, 10))
ax = fig.add_subplot(1, 1, 1, projection=proj)
ax.coastlines(resolution='50m', color='lightblue')
ax.add_feature(cf.STATES, linestyle=':', edgecolor='lightblue')
ax.add_feature(cf.BORDERS, linewidth=1, edgecolor='lightblue')
for im in ax.images:
im.remove()
im = ax.imshow(z, extent=(x.min(), x.max(), y.min(), y.max()), origin='upper', cmap='jet')
plt.colorbar(im)
Now plot an interactive image using Holoviews (which uses Bokeh backend)
goes = hv.Dataset((x, y, z),['Lat', 'Lon'], 'ABI13')
%opts Image (cmap='jet') [width=1000 height=800 xaxis='bottom' yaxis='left' colorbar=True toolbar='above' projection=proj]
goes.to.image()* gf.coastline().options(projection=crs.LambertConformal(central_longitude=proj_var.longitude_of_central_meridian,central_latitude=proj_var.latitude_of_projection_origin,standard_parallels=[proj_var.standard_parallel],globe=globe))
I must not be translating it properly, though I've found documentation on Holoviews with regards to Lambert Conformal Conical projection to be sparse. I'm open to using any other interactive map package. My main desire is to be able to plot relatively quickly, get state/country lines on the image properly, and be able to zoom in. I've tried folium but also fell into projection issues there.
So I think the main thing to understand, which is explained here: is how projections are declared. The elements (e.g. Image, Points etc.) in GeoViews have a parameter called crs which declares the coordinate system the data is in, while the projection plot option declares what to project the data to for display.
In your case I think you want to display the image in the same coordinate system it is already in (Lambert Conformal), so technically you don't have to declare the coordinate system (crs) on the element at all and can just use a hv.Image (which is entirely unaware of projections).
As far as I can tell your code should already work as expected if you are using GeoViews 1.5, but here is what I would do:
# Apply mask
masked = np.ma.filled(z, np.NaN)
# Declare image from data
goes = hv.Image((x, y, masked),['Lat', 'Lon'], 'ABI13')
# Declare some options
options = dict(width=1000, height=800, yaxis='left', colorbar=True,
toolbar='above', cmap='jet', projection=proj)
# Display plot
gf.ocean * gf.land * goes.options(**options) * gf.coastline.options(show_bounds=False)
Note how we declare the projection on the Image but not crs. If you do want to display the data in a different projection it is defined in, you do have to declare the crs and use a gv.Image. In this case I'd recommend using project_image with the fast option enabled (which might introduce some artifacts but is much faster):
# Apply mask
masked = np.ma.filled(z, np.NaN)
# Declare the gv.Image with the crs
goes = gv.Image((x, y, masked), ['Lat', 'Lon'], 'ABI13', crs=proj)
# Now regrid the data and apply the reprojection
projected_goes = gv.operation.project_image(goes, fast=False, projection=ccrs.GOOGLE_MERCATOR)
# Declare some options
options = dict(width=1000, height=800, yaxis='left', colorbar=True,
toolbar='above', cmap='jet')
# Display plot
projected_goes.options(**options) * gv.tile_sources.ESRI.options(show_bounds=False)
Another final tip, when you plot with bokeh all the data you are plotting will be sent to the browser, so when dealing with images any larger than you are already using I'd recommend using the holoviews' regrid operation which uses datashader to dynamically resize the image as you are zooming. To use that simply apply the operation to the image like so:
from holoviews.operation.datashader import regrid
regridded = regrid(goes)

Is it possible to patch an image in matplotlib?

I'm developing and automata in Python with matplotlib, and I would like to design it with a robot-look I picked on the web. I chose a file and I would like to place it in place of the black squares in the image below...
I have been looking for a way to do it on the web but I haven't found any answer.
FYI, I use the fig = plt.Figure() method and then the fig.add_subplot to create my subplot and I finally generate the black square by creating black patches.
I don't believe patches are meant for this purpose. However, since you undoubtedly know the location and bounding area of the black boxes, OffsetImage and AnnotationBbox is a viable alternative.
import math
import numpy as np
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
x = np.linspace(0,10, 10)
y = [math.sin(i) for i in x]
fig, ax = plt.subplots()
im = plt.imread('pacman.png')
oi = OffsetImage(im, zoom = 0.15)
a = []
for px, py in zip(x,y):
box = AnnotationBbox(oi, (px, py), frameon=False)
a.append(ax.add_artist(box))
ax.plot(x,y,'r--')
Hope this helps.

map a hexagonal grid in matplotlib

I'm wanting to draw a figure with a hexagonal grid. The end result should look like a honeycomb. However, I'm having trouble getting my hexagons sized correctly using matplotlib.collections.RegularPolyCollection. Can anyone see what I am doing wrong, or offer another solution. I imagine this has been done before, so no need for me to reinvent the wheel.
import matplotlib.pyplot as plt
from matplotlib import collections, transforms
from matplotlib.colors import colorConverter
import numpy as np
# Make some offsets, doing 4 polygons for simplicity here
xyo = [(0,0), (1,0), (0,1), (1,1)]
# length of hexagon side
hexside = 1
# area of circle circumscribing the hexagon
circ_area = np.pi * hexside ** 2
fig, ax = plt.subplots(1,1)
col = collections.RegularPolyCollection(6, np.radians(90), sizes = (circ_area,),
offsets=xyo,transOffset=ax.transData)
ax.add_collection(col, autolim=True)
colors = [colorConverter.to_rgba(c) for c in ('r','g','b','c')]
col.set_color(colors)
ax.autoscale_view()
plt.show()
Whoever struggles with the same issue in 2020+, check out my hexalattice module:
It allows to create hexagonal grids (hexagonal lattices) in 2D with fine control over spatial distribution of the hexagons, circular clop of the lattice and rotations around the central slot.
Usage and graphical output:
from hexalattice.hexalattice import *
hex_centers, _ = create_hex_grid(nx=10,
ny=10,
do_plot=True)
plt.show() # import matplotlib.pyplot as plt
Installation:
'>> pip install hexalattice'
Advanced features
The module allows stacking of few grids, arbitrary grid rotation around its center, advanced control over gaps between the hexagons etc.
Example:
hex_grid1, h_ax = create_hex_grid(nx=50,
ny=50,
rotate_deg=0,
min_diam=1,
crop_circ=20,
do_plot=True)
create_hex_grid(nx=50,
ny=50,
min_diam=1,
rotate_deg=5,
crop_circ=20,
do_plot=True,
h_ax=h_ax)

Creating intersecting images in matplotlib with imshow or other function

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.

Make matplotlib colormap from numpy array

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.

Categories

Resources