Source code for cortex.webgl.view

import binascii
import copy
import functools
import glob
import json
import mimetypes
import os
import random
import shutil
import threading
import time
import warnings
import webbrowser
from configparser import NoOptionError

# Now assumes python 3
from queue import Queue

import numpy as np
from tornado import web

from .. import dataset, options, utils, volume
from ..database import db
from . import serve
from .data import Package
from .FallbackLoader import FallbackLoader

try:
    cmapdir = options.config.get('webgl', 'colormaps')
    if not os.path.exists(cmapdir):
        raise Exception("Colormap directory (%s) does not exist"%cmapdir)
except NoOptionError:
    cmapdir = os.path.join(options.config.get("basic", "filestore"), "colormaps")
    if not os.path.exists(cmapdir):
        raise Exception("Colormap directory was not defined in the config file and the default (%s) does not exist"%cmapdir)

domain_name = options.config.get("webgl", "domain_name")

colormaps = glob.glob(os.path.join(cmapdir, "*.png"))
colormaps = [(os.path.splitext(os.path.split(cm)[1])[0], serve.make_base64(cm))
             for cm in sorted(colormaps)]

[docs] def make_static( outpath, data, recache=False, template="static.html", anonymize=False, overlays_available=None, overlays_visible=("rois", "sulci"), labels_visible=("rois",), types=("inflated",), html_embed=True, copy_ctmfiles=True, title="Brain", layout=None, overlay_file=None, curvature_brightness=None, curvature_contrast=None, curvature_smoothness=None, surface_specularity=None, **kwargs, ): """ Creates a static webGL MRI viewer in your filesystem so that it can easily be posted publicly for sharing or just saved for later viewing. Parameters ---------- outpath : string The directory where the static viewer will be saved. Will be created if it doesn't already exist. data : Dataset object or implicit Dataset Dataset object containing all the data you wish to plot. Can be any type of implicit dataset, such as a single Volume, Vertex, etc. object or a dictionary of Volume, Vertex. etc. objects. recache : bool, optional Force recreation of CTM and SVG files for surfaces. Default False template : string, optional Name of template HTML file. Default 'static.html' anonymize : bool, optional Whether to rename CTM and SVG files generically, for public distribution. Default False overlays_available : tuple, optional Overlays available in the viewer. If None, then all overlay layers of the svg file will be potentially available in the viewer (whether initially visible or not). This provides the option to include, e.g., only a subset of layers for a given static viewer. overlays_visible : tuple, optional The listed overlay layers will be set visible by default. Layers not listed here will be hidden by default (but can be enabled in the viewer GUI). Default ('rois', 'sulci') labels_visible : tuple, optional Labels for the listed layers will be set visible by default. Labels for layers not listed here will be hidden by default (but can be enabled in the viewer GUI). Default ('rois', ) Other parameters ---------------- types : tuple, optional Types of surfaces to include in addition to the original (fiducial, pial, and white matter) and flat surfaces. Default ('inflated', ) html_embed : bool, optional Whether to embed the webgl resources in the html output. Default 'True'. If 'False', the webgl resources must be served by your web server. copy_ctmfiles : bool, optional Whether to copy the CTM files to the static directory. Default 'True'. In some use cases, the same CTM data will be used in many static views. To avoid duplication of files, set to 'False'. (The datastore cache must then be served with your web server). title : str, optional The title that is displayed on the viewer website when it is loaded in a browser. layout : None or list of (int, int) The layout of the viewer subwindows for showing multiple subjects, passed to the template generator. Default to None, corresponding to no subwindows. overlay_file : str or None, optional Custom overlays.svg file to use instead of the default one for this subject (if not None). Default None. curvature_brightness : float or None, optional Brightness of curvature overlay. Default None, which uses the value specified in the config file. curvature_contrast : float or None, optional Contrast of curvature overlay. Default None, which uses the value specified in the config file. curvature_smoothness : float or None, optional Smoothness of curvature overlay. Default None, which uses the value specified in the config file. surface_specularity : float or None, optional Specularity of surfaces visualized with the WebGL viewer. Default None, which uses the value specified in the config file under `webgl_viewopts.specularity`. **kwargs All additional keyword arguments are passed to the template renderer. Notes ----- You will need a real web server to view this, since `file://` paths don't handle xsrf correctly """ outpath = os.path.abspath(os.path.expanduser(outpath)) # To handle ~ expansion os.makedirs(os.path.join(outpath, "data"), exist_ok=True) data = dataset.normalize(data) if not isinstance(data, dataset.Dataset): data = dataset.Dataset(data=data) db.auxfile = data package = Package(data) subjects = list(package.subjects) ctmargs = dict( method="mg2", level=9, recache=recache, external_svg=overlay_file, overlays_available=overlays_available, ) ctms = dict((subj, utils.get_ctmpack(subj, types, **ctmargs)) for subj in subjects) package.reorder(ctms) db.auxfile = None ## Rename files to anonymize submap = dict() for i, (subj, ctmfile) in enumerate(ctms.items()): oldpath, fname = os.path.split(ctmfile) fname, ext = os.path.splitext(fname) if anonymize: newfname = "S%d" % i submap[subj] = newfname else: newfname = fname ctms[subj] = newfname + ".json" for ext in ["json", "ctm", "svg"]: srcfile = os.path.join(oldpath, "%s.%s" % (fname, ext)) newfile = os.path.join(outpath, "%s.%s" % (newfname, ext)) if os.path.exists(newfile): os.unlink(newfile) if os.path.exists(srcfile) and copy_ctmfiles: shutil.copy2(srcfile, newfile) if ext == "json" and anonymize: ## change filenames in json nfh = open(newfile) jsoncontents = nfh.read() nfh.close() ofh = open(newfile, "w") ofh.write(jsoncontents.replace(fname, newfname)) ofh.close() if anonymize: old_subjects = sorted(list(ctms.keys())) ctms = dict(("S%d" % i, ctms[k]) for i, k in enumerate(old_subjects)) if len(submap) == 0: submap = None # Process the data metadata = package.metadata(fmt="data/{name}_{frame}.png", submap=submap) images = package.images # Write out the PNGs for name, imgs in images.items(): impath = os.path.join(outpath, "data", "{name}_{frame}.png") for i, img in enumerate(imgs): with open(impath.format(name=name, frame=i), "wb") as binfile: binfile.write(img) # Copy any stimulus files stimpath = os.path.join(outpath, "stim") for name, view in data: if "stim" in view.attrs and os.path.exists(view.attrs["stim"]): if not os.path.exists(stimpath): os.makedirs(stimpath) shutil.copy2(view.attrs["stim"], stimpath) # Parse the html file and paste all the js and css files directly into the html from . import htmlembed if os.path.exists(template): ## Load locally templatedir, templatefile = os.path.split(os.path.abspath(template)) rootdirs = [templatedir, serve.cwd] else: ## Load system templates templatefile = template rootdirs = [serve.cwd] loader = FallbackLoader(rootdirs) tpl = loader.load(templatefile) # Put together all view options my_viewopts = dict(options.config.items("webgl_viewopts")) my_viewopts["overlays_visible"] = overlays_visible my_viewopts["labels_visible"] = labels_visible my_viewopts["brightness"] = ( options.config.get("curvature", "brightness") if curvature_brightness is None else curvature_brightness ) my_viewopts["contrast"] = ( options.config.get("curvature", "contrast") if curvature_contrast is None else curvature_contrast ) my_viewopts["smoothness"] = ( options.config.get("curvature", "webgl_smooth") if curvature_smoothness is None else curvature_smoothness ) my_viewopts["specularity"] = ( options.config.get("webgl_viewopts", "specularity") if surface_specularity is None else surface_specularity ) for sec in options.config.sections(): if "paths" in sec or "labels" in sec: my_viewopts[sec] = dict(options.config.items(sec)) html = tpl.generate( data=json.dumps(metadata), colormaps=colormaps, default_cmap="RdBu_r", python_interface=False, leapmotion=True, layout=layout, subjects=json.dumps(ctms), viewopts=json.dumps(my_viewopts), title=title, **kwargs, ) desthtml = os.path.join(outpath, "index.html") if html_embed: htmlembed.embed(html, desthtml, rootdirs) else: with open(desthtml, "w") as htmlfile: htmlfile.write(html)
[docs] def show( data, autoclose=None, open_browser=None, port=None, pickerfun=None, recache=False, template="mixer.html", overlays_available=None, overlays_visible=("rois", "sulci"), labels_visible=("rois",), types=("inflated",), overlay_file=None, curvature_brightness=None, curvature_contrast=None, curvature_smoothness=None, surface_specularity=None, title="Brain", layout=None, **kwargs, ): """ Creates a webGL MRI viewer that is dynamically served by a tornado server running inside the current python process. Parameters ---------- data : Dataset object or implicit Dataset Dataset object containing all the data you wish to plot. Can be any type of implicit dataset, such as a single Volume, Vertex, etc. object or a dictionary of Volume, Vertex. etc. objects. autoclose : bool, optional If True, the tornado server will automatically be destroyed when the last web client has disconnected. If False, the server will stay open, allowing more connections. Default True open_browser : bool, optional If True, uses the webbrowser library to open the viewer in the default local browser. Default True port : int or None, optional The port that will be used by the server. If None, a random port will be selected from the range 1024-65536. Default None pickerfun : function or None, optional Should be a function that takes two arguments, a voxel index and a vertex index. Is called whenever a location on the surface is clicked in the viewer. This can be used to print information about individual voxels or vertices, plot receptive fields, or many other uses. Default None recache : bool, optional Force recreation of CTM and SVG files for surfaces. Default False template : string, optional Name of template HTML file. Default 'mixer.html' overlays_available : tuple, optional Overlays available in the viewer. If None, then all overlay layers of the svg file will be potentially available in the viewer (whether initially visible or not). overlays_visible : tuple, optional The listed overlay layers will be set visible by default. Layers not listed here will be hidden by default (but can be enabled in the viewer GUI). Default ('rois', 'sulci') labels_visible : tuple, optional Labels for the listed layers will be set visible by default. Labels for layers not listed here will be hidden by default (but can be enabled in the viewer GUI). Default ('rois', ) Other parameters ---------------- types : tuple, optional Types of surfaces to include in addition to the original (fiducial, pial, and white matter) and flat surfaces. Default ('inflated', ) overlay_file : str or None, optional Custom overlays.svg file to use instead of the default one for this subject (if not None). Default None. curvature_brightness : float or None, optional Brightness of curvature overlay. Default None, which uses the value specified in the config file. curvature_contrast : float or None, optional Contrast of curvature overlay. Default None, which uses the value specified in the config file. curvature_smoothness : float or None, optional Smoothness of curvature overlay. Default None, which uses the value specified in the config file. surface_specularity : float or None, optional Specularity of surfaces visualized with the WebGL viewer. Default None, which uses the value specified in the config file under `webgl_viewopts.specularity`. title : str, optional The title that is displayed on the viewer website when it is loaded in a browser. layout : None or list of (int, int), optional The layout of the viewer subwindows for showing multiple subjects, passed to the template generator. Default None, corresponding to no subwindows. **kwargs All additional keyword arguments are passed to the template renderer. """ # populate default webshow args if autoclose is None: autoclose = options.config.get('webshow', 'autoclose', fallback='true') == 'true' if open_browser is None: open_browser = options.config.get('webshow', 'open_browser', fallback='true') == 'true' data = dataset.normalize(data) if not isinstance(data, dataset.Dataset): data = dataset.Dataset(data=data) html = FallbackLoader([os.path.split(os.path.abspath(template))[0], serve.cwd]).load(template) db.auxfile = data #Extract the list of stimuli, for special-casing stims = dict() for name, view in data: if 'stim' in view.attrs and os.path.exists(view.attrs['stim']): sname = os.path.split(view.attrs['stim'])[1] stims[sname] = view.attrs['stim'] package = Package(data) metadata = json.dumps(package.metadata()) images = package.images subjects = list(package.subjects) ctmargs = dict(method='mg2', level=9, recache=recache, external_svg=overlay_file, overlays_available=overlays_available) ctms = dict((subj, utils.get_ctmpack(subj, types, **ctmargs)) for subj in subjects) package.reorder(ctms) subjectjs = json.dumps(dict((subj, "ctm/%s/"%subj) for subj in subjects)) db.auxfile = None linear = lambda x, y, m: (1.-m)*x + m*y mixes = dict( linear=linear, smoothstep=(lambda x, y, m: linear(x, y, 3*m**2 - 2*m**3)), smootherstep=(lambda x, y, m: linear(x, y, 6*m**5 - 15*m**4 + 10*m**3)) ) post_name = Queue() # Put together all view options my_viewopts = dict(options.config.items('webgl_viewopts')) my_viewopts['overlays_visible'] = overlays_visible my_viewopts['labels_visible'] = labels_visible my_viewopts["brightness"] = ( options.config.get("curvature", "brightness") if curvature_brightness is None else curvature_brightness ) my_viewopts["contrast"] = ( options.config.get("curvature", "contrast") if curvature_contrast is None else curvature_contrast ) my_viewopts["smoothness"] = ( options.config.get("curvature", "webgl_smooth") if curvature_smoothness is None else curvature_smoothness ) my_viewopts["specularity"] = ( options.config.get("webgl_viewopts", "specularity") if surface_specularity is None else surface_specularity ) for sec in options.config.sections(): if 'paths' in sec or 'labels' in sec: my_viewopts[sec] = dict(options.config.items(sec)) if pickerfun is None: pickerfun = lambda a, b: None class CTMHandler(web.RequestHandler): def get(self, path): subj, path = path.split('/') if path == '': self.set_header("Content-Type", "application/json") self.write(open(ctms[subj]).read()) else: fpath = os.path.split(ctms[subj])[0] mtype = mimetypes.guess_type(os.path.join(fpath, path))[0] if mtype is None: mtype = "application/octet-stream" self.set_header("Content-Type", mtype) self.write(open(os.path.join(fpath, path), 'rb').read()) class DataHandler(web.RequestHandler): def get(self, path): path = path.strip("/") try: dataname, frame = path.split('/') except ValueError: dataname = path frame = 0 if dataname in images: dataimg = images[dataname][int(frame)] if dataimg[1:6] == "NUMPY": self.set_header("Content-Type", "application/octet-stream") else: self.set_header("Content-Type", "image/png") if 'Range' in self.request.headers: self.set_status(206) rangestr = self.request.headers['Range'].split('=')[1] start, end = [ int(i) if len(i) > 0 else None for i in rangestr.split('-') ] clenheader = 'bytes %s-%s/%s' % (start, end or len(dataimg), len(dataimg) ) self.set_header('Content-Range', clenheader) self.set_header('Content-Length', end-start+1) self.write(dataimg[start:end+1]) else: self.write(dataimg) else: self.set_status(404) self.write_error(404) class StimHandler(web.StaticFileHandler): def initialize(self): pass def get(self, path): if path not in stims: self.set_status(404) self.write_error(404) else: self.root, fname = os.path.split(stims[path]) super(StimHandler, self).get(fname) class StaticHandler(web.StaticFileHandler): def initialize(self): self.root = '' class MixerHandler(web.RequestHandler): def get(self): self.set_header("Content-Type", "text/html") generated = html.generate(data=metadata, colormaps=colormaps, default_cmap="RdBu_r", python_interface=True, leapmotion=True, layout=layout, subjects=subjectjs, viewopts=json.dumps(my_viewopts), title=title, **kwargs) #overlays_visible=json.dumps(overlays_visible), #labels_visible=json.dumps(labels_visible), #**viewopts) self.write(generated) def post(self): data = self.get_argument("svg", default=None) png = self.get_argument("png", default=None) with open(post_name.get(), "wb") as svgfile: if png is not None: data = png[22:].strip() try: data = binascii.a2b_base64(data) except: print("Error writing image!") data = png svgfile.write(data) class JSMixer(serve.JSProxy): @property def view_props(self): """An enumerated list of settable properties for views. There may be a way to get this from the javascript object, but I (ML) don't know how. There may be additional properties we want to set in views and animations; those must be added here. Old property list that used to be settable before webgl refactor: view_props = ['altitude', 'azimuth', 'target', 'mix', 'radius', 'pivot', 'visL', 'visR', 'alpha', 'rotationR', 'rotationL', 'projection', 'volume_vis', 'frame', 'slices'] """ camera = getattr(self.ui, "camera") _camera_props = ['camera.%s' % k for k in camera._controls.attrs.keys()] surface = getattr(self.ui, "surface") _subject = list(surface._folders.attrs.keys())[0] _surface = getattr(surface, _subject) _surface_props = ['surface.{subject}.%s'%k for k in _surface._controls.attrs.keys()] _curvature_props = ['surface.{subject}.curvature.brightness', 'surface.{subject}.curvature.contrast', 'surface.{subject}.curvature.smoothness'] return _camera_props + _surface_props + _curvature_props def _set_view(self, **kwargs): """Low-level command: sets view parameters in the current viewer Sets each the state of each keyword argument provided. View parameters that can be set include all parameters in the data.gui in the html view. """ # Set unfolding level first, as it interacts with other arguments surface = getattr(self.ui, "surface") subject_list = surface._folders.attrs.keys() # Better to only self.view_props once; it interacts with javascript, # don't want to do that too often, it leads to glitches. vw_props = copy.copy(self.view_props) for subject in subject_list: if 'surface.{subject}.unfold' in kwargs: unfold = kwargs.pop('surface.{subject}.unfold') self.ui.set('surface.{subject}.unfold'.format(subject=subject), unfold) for k, v in kwargs.items(): if not k in vw_props: print('Unknown parameter %s!'%k) continue else: self.ui.set(k.format(subject=subject) if '{subject}' in k else k, v) # Wait for webgl. Wait for it. .... WAAAAAIIIT. time.sleep(0.03) def _capture_view(self, frame_time=None): """Low-level command: returns a dict of current view parameters Retrieves the following view parameters from current viewer: altitude, azimuth, target, mix, radius, visL, visR, alpha, rotationR, rotationL, projection, pivot Parameters ---------- frame_time : scalar time (in seconds) to specify for this frame. Notes ----- If multiple subjects are present, only retrieves view for first subject. """ view = {} subject = list(self.ui.surface._folders.attrs.keys())[0] for p in self.view_props: try: view[p] = self.ui.get(p.format(subject=subject) if '{subject}' in p else p)[0] # Wait for webgl. time.sleep(0.03) except Exception as err: # TO DO: Fix this hack with an error class in serve.py & catch it here print(err) #msg = "Cannot read property 'undefined'" #if err.message[:len(msg)] != msg: # raise err if frame_time is not None: view['time'] = frame_time return view def save_view(self, subject, name, is_overwrite=False): """Saves current view parameters to pycortex database Parameters ---------- subject : string pycortex subject id name : string name for view to store is_overwrite: bool whether to overwrite an extant view (default : False) Notes ----- Equivalent to call to cortex.db.save_view(subject, vw, name) For a list of the view parameters saved, see viewer._capture_view """ db.save_view(self, subject, name, is_overwrite) def get_view(self, subject, name): """Get saved view from pycortex database. Retrieves named view from pycortex database and sets current viewer parameters to retrieved values. Parameters ---------- subject : string pycortex subject ID name : string name of saved view to re-load Notes ----- Equivalent to call to cortex.db.get_view(subject, vw, name) For a list of the view parameters set, see viewer._capture_view """ view = db.get_view(self, subject, name) def addData(self, **kwargs): Proxy = serve.JSProxy(self.send, "window.viewers.addData") new_meta, new_ims = _convert_dataset(Dataset(**kwargs), path='/data/', fmt='%s_%d.png') metadata.update(new_meta) images.update(new_ims) return Proxy(metadata) def getImage(self, filename, size=(1920, 1080)): """Saves currently displayed view to a .png image file Parameters ---------- filename : string duh. size : tuple (x, y) size (in pixels) of image to save. """ post_name.put(filename) Proxy = serve.JSProxy(self.send, "window.viewer.getImage") return Proxy(size[0], size[1], "mixer.html") def makeMovie(self, animation, filename="brainmovie%07d.png", offset=0, fps=30, size=(1920, 1080), interpolation="linear"): """Renders movie frames for animation of mesh movement Makes an animation (for example, a transition between inflated and flattened brain or a rotating brain) of a cortical surface. Takes a list of dictionaries (`animation`) as input, and uses the values in the dictionaries as keyframes for the animation. Mesh display parameters that can be animated include 'elevation', 'azimuth', 'mix', 'radius', 'target' (more?) Parameters ---------- animation : list of dicts Each dict should have keys `idx`, `state`, and `value`. `idx` is the time (in seconds) at which you want to set `state` to `value` `state` is the parameter to animate (e.g. 'altitude', 'azimuth') `value` is the value to set for `state` filename : string path name Must contain '%d' (or some variant thereof) to account for frame number, e.g. '/some/directory/brainmovie%07d.png' offset : int Frame number for first frame rendered. Useful for concatenating animations. fps : int Frame rate of resultant movie size : tuple (x, y) Size (in pixels) of resulting movie interpolation : {"linear", "smoothstep", "smootherstep"} Interpolation method for values between keyframes. Example ------- # Called after a call of the form: js_handle = cortex.webgl.show(DataViewObject) # Start with left hemisphere view js_handle._setView(azimuth=[90], altitude=[90.5], mix=[0]) # Initialize list animation = [] # Append 5 key frames for a simple rotation for az, idx in zip([90, 180, 270, 360, 450], [0, .5, 1.0, 1.5, 2.0]): animation.append({'state':'azimuth', 'idx':idx, 'value':[az]}) # Animate! (use default settings) js_handle.makeMovie(animation) """ # build up two variables: State and Anim. # state is a dict of all values being modified at any time state = dict() # anim is a list of transitions between keyframes anim = [] setfunc = self.ui.set for f in sorted(animation, key=lambda x:x['idx']): if f['idx'] == 0: setfunc(f['state'], f['value']) state[f['state']] = dict(idx=f['idx'], val=f['value']) else: if f['state'] not in state: state[f['state']] = dict(idx=0, val=self.getState(f['state'])[0]) start = dict(idx=state[f['state']]['idx'], state=f['state'], value=state[f['state']]['val']) end = dict(idx=f['idx'], state=f['state'], value=f['value']) state[f['state']]['idx'] = f['idx'] state[f['state']]['val'] = f['value'] if start['value'] != end['value']: anim.append((start, end)) for i, sec in enumerate(np.arange(0, anim[-1][1]['idx']+1./fps, 1./fps)): for start, end in anim: if start['idx'] < sec <= end['idx']: idx = (sec - start['idx']) / float(end['idx'] - start['idx']) if start['state'] == 'frame': func = mixes['linear'] else: func = mixes[interpolation] val = func(np.array(start['value']), np.array(end['value']), idx) if isinstance(val, np.ndarray): setfunc(start['state'], val.ravel().tolist()) else: setfunc(start['state'], val) self.getImage(filename%(i+offset), size=size) def _get_anim_seq(self, keyframes, fps=30, interpolation='linear'): """Convert a list of keyframes to a list of EVERY frame in an animation. Utility function called by make_movie; separated out so that individual frames of an animation can be re-rendered, or for more control over the animation process in general. """ # Misc. setup fr = 0 a = np.array func = mixes[interpolation] #skip_props = ['surface.{subject}.right', 'surface.{subject}.left', ] #'projection', # Get keyframes keyframes = sorted(keyframes, key=lambda x:x['time']) # Normalize all time to frame rate fs = 1./fps for k in range(len(keyframes)): t = keyframes[k]['time'] t = np.round(t/fs)*fs keyframes[k]['time'] = t allframes = [] for start, end in zip(keyframes[:-1], keyframes[1:]): t0 = start['time'] t1 = end['time'] tdif = float(t1-t0) # Check whether to continue frame sequence to endpoint use_endpoint = keyframes[-1]==end nvalues = np.round(tdif/fs).astype(int) if use_endpoint: nvalues += 1 fr_time = np.linspace(0, 1, nvalues, endpoint=use_endpoint) # Interpolate between values for t in fr_time: frame = {} for prop in start.keys(): if prop=='time': continue if (start[prop] is None) or (start[prop] == end[prop]) or isinstance(start[prop], (bool, str)): frame[prop] = start[prop] continue val = func(a(start[prop]), a(end[prop]), t) if isinstance(val, np.ndarray): frame[prop] = val.tolist() else: frame[prop] = val allframes.append(frame) return allframes def make_movie_views(self, animation, filename="brainmovie%07d.png", offset=0, fps=30, size=(1920, 1080), alpha=1, frame_sleep=0.05, frame_start=0, interpolation="linear"): """Renders movie frames for animation of mesh movement Makes an animation (for example, a transition between inflated and flattened brain or a rotating brain) of a cortical surface. Takes a list of dictionaries (`animation`) as input, and uses the values in the dictionaries as keyframes for the animation. Mesh display parameters that can be animated include 'elevation', 'azimuth', 'mix', 'radius', 'target' (more?) Parameters ---------- animation : list of dicts This is a list of keyframes for the animation. Each keyframe should be a dict in the form captured by the ._capture_view method. NOTE: every view must include all view parameters. Additionally, there should be one extra key/value pair for "time". The value for time should be in seconds. The list of keyframes is sorted by time before applying, so they need not be in order in the input. filename : string path name Must contain '%d' (or some variant thereof) to account for frame number, e.g. '/some/directory/brainmovie%07d.png' offset : int Frame number for first frame rendered. Useful for concatenating animations. fps : int Frame rate of resultant movie size : tuple (x, y) Size (in pixels) of resulting movie interpolation : {"linear", "smoothstep", "smootherstep"} Interpolation method for values between keyframes. Notes ----- Make sure that all values that will be modified over the course of the animation are initialized (have some starting value) in the first frame. Example ------- # Called after a call of the form: js_handle = cortex.webgl.show(DataViewObject) # Start with left hemisphere view js_handle._setView(azimuth=[90], altitude=[90.5], mix=[0]) # Initialize list animation = [] # Append 5 key frames for a simple rotation for az, t in zip([90, 180, 270, 360, 450], [0, .5, 1.0, 1.5, 2.0]): animation.append({'time':t, 'azimuth':[az]}) # Animate! (use default settings) js_handle.make_movie(animation) """ allframes = self._get_anim_seq(animation, fps, interpolation) for fr, frame in enumerate(allframes[frame_start:], frame_start): self._set_view(**frame) time.sleep(frame_sleep) self.getImage(filename%(fr+offset+1), size=size) time.sleep(frame_sleep) class PickerHandler(web.RequestHandler): def get(self): pickerfun(int(self.get_argument("voxel")), int(self.get_argument("vertex"))) class WebApp(serve.WebApp): disconnect_on_close = autoclose def get_client(self): self.connect.wait() self.connect.clear() return JSMixer(self.send, "window.viewer") def get_local_client(self): return JSMixer(self.srvsend, "window.viewer") if port is None: port = random.randint(1024, 65536) server = WebApp([(r'/ctm/(.*)', CTMHandler), (r'/data/(.*)', DataHandler), (r'/stim/(.*)', StimHandler), (r'/mixer.html', MixerHandler), (r'/picker', PickerHandler), (r'/', MixerHandler), (r'/static/(.*)', StaticHandler)], port) server.start() print("Started server on port %d"%server.port) url = "http://%s%s:%d/mixer.html"%(serve.hostname, domain_name, server.port) if open_browser: webbrowser.open(url) client = server.get_client() client.server = server return client else: try: from IPython.display import HTML, display display(HTML('Open viewer: <a href="{0}" target="_blank">{0}</a>'.format(url))) except: pass return server