Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to light and shade a Poly3DCollection

I am trying to get a 3D contour plot to shade, or have shadows so that it 'looks' 3D. I am using matplotlib, mainly due to the high quality of plots and would prefer to continue to use it.

Ultimately I would like a single or flat coloured surface with shadows cast on it in a matplotlib style plot.

I am using scipy to do some interpolation and skimage and marching cube algorithm to generate the contours. Then finally use that to create and shade the poly collection.

import numpy as np
from skimage import measure
from scipy.interpolate import griddata
import matplotlib as mpl
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.colors import LightSource

# Generate an grid to inerpolate to
X, Y, Z = np.meshgrid(0.0:1.0:50j, 0.0:1.0:50j, 0.0:1.0:50j)

# Interpolate (coor and phi are the numerical grid and scalar values)
F = griddata(coor, phi, (X, Y, Z), method='nearest')

# Make the contour, marching cubes
marchCubeSpace = 1.0 / 50.0
verts, faces, normals, values = measure.marching_cubes_lewiner(F, 0.5, spacing=(marchCubeSpace, marchCubeSpace, marchCubeSpace))

# Create Ploy3D
mesh = Poly3DCollection(verts[faces], alpha=1.0)

# An attempt to get some sort of height data.
facearray = np.array([np.array((np.sum(verts[face[:], 0]/3), np.sum(verts[face[:], 1]/3), np.sum(verts[face[:], 2]/3))) for face in faces])

# light source, ultimately I want to use not `reds` but just a red for all faces.
ls = LightSource(azdeg=45.0, altdeg=90.0)
rgb = ls.blend_hsv(rgb=ls.shade(facearray, plt.cm.Reds), intensity=ls.shade_normals(normals, fraction=0.25))
mesh.set_facecolor(rgb[:, 0])

# Plot
fig = plt.figure()
ax = fig.add_subplot(0, 0, 0, projection='3d')
ax.add_collection3d(mesh)

I am looking to generate something like this: enter image description here

like image 486
Bevan Jones Avatar asked Oct 27 '25 17:10

Bevan Jones


2 Answers

Ok, so I have an acceptable solution. Message me if you need more help, I would be happy to walk anyone through this. Note the code below requires coor and phi from your data set, so no this code will not run if you do not provide a 3D scalar field to it.

import numpy as np
from skimage import measure
from scipy.interpolate import griddata
import matplotlib as mpl
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.colors import LightSource

# Generate an grid to inerpolate to
X, Y, Z = np.meshgrid(0.0:1.0:50j, 0.0:1.0:50j, 0.0:1.0:50j)

# Interpolate (coor and phi are the numerical grid and scalar values)
F = griddata(coor, phi, (X, Y, Z), method='nearest')

# Make the contour, marching cubes
marchCubeSpace = 1.0 / 50.0
verts, faces, normals, values = measure.marching_cubes_lewiner(F, 0.5, spacing=(marchCubeSpace, marchCubeSpace, marchCubeSpace))

# Create Ploy3D and set up a light source
mesh = Poly3DCollection(verts[faces], alpha=1.0)
ls = LightSource(azdeg=225.0, altdeg=45.0)

# First change - normals are per vertex, so I made it per face.
normalsarray = np.array([np.array((np.sum(normals[face[:], 0]/3), np.sum(normals[face[:], 1]/3), np.sum(normals[face[:], 2]/3))/np.sqrt(np.sum(normals[face[:], 0]/3)**2 + np.sum(normals[face[:], 1]/3)**2 + np.sum(normals[face[:], 2]/3)**2)) for face in faces])

# Next this is more asthetic, but it prevents the shadows of the image being too dark. (linear interpolation to correct)
min = np.min(ls.shade_normals(normalsarray, fraction=1.0)) # min shade value
max = np.max(ls.shade_normals(normalsarray, fraction=1.0)) # max shade value
diff = max-min
newMin = 0.3
newMax = 0.95
newdiff = newMax-newMin

# Using a constant color, put in desired RGB values here.
colourRGB = np.array((255.0/255.0, 54.0/255.0, 57/255.0, 1.0))

# The correct shading for shadows are now applied. Use the face normals and light orientation to generate a shading value and apply to the RGB colors for each face.
rgbNew = np.array([colourRGB*(newMin + newdiff*((shade-min)/diff)) for shade in ls.shade_normals(normalsarray, fraction=1.0)])

# Apply color to face
mesh.set_facecolor(rgbNew)

# Plot
fig = plt.figure()
ax = fig.add_subplot(0, 0, 0, projection='3d')
ax.add_collection3d(mesh)

So this is what I was looking for. (Note this is not the exact same case as the above picture) enter image description here

like image 78
Bevan Jones Avatar answered Oct 29 '25 05:10

Bevan Jones


New in v 3.7: There is no need to compute the shade normals. This is built into the Poly3DCollection see parameter lightsource.

like image 44
John Henckel Avatar answered Oct 29 '25 05:10

John Henckel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!