Tormod Landet

Articles tagged with visualization

  1. 3D visualization in IPython notebook

    The IPython notebook is a great tool for interactive computing. Below I show a short Python example on how to work interactively with simple 3D models in the IPython notebook.

    To begin with, we need a simple way to represent a 3D model in Python. We will here restrict ourselves to geometries made up of plane 2D polygons in 3D:

    class PolygonCollection(object):
        def __init__(self, nodes, polygons):
            """
            A collection of plane polygons in 3D
    
            Nodes is a list of 3-tuples of coordinates
            Polygons is a nested list of node indices for each polygon, one
            list of indices for each polygon
            """
            self.nodes = nodes
            self.polygons = polygons
    
        def extents(self):
            """
            Calculate the size of the 3D polygon model
            """
            xmin, ymin, zmin = 1e100, 1e100, 1e100
            xmax, ymax, zmax = -1e100, -1e100, -1e100
            for x, y, z in self.nodes:
                xmin = min(x, xmin)
                ymin = min(y, ymin)
                zmin = min(z, zmin)
                xmax = max(x, xmax)
                ymax = max(y, ymax)
                zmax = max(z, zmax)
            return xmin, xmax, ymin, ymax, zmin, zmax
    

    We then populate our 3D model with some polygons. The polygon <-> node/vertex connectivity is simply represented as indices into the list of nodes/vertices:

    nodes = [(0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0),
             (1.0, 0.0, 0.0), (1.0, 0.0, 1.0), (0.0, 0.0, 1.0),
             (0.0, 1.0, 0.0), (0.0, 1.0, 1.0), (0.0, 0.0, 1.0)]
    elements = [(0, 1, 2, 3), (0, 4, 5, 6), (0, 7, 8, 9)]
    polygons = PolygonCollection(nodes, elements)
    

    IPython notebook allows us to embed images, Latex equations, HTML, Javascript and more. In this case we will use Javascript to render our 3D model. This can be done as follows:

    from IPython.display import display, Javascript
    js = polygons2js(polygons)
    display(Javascript(js))
    

    The end result can be seen below. In a reasonable modern browser you should be able to rotate the 3D model. If it does not work it may also be that the Javascript libraries imported to do the rendering have changed since the time when this was written. I do not believe the libraries have stable APIs just yet. As of May 2014 it should work in the latest versions of Firefox, Chrome and Internet Explorer.

    Update March 2017: fixed the javascript (and the below Python code) so that it works with the latest (Nov 2016) version of JSModeler. The python code below should produce the same Javascript code that is running on this page, but it has not been tested, so there might be a typo somewhere.

    The "only" thing needed to make this work is a function polygons2js(polygons) that converts the 3D model to a set of javascript commands that will run inside the notebook. This can be done quite easily with the help of JSModeler and three.js. A simple implementation will look something like this:

    import random
    
    def js_wrap(js, js_libraries):
        """
        Wrap javascript commands in code that preloads needed libraries
        """
        lines = ['function getMultipleScripts(scripts, callback) {',
                 '  if(scripts.length == 0)',
                 '    callback();',
                 '  jQuery.getScript(scripts[0], function () {',
                 '    getMultipleScripts(scripts.slice(1), callback);',
                 '  });',
                 '}',
                 'var scripts = [']
        lines += ['"%s",' % script for script in js_libraries]
        lines += ['];',
                  'getMultipleScripts(scripts, function() {',
                  js,
                  '});']
        return '\n'.join(lines)
    
    def polygons2js(polygons):
        # Find the size of the 3D model. We use this later to make the default camera and rotation point work
        xmin, xmax, ymin, ymax, zmin, zmax = polygons.extents()
        length = max([xmax-xmin, ymax-ymin, zmax-zmin])
        xmean = (xmin + xmax)/2
        ymean = (ymin + ymax)/2
        zmean = (zmin + zmax)/2
    
        # Generate the Javascript code
        # see http://kovacsv.github.io/JSModeler/documentation/tutorial/tutorial.html for details
        canvas_id = 'js3dcanvas_%d' % random.randint(0, 1e10)
        canvas_style = 'style="border: 1px solid black;" width="800" height="600"'
        js = ['var widget = jQuery(\'<canvas id="%s" %s></canvas>\');' % (canvas_id, canvas_style),
              'element.append(widget);',
              'container.show();',
              'var viewerSettings = {',
              '  cameraEyePosition : [-2.0, -1.5, 1.0],',
              '  cameraCenterPosition : [0.0, 0.0, 0.0],',
              '  cameraUpVector : [0.0, 0.0, 1.0]',
              '};',
              'var viewer = new JSM.ThreeViewer();',
              'viewer.Start(widget, viewerSettings);',
              'var body = new JSM.Body();']
    
        # Add node coordinates
        coords = []
        for coord in polygons.nodes:
            coords.append('[%f, %f, %f]' % ((coord[0]-xmean)/length,
                                            (coord[1]-ymean)/length,
                                            (coord[2]-ymean)/length))
            #print coord, coords[-1]
        js.append('var coords = [%s];' % ', '.join(coords))
        js.append('for (var i = 0, len = coords.length; i < len; i++) {')
        js.append('  body.AddVertex(new JSM.BodyVertex('
                  'new JSM.Coord(coords[i][0], coords[i][1], coords[i][2])));')
        js.append('}')
    
        # Add elements
        polys = [repr(list(poly)) for poly in polygons.polygons]
        js.append('var elems = [%s];' % ', '.join(polys))
        js.append('for (var i = 0, len = elems.length; i < len; i++) {')
        js.append('  body.AddPolygon(new JSM.BodyPolygon(elems[i]));')
        js.append('}')
        js.append('var meshes = JSM.ConvertBodyToThreeMeshes(body);')
        js.append('for (var i = 0; i < meshes.length; i++) { viewer.AddMesh (meshes[i]); }')
        js.append('viewer.Draw();')
    
        # Wrap final js code in a library loader to make sure JSModeler and three.js are available
        js = '\n'.join(js)
        libraries =  ['https://rawgit.com/kovacsv/JSModeler/master/build/lib/three.min.js',
                      'https://rawgit.com/kovacsv/JSModeler/master/build/jsmodeler.js',
                      'https://rawgit.com/kovacsv/JSModeler/master/build/jsmodeler.ext.three.js']
        return js_wrap(js, libraries)
    

    I have found this to be quite handy for inspecting small 3D models and modifying the interactively. The PolygonCollection class can also be updated to automatically display the 3D model in the notebook if it objcts of the class are left on the last line of an IPython input cell, or if you call display() on them. For this to work you will have to extend the PolygonCollection class with a _repr_javascript_ metod that returns polygons2js(self) (as a string). IPython will automatically look for this method and call display(Javascript(polygons._repr_javascript_())) for you.