{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Plotting trisurfs with Plotly ##" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A triangulation of a compact surface is a finite collection of triangles that cover the surface in such a way that every point on the surface is in a triangle, and the intersection of any two triangles is either void, a common edge or a common vertex. A triangulated surface is called tri-surface.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The triangulation of a surface defined as the graph of a continuous function, $z=f(x,y), (x,y)\\in D\\subset\\mathbb{R}^2$ or in a parametric form:\n", "$$x=x(u,v), y=y(u,v), z=z(u,v), (u,v)\\in U\\subset\\mathbb{R}^2,$$\n", "is the image through $f$, respectively through the parameterization, of the Delaunay triangulation or an user defined triangulation of the planar domain $D$, respectively $U$.\n", "\n", "The Delaunay triangulation of a planar region is defined and illustrated in this [Jupyter Notebook](http://nbviewer.jupyter.org/github/empet/Plotly-plots/blob/master/Plotly-Mesh3d.ipynb).\n", "\n", "If the planar region $D$ ($U$) is rectangular, then one defines a meshgrid on it, and the points\n", "of the grid are the input points for the `scipy.spatial.Delaunay` function that defines the triangulation of $D$, respectively $U$.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Triangulation of the Moebius band ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Moebius band is parameterized by:\n", "\n", "$$\\begin{align*}\n", "x(u,v)&=(1+0.5 v\\cos(u/2))\\cos(u)\\\\\n", "y(u,v)&=(1+0.5 v\\cos(u/2))\\sin(u)\\quad\\quad u\\in[0,2\\pi],\\: v\\in[-1,1]\\\\\n", "z(u,v)&=0.5 v\\sin(u/2) \n", "\\end{align*}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define a meshgrid on the rectangle $U=[0,2\\pi]\\times[-1,1]$:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import numpy as np\n", "from scipy.spatial import Delaunay" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import plotly.plotly as py\n", "py.sign_in('empet','api_key')" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "u=np.linspace(0,2*np.pi, 24)\n", "v=np.linspace(-1,1, 8)\n", "u,v=np.meshgrid(u,v)\n", "u=u.flatten()\n", "v=v.flatten()\n", "\n", "#evaluate the parameterization at the flattened u and v\n", "tp=1+0.5*v*np.cos(u/2.)\n", "x=tp*np.cos(u)\n", "y=tp*np.sin(u)\n", "z=0.5*v*np.sin(u/2.)\n", "\n", "#define 2D points, as input data for the Delaunay triangulation of U\n", "points2D=np.vstack([u,v]).T\n", "tri = Delaunay(points2D)#triangulate the rectangle U\n", "points3D=np.vstack((x,y,z)).T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`tri.simplices` is a `np.array` of integers, of shape (`ntri`,3), where `ntri` is the number of triangles generated by `scipy.spatial.Delaunay`.\n", "Each row in this array contains three indices, i, j, k, such that points2D[i,:], points2D[j,:], points2D[k,:] are vertices of a triangle in the Delaunay triangulation of the rectangle $U$.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The images of the `points2D` through the surface parameterization are 3D points. The same simplices define the triangles on the surface." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setting a combination of keys in `Mesh3d` leads to generating and plotting a tri-surface, in the same way as `plot_trisurf` in matplotlib or `trisurf` in Matlab does.\n", "\n", "We note that `Mesh3d` with different combination of keys can generate [alpha-shapes](http://nbviewer.jupyter.org/github/empet/Plotly-plots/blob/master/Plotly-Mesh3d.ipynb)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To plot the triangles on a surface, we set in Plotly `Mesh3d` the lists of x, y, respectively z- coordinates of the vertices, and the lists of indices, i, j, k, for x, y, z coordinates of all vertices:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we define a function that returns data for a Plotly plot of a tri-surface:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from plotly.graph_objs import *" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def standard_intensity(x,y,z):\n", " return z" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def plotly_triangular_mesh(x,y,z, faces, intensities=standard_intensity, colorscale=\"Viridis\", \n", " showscale=False, reversescale=False, plot_edges=False):\n", " #x,y,z lists or np.arrays of vertex coordinates\n", " #faces = a numpy array of shape (n_faces, 3)\n", " #intensities can be either a function of (x,y,z) or a list of values\n", " \n", " vertices=np.vstack((x,y,z)).T\n", " I,J,K=faces.T\n", " if hasattr(intensities, '__call__'):\n", " intensity=intensities(x,y,z)#the intensities are computed here via the set function, \n", " #that returns the list of vertices intensities\n", " \n", " elif isinstance(intensities, (list, np.ndarray)):\n", " intensity=intensities#intensities are given in a list\n", " else:\n", " raise ValueError(\"intensities can be either a function or a list, np.array\")\n", " \n", " mesh=dict(type='mesh3d',\n", " x=x,\n", " y=y,\n", " z=z,\n", " colorscale=colorscale, \n", " reversescale=reversescale,\n", " intensity= intensity,\n", " i=I,\n", " j=J,\n", " k=K,\n", " name='',\n", " showscale=showscale\n", " )\n", " \n", " \n", " if showscale is True:\n", " mesh.update(colorbar=dict(thickness=20, ticklen=4, len=0.75))\n", " \n", " if plot_edges is False: # the triangle sides are not plotted \n", " return [mesh]\n", " else:#plot edges\n", " #define the lists Xe, Ye, Ze, of x, y, resp z coordinates of edge end points for each triangle\n", " #None separates data corresponding to two consecutive triangles\n", " tri_vertices= vertices[faces]\n", " Xe=[]\n", " Ye=[]\n", " Ze=[]\n", " for T in tri_vertices:\n", " Xe+=[T[k%3][0] for k in range(4)]+[ None]\n", " Ye+=[T[k%3][1] for k in range(4)]+[ None]\n", " Ze+=[T[k%3][2] for k in range(4)]+[ None]\n", " \n", " #define the lines to be plotted\n", " lines=dict(type='scatter3d',\n", " x=Xe,\n", " y=Ye,\n", " z=Ze,\n", " mode='lines',\n", " name='',\n", " line=dict(color= 'rgb(50,50,50)', width=1.5)\n", " \n", " )\n", " \n", " \n", " return [mesh, lines]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Call this function for data associated to Moebius band:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "pl_RdBu=[[0.0, 'rgb(103, 0, 31)'],\n", " [0.1, 'rgb(176, 23, 42)'],\n", " [0.2, 'rgb(214, 96, 77)'],\n", " [0.3, 'rgb(243, 163, 128)'],\n", " [0.4, 'rgb(253, 219, 199)'],\n", " [0.5, 'rgb(246, 246, 246)'],\n", " [0.6, 'rgb(209, 229, 240)'],\n", " [0.7, 'rgb(144, 196, 221)'],\n", " [0.8, 'rgb(67, 147, 195)'],\n", " [0.9, 'rgb(32, 100, 170)'],\n", " [1.0, 'rgb(5, 48, 97)']]\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "data1=plotly_triangular_mesh(x,y,z, tri.simplices, intensities=standard_intensity, colorscale=pl_RdBu, \n", " showscale=True, plot_edges=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Set the layout of the plot:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "axis = dict(\n", "showbackground=True, \n", "backgroundcolor=\"rgb(230, 230,230)\",\n", "gridcolor=\"rgb(255, 255, 255)\", \n", "zerolinecolor=\"rgb(255, 255, 255)\", \n", " )\n", "\n", "layout = Layout(\n", " title='Moebius band triangulation',\n", " width=800,\n", " height=800,\n", " showlegend=False,\n", " scene=Scene(xaxis=XAxis(axis),\n", " yaxis=YAxis(axis), \n", " zaxis=ZAxis(axis), \n", " aspectratio=dict(x=1,\n", " y=1,\n", " z=0.5\n", " ),\n", " )\n", " )\n", "\n", "fig1 = Figure(data=data1, layout=layout)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "py.iplot(fig1, filename='Mobius-band-trisurf')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Triangulation of the surface $z=\\sin(-xy)$, defined over a disk ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We consider polar coordinates on the disk, $D(0, 1)$, centered at origin and of radius 1, and define\n", "a meshgrid on the set of points $(r, \\theta)$, with $r\\in[0,1]$ and $\\theta\\in[0,2\\pi]$:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "n=12# number of radii\n", "h=1.0/(n-1)\n", "r = np.linspace(h, 1.0, n)\n", "theta= np.linspace(0, 2*np.pi, 36)\n", "\n", "r,theta=np.meshgrid(r,theta)\n", "r=r.flatten()\n", "theta=theta.flatten()\n", "\n", "#Convert polar coordinates to cartesian coordinates (x,y)\n", "x=r*np.cos(theta)\n", "y=r*np.sin(theta)\n", "x=np.append(x, 0)# a trick to include the center of the disk in the set of points. It was avoided\n", " # initially when we defined r=np.linspace(h, 1.0, n)\n", "y=np.append(y,0)\n", "z = np.sin(-x*y) \n", "\n", "points2D=np.vstack([x,y]).T\n", "tri=Delaunay(points2D)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Plot the surface with a modified layout:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "pl_cubehelix=[[0.0, 'rgb(0, 0, 0)'],\n", " [0.1, 'rgb(25, 20, 47)'],\n", " [0.2, 'rgb(21, 60, 77)'],\n", " [0.3, 'rgb(30, 101, 66)'],\n", " [0.4, 'rgb(83, 121, 46)'],\n", " [0.5, 'rgb(161, 121, 74)'],\n", " [0.6, 'rgb(207, 126, 146)'],\n", " [0.7, 'rgb(207, 157, 218)'],\n", " [0.8, 'rgb(193, 202, 243)'],\n", " [0.9, 'rgb(210, 238, 238)'],\n", " [1.0, 'rgb(255, 255, 255)']]" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "data2=plotly_triangular_mesh(x,y,z, tri.simplices, intensities=standard_intensity, colorscale=pl_cubehelix, \n", " showscale=True, reversescale=False, plot_edges=False)\n", "\n", "fig2 = Figure(data=data2, layout=layout)\n", "fig2['layout'].update(dict(title='Triangulated surface',\n", " scene=dict(camera=dict(eye=dict(x=1.75, \n", " y=-0.7, \n", " z= 0.75)\n", " )\n", " )))\n" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "py.iplot(fig2, filename='cubehexn')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Plotting tri-surfaces from data stored in ply-files ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A PLY (Polygon File Format or Stanford Triangle Format) format is a format for storing graphical objects\n", "that are represented by a triangulation of an object, resulted usually from scanning that object. A Ply file contains the coordinates of vertices, the codes for faces (triangles) and other elements, as well as the color for faces or the normal direction to faces.\n", "\n", "In the following we show how we can read a ply file via the Python package, `plyfile`. This package can be installed with `pip`." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "from plyfile import PlyData, PlyElement" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define a function that extract from plydata the vertices and the faces of a triangulated 3D object:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def extract_data(plydata):\n", " vertices=list(plydata['vertex'])\n", " vertices=np.asarray(map(list, vertices))\n", " nr_faces=plydata.elements[1].count\n", " faces=np.array([plydata['face'][k][0] for k in range(nr_faces)])\n", " return vertices, faces\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We choose a ply file from a list provided [here](http://people.sc.fsu.edu/~jburkardt/data/ply/ply.html)." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import urllib2\n", "req = urllib2.Request('http://people.sc.fsu.edu/~jburkardt/data/ply/chopper.ply') \n", "opener = urllib2.build_opener()\n", "f = opener.open(req)\n", "plydata = PlyData.read(f)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": true }, "outputs": [], "source": [ "vertices, faces=extract_data(plydata)\n", "x, y, z=vertices.T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Get data for a Plotly plot of the graphical object read from the ply file:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "data3=plotly_triangular_mesh(x,y,z, faces, intensities=standard_intensity, colorscale=pl_RdBu, \n", " showscale=True, reversescale=False, plot_edges=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Update the layout for this new plot:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": true }, "outputs": [], "source": [ "title=\"Trisurf from a PLY file
\"+\\\n", " \"Data Source: [1]\"" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "noaxis=dict(showbackground=False,\n", " showline=False, \n", " zeroline=False,\n", " showgrid=False,\n", " showticklabels=False,\n", " title='' \n", " )\n", "\n", "fig3 = Figure(data=data3, layout=layout)\n", "fig3['layout'].update(dict(title=title,\n", " width=1000,\n", " height=1000,\n", " scene=dict(xaxis=noaxis,\n", " yaxis=noaxis, \n", " zaxis=noaxis, \n", " aspectratio=dict(x=1, y=1, z=0.4),\n", " camera=dict(eye=dict(x=1.25, y=1.25, z= 1.25) \n", " )\n", " )\n", " ))\n", " " ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "py.iplot(fig3, filename='Chopper-Ply-lines')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Plotly plot of an isosurface ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An isosurface, F(x,y,z) = c, is discretized by a triangular mesh, extracted by the [Marching cubes algorithm](http://scikit-image.org/docs/dev/auto_examples/edges/plot_marching_cubes.html) from a volume given as a (M, N, P) array of doubles. \n", "\n", "The scikit image function, `measure.marching_cubes_lewiner(F, c)` \n", " returns the vertices and simplices of the triangulated surface.\n" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "from skimage import measure\n", "X,Y,Z = np.mgrid[-2:2:40j, -2:2:40j, -2:2:40j]\n", "F = X**4 + Y**4 + Z**4 - (X**2+Y**2+Z**2)**2 + 3*(X**2+Y**2+Z**2) - 3 \n", "vertices, simplices = measure.marching_cubes_lewiner(F, 0, \n", " spacing=(X[1,0, 0]-X[0,0,0], Y[0,1, 0]-Y[0,0,0], Z[0,0, 1]-Z[0,0,0]))[:2]\n", "x,y,z = zip(*vertices) " ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": true }, "outputs": [], "source": [ "pl_amp=[[0.0, 'rgb(241, 236, 236)'],\n", " [0.1, 'rgb(229, 207, 200)'],\n", " [0.2, 'rgb(219, 177, 163)'],\n", " [0.3, 'rgb(211, 148, 127)'],\n", " [0.4, 'rgb(201, 119, 91)'],\n", " [0.5, 'rgb(191, 88, 58)'],\n", " [0.6, 'rgb(179, 55, 38)'],\n", " [0.7, 'rgb(157, 24, 38)'],\n", " [0.8, 'rgb(126, 13, 41)'],\n", " [0.9, 'rgb(92, 14, 32)'],\n", " [1.0, 'rgb(60, 9, 17)']]" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data4=plotly_triangular_mesh(x,y,z,simplices, intensities=standard_intensity, colorscale=pl_amp, \n", " showscale=True, reversescale=True, plot_edges=False)\n", "fig4 = Figure(data=data4, layout=layout)\n", "fig4['layout'].update(dict(title='Isosurface',\n", " width=600,\n", " height=600,\n", " scene=dict(camera=dict(eye=dict(x=1, \n", " y=1, \n", " z=1)\n", " ),\n", " aspectratio=dict(x=1, y=1, z=1)\n", " )))\n", "py.iplot(fig4,filename='Isosurface')" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.core.display import HTML\n", "def css_styling():\n", " styles = open(\"./custom.css\", \"r\").read()\n", " return HTML(styles)\n", "css_styling()" ] } ], "metadata": { "kernelspec": { "display_name": "Python [default]", "language": "python", "name": "python2" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 2 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "version": "2.7.13" } }, "nbformat": 4, "nbformat_minor": 1 }