From 1716aabd4633c1c1bd2da5c6a6e0000dfb45167f Mon Sep 17 00:00:00 2001 From: Cristi Donos Date: Sat, 30 Jun 2018 21:42:02 -0500 Subject: [PATCH 01/46] - Brain.add_foci can now map values using foci color and size. - added Brain.add_text3d for labeling foci in 3d --- surfer/viz.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index ad00748..5083c6c 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1570,7 +1570,7 @@ def add_morphometry(self, measure, grayscale=False, hemi=None, def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, - hemi=None): + hemi=None, allow_data=False, data=None): """Add spherical foci, possibly mapping to displayed surf. The foci spheres can be displayed at the coordinates given, or @@ -1588,8 +1588,8 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, whether the coords parameter should be interpreted as vertex ids map_surface : Freesurfer surf or None surface to map coordinates through, or None to use raw coords - scale_factor : float - Controls the size of the foci spheres (relative to 1cm). + scale_factor : int + controls the size of the foci spheres color : matplotlib color code HTML name, RBG tuple, or hex code alpha : float in [0, 1] @@ -1600,6 +1600,14 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, If None, it is assumed to belong to the hemipshere being shown. If two hemispheres are being shown, an error will be thrown. + allow_data : bool | False + If True, will plot foci using quiver3d instead of points3d to allow + mapping data to spheres. + data : numpy array + 1D array the same size as the number of foci. Spheres will be + colorcoded acording to data values, whereas spheres' sizes will be + coded using the absolute values of data. + """ from matplotlib.colors import colorConverter hemi = self._check_hemi(hemi) @@ -1614,8 +1622,7 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, foci_coords = np.atleast_2d(coords) else: foci_surf = Surface(self.subject_id, hemi, map_surface, - subjects_dir=self.subjects_dir, - units=self._units) + subjects_dir=self.subjects_dir) foci_surf.load_geometry() foci_vtxs = utils.find_closest_vertices(foci_surf.coords, coords) foci_coords = self.geo[hemi].coords[foci_vtxs] @@ -1630,12 +1637,16 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, views = self._toggle_render(False) fl = [] - if self._units == 'm': - scale_factor = scale_factor / 1000. for brain in self._brain_list: if brain['hemi'] == hemi: - fl.append(brain['brain'].add_foci(foci_coords, scale_factor, - color, alpha, name)) + if allow_data: + vtxs = utils.find_closest_vertices(self.geo[hemi].coords, foci_coords) + fl.append(brain['brain'].add_foci_data(foci_coords, + scale_factor, color, alpha, name, data, + self.geo[hemi].nn[vtxs, :])) + else: + fl.append(brain['brain'].add_foci(foci_coords, + scale_factor, color, alpha, name)) self.foci_dict[name] = fl self._toggle_render(True, views) @@ -1737,6 +1748,45 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, if justification is not None: text.property.justification = justification + def add_text3d(self, x, y, z, text, name, color=None, opacity=1.0, + row=-1, col=-1, font_size=None, justification=None): + """ Add a text to the visualization + + Parameters + ---------- + x : Float + x coordinate + y : Float + y coordinate + z : Float + z coordinate + text : str + Text to add + name : str + Name of the text (text label can be updated using update_text()) + color : Tuple + Color of the text. Default is the foreground color set during + initialization (default is black or white depending on the + background color). + opacity : Float + Opacity of the text. Default: 1.0 + row : int + Row index of which brain to use + col : int + Column index of which brain to use + """ + if name in self.texts_dict: + self.texts_dict[name]['text'].remove() + text = self.brain_matrix[row, col].add_text3d(x, y, z, text, + name, color, opacity) + self.texts_dict[name] = dict(row=row, col=col, text=text) + if font_size is not None: + text.property.font_size = font_size + text.actor.text_scale_mode = 'viewport' + if justification is not None: + text.property.justification = justification + + def update_text(self, text, name, row=-1, col=-1): """Update text label @@ -3290,6 +3340,27 @@ def add_foci(self, foci_coords, scale_factor, color, alpha, name): scale_factor=(10. * scale_factor), color=color, opacity=alpha) return points + def add_foci_data(self, foci_coords, scale_factor, color, alpha, name,data, normals): + """Add spherical foci with attached data, possibly mapping to displayed surf""" + # Create the visualization + hemi = self.hemi + if data == None: + u = np.array(normals[:,0]) + v = np.array(normals[:,1]) + w = np.array(normals[:,2]) + else: + u = self.geo[hemi].nn[:,0] * np.abs(data) + v = self.geo[hemi].nn[:,1] * np.abs(data) + w = self.geo[hemi].nn[:,2] * np.abs(data) + with warnings.catch_warnings(record=True): # traits + points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], + u,v,w, scalars = data, colormap='coolwarm', mode='sphere', + name=name, figure=self._f, scale_factor=(10. * scale_factor), + color=color, opacity=alpha) + points.glyph.color_mode = 'color_by_scalar' + points.glyph.glyph_source.glyph_source.center = [0, 0, 0] + return points + def add_contour_overlay(self, scalar_data, min=None, max=None, n_contours=7, line_width=1.5, lut=None, colorbar=True): @@ -3325,6 +3396,14 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0): opacity=opacity, figure=self._f) return text + def add_text3d(self, x, y, z, text, name, color=None, opacity=1.0): + """ Add a text in 3D to the visualization""" + color = self._fg_color if color is None else color + with warnings.catch_warnings(record=True): + text = mlab.text3d(x, y, z, text, name=name, color=color, + opacity=opacity, figure=self._f) + return text + def remove_data(self, layer_id): """Remove data shown with .add_data()""" data = self.data.pop(layer_id) From 9fbd69f91aa130e0d5a5c3820c40811df3e3c3d7 Mon Sep 17 00:00:00 2001 From: Cristi Donos Date: Tue, 23 Oct 2018 13:42:00 +0300 Subject: [PATCH 02/46] re-organized add_foci function to allow data mapping and extended plot_foci example --- examples/plot_foci.py | 31 ++++++++++++++++++- surfer/viz.py | 69 ++++++++++++++++++++----------------------- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/examples/plot_foci.py b/examples/plot_foci.py index 0d0c364..639b7c9 100644 --- a/examples/plot_foci.py +++ b/examples/plot_foci.py @@ -9,10 +9,11 @@ """ import os import os.path as op -from numpy import arange +from numpy import arange, linspace from numpy.random import permutation import nibabel as nib from surfer import Brain +from mayavi import mlab print(__doc__) @@ -78,3 +79,31 @@ """ brain.add_foci(coords, coords_as_verts=True, scale_factor=scale_factor, color="#A52A2A") + +""" +Now we demonstrate plotting some data with a set of randomly +choosen vertices from within the middle temporal sulcus. +""" +verts = arange(0, len(ids)) +coords = permutation(verts[ids == 26])[:10] + +""" +Generate a dataset with some values in the [-1, 1] range +""" +dataset_name ='some_data' +data = linspace(start=-1, stop=1, num=10) + +""" +Now we plot the foci colorcoded acording to data values, whereas foci sizes will be +coded using the absolute values of data. +Using the 'name' argument we can reference newly added foci to create a colorbar using mayavi. +""" +brain.add_foci(coords, coords_as_verts=True, name=dataset_name, + scale_factor=1, data=data, colormap='cool') + +""" +Sometimes a qualitative representation is not enough, and we need to plot the colorbar as well. +This can be done using the 'name' argument in add_foci, by referencing the newly added foci to +create a colorbar with mayavi. +""" +mlab.colorbar(brain.foci[dataset_name], nb_labels=5, label_fmt=' %.2f ') \ No newline at end of file diff --git a/surfer/viz.py b/surfer/viz.py index 5083c6c..6c2204b 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1570,7 +1570,7 @@ def add_morphometry(self, measure, grayscale=False, hemi=None, def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, - hemi=None, allow_data=False, data=None): + hemi=None, data=None, colormap=None): """Add spherical foci, possibly mapping to displayed surf. The foci spheres can be displayed at the coordinates given, or @@ -1588,7 +1588,7 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, whether the coords parameter should be interpreted as vertex ids map_surface : Freesurfer surf or None surface to map coordinates through, or None to use raw coords - scale_factor : int + scale_factor : float controls the size of the foci spheres color : matplotlib color code HTML name, RBG tuple, or hex code @@ -1600,13 +1600,14 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, If None, it is assumed to belong to the hemipshere being shown. If two hemispheres are being shown, an error will be thrown. - allow_data : bool | False - If True, will plot foci using quiver3d instead of points3d to allow - mapping data to spheres. data : numpy array - 1D array the same size as the number of foci. Spheres will be - colorcoded acording to data values, whereas spheres' sizes will be - coded using the absolute values of data. + None or 1D array the same size as the number of foci (n,). + Spheres sizes will be coded using the absolute values of data. + If data is None, all spheres will have the same size. + colormap : str + Mayavi colormap name. Default is None, which means all foci will + have the same color given by the 'color' argument. If colormap is + not None, foci will be colorcoded acording to data values. """ from matplotlib.colors import colorConverter @@ -1639,14 +1640,12 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, fl = [] for brain in self._brain_list: if brain['hemi'] == hemi: - if allow_data: - vtxs = utils.find_closest_vertices(self.geo[hemi].coords, foci_coords) - fl.append(brain['brain'].add_foci_data(foci_coords, - scale_factor, color, alpha, name, data, - self.geo[hemi].nn[vtxs, :])) - else: - fl.append(brain['brain'].add_foci(foci_coords, - scale_factor, color, alpha, name)) + vtxs = utils.find_closest_vertices(self.geo[hemi].coords, + foci_coords) + fl.append(brain['brain'].add_foci(foci_coords, scale_factor, + color, alpha, name, data, + self.geo[hemi].nn[vtxs, :], + colormap)) self.foci_dict[name] = fl self._toggle_render(True, views) @@ -1736,6 +1735,7 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, Row index of which brain to use col : int Column index of which brain to use + font_size : """ if name in self.texts_dict: self.texts_dict[name]['text'].remove() @@ -3330,34 +3330,29 @@ def add_morphometry(self, morph_data, colormap, measure, return dict(surface=surf, colorbar=bar, measure=measure, brain=self, array_id=array_id) - def add_foci(self, foci_coords, scale_factor, color, alpha, name): - """Add spherical foci, possibly mapping to displayed surf""" - # Create the visualization - with warnings.catch_warnings(record=True): # traits - points = mlab.points3d( - foci_coords[:, 0], foci_coords[:, 1], foci_coords[:, 2], - np.ones(foci_coords.shape[0]), name=name, figure=self._f, - scale_factor=(10. * scale_factor), color=color, opacity=alpha) - return points - - def add_foci_data(self, foci_coords, scale_factor, color, alpha, name,data, normals): + def add_foci(self, foci_coords, scale_factor, color, alpha, name, data, normals, colormap): """Add spherical foci with attached data, possibly mapping to displayed surf""" # Create the visualization - hemi = self.hemi - if data == None: + if data is None: u = np.array(normals[:,0]) v = np.array(normals[:,1]) w = np.array(normals[:,2]) else: - u = self.geo[hemi].nn[:,0] * np.abs(data) - v = self.geo[hemi].nn[:,1] * np.abs(data) - w = self.geo[hemi].nn[:,2] * np.abs(data) + u = normals[:,0] * np.abs(data) + v = normals[:,1] * np.abs(data) + w = normals[:,2] * np.abs(data) with warnings.catch_warnings(record=True): # traits - points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], - u,v,w, scalars = data, colormap='coolwarm', mode='sphere', - name=name, figure=self._f, scale_factor=(10. * scale_factor), - color=color, opacity=alpha) - points.glyph.color_mode = 'color_by_scalar' + if colormap is None: + points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], + u,v,w, scalars = data, mode='sphere', + name=name, figure=self._f, scale_factor=(10. * scale_factor), + color=color, opacity=alpha) + else: + points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], + u,v,w, scalars = data, colormap=colormap, mode='sphere', + name=name, figure=self._f, scale_factor=(10. * scale_factor), + color=color, opacity=alpha) + points.glyph.color_mode = 'color_by_scalar' points.glyph.glyph_source.glyph_source.center = [0, 0, 0] return points From 16e5ed218a9ca8e0fbcece964027e3803de52003 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 26 Oct 2018 08:27:01 -0400 Subject: [PATCH 03/46] MRG: Fix scale_data_colormap and add FXAA (#261) * FIX: Render tweaks * FIX: Use correct data_dict * FIX: test backend * FIX: Better check * FIX: Old VTK --- surfer/viz.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index cb76a1c..75eab52 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -229,6 +229,11 @@ def _make_viewer(figure, n_row, n_col, title, scene_size, offscreen, for f in figure: f.scene.interactor.interactor_style = \ tvtk.InteractorStyleTerrain() + for figure in figures: + for f in figure: + # on a non-testing backend, and using modern VTK/Mayavi + if hasattr(getattr(f.scene, 'renderer', None), 'use_fxaa'): + f.scene.renderer.use_fxaa = True else: if isinstance(figure, int): # use figure with specified id figure = [mlab.figure(figure, size=scene_size)] @@ -387,6 +392,7 @@ class Brain(object): texts : dict The text objects. """ + def __init__(self, subject_id, hemi, surf, title=None, cortex="classic", alpha=1.0, size=800, background="black", foreground=None, figure=None, subjects_dir=None, @@ -1182,7 +1188,8 @@ def time_label(x): self._data_dicts[hemi].append(data) - self.scale_data_colormap(min, mid, max, transparent, center, alpha) + self.scale_data_colormap(min, mid, max, transparent, center, alpha, + data) if initial_time_index is not None: self.set_data_time_index(initial_time_index) @@ -1903,7 +1910,7 @@ def _brain_color(self): @verbose def scale_data_colormap(self, fmin, fmid, fmax, transparent, - center=None, alpha=1.0, verbose=None): + center=None, alpha=1.0, data=None, verbose=None): """Scale the data colormap. The colormap may be sequential or divergent. When the colormap is @@ -1942,17 +1949,22 @@ def scale_data_colormap(self, fmin, fmid, fmax, transparent, center of the (divergent) colormap alpha : float sets the overall opacity of colors, maintains transparent regions + data : dict | None + The data entry for which to scale the colormap. + If None, will use the data dict from either the left or right + hemisphere (in that order). verbose : bool, str, int, or None If not None, override default verbose level (see surfer.verbose). """ divergent = center is not None # Get the original colormap - for h in ['lh', 'rh']: - data = self.data_dict[h] - if data is not None: - table = data["orig_ctable"].copy() - break + if data is None: + for h in ['lh', 'rh']: + data = self.data_dict[h] + if data is not None: + break + table = data["orig_ctable"].copy() lut = _scale_mayavi_lut(table, fmin, fmid, fmax, transparent, center, alpha) @@ -2957,6 +2969,7 @@ def _scale_mayavi_lut(lut_table, fmin, fmid, fmax, transparent, class _Hemisphere(object): """Object for visualizing one hemisphere with mlab""" + def __init__(self, subject_id, hemi, figure, geo, geo_curv, geo_kwargs, geo_reverse, subjects_dir, bg_color, backend, fg_color): @@ -3139,7 +3152,7 @@ def _add_vector_data(self, vectors, vector_values, fmin, fmid, fmax, vmax=fmax, figure=self._f, opacity=vector_alpha) # Enable backface culling - quiver.actor.property.backface_culling = False + quiver.actor.property.backface_culling = True quiver.mlab_source.update() # Compute scaling for the glyphs From df6852705bcbb055e171e94cf77825ba8564475b Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 22 Mar 2019 19:24:03 +1100 Subject: [PATCH 04/46] MRG: width/height in offscreen mode (#265) * FIX: width/height in offscreen mode * MAINT: Sisyphus * BUG: Modernize labels * FIX: Circle * FIX: Stretch * FIX: More * MAINT: Push the rock * FIX: Skip for 3 --- .circleci/config.yml | 95 ++++++++++++++++++++++++++------------ .travis.yml | 7 +-- appveyor.yml | 3 +- setup.cfg | 5 +- surfer/tests/test_utils.py | 4 +- surfer/tests/test_viz.py | 48 ++++++++++--------- surfer/utils.py | 7 +++ surfer/viz.py | 2 +- 8 files changed, 107 insertions(+), 64 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 849d72a..bbb755d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,48 +1,66 @@ version: 2 jobs: - build: + build_docs: docker: - - image: circleci/python:3.6-jessie + - image: circleci/python:3.7-stretch steps: - # Get our data and merge with upstream - checkout - - run: echo $(git log -1 --pretty=%B) | tee gitlog.txt - - run: echo ${CI_PULL_REQUEST//*pull\//} | tee merge.txt - run: - command: | - if [[ $(cat merge.txt) != "" ]]; then - echo "Merging $(cat merge.txt)"; - git pull --ff-only origin "refs/pull/$(cat merge.txt)/merge"; - fi + name: Set BASH_ENV + command: | + echo "set -e" >> $BASH_ENV; + echo "export SUBJECTS_DIR=~/subjects" >> $BASH_ENV; + echo "export DISPLAY=:99" >> $BASH_ENV; + echo "export OPENBLAS_NUM_THREADS=4" >> $BASH_ENV; + echo "export PATH=~/.local/bin:$PATH" >> $BASH_ENV; + - run: + name: Merge with upstream + command: | + echo $(git log -1 --pretty=%B) | tee gitlog.txt + echo ${CI_PULL_REQUEST//*pull\//} | tee merge.txt + if [[ $(cat merge.txt) != "" ]]; then + echo "Merging $(cat merge.txt)"; + git remote add upstream git://github.com/nipy/PySurfer.git; + git pull --ff-only upstream "refs/pull/$(cat merge.txt)/merge"; + git fetch upstream master; + fi # Load our data - restore_cache: keys: - - data-cache + - data-cache-0 - pip-cache - # Fix libgcc_s.so.1 pthread_cancel bug: + - run: + name: Spin up Xvfb + command: | + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset; + # https://github.com/ContinuumIO/anaconda-issues/issues/9190#issuecomment-386508136 # https://github.com/golemfactory/golem/issues/1019 - - run: sudo apt-get install libgl1-mesa-glx libegl1-mesa libxrandr2 libxrandr2 libxss1 libxcursor1 libxcomposite1 libasound2 libxi6 libxtst6 qt5-default - - run: echo "export SUBJECTS_DIR=~/subjects" >> $BASH_ENV - - run: echo "export PATH=~/.local/bin:$PATH" >> $BASH_ENV - - run: echo "export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0.4200.1" >> $BASH_ENV - # Spin up Xvfb - - run: echo "export DISPLAY=:99" >> $BASH_ENV - - run: /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset; - # Python env - - run: pip install -U --user --progress-bar off numpy scipy matplotlib vtk PyQt5 sip PyQt5-sip nibabel sphinx numpydoc pillow imageio https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master - - run: pip install -U --user --progress-bar off mayavi + - run: + name: Fix libgcc_s.so.1 pthread_cancel bug + command: | + sudo apt-get install qt5-default + + - run: + name: Get Python running + command: | + pip install --user -q --upgrade pip numpy + pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master mayavi - save_cache: key: pip-cache paths: - - "~/.cache/pip" - - run: python setup.py develop --user - # Check libs - - run: LIBGL_DEBUG=verbose python -c "from mayavi import mlab; import matplotlib.pyplot as plt; mlab.figure(); plt.figure()" - - run: echo $SUBJECTS_DIR + - ~/.cache/pip + + # Look at what we have and fail early if there is some library conflict + - run: + name: Check installation + command: | + LIBGL_DEBUG=verbose python -c "from mayavi import mlab; import matplotlib.pyplot as plt; mlab.figure(); plt.figure()" + echo $SUBJECTS_DIR - run: + name: Get data command: | if [ ! -d $SUBJECTS_DIR ]; then mkdir $SUBJECTS_DIR; @@ -51,14 +69,31 @@ jobs: unzip fsaverage_min.zip; rm fsaverage_min.zip; fi; - - run: ls $SUBJECTS_DIR - - run: cd doc && sphinx-build -D plot_gallery=1 -D sphinx_gallery_conf.filename_pattern=^\(\(?\!plot_fmri_activation_volume\|plot_morphometry\|plot_label\.py\|plot_probabilistic_label\|plot_resting_correlations\|plot_transparent_brain\|rotate_animation\|save_movie\|save_views\).\)*\$ -b html -d _build/doctrees . _build/html + ls $SUBJECTS_DIR + - run: + name: Install PySurfer + command: | + python setup.py develop --user + + - run: + name: Build docs + command: | + cd doc + sphinx-build -D plot_gallery=1 -D sphinx_gallery_conf.filename_pattern=^\(\(?\!plot_fmri_activation_volume\|plot_morphometry\|plot_label\.py\|plot_probabilistic_label\|plot_resting_correlations\|plot_transparent_brain\|rotate_animation\|save_movie\|save_views\).\)*\$ -b html -d _build/doctrees . _build/html - store_artifacts: path: doc/_build/html/ destination: html - save_cache: - key: data-cache + key: data-cache-0 paths: - "~/subjects" + +workflows: + version: 2 + + default: + jobs: + - build_docs + diff --git a/.travis.yml b/.travis.yml index f44bb7c..ed5c16c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,9 @@ language: c sudo: false env: - global: PYTHON_VERSION=3.6 + global: PYTHON_VERSION=3.7 CONDA_DEPENDENCIES="numpy scipy matplotlib pyqt>=5.9 coverage pytest pytest-cov flake8 pygments traits traitsui pyface" - PIP_DEPENDENCIES="codecov pytest-sugar pytest-faulthandler nibabel imageio" + PIP_DEPENDENCIES="codecov pytest-sugar pytest-faulthandler nibabel imageio imageio-ffmpeg" DISPLAY=:99.0 matrix: @@ -57,9 +57,6 @@ before_script: - unzip ../fsaverage_min.zip - cd .. - export SUBJECTS_DIR="${PWD}/subjects" - - if [[ $PIP_DEPENDENCIES == *"imageio"* ]] || [ ! -z "$CONDA_ENVIRONMENT" ]; then - python -c "import imageio; imageio.plugins.ffmpeg.download()"; - fi script: - cd ${SRC_DIR} diff --git a/appveyor.yml b/appveyor.yml index 4a79ae0..39520c5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,9 +6,8 @@ environment: install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - - "pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio codecov pyqt5==5.9" + - "pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9" - "pip install traits traitsui pyface vtk mayavi nibabel" - - "python -c \"import imageio; imageio.plugins.ffmpeg.download()\"" - "powershell make/get_fsaverage.ps1" - "python setup.py develop" - "SET SUBJECTS_DIR=%CD%\\subjects" diff --git a/setup.cfg b/setup.cfg index 133852e..d9bfc29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,7 @@ filterwarnings = ignore:can't resolve package from __spec__ or __package__, falling back on __name__ and __path__:ImportWarning ignore:The binary mode of fromstring is deprecated, as it behaves surprisingly on unicode inputs. Use frombuffer instead:DeprecationWarning ignore:elementwise == comparison failed:DeprecationWarning - ignore:Matplotlib is building the font cache using fc-list. This may take a moment.:UserWarning \ No newline at end of file + ignore:Importing from numpy:DeprecationWarning + ignore:.*ufunc size changed.*:RuntimeWarning + ignore:Using or importing the ABCs:DeprecationWarning + ignore:Matplotlib is building the font cache using fc-list. This may take a moment.:UserWarning diff --git a/surfer/tests/test_utils.py b/surfer/tests/test_utils.py index 8d71382..d9a6d72 100644 --- a/surfer/tests/test_utils.py +++ b/surfer/tests/test_utils.py @@ -32,12 +32,12 @@ def _slow_compute_normals(rr, tris): @utils.requires_fsaverage() def test_surface(): """Test IO for Surface class""" - subj_dir = utils._get_subjects_dir() + extra, subj_dir = utils._get_extra() for subjects_dir in [None, subj_dir]: surface = utils.Surface('fsaverage', 'lh', 'inflated', subjects_dir=subjects_dir) surface.load_geometry() - surface.load_label('BA1') + surface.load_label('BA1' + extra) surface.load_curvature() xfm = np.eye(4) xfm[:3, -1] += 2 # translation diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 5d534a2..158518d 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -13,7 +13,8 @@ from unittest import SkipTest from surfer import Brain, io, utils -from surfer.utils import requires_fsaverage, requires_imageio, requires_fs +from surfer.utils import (requires_fsaverage, requires_imageio, requires_fs, + _get_extra) warnings.simplefilter('always') @@ -247,30 +248,30 @@ def test_label(): brain = Brain(subject_id, hemi, surf) view = get_view(brain) - brain.add_label("BA1") + extra, subj_dir = _get_extra() + brain.add_label("BA1" + extra) check_view(brain, view) - brain.add_label("BA1", color="blue", scalar_thresh=.5) - subj_dir = utils._get_subjects_dir() + brain.add_label("BA1" + extra, color="blue", scalar_thresh=.5) label_file = pjoin(subj_dir, subject_id, - "label", "%s.MT.label" % hemi) + "label", "%s.MT%s.label" % (hemi, extra)) brain.add_label(label_file) - brain.add_label("BA44", borders=True) - brain.add_label("BA6", alpha=.7) + brain.add_label("BA44" + extra, borders=True) + brain.add_label("BA6" + extra, alpha=.7) brain.show_view("medial") - brain.add_label("V1", color="steelblue", alpha=.6) - brain.add_label("V2", color="#FF6347", alpha=.6) - brain.add_label("entorhinal", color=(.2, 1, .5), alpha=.6) + brain.add_label("V1" + extra, color="steelblue", alpha=.6) + brain.add_label("V2" + extra, color="#FF6347", alpha=.6) + brain.add_label("entorhinal" + extra, color=(.2, 1, .5), alpha=.6) brain.set_surf('white') brain.show_view(dict(elevation=40, distance=430), distance=430) with pytest.raises(ValueError, match='!='): brain.show_view(dict(elevation=40, distance=430), distance=431) # remove labels - brain.remove_labels('V1') - assert 'V2' in brain.labels_dict - assert 'V1' not in brain.labels_dict + brain.remove_labels('V1' + extra) + assert 'V2' + extra in brain.labels_dict + assert 'V1' + extra not in brain.labels_dict brain.remove_labels() - assert 'V2' not in brain.labels_dict + assert 'V2' + extra not in brain.labels_dict brain.close() @@ -354,7 +355,8 @@ def test_morphometry(): def test_movie(tmpdir): """Test saving a movie of an MEG inverse solution.""" import imageio - + if sys.version_info < (3,): + raise SkipTest('imageio ffmpeg requires Python 3') # create and setup the Brain instance _set_backend() brain = Brain(*std_args) @@ -432,15 +434,15 @@ def test_probabilistic_labels(): brain = Brain("fsaverage", "lh", "inflated", cortex="low_contrast") - brain.add_label("BA1", color="darkblue") - - brain.add_label("BA1", color="dodgerblue", scalar_thresh=.5) + extra, subj_dir = _get_extra() + brain.add_label("BA1" + extra, color="darkblue") + brain.add_label("BA1" + extra, color="dodgerblue", scalar_thresh=.5) + brain.add_label("BA45" + extra, color="firebrick", borders=True) + brain.add_label("BA45" + extra, color="salmon", borders=True, + scalar_thresh=.5) - brain.add_label("BA45", color="firebrick", borders=True) - brain.add_label("BA45", color="salmon", borders=True, scalar_thresh=.5) - - subj_dir = utils._get_subjects_dir() - label_file = pjoin(subj_dir, "fsaverage", "label", "lh.BA6.label") + label_file = pjoin(subj_dir, "fsaverage", "label", + "lh.BA6%s.label" % (extra,)) prob_field = np.zeros_like(brain.geo['lh'].x) ids, probs = nib.freesurfer.read_label(label_file, read_scalars=True) prob_field[ids] = probs diff --git a/surfer/utils.py b/surfer/utils.py index cd2b9f3..8e3e22c 100644 --- a/surfer/utils.py +++ b/surfer/utils.py @@ -745,3 +745,10 @@ def requires_fs(): has = ('FREESURFER_HOME' in os.environ) return pytest.mark.skipif( not has, reason='Requires FreeSurfer command line tools') + + +def _get_extra(): + # Get extra label for newer freesurfer + subj_dir = _get_subjects_dir() + fname = op.join(subj_dir, 'fsaverage', 'label', 'lh.BA1.label') + return '_exvivo' if not op.isfile(fname) else '', subj_dir diff --git a/surfer/viz.py b/surfer/viz.py index 75eab52..4fcde5d 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -208,7 +208,7 @@ def _make_viewer(figure, n_row, n_col, title, scene_size, offscreen, try: mlab.options.offscreen = True with warnings.catch_warnings(record=True): # traits - figures = [[mlab.figure(size=(h / n_row, w / n_col)) + figures = [[mlab.figure(size=(w / n_col, h / n_row)) for _ in range(n_col)] for __ in range(n_row)] finally: mlab.options.offscreen = orig_val From 8b0d631d93b2e7074a33a51148bbd034410171de Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Wed, 22 May 2019 17:22:50 +0300 Subject: [PATCH 05/46] Integrate better with Jupyter notebook (#268) * Integrate better with Jupyuter notebook Mayavi can render things inside a jupyter notebook as either PNG or X3D. This functionality can be enabled with `mlab.init_notebook()`. This PR adds an `_ipython_display_` hook to the `Brain` class that renders the brain accordgin to the Mayavi notebook integration settings. * Add note on Jupyter notebook integration to docs * Fix links in doc --- doc/install.rst | 11 +++++++++++ surfer/viz.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/doc/install.rst b/doc/install.rst index bb6c441..91b5691 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -77,6 +77,17 @@ notebook), you have to activate the correct GUI backend, which is probably qt:: This will allow you to have an open PySurfer window while still being able to execute code in the console/notebook. +It is also possible to embed the PySurfer visualization into a Jupyter notebook. +This is achieved by leveraging `Mayavi's notebook integration +`_:: + + from mayavi import mlab + mlab.init_notebook(backend='png') + +The ``backend`` parameter can either be ``'png'`` to render the visualization +as a static PNG image, or ``'x3d'`` to render it using +`X3D `_ (still experimental). + If you are having trouble getting started using PySurfer, please describe the problem on the `nipy mailing list`_. .. include:: links_names.txt diff --git a/surfer/viz.py b/surfer/viz.py index 4fcde5d..14b921c 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2776,6 +2776,23 @@ def animate(self, views, n_steps=180., fname=None, use_cache=False, if ret: print("\n\nError occured when exporting movie\n\n") + def __repr__(self): + return ('' % + (self.subject_id, self._hemi, self.surf)) + + def _ipython_display_(self): + """Called by Jupyter notebook to display a brain.""" + from IPython.display import display as idisplay + + if mlab.options.offscreen: + # Render the mayavi scenes to the notebook + for figure in self._figures: + for scene in figure: + idisplay(scene.scene) + else: + # Render string representation + print(repr(self)) + def _scale_sequential_lut(lut_table, fmin, fmid, fmax): """Scale a sequential colormap.""" From 27f54c652aa1d4999d8cd8d3d7e42fe3938cda31 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Jun 2019 07:43:12 -0400 Subject: [PATCH 06/46] MAINT: Update fsaverage (#270) * MAINT: Update fsaverage * FIX: Bump to 3.5 * ENH: Run more on CircleCI * MAINT: CI * MAINT: CI * MAINT: CI * MAINT: Circle * FIX: Pattern * FIX: Circle * FIX: Try again * FIX: Better * FIX: One more --- .circleci/config.yml | 41 ++++++++++++++-------------- .travis.yml | 36 +++++++++--------------- doc/Makefile | 4 +-- doc/conf.py | 26 +++++++----------- doc/documentation/command_line.rst | 3 -- examples/plot_label.py | 16 +++++------ examples/plot_probabilistic_label.py | 10 +++---- 7 files changed, 58 insertions(+), 78 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bbb755d..dd7ca25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,6 +13,7 @@ jobs: echo "export DISPLAY=:99" >> $BASH_ENV; echo "export OPENBLAS_NUM_THREADS=4" >> $BASH_ENV; echo "export PATH=~/.local/bin:$PATH" >> $BASH_ENV; + echo "export PATTERN=\"plot_\(?\!fmri_activation_volume\|resting_correlations\)\"" >> $BASH_ENV; - run: name: Merge with upstream command: | @@ -25,12 +26,6 @@ jobs: git fetch upstream master; fi - # Load our data - - restore_cache: - keys: - - data-cache-0 - - pip-cache - - run: name: Spin up Xvfb command: | @@ -43,11 +38,18 @@ jobs: command: | sudo apt-get install qt5-default + # Load our data + - restore_cache: + keys: + - data-cache-0 + - pip-cache + - run: name: Get Python running command: | pip install --user -q --upgrade pip numpy - pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg https://api.github.com/repos/sphinx-gallery/sphinx-gallery/zipball/master mayavi + pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg sphinx-gallery + pip install --user -q --upgrade mayavi "https://api.github.com/repos/mne-tools/mne-python/zipball/master" - save_cache: key: pip-cache paths: @@ -58,18 +60,19 @@ jobs: name: Check installation command: | LIBGL_DEBUG=verbose python -c "from mayavi import mlab; import matplotlib.pyplot as plt; mlab.figure(); plt.figure()" - echo $SUBJECTS_DIR + - run: name: Get data command: | - if [ ! -d $SUBJECTS_DIR ]; then - mkdir $SUBJECTS_DIR; - cd $SUBJECTS_DIR; - wget http://faculty.washington.edu/larsoner/fsaverage_min.zip; - unzip fsaverage_min.zip; - rm fsaverage_min.zip; - fi; + echo $SUBJECTS_DIR + mkdir -p $SUBJECTS_DIR + python -c "import mne; mne.datasets.fetch_fsaverage(verbose=True)" ls $SUBJECTS_DIR + - save_cache: + key: data-cache-0 + paths: + - "~/subjects" + - run: name: Install PySurfer command: | @@ -79,16 +82,13 @@ jobs: name: Build docs command: | cd doc - sphinx-build -D plot_gallery=1 -D sphinx_gallery_conf.filename_pattern=^\(\(?\!plot_fmri_activation_volume\|plot_morphometry\|plot_label\.py\|plot_probabilistic_label\|plot_resting_correlations\|plot_transparent_brain\|rotate_animation\|save_movie\|save_views\).\)*\$ -b html -d _build/doctrees . _build/html + echo $PATTERN + make html_dev-pattern - store_artifacts: path: doc/_build/html/ destination: html - - save_cache: - key: data-cache-0 - paths: - - "~/subjects" workflows: version: 2 @@ -96,4 +96,3 @@ workflows: default: jobs: - build_docs - diff --git a/.travis.yml b/.travis.yml index ed5c16c..523612e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ language: c sudo: false - +dist: xenial +services: + - xvfb env: global: PYTHON_VERSION=3.7 CONDA_DEPENDENCIES="numpy scipy matplotlib pyqt>=5.9 coverage pytest pytest-cov flake8 pygments traits traitsui pyface" PIP_DEPENDENCIES="codecov pytest-sugar pytest-faulthandler nibabel imageio imageio-ffmpeg" DISPLAY=:99.0 + SUBJECTS_DIR=~/subjects matrix: include: @@ -16,11 +19,11 @@ matrix: packages: - mencoder - # 2.7, no mencoder + # 3.5, no mencoder - os: linux - env: PYTHON_VERSION=2.7 - CONDA_DEPENDENCIES="numpy scipy matplotlib coverage pytest pytest-cov flake8 mayavi" - PIP_DEPENDENCIES="codecov pytest-sugar faulthandler pytest-faulthandler nibabel imageio" + env: PYTHON_VERSION=3.5 + CONDA_DEPENDENCIES="numpy scipy matplotlib coverage pytest pytest-cov flake8" + PIP_DEPENDENCIES="codecov pytest-sugar nibabel imageio imageio-ffmpeg" # OSX - os: osx @@ -32,32 +35,19 @@ before_install: - if [ "${TRAVIS_OS_NAME}" == "osx" ]; then unset -f cd; fi; - - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then - echo "Starting Xvfb..."; - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render; - fi; - git clone https://github.com/astropy/ci-helpers.git - source ci-helpers/travis/setup_conda.sh - - if [ "$PYTHON_VERSION" != "2.7" ]; then - pip install vtk; - pip install mayavi; - fi; + - pip install vtk + - pip install mayavi + - mkdir -p $SUBJECTS_DIR + - pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master" + - python -c "import mne; mne.datasets.fetch_fsaverage(verbose=True)" install: - python setup.py build - python setup.py install - SRC_DIR=$(pwd) -before_script: - # Let's create a (fake) display on Travis, and let's use a funny resolution - - cd ~ - - wget --quiet http://faculty.washington.edu/larsoner/fsaverage_min.zip - - mkdir subjects - - cd subjects - - unzip ../fsaverage_min.zip - - cd .. - - export SUBJECTS_DIR="${PWD}/subjects" - script: - cd ${SRC_DIR} - pytest surfer --cov=surfer -v diff --git a/doc/Makefile b/doc/Makefile index 96649d1..77929cb 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -2,9 +2,9 @@ # # You can set these variables from the command line. -SPHINXOPTS = +SPHINXOPTS = -nWT --keep-going SPHINXBUILD = sphinx-build -PAPER = +GPAPER = BUILDDIR = _build # Internal variables. diff --git a/doc/conf.py b/doc/conf.py index 5e60d4c..83feed8 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -51,7 +51,7 @@ ] autosummary_generate = True -autodoc_default_flags = ['inherited-members'] +autodoc_default_options = {'inherited-members': None} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -250,10 +250,10 @@ # 'python': ('http://docs.python.org/', None), # 'numpy': ('http://docs.scipy.org/doc/numpy-dev/', None), # 'scipy': ('http://scipy.github.io/devdocs/', None), - 'matplotlib': ('http://matplotlib.org', None), - 'imageio': ('http://imageio.readthedocs.io/en/latest', None), - 'mayavi': ('http://docs.enthought.com/mayavi/mayavi', None), - 'nibabel': ('http://nipy.org/nibabel', None), + 'matplotlib': ('https://matplotlib.org', None), + 'imageio': ('https://imageio.readthedocs.io/en/latest', None), + 'mayavi': ('https://docs.enthought.com/mayavi/mayavi', None), + 'nibabel': ('https://nipy.org/nibabel', None), } # One entry per manual page. List of tuples @@ -268,31 +268,25 @@ try: from mayavi import mlab - find_mayavi_figures = True # Do not pop up any mayavi windows while running the # examples. These are very annoying since they steal the focus. mlab.options.offscreen = True + scrapers = ('matplotlib', 'mayavi') except Exception: - find_mayavi_figures = False + scrapers = ('matplotlib',) sphinx_gallery_conf = { 'doc_module': ('surfer',), - 'reference_url': { - 'surfer': None, - 'matplotlib': 'http://matplotlib.org', - 'numpy': 'http://docs.scipy.org/doc/numpy', - 'scipy': 'http://docs.scipy.org/doc/scipy/reference', - 'mayavi': 'http://docs.enthought.com/mayavi/mayavi', - }, + 'reference_url': {'surfer': None}, 'examples_dirs': examples_dirs, 'gallery_dirs': gallery_dirs, 'within_subsection_order': FileNameSortKey, - 'find_mayavi_figures': find_mayavi_figures, + 'image_scrapers': scrapers, 'default_thumb_file': os.path.join('_static', 'pysurfer_logo_small.png'), 'backreferences_dir': 'generated', 'download_section_examples': False, 'thumbnail_size': (250, 250), - } +} numpydoc_class_members_toctree = False numpydoc_show_inherited_class_members = False diff --git a/doc/documentation/command_line.rst b/doc/documentation/command_line.rst index cd21a64..6015a13 100644 --- a/doc/documentation/command_line.rst +++ b/doc/documentation/command_line.rst @@ -50,6 +50,3 @@ Other command-line options As in tksurfer, most aspects of the visualization can be initialized from the command-line. To get a full documentation of the command-line interface, simply type ``pysurfer`` at a terminal prompt and hit enter. -For convenience, this usage message is reproduced below. - -.. literalinclude:: pysurfer_usage.txt diff --git a/examples/plot_label.py b/examples/plot_label.py index 8083347..8b602d7 100644 --- a/examples/plot_label.py +++ b/examples/plot_label.py @@ -18,31 +18,31 @@ # If the label lives in the normal place in the subjects directory, # you can plot it by just using the name -brain.add_label("BA1_exvivo") +brain.add_label("BA1") # Some labels have an associated scalar value at each ID in the label. # For example, they may be probabilistically defined. You can threshold # what vertices show up in the label using this scalar data -brain.add_label("BA1_exvivo", color="blue", scalar_thresh=.5) +brain.add_label("BA1", color="blue", scalar_thresh=.5) # Or you can give a path to a label in an arbitrary location subj_dir = brain.subjects_dir label_file = os.path.join(subj_dir, subject_id, - "label", "%s.MT_exvivo.label" % hemi) + "label", "%s.MT.label" % hemi) brain.add_label(label_file) # By default the label is 'filled-in', but you can # plot just the label boundaries -brain.add_label("BA44_exvivo", borders=True) +brain.add_label("BA44", borders=True) # You can also control the opacity of the label color -brain.add_label("BA6_exvivo", alpha=.7) +brain.add_label("BA6", alpha=.7) # Finally, you can plot the label in any color you want. brain.show_view(dict(azimuth=-42, elevation=105, distance=225, focalpoint=[-30, -20, 15])) # Use any valid matplotlib color. -brain.add_label("V1_exvivo", color="steelblue", alpha=.6) -brain.add_label("V2_exvivo", color="#FF6347", alpha=.6) -brain.add_label("entorhinal_exvivo", color=(.2, 1, .5), alpha=.6) +brain.add_label("V1", color="steelblue", alpha=.6) +brain.add_label("V2", color="#FF6347", alpha=.6) +brain.add_label("entorhinal", color=(.2, 1, .5), alpha=.6) diff --git a/examples/plot_probabilistic_label.py b/examples/plot_probabilistic_label.py index 30ba576..213445b 100644 --- a/examples/plot_probabilistic_label.py +++ b/examples/plot_probabilistic_label.py @@ -28,27 +28,27 @@ The easiest way to label any vertex that could be in the region is with add_label. """ -brain.add_label("BA1_exvivo", color="#A6BDDB") +brain.add_label("BA1", color="#A6BDDB") """ You can also threshold based on the probability of that region being at each vertex. """ -brain.add_label("BA1_exvivo", color="#2B8CBE", scalar_thresh=.5) +brain.add_label("BA1", color="#2B8CBE", scalar_thresh=.5) """ It's also possible to plot just the label boundary, in case you wanted to overlay the label on an activation plot to asses whether it falls within that region. """ -brain.add_label("BA45_exvivo", color="#F0F8FF", borders=3, scalar_thresh=.5) -brain.add_label("BA45_exvivo", color="#F0F8FF", alpha=.3, scalar_thresh=.5) +brain.add_label("BA45", color="#F0F8FF", borders=3, scalar_thresh=.5) +brain.add_label("BA45", color="#F0F8FF", alpha=.3, scalar_thresh=.5) """ Finally, with a few tricks, you can display the whole probabilistic map. """ subjects_dir = environ["SUBJECTS_DIR"] -label_file = join(subjects_dir, "fsaverage", "label", "lh.BA6_exvivo.label") +label_file = join(subjects_dir, "fsaverage", "label", "lh.BA6.label") prob_field = np.zeros_like(brain.geo['lh'].x) ids, probs = read_label(label_file, read_scalars=True) From b9a7dd6060e9228023482764b1bc5c576a6989c4 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 30 Jul 2019 14:23:49 -0400 Subject: [PATCH 07/46] MAINT: Fix ignores for CIs (#272) --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index d9bfc29..9bc24df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,4 +11,5 @@ filterwarnings = ignore:Importing from numpy:DeprecationWarning ignore:.*ufunc size changed.*:RuntimeWarning ignore:Using or importing the ABCs:DeprecationWarning + ignore:the imp module is deprecated in favour of importlib:DeprecationWarning ignore:Matplotlib is building the font cache using fc-list. This may take a moment.:UserWarning From b2474cfa1f3f31c748f6fba30b4cd0e95528043c Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Tue, 30 Jul 2019 15:05:34 -0400 Subject: [PATCH 08/46] Set annotation LUT directly (#271) --- surfer/viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/viz.py b/surfer/viz.py index 14b921c..f76184f 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -3304,7 +3304,7 @@ def add_annotation(self, annot, ids, cmap): # Set the color table l_m = surf.module_manager.scalar_lut_manager - l_m.load_lut_from_list(cmap / 255.) + l_m.lut.table = np.round(cmap).astype(np.uint8) # Set the brain attributes return dict(surface=surf, name=annot, colormap=cmap, brain=self, From 80c3ccb8161baca59a875c30da668bb4f8b52a93 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Mon, 12 Aug 2019 13:21:08 -0400 Subject: [PATCH 09/46] Allow painting all annotation vertices in a single color (#273) --- surfer/tests/test_viz.py | 6 ++++++ surfer/viz.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 158518d..f62d1ca 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -165,6 +165,12 @@ def test_annot(): labels, ctab, names = nib.freesurfer.read_annot(annot_path) brain.add_annotation((labels, ctab)) + brain.add_annotation('aparc', color="red", remove_existing=True) + surf = brain.annot["surface"] + ctab = surf.module_manager.scalar_lut_manager.lut.table + for color in ctab: + assert color[:3] == (255, 0, 0) + brain.close() diff --git a/surfer/viz.py b/surfer/viz.py index f76184f..5ff64dd 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1196,7 +1196,7 @@ def time_label(x): self._toggle_render(True, views) def add_annotation(self, annot, borders=True, alpha=1, hemi=None, - remove_existing=True): + remove_existing=True, color=None): """Add an annotation file. Parameters @@ -1220,6 +1220,10 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, for both hemispheres. remove_existing : bool If True (default), remove old annotations. + color : matplotlib-style color code + If used, show all annotations in the same (specified) color. + Probably useful only when showing annotation borders. + """ hemis = self._check_hemis(hemi) @@ -1292,6 +1296,12 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, alpha_vec = cmap[:, 3] alpha_vec[alpha_vec > 0] = alpha * 255 + # Override the cmap when a single color is used + if color is not None: + from matplotlib.colors import colorConverter + rgb = np.round(np.multiply(colorConverter.to_rgb(color), 255)) + cmap[:, :3] = rgb.astype(cmap.dtype) + for brain in self._brain_list: if brain['hemi'] == hemi: self.annot_list.append( From 9cd751178d1bad6f03199d651a3c359aa540f59a Mon Sep 17 00:00:00 2001 From: Christian Brodbeck Date: Mon, 9 Sep 2019 12:19:47 -0400 Subject: [PATCH 10/46] FIX: don't change view when adding foci (#274) --- surfer/viz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/surfer/viz.py b/surfer/viz.py index 5ff64dd..102482d 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -3365,7 +3365,8 @@ def add_foci(self, foci_coords, scale_factor, color, alpha, name): points = mlab.points3d( foci_coords[:, 0], foci_coords[:, 1], foci_coords[:, 2], np.ones(foci_coords.shape[0]), name=name, figure=self._f, - scale_factor=(10. * scale_factor), color=color, opacity=alpha) + scale_factor=(10. * scale_factor), color=color, opacity=alpha, + reset_zoom=False) return points def add_contour_overlay(self, scalar_data, min=None, max=None, From 1fb7dafbde8a0b2e40ac31701daa222b72ea2910 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 15 Oct 2019 10:52:42 -0400 Subject: [PATCH 11/46] ENH: Add option to smooth to nearest (#275) * ENH: Add option to smooth to nearest * DOC: Update docstring * FIX: Flake * FIX: Undordered vertices are the worst * FIX: Orders are hard * DOC: Changes --- .gitignore | 1 + CHANGES | 23 ++++++++++++++++++-- doc/changes.rst | 1 + doc/index.rst | 1 + examples/plot_meg_inverse_solution.py | 5 +++-- surfer/tests/test_utils.py | 25 +++++++++++++++++++--- surfer/utils.py | 30 +++++++++++++++++++++++++-- surfer/viz.py | 19 +++++++++++------ 8 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 doc/changes.rst diff --git a/.gitignore b/.gitignore index 35adb4a..884e2b8 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .#* *.swp *.orig +*.mov build dist/ diff --git a/CHANGES b/CHANGES index dc0902e..fae96a9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,24 @@ -PySurfer Changes -================ +Changelog +========= + +.. currentmodule:: surfer + +Development version (0.10.dev0) +------------------------------- + +- Added an option to smooth to nearest vertex in :meth:`Brain.add_data` using + ``smoothing_steps='nearest'`` +- Added options for using offscreen mode +- Improved integration with Jupyter notebook +- Avoided view changes when using :meth:`Brain.add_foci` + +Version 0.9 +----------- + +- Fixed transparency issues with colormaps with + :meth:`Brain.scale_data_colormap` +- Added an example of using custom colors +- Added options for choosing units for :class:`Brain` (``m`` or ``mm``) Version 0.8 ----------- diff --git a/doc/changes.rst b/doc/changes.rst new file mode 100644 index 0000000..8a05a51 --- /dev/null +++ b/doc/changes.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index 4a95f39..940ffad 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ More Information auto_examples/index.rst documentation/index.rst python_reference.rst + changes.rst Authors ------- diff --git a/examples/plot_meg_inverse_solution.py b/examples/plot_meg_inverse_solution.py index b3cb23e..9dd20a5 100644 --- a/examples/plot_meg_inverse_solution.py +++ b/examples/plot_meg_inverse_solution.py @@ -50,9 +50,10 @@ def time_label(t): # colormap to use colormap = 'hot' - # add data and set the initial time displayed to 100 ms + # add data and set the initial time displayed to 100 ms, + # plotted using the nearest relevant colors brain.add_data(data, colormap=colormap, vertices=vertices, - smoothing_steps=5, time=time, time_label=time_label, + smoothing_steps='nearest', time=time, time_label=time_label, hemi=hemi, initial_time=0.1, verbose=False) # scale colormap diff --git a/surfer/tests/test_utils.py b/surfer/tests/test_utils.py index d9a6d72..f04cd82 100644 --- a/surfer/tests/test_utils.py +++ b/surfer/tests/test_utils.py @@ -1,6 +1,10 @@ +from distutils.version import LooseVersion import numpy as np +import scipy +from scipy import sparse +import pytest import matplotlib as mpl -from numpy.testing import assert_array_almost_equal, assert_array_equal +from numpy.testing import assert_allclose, assert_array_equal from surfer import utils @@ -44,12 +48,12 @@ def test_surface(): x = surface.x surface.apply_xfm(xfm) x_ = surface.x - assert_array_almost_equal(x + 2, x_) + assert_allclose(x + 2, x_) # normals nn = _slow_compute_normals(surface.coords, surface.faces[:10000]) nn_fast = utils._compute_normals(surface.coords, surface.faces[:10000]) - assert_array_almost_equal(nn, nn_fast) + assert_allclose(nn, nn_fast) assert 50 < np.linalg.norm(surface.coords, axis=-1).mean() < 100 # mm surface = utils.Surface('fsaverage', 'lh', 'inflated', subjects_dir=subj_dir, units='m') @@ -99,3 +103,18 @@ def test_create_color_lut(): # Test that we can ask for a specific number of colors cmap_out = utils.create_color_lut("Reds", 12) assert cmap_out.shape == (12, 4) + + +def test_smooth(): + """Test smoothing support.""" + adj_mat = sparse.csc_matrix(np.repeat(np.repeat(np.eye(2), 2, 0), 2, 1)) + vertices = np.array([0, 2]) + want = np.repeat(np.eye(2), 2, axis=0) + smooth = utils.smoothing_matrix(vertices, adj_mat).toarray() + assert_allclose(smooth, want) + if LooseVersion(scipy.__version__) < LooseVersion('1.3'): + with pytest.raises(RuntimeError, match='nearest.*requires'): + utils.smoothing_matrix(vertices, adj_mat, 'nearest') + else: + smooth = utils.smoothing_matrix(vertices, adj_mat, 'nearest').toarray() + assert_allclose(smooth, want) diff --git a/surfer/utils.py b/surfer/utils.py index 8e3e22c..4a6e4cf 100644 --- a/surfer/utils.py +++ b/surfer/utils.py @@ -579,10 +579,36 @@ def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): smooth_mat : sparse matrix smoothing matrix with size N x len(vertices) """ + if smoothing_steps == 'nearest': + mat = _nearest(vertices, adj_mat) + else: + mat = _smooth(vertices, adj_mat, smoothing_steps) + return mat + + +def _nearest(vertices, adj_mat): + import scipy + from scipy.sparse.csgraph import dijkstra + if LooseVersion(scipy.__version__) < LooseVersion('1.3'): + raise RuntimeError('smoothing_steps="nearest" requires SciPy >= 1.3') + # Vertices can be out of order, so sort them to start ... + order = np.argsort(vertices) + vertices = vertices[order] + _, _, sources = dijkstra(adj_mat, False, indices=vertices, min_only=True, + return_predecessors=True) + col = np.searchsorted(vertices, sources) + # ... then get things back to the correct configuration. + col = order[col] + row = np.arange(len(col)) + data = np.ones(len(col)) + mat = sparse.coo_matrix((data, (row, col))) + assert mat.shape == (adj_mat.shape[0], len(vertices)), mat.shape + return mat + + +def _smooth(vertices, adj_mat, smoothing_steps): from scipy import sparse - logger.info("Updating smoothing matrix, be patient..") - e = adj_mat.copy() e.data[e.data == 2] = 1 n_vertices = e.shape[0] diff --git a/surfer/viz.py b/surfer/viz.py index 102482d..c4ed3a8 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1005,9 +1005,12 @@ def add_data(self, array, min=None, max=None, thresh=None, alpha level to control opacity of the overlay. vertices : numpy array vertices for which the data is defined (needed if len(data) < nvtx) - smoothing_steps : int or None - number of smoothing steps (smoothing is used if len(data) < nvtx) - Default : 20 + smoothing_steps : int | str | None + Number of smoothing steps (if data come from surface subsampling). + Can be None to use the fewest steps that result in all vertices + taking on data values, or "nearest" such that each high resolution + vertex takes the value of the its nearest (on the sphere) + low-resolution vertex. Default is 20. time : numpy array time points in the data array (if data is 2D or 3D) time_label : str | callable | None @@ -2114,13 +2117,17 @@ def data_time_index(self): raise RuntimeError("Brain instance has no data overlay") @verbose - def set_data_smoothing_steps(self, smoothing_steps, verbose=None): + def set_data_smoothing_steps(self, smoothing_steps=20, verbose=None): """Set the number of smoothing steps Parameters ---------- - smoothing_steps : int - Number of smoothing steps + smoothing_steps : int | str | None + Number of smoothing steps (if data come from surface subsampling). + Can be None to use the fewest steps that result in all vertices + taking on data values, or "nearest" such that each high resolution + vertex takes the value of the its nearest (on the sphere) + low-resolution vertex. Default is 20. verbose : bool, str, int, or None If not None, override default verbose level (see surfer.verbose). """ From 64a10695f5c3173f7805e9accd75e28713588be2 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 15 Oct 2019 12:08:43 -0400 Subject: [PATCH 12/46] MAINT: CircleCI for latest pip (#277) --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dd7ca25..0071f48 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,9 +47,9 @@ jobs: - run: name: Get Python running command: | - pip install --user -q --upgrade pip numpy - pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg sphinx-gallery - pip install --user -q --upgrade mayavi "https://api.github.com/repos/mne-tools/mne-python/zipball/master" + python -m pip install --user -q --upgrade pip numpy + python -m pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg sphinx-gallery + python -m pip install --user -q --upgrade mayavi "https://api.github.com/repos/mne-tools/mne-python/zipball/master" - save_cache: key: pip-cache paths: From e85753e35a2afa63cc7b232b4890ca9f9c8e0084 Mon Sep 17 00:00:00 2001 From: "Dingkun.Liu" Date: Fri, 1 Nov 2019 22:23:57 +0800 Subject: [PATCH 13/46] ENH: Add kwargs in Brain.add_* and pass it to mayavi modules (#276) * Feat: add kwargs in Brain.add_* and pass it to mayavi modules, so that we could directly manipulate mayavi display settings, like point resolution and overlay opacity. * Fix: Fix code styles which failed in the flake test. * Modify test_viz to pass the kwargs to the mayavi module. * fix flake test. * Specifying `kwargs` as `mlab_kws` and make the documentation more clear. * Revert "Specifying `kwargs` as `mlab_kws` and make the documentation more clear." This reverts commit c7a5912e593fa075fb658dda75f05d6a5009eb02. * Link mayavi documents. * Give detailed explanations about the kwargs passed to the functions. * Revert "Link mayavi documents." This reverts commit 2c86a04e18b7fbb5502e1d9255a831526286f5b7. * Change the position of line breaking. * Fix the typo and unavailable links. * Fix render errors. * Revert "Fix render errors." This reverts commit ac0b7b8466e62b01c83ace43b8e56be99e2f1410. * Fix render error. * Improve code rendering. * Revert "Fix render error." This reverts commit bbf76311b6d626cb33a973784dd21cb5f4580e8e. * Fix line breaking error. * Revert "Fix line breaking error." This reverts commit 6efccace7ea95defb397ba6d099c3d50c61c2acc. * Fix line breaking error. * Revert "Fix line breaking error." This reverts commit a7abf6174ef184a367f8277711ed34299f914ac4. * Force line breaking to fix the rendering error on long code. * Revert "Force line breaking to fix the rendering error on long code." This reverts commit 6a9cdf1b6455c7980ae98c22d0df0f0afd51a02e. * Force line breaking to fix the rendering error on long code. * Revert "Force line breaking to fix the rendering error on long code." This reverts commit c7f6a9fc33322ae01e9ed8ea7346e1e45212c282. * Force line breaking to fix the rendering error on long code. * Revert "Force line breaking to fix the rendering error on long code." This reverts commit de31e5fd7232d12bd360a4e8c0c0d22d0bd94296. * Fix render error Force line breaking to avoid render error. * Render undocumented function as plain code. * Fix sectioning. * FIX: Formatting --- .gitignore | 1 + doc/_static/navy.css | 6 ++- surfer/tests/test_viz.py | 10 ++-- surfer/viz.py | 102 +++++++++++++++++++++++++-------------- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 884e2b8..5ded8ed 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.orig *.mov build +.idea/ dist/ doc/_build/ diff --git a/doc/_static/navy.css b/doc/_static/navy.css index 4b1c4e7..79b3038 100755 --- a/doc/_static/navy.css +++ b/doc/_static/navy.css @@ -546,4 +546,8 @@ ul.keywordmatches li.goodmatch a { div.sphx-glr-footer-example a code span:last-child { font-size: unset; } -} \ No newline at end of file +} + +table.longtable.align-default { + width: 100%; +} diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index f62d1ca..46dfce6 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -153,7 +153,7 @@ def test_annot(): view = get_view(brain) for a, b, p in zip(annots, borders, alphas): - brain.add_annotation(a, b, p) + brain.add_annotation(a, b, p, opacity=0.8) check_view(brain, view) brain.set_surf('white') @@ -226,7 +226,11 @@ def test_foci(): coords = [[-36, 18, -3], [-43, 25, 24], [-48, 26, -2]] - brain.add_foci(coords, map_surface="white", color="gold", name='test1') + brain.add_foci(coords, + map_surface="white", + color="gold", + name='test1', + resolution=25) subj_dir = utils._get_subjects_dir() annot_path = pjoin(subj_dir, subject_id, 'label', 'lh.aparc.a2009s.annot') @@ -398,7 +402,7 @@ def test_overlay(): brain = Brain(*std_args) brain.add_overlay(overlay_file) brain.overlays["sig"].remove() - brain.add_overlay(overlay_file, min=5, max=20, sign="pos") + brain.add_overlay(overlay_file, min=5, max=20, sign="pos", opacity=0.7) sig1 = io.read_scalar_data(pjoin(data_dir, "lh.sig.nii.gz")) sig2 = io.read_scalar_data(pjoin(data_dir, "lh.alt_sig.nii.gz")) diff --git a/surfer/viz.py b/surfer/viz.py index c4ed3a8..2405c50 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -913,7 +913,7 @@ def _iter_time(self, time_idx, interpolation): ########################################################################### # ADDING DATA PLOTS def add_overlay(self, source, min=2, max="robust_max", sign="abs", - name=None, hemi=None): + name=None, hemi=None, **kwargs): """Add an overlay to the overlay dict from a file or array. Parameters @@ -932,6 +932,10 @@ def add_overlay(self, source, min=2, max="robust_max", sign="abs", If None, it is assumed to belong to the hemipshere being shown. If two hemispheres are being shown, an error will be thrown. + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. + """ hemi = self._check_hemi(hemi) # load data here @@ -944,7 +948,7 @@ def add_overlay(self, source, min=2, max="robust_max", sign="abs", views = self._toggle_render(False) for brain in self._brain_list: if brain['hemi'] == hemi: - ol.append(brain['brain'].add_overlay(old)) + ol.append(brain['brain'].add_overlay(old, **kwargs)) if name in self.overlays_dict: name = "%s%d" % (name, len(self.overlays_dict) + 1) self.overlays_dict[name] = ol @@ -957,7 +961,8 @@ def add_data(self, array, min=None, max=None, thresh=None, time_label="time index=%d", colorbar=True, hemi=None, remove_existing=False, time_label_size=14, initial_time=None, scale_factor=None, vector_alpha=None, - mid=None, center=None, transparent=False, verbose=None): + mid=None, center=None, transparent=False, verbose=None, + **kwargs): """Display data from a numpy array on the surface. This provides a similar interface to @@ -1038,6 +1043,9 @@ def add_data(self, array, min=None, max=None, thresh=None, vector-valued data. If None (default), ``alpha`` is used. verbose : bool, str, int, or None If not None, override default verbose level (see surfer.verbose). + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. Notes ----- @@ -1175,7 +1183,7 @@ def time_label(x): s, ct, bar, gl = brain['brain'].add_data( array, min, mid, max, thresh, lut, colormap, alpha, colorbar, layer_id, smooth_mat, magnitude, magnitude_max, - scale_factor, vertices, vector_alpha) + scale_factor, vertices, vector_alpha, **kwargs) surfs.append(s) bars.append(bar) glyphs.append(gl) @@ -1199,7 +1207,7 @@ def time_label(x): self._toggle_render(True, views) def add_annotation(self, annot, borders=True, alpha=1, hemi=None, - remove_existing=True, color=None): + remove_existing=True, color=None, **kwargs): """Add an annotation file. Parameters @@ -1226,7 +1234,9 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, color : matplotlib-style color code If used, show all annotations in the same (specified) color. Probably useful only when showing annotation borders. - + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. """ hemis = self._check_hemis(hemi) @@ -1308,11 +1318,12 @@ def add_annotation(self, annot, borders=True, alpha=1, hemi=None, for brain in self._brain_list: if brain['hemi'] == hemi: self.annot_list.append( - brain['brain'].add_annotation(annot, ids, cmap)) + brain['brain'].add_annotation(annot, ids, cmap, + **kwargs)) self._toggle_render(True, views) def add_label(self, label, color=None, alpha=1, scalar_thresh=None, - borders=False, hemi=None, subdir=None): + borders=False, hemi=None, subdir=None, **kwargs): """Add an ROI label to the image. Parameters @@ -1344,6 +1355,9 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, label directory rather than in the label directory itself (e.g. for ``$SUBJECTS_DIR/$SUBJECT/label/aparc/lh.cuneus.label`` ``brain.add_label('cuneus', subdir='aparc')``). + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. Notes ----- @@ -1425,7 +1439,7 @@ def add_label(self, label, color=None, alpha=1, scalar_thresh=None, for brain in self.brains: if brain.hemi == hemi: array_id, surf = brain.add_label(label, label_name, color, - alpha) + alpha, **kwargs) surfaces.append(surf) array_ids.append((brain, array_id)) self._label_dicts[label_name] = {'surfaces': surfaces, @@ -1535,7 +1549,7 @@ def remove_labels(self, labels=None, hemi=None): def add_morphometry(self, measure, grayscale=False, hemi=None, remove_existing=True, colormap=None, - min=None, max=None, colorbar=True): + min=None, max=None, colorbar=True, **kwargs): """Add a morphometry overlay to the image. Parameters @@ -1557,7 +1571,9 @@ def add_morphometry(self, measure, grayscale=False, hemi=None, of the data is used. colorbar : bool If True, show a colorbar corresponding to the overlay data. - + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. """ hemis = self._check_hemis(hemi) morph_files = [] @@ -1618,12 +1634,13 @@ def add_morphometry(self, measure, grayscale=False, hemi=None, for brain in self.brains: if brain.hemi == hemi: self.morphometry_list.append(brain.add_morphometry( - morph_data, colormap, measure, min, max, colorbar)) + morph_data, colormap, measure, min, max, colorbar, + **kwargs)) self._toggle_render(True, views) def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, - hemi=None): + hemi=None, **kwargs): """Add spherical foci, possibly mapping to displayed surf. The foci spheres can be displayed at the coordinates given, or @@ -1653,6 +1670,9 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, If None, it is assumed to belong to the hemipshere being shown. If two hemispheres are being shown, an error will be thrown. + **kwargs : additional keyword arguments + These are passed to the underlying + :func:`mayavi.mlab.points3d` call. """ from matplotlib.colors import colorConverter hemi = self._check_hemi(hemi) @@ -1688,13 +1708,15 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, for brain in self._brain_list: if brain['hemi'] == hemi: fl.append(brain['brain'].add_foci(foci_coords, scale_factor, - color, alpha, name)) + color, alpha, name, + **kwargs)) self.foci_dict[name] = fl self._toggle_render(True, views) def add_contour_overlay(self, source, min=None, max=None, n_contours=7, line_width=1.5, colormap="YlOrRd_r", - hemi=None, remove_existing=True, colorbar=True): + hemi=None, remove_existing=True, colorbar=True, + **kwargs): """Add a topographic contour overlay of the positive data. Note: This visualization will look best when using the "low_contrast" @@ -1724,7 +1746,9 @@ def add_contour_overlay(self, source, min=None, max=None, If there is an existing contour overlay, remove it before plotting. colorbar : bool If True, show the colorbar for the scalar value. - + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.contour_surface`` call. """ hemi = self._check_hemi(hemi) @@ -1751,11 +1775,11 @@ def add_contour_overlay(self, source, min=None, max=None, if brain.hemi == hemi: self.contour_list.append(brain.add_contour_overlay( scalar_data, min, max, n_contours, line_width, lut, - colorbar)) + colorbar, **kwargs)) self._toggle_render(True, views) def add_text(self, x, y, text, name, color=None, opacity=1.0, - row=-1, col=-1, font_size=None, justification=None): + row=-1, col=-1, font_size=None, justification=None, **kwargs): """ Add a text to the visualization Parameters @@ -1778,11 +1802,15 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, Row index of which brain to use col : int Column index of which brain to use + **kwargs : additional keyword arguments + These are passed to the underlying + :func:`mayavi.mlab.text3d` call. """ if name in self.texts_dict: self.texts_dict[name]['text'].remove() text = self.brain_matrix[row, col].add_text(x, y, text, - name, color, opacity) + name, color, opacity, + **kwargs) self.texts_dict[name] = dict(row=row, col=col, text=text) if font_size is not None: text.property.font_size = font_size @@ -3203,7 +3231,7 @@ def _remove_vector_data(self, glyphs): if glyphs is not None: glyphs.parent.parent.remove() - def add_overlay(self, old): + def add_overlay(self, old, **kwargs): """Add an overlay to the overlay dict from a file or array""" array_id, mesh = self._add_scalar_data(old.mlab_data) @@ -3213,7 +3241,7 @@ def add_overlay(self, old): pos = mlab.pipeline.surface( pos_thresh, colormap="YlOrRd", figure=self._f, vmin=old.pos_lims[1], vmax=old.pos_lims[2], - reset_zoom=False) + reset_zoom=False, **kwargs) pos.actor.property.backface_culling = False pos_bar = mlab.scalarbar(pos, nb_labels=5) pos_bar.reverse_lut = True @@ -3229,7 +3257,7 @@ def add_overlay(self, old): neg = mlab.pipeline.surface( neg_thresh, colormap="PuBu", figure=self._f, vmin=old.neg_lims[1], vmax=old.neg_lims[2], - reset_zoom=False) + reset_zoom=False, **kwargs) neg.actor.property.backface_culling = False neg_bar = mlab.scalarbar(neg, nb_labels=5) neg_bar.scalar_bar_representation.position = (0.05, 0.01) @@ -3243,7 +3271,7 @@ def add_overlay(self, old): @verbose def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, colorbar, layer_id, smooth_mat, magnitude, magnitude_max, - scale_factor, vertices, vector_alpha): + scale_factor, vertices, vector_alpha, **kwargs): """Add data to the brain""" # Calculate initial data to plot if array.ndim == 1: @@ -3285,7 +3313,8 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, with warnings.catch_warnings(record=True): surf = mlab.pipeline.surface( pipe, colormap=colormap, vmin=fmin, vmax=fmax, - opacity=float(alpha), figure=self._f, reset_zoom=False) + opacity=float(alpha), figure=self._f, reset_zoom=False, + **kwargs) surf.actor.property.backface_culling = False # apply look up table if given @@ -3310,13 +3339,13 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, scale_factor_norm=scale_factor_norm) return surf, orig_ctable, bar, glyphs - def add_annotation(self, annot, ids, cmap): + def add_annotation(self, annot, ids, cmap, **kwargs): """Add an annotation file""" # Add scalar values to dataset array_id, pipe = self._add_scalar_data(ids) with warnings.catch_warnings(record=True): surf = mlab.pipeline.surface(pipe, name=annot, figure=self._f, - reset_zoom=False) + reset_zoom=False, **kwargs) surf.actor.property.backface_culling = False # Set the color table @@ -3327,13 +3356,13 @@ def add_annotation(self, annot, ids, cmap): return dict(surface=surf, name=annot, colormap=cmap, brain=self, array_id=array_id) - def add_label(self, label, label_name, color, alpha): + def add_label(self, label, label_name, color, alpha, **kwargs): """Add an ROI label to the image""" from matplotlib.colors import colorConverter array_id, pipe = self._add_scalar_data(label) with warnings.catch_warnings(record=True): surf = mlab.pipeline.surface(pipe, name=label_name, figure=self._f, - reset_zoom=False) + reset_zoom=False, **kwargs) surf.actor.property.backface_culling = False color = colorConverter.to_rgba(color, alpha) cmap = np.array([(0, 0, 0, 0,), color]) @@ -3345,13 +3374,13 @@ def add_label(self, label, label_name, color, alpha): return array_id, surf def add_morphometry(self, morph_data, colormap, measure, - min, max, colorbar): + min, max, colorbar, **kwargs): """Add a morphometry overlay to the image""" array_id, pipe = self._add_scalar_data(morph_data) with warnings.catch_warnings(record=True): surf = mlab.pipeline.surface( pipe, colormap=colormap, vmin=min, vmax=max, name=measure, - figure=self._f, reset_zoom=False) + figure=self._f, reset_zoom=False, **kwargs) # Get the colorbar if colorbar: @@ -3365,7 +3394,8 @@ def add_morphometry(self, morph_data, colormap, measure, return dict(surface=surf, colorbar=bar, measure=measure, brain=self, array_id=array_id) - def add_foci(self, foci_coords, scale_factor, color, alpha, name): + def add_foci(self, foci_coords, scale_factor, color, alpha, name, + **kwargs): """Add spherical foci, possibly mapping to displayed surf""" # Create the visualization with warnings.catch_warnings(record=True): # traits @@ -3373,19 +3403,19 @@ def add_foci(self, foci_coords, scale_factor, color, alpha, name): foci_coords[:, 0], foci_coords[:, 1], foci_coords[:, 2], np.ones(foci_coords.shape[0]), name=name, figure=self._f, scale_factor=(10. * scale_factor), color=color, opacity=alpha, - reset_zoom=False) + reset_zoom=False, **kwargs) return points def add_contour_overlay(self, scalar_data, min=None, max=None, n_contours=7, line_width=1.5, lut=None, - colorbar=True): + colorbar=True, **kwargs): """Add a topographic contour overlay of the positive data""" array_id, pipe = self._add_scalar_data(scalar_data) with warnings.catch_warnings(record=True): thresh = threshold_filter(pipe, low=min) surf = mlab.pipeline.contour_surface( thresh, contours=n_contours, line_width=line_width, - reset_zoom=False) + reset_zoom=False, **kwargs) if lut is not None: l_m = surf.module_manager.scalar_lut_manager l_m.load_lut_from_list(lut / 255.) @@ -3403,12 +3433,12 @@ def add_contour_overlay(self, scalar_data, min=None, max=None, # Set up a dict attribute with pointers at important things return dict(surface=surf, colorbar=bar, brain=self, array_id=array_id) - def add_text(self, x, y, text, name, color=None, opacity=1.0): + def add_text(self, x, y, text, name, color=None, opacity=1.0, **kwargs): """ Add a text to the visualization""" color = self._fg_color if color is None else color with warnings.catch_warnings(record=True): text = mlab.text(x, y, text, name=name, color=color, - opacity=opacity, figure=self._f) + opacity=opacity, figure=self._f, **kwargs) return text def remove_data(self, layer_id): From 0e1da27ac77de470e2cff3ebcb178ca9ab177029 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 10 Dec 2019 10:05:12 -0500 Subject: [PATCH 14/46] BUG: Properly clean up on del --- surfer/viz.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index 2405c50..094fc37 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2298,14 +2298,12 @@ def close(self): _force_render([]) # should we tear down other variables? - if self._v is not None: + if getattr(self, '_v', None) is not None: self._v.dispose() self._v = None def __del__(self): - if hasattr(self, '_v') and self._v is not None: - self._v.dispose() - self._v = None + self.close() ########################################################################### # SAVING OUTPUT From e79624d5bc53fd6763cbcf73df6fd9c8fc5b7bcc Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 6 Feb 2020 14:38:51 -0500 Subject: [PATCH 15/46] FIX: Safer exit --- surfer/viz.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index 094fc37..b13b21a 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2290,20 +2290,30 @@ def hide_colorbar(self, row=-1, col=-1): def close(self): """Close all figures and cleanup data structure.""" + + def _close(self, force_render=True): for ri, ff in enumerate(self._figures): for ci, f in enumerate(ff): if f is not None: - mlab.close(f) + try: + mlab.close(f) + except Exception: + pass self._figures[ri][ci] = None - _force_render([]) + + if force_render: + _force_render([]) # should we tear down other variables? if getattr(self, '_v', None) is not None: - self._v.dispose() + try: + self._v.dispose() + except Exception: + pass self._v = None def __del__(self): - self.close() + self._close(force_render=False) ########################################################################### # SAVING OUTPUT From 78950ed3690be87915ca9ccc92c4fcdb30e292b8 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 7 Feb 2020 09:35:06 -0500 Subject: [PATCH 16/46] FIX: Actually close --- surfer/io.py | 2 +- surfer/tests/test_viz.py | 2 ++ surfer/viz.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/surfer/io.py b/surfer/io.py index 8ddcf69..efda923 100644 --- a/surfer/io.py +++ b/surfer/io.py @@ -29,7 +29,7 @@ def read_scalar_data(filepath): flat numpy array of scalar data """ try: - scalar_data = nib.load(filepath).get_data() + scalar_data = np.asanyarray(nib.load(filepath).dataobj) scalar_data = np.ravel(scalar_data, order="F") return scalar_data diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 46dfce6..6a17762 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -87,6 +87,8 @@ def test_image(tmpdir): brain.close() brain = Brain(*std_args, size=100) + for b in brain.brain_matrix.ravel(): + assert b._f.scene is not None brain.save_image(tmp_name) brain.save_image(tmp_name, 'rgba', True) brain.screenshot() diff --git a/surfer/viz.py b/surfer/viz.py index b13b21a..6c4ddd8 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2290,6 +2290,7 @@ def hide_colorbar(self, row=-1, col=-1): def close(self): """Close all figures and cleanup data structure.""" + self._close() def _close(self, force_render=True): for ri, ff in enumerate(self._figures): From e79e15d128fe113f566db34c3b7c1d2a6209d923 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 7 Feb 2020 09:45:22 -0500 Subject: [PATCH 17/46] FIX: Try again --- surfer/viz.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/surfer/viz.py b/surfer/viz.py index 6c4ddd8..0067974 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1,4 +1,5 @@ from copy import deepcopy +import gc import logging from math import floor import os @@ -2312,6 +2313,7 @@ def _close(self, force_render=True): except Exception: pass self._v = None + gc.collect() def __del__(self): self._close(force_render=False) From 0b20e8b84a5935c5167ac1c03c5a96ecc4d0aa92 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 7 Feb 2020 09:54:37 -0500 Subject: [PATCH 18/46] FIX: Again again --- surfer/tests/test_viz.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 6a17762..70dd9c2 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -1,3 +1,4 @@ +import gc import os import os.path as op from os.path import join as pjoin @@ -85,6 +86,7 @@ def test_image(tmpdir): brain.add_overlay(overlay_fname, hemi='lh', min=5, max=20, sign="pos") brain.save_imageset(tmp_name, ['med', 'lat'], 'jpg') brain.close() + del brain brain = Brain(*std_args, size=100) for b in brain.brain_matrix.ravel(): @@ -151,6 +153,7 @@ def test_annot(): annots = ['aparc', 'aparc.a2005s'] borders = [True, False, 2] alphas = [1, 0.5] + gc.collect() brain = Brain(*std_args) view = get_view(brain) From 1435b57b3a40e67257a983e785c39b71d28da45e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 7 Feb 2020 10:05:11 -0500 Subject: [PATCH 19/46] FIX: Different --- surfer/tests/test_viz.py | 30 +++++++----------------------- surfer/viz.py | 10 ++-------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 70dd9c2..8d355be 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -3,7 +3,6 @@ import os.path as op from os.path import join as pjoin import sys -import warnings import pytest from mayavi import mlab @@ -17,8 +16,6 @@ from surfer.utils import (requires_fsaverage, requires_imageio, requires_fs, _get_extra) -warnings.simplefilter('always') - subject_id = 'fsaverage' std_args = [subject_id, 'lh', 'inflated'] data_dir = pjoin(op.dirname(__file__), '..', '..', 'examples', 'example_data') @@ -85,21 +82,12 @@ def test_image(tmpdir): brain = Brain(subject_id, 'both', surf=surf, size=100) brain.add_overlay(overlay_fname, hemi='lh', min=5, max=20, sign="pos") brain.save_imageset(tmp_name, ['med', 'lat'], 'jpg') - brain.close() - del brain - - brain = Brain(*std_args, size=100) - for b in brain.brain_matrix.ravel(): - assert b._f.scene is not None brain.save_image(tmp_name) brain.save_image(tmp_name, 'rgba', True) brain.screenshot() - if os.getenv('TRAVIS', '') != 'true': - # for some reason these fail on Travis sometimes - brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='v') - brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='h') - brain.save_montage(tmp_name, [['l', 'v'], ['m', 'f']]) - brain.close() + brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='v') + brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='h') + brain.save_montage(tmp_name, [['l', 'v'], ['m', 'f']]) @requires_fsaverage() @@ -108,8 +96,7 @@ def test_brains(): # testing backend breaks when passing in a figure, so we use 'auto' here # (shouldn't affect usability, but it makes testing more annoying) _set_backend('auto') - with warnings.catch_warnings(record=True): # traits for mlab.figure() - mlab.figure(101) + mlab.figure(101) surfs = ['inflated', 'white', 'white', 'white', 'white', 'white', 'white'] hemis = ['lh', 'rh', 'both', 'both', 'rh', 'both', 'both'] titles = [None, 'Hello', 'Good bye!', 'lut test', @@ -122,8 +109,7 @@ def test_brains(): (0.2, 0.2, 0.2), "black", "0.75"] foregrounds = ["black", "white", "0.75", "red", (0.2, 0.2, 0.2), "blue", "black"] - with warnings.catch_warnings(record=True): # traits for mlab.figure() - figs = [101, mlab.figure(), None, None, mlab.figure(), None, None] + figs = [101, mlab.figure(), None, None, mlab.figure(), None, None] subj_dir = utils._get_subjects_dir() subj_dirs = [None, subj_dir, subj_dir, subj_dir, subj_dir, subj_dir, subj_dir] @@ -153,7 +139,6 @@ def test_annot(): annots = ['aparc', 'aparc.a2005s'] borders = [True, False, 2] alphas = [1, 0.5] - gc.collect() brain = Brain(*std_args) view = get_view(brain) @@ -463,9 +448,8 @@ def test_probabilistic_labels(): prob_field[ids] = probs brain.add_data(prob_field, thresh=1e-5) - with warnings.catch_warnings(record=True): - brain.data["colorbar"].number_of_colors = 10 - brain.data["colorbar"].number_of_labels = 11 + brain.data["colorbar"].number_of_colors = 10 + brain.data["colorbar"].number_of_labels = 11 brain.close() diff --git a/surfer/viz.py b/surfer/viz.py index 0067974..45e2ea3 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1,5 +1,4 @@ from copy import deepcopy -import gc import logging from math import floor import os @@ -2291,9 +2290,6 @@ def hide_colorbar(self, row=-1, col=-1): def close(self): """Close all figures and cleanup data structure.""" - self._close() - - def _close(self, force_render=True): for ri, ff in enumerate(self._figures): for ci, f in enumerate(ff): if f is not None: @@ -2303,8 +2299,7 @@ def _close(self, force_render=True): pass self._figures[ri][ci] = None - if force_render: - _force_render([]) + _force_render([]) # should we tear down other variables? if getattr(self, '_v', None) is not None: @@ -2313,10 +2308,9 @@ def _close(self, force_render=True): except Exception: pass self._v = None - gc.collect() def __del__(self): - self._close(force_render=False) + self.close() ########################################################################### # SAVING OUTPUT From 2cf1d7381ee16f76e2e4d59293fbd0f0a2d06a0d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 7 Feb 2020 10:29:41 -0500 Subject: [PATCH 20/46] FIX: One more --- surfer/tests/test_viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 8d355be..3f7ced5 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -1,4 +1,3 @@ -import gc import os import os.path as op from os.path import join as pjoin @@ -88,6 +87,7 @@ def test_image(tmpdir): brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='v') brain.save_montage(tmp_name, ['l', 'v', 'm'], orientation='h') brain.save_montage(tmp_name, [['l', 'v'], ['m', 'f']]) + brain.close() @requires_fsaverage() From 5aaf0430e43c700dc358ad946e193f78d1137ad4 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 10 Feb 2020 10:41:50 -0500 Subject: [PATCH 21/46] FIX: Dont reuse by default --- surfer/tests/test_viz.py | 13 +++++++++++++ surfer/viz.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 3f7ced5..5bb6b18 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -90,6 +90,19 @@ def test_image(tmpdir): brain.close() +@requires_fsaverage() +def test_brain_separate(): + """Test that Brain does not reuse existing figures by default.""" + _set_backend('auto') + brain = Brain(*std_args) + assert brain.brain_matrix.size == 1 + brain_2 = Brain(*std_args) + assert brain_2.brain_matrix.size == 1 + assert brain._figures[0][0] is not brain_2._figures[0][0] + brain_3 = Brain(*std_args, figure=brain._figures[0][0]) + assert brain._figures[0][0] is brain_3._figures[0][0] + + @requires_fsaverage() def test_brains(): """Test plotting of Brain with different arguments.""" diff --git a/surfer/viz.py b/surfer/viz.py index 45e2ea3..f600d69 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -217,8 +217,8 @@ def _make_viewer(figure, n_row, n_col, title, scene_size, offscreen, # Triage: don't make TraitsUI if we don't have to if n_row == 1 and n_col == 1: with warnings.catch_warnings(record=True): # traits - figure = mlab.figure(title, size=(w, h)) - mlab.clf(figure) + figure = mlab.figure(size=(w, h)) + figure.name = title # should set the figure title figures = [[figure]] _v = None else: From dd9f3b5d2225dc78b339d61c7e7bc4d5316fc6ea Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 11 Feb 2020 18:06:59 +0000 Subject: [PATCH 22/46] Fix (#286) --- surfer/viz.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/surfer/viz.py b/surfer/viz.py index f600d69..2b46ff1 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -3322,6 +3322,11 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, **kwargs) surf.actor.property.backface_culling = False + # There is a bug on some graphics cards concerning transparant + # overlays that is fixed by setting force_opaque. + if float(alpha) == 1: + surf.actor.actor.force_opaque = True + # apply look up table if given if lut is not None: l_m = surf.module_manager.scalar_lut_manager From cd2f074fe3c458c02fa9fd2d57e332ef64b8d179 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 21 Feb 2020 07:31:29 -0500 Subject: [PATCH 23/46] MRG, FIX: Fix bug with scale_data_colormap (#287) * FIX: Fix bug with scale_data_colormap * FIX: Fix for deprecation * FIX: One more --- setup.cfg | 2 ++ surfer/tests/test_viz.py | 10 +++++++--- surfer/viz.py | 20 +++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9bc24df..b869886 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,4 +12,6 @@ filterwarnings = ignore:.*ufunc size changed.*:RuntimeWarning ignore:Using or importing the ABCs:DeprecationWarning ignore:the imp module is deprecated in favour of importlib:DeprecationWarning + ignore:.*trait handler has been deprecated.*:DeprecationWarning + ignore:.*rich_compare.*metadata.*deprecated.*:DeprecationWarning ignore:Matplotlib is building the font cache using fc-list. This may take a moment.:UserWarning diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 5bb6b18..206d1f2 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -214,10 +214,14 @@ def test_data(): def test_data_limits(): """Test handling of data limits.""" _set_backend() - brain = Brain(*std_args) - surf_data = np.zeros(163842) + brain = Brain('fsaverage', 'both', 'inflated') + surf_data = np.linspace(0, 1, 163842) pytest.raises(ValueError, brain.add_data, surf_data, 0, 0) - brain.add_data(surf_data, 0, 1) + brain.add_data(surf_data, 0, 1, hemi='lh') + assert brain.data_dict['lh']['fmax'] == 1. + brain.add_data(surf_data, 0, 0.5, hemi='rh') + assert brain.data_dict['lh']['fmax'] == 1. # unmodified + assert brain.data_dict['rh']['fmax'] == 0.5 brain.close() diff --git a/surfer/viz.py b/surfer/viz.py index 2b46ff1..846d3e5 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -1200,7 +1200,7 @@ def time_label(x): self._data_dicts[hemi].append(data) self.scale_data_colormap(min, mid, max, transparent, center, alpha, - data) + data, hemi=hemi) if initial_time_index is not None: self.set_data_time_index(initial_time_index) @@ -1951,7 +1951,8 @@ def _brain_color(self): @verbose def scale_data_colormap(self, fmin, fmid, fmax, transparent, - center=None, alpha=1.0, data=None, verbose=None): + center=None, alpha=1.0, data=None, + hemi=None, verbose=None): """Scale the data colormap. The colormap may be sequential or divergent. When the colormap is @@ -1994,15 +1995,19 @@ def scale_data_colormap(self, fmin, fmid, fmax, transparent, The data entry for which to scale the colormap. If None, will use the data dict from either the left or right hemisphere (in that order). + hemi : str | None + If None, all hemispheres will be scaled. verbose : bool, str, int, or None If not None, override default verbose level (see surfer.verbose). """ divergent = center is not None + hemis = self._check_hemis(hemi) + del hemi # Get the original colormap if data is None: - for h in ['lh', 'rh']: - data = self.data_dict[h] + for hemi in hemis: + data = self.data_dict[hemi] if data is not None: break table = data["orig_ctable"].copy() @@ -2015,14 +2020,15 @@ def scale_data_colormap(self, fmin, fmid, fmax, transparent, views = self._toggle_render(False) # Use the new colormap - for hemi in ['lh', 'rh']: + for hemi in hemis: data = self.data_dict[hemi] if data is not None: for surf in data['surfaces']: cmap = surf.module_manager.scalar_lut_manager cmap.load_lut_from_list(lut / 255.) if divergent: - cmap.data_range = np.array([center-fmax, center+fmax]) + cmap.data_range = np.array( + [center - fmax, center + fmax]) else: cmap.data_range = np.array([fmin, fmax]) @@ -2050,7 +2056,7 @@ def scale_data_colormap(self, fmin, fmid, fmax, transparent, l_m.load_lut_from_list(lut / 255.) if divergent: l_m.data_range = np.array( - [center-fmax, center+fmax]) + [center - fmax, center + fmax]) else: l_m.data_range = np.array([fmin, fmax]) From 6962fe93522a34cdc806f4517407db5a1a6ac936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-R=C3=A9mi=20KING?= Date: Fri, 21 Feb 2020 13:38:51 +0100 Subject: [PATCH 24/46] fix int (#288) --- surfer/viz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index 846d3e5..df6763b 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2909,8 +2909,8 @@ def _get_fill_colors(cols, n_fill): if ind.size > 0: # choose the two colors between which there is the large step ind = ind[0] + 1 - fillcols = np.r_[np.tile(cols[ind, :], (n_fill / 2, 1)), - np.tile(cols[ind + 1, :], (n_fill - n_fill / 2, 1))] + fillcols = np.r_[np.tile(cols[ind, :], (n_fill // 2, 1)), + np.tile(cols[ind + 1, :], (n_fill - n_fill // 2, 1))] else: # choose a color from the middle of the colormap fillcols = np.tile(cols[int(cols.shape[0] / 2), :], (n_fill, 1)) From f5761451fb9d98b33a06dbdaea4e72c6ffe6c149 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 21 Feb 2020 07:39:39 -0500 Subject: [PATCH 25/46] Update __init__.py --- surfer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/__init__.py b/surfer/__init__.py index 5af068d..d5dee54 100644 --- a/surfer/__init__.py +++ b/surfer/__init__.py @@ -2,7 +2,7 @@ from .utils import Surface, verbose, set_log_level, set_log_file # noqa from .io import project_volume_data # noqa -__version__ = "0.10.dev0" +__version__ = "0.10.0" set_log_file() # initialize handlers set_log_level() # initialize logging level From bd2bbfe397ca9a261e42ce26186d91bda3fefc73 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 21 Feb 2020 07:40:26 -0500 Subject: [PATCH 26/46] Update __init__.py --- surfer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/__init__.py b/surfer/__init__.py index d5dee54..54f3e64 100644 --- a/surfer/__init__.py +++ b/surfer/__init__.py @@ -2,7 +2,7 @@ from .utils import Surface, verbose, set_log_level, set_log_file # noqa from .io import project_volume_data # noqa -__version__ = "0.10.0" +__version__ = "0.11.dev0" set_log_file() # initialize handlers set_log_level() # initialize logging level From f67f4443c96025387b9e3adedf01a23e3d8d4714 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 21 Feb 2020 08:01:14 -0500 Subject: [PATCH 27/46] FIX: CSS --- doc/_static/navy.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/_static/navy.css b/doc/_static/navy.css index 79b3038..b8d38e7 100755 --- a/doc/_static/navy.css +++ b/doc/_static/navy.css @@ -514,9 +514,11 @@ ul.keywordmatches li.goodmatch a { } .sphx-glr-thumbcontainer .figure { width: 250px !important; + max-width: 250px !important; } .sphx-glr-thumbcontainer img { max-height: 250px !important; + max-width: 250px !important; width: 250px !important; } .sphx-glr-thumbcontainer a.internal { From 696730654fc882c6a8b5a4bd48193904639c8849 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 21 Feb 2020 10:12:14 -0500 Subject: [PATCH 28/46] FIX: Update changes --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index fae96a9..2da1611 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,10 @@ Changelog Development version (0.10.dev0) ------------------------------- + +Version 0.10.0 +-------------- + - Added an option to smooth to nearest vertex in :meth:`Brain.add_data` using ``smoothing_steps='nearest'`` - Added options for using offscreen mode From 7952423aab1fb59e048a4606d6f8411a513f3f3c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:01:41 -0500 Subject: [PATCH 29/46] Set up CI with Azure Pipelines [skip ci] --- azure-pipelines.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..aa91291 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,19 @@ +# Starter pipeline +# Start with a minimal pipeline that you can customize to build and deploy your code. +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +trigger: +- master + +pool: + vmImage: 'ubuntu-latest' + +steps: +- script: echo Hello, world! + displayName: 'Run a one-line script' + +- script: | + echo Add other tasks to build, test, and deploy your project. + echo See https://aka.ms/yaml + displayName: 'Run a multi-line script' From 9c43880da987e1c7bc03348928603f4aecec572a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 10:34:42 -0500 Subject: [PATCH 30/46] FIX: Remove force render --- surfer/viz.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index df6763b..935b0f8 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2296,6 +2296,9 @@ def hide_colorbar(self, row=-1, col=-1): def close(self): """Close all figures and cleanup data structure.""" + self._close() + + def _close(self, force_render=True): for ri, ff in enumerate(self._figures): for ci, f in enumerate(ff): if f is not None: @@ -2305,7 +2308,8 @@ def close(self): pass self._figures[ri][ci] = None - _force_render([]) + if force_render: + _force_render([]) # should we tear down other variables? if getattr(self, '_v', None) is not None: @@ -2316,7 +2320,8 @@ def close(self): self._v = None def __del__(self): - self.close() + # Forcing the GUI updates during GC seems to be problematic + self.close(force_render=False) ########################################################################### # SAVING OUTPUT From 38db2038b8de9c0fdc33cc511b81f8885391e28d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:10:55 -0500 Subject: [PATCH 31/46] ENH: Azure --- azure-pipelines.yml | 65 +++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index aa91291..cc162c3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,19 +1,50 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - trigger: -- master - -pool: - vmImage: 'ubuntu-latest' - -steps: -- script: echo Hello, world! - displayName: 'Run a one-line script' + # start a new build for every push + batch: False + branches: + include: + - master -- script: | - echo Add other tasks to build, test, and deploy your project. - echo See https://aka.ms/yaml - displayName: 'Run a multi-line script' +jobs: +- job: Windows + pool: + vmIMage: 'VS2017-Win2016' + strategy: + maxParallel: 4 + matrix: + Python37-64bit: + PYTHON_VERSION: '3.7' + PYTHON_ARCH: 'x64' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(PYTHON_VERSION) + architecture: $(PYTHON_ARCH) + addToPath: true + - powershell: | + pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9 + pip install traits traitsui pyface vtk mayavi nibabel + displayName: 'Install pip dependencies' + - powershell: | + powershell make/get_fsaverage.ps1 + $env:SUBJECTS_DIR = '$(System.DefaultWorkingDirectory)' + '\subjects' + Write-Host ("##vso[task.setvariable variable=PATH]" + $env:PATH) SET SUBJECTS_DIR=%CD%\\subjects" + displayName: 'Get fsaverage' + - powershell: | + git clone --depth 1 git://github.com/pyvista/gl-ci-helpers.git + powershell gl-ci-helpers/appveyor/install_opengl.ps1 + displayName: 'Get OpenGL' + - script: python setup.py develop + displayName: 'Install' + - script: pytest surfer --cov=surfer -v + displayName: 'Run tests' + - script: codecov --root %BUILD_REPOSITORY_LOCALPATH% -t %CODECOV_TOKEN% + displayName: 'Codecov' + env: + CODECOV_TOKEN: $(CODECOV_TOKEN) + condition: always() + - task: PublishTestResults@2 + inputs: + testResultsFiles: 'junit-*.xml' + testRunTitle: 'Publish test results for Python $(python.version)' + condition: always() From b7f25dba1fe53a1417a4722c4f87d0fc76ffd38c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:15:37 -0500 Subject: [PATCH 32/46] FIX: Missed --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index cc162c3..a4b0840 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,7 +28,7 @@ jobs: - powershell: | powershell make/get_fsaverage.ps1 $env:SUBJECTS_DIR = '$(System.DefaultWorkingDirectory)' + '\subjects' - Write-Host ("##vso[task.setvariable variable=PATH]" + $env:PATH) SET SUBJECTS_DIR=%CD%\\subjects" + Write-Host ("##vso[task.setvariable variable=PATH]" + $env:PATH) displayName: 'Get fsaverage' - powershell: | git clone --depth 1 git://github.com/pyvista/gl-ci-helpers.git From 963c9b655704bb0257f95836d90e034a441068d2 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:22:12 -0500 Subject: [PATCH 33/46] FIX: Missed again --- azure-pipelines.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a4b0840..1b25558 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,6 +7,8 @@ trigger: jobs: - job: Windows + variables: + PIP_CACHE_FOLDER: $(Pipeline.Workspace)/.cache/pip pool: vmIMage: 'VS2017-Win2016' strategy: @@ -21,14 +23,19 @@ jobs: versionSpec: $(PYTHON_VERSION) architecture: $(PYTHON_ARCH) addToPath: true +- task: Cache@2 + inputs: + key: 'python -VV' + path: $(PIP_CACHE_FOLDER) + displayName: Cache pip packages - powershell: | - pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9 - pip install traits traitsui pyface vtk mayavi nibabel + pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9 --cache-dir $(PIP_CACHE_FOLDER) + pip install traits traitsui pyface vtk mayavi nibabel --cache-dir $(PIP_CACHE_FOLDER) displayName: 'Install pip dependencies' - powershell: | powershell make/get_fsaverage.ps1 $env:SUBJECTS_DIR = '$(System.DefaultWorkingDirectory)' + '\subjects' - Write-Host ("##vso[task.setvariable variable=PATH]" + $env:PATH) + Write-Host ("##vso[task.setvariable variable=SUBJECTS_DIR]" + $env:SUBJECTS_DIR) displayName: 'Get fsaverage' - powershell: | git clone --depth 1 git://github.com/pyvista/gl-ci-helpers.git From b3522d472f87a3dfa86396c25d1b022a81d3d2d8 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:29:01 -0500 Subject: [PATCH 34/46] FIX: Try again --- azure-pipelines.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1b25558..7d98a8e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,11 +23,11 @@ jobs: versionSpec: $(PYTHON_VERSION) architecture: $(PYTHON_ARCH) addToPath: true -- task: Cache@2 - inputs: - key: 'python -VV' - path: $(PIP_CACHE_FOLDER) - displayName: Cache pip packages + - task: Cache@2 + inputs: + key: 'pip' + path: $(PIP_CACHE_FOLDER) + displayName: Cache pip packages - powershell: | pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9 --cache-dir $(PIP_CACHE_FOLDER) pip install traits traitsui pyface vtk mayavi nibabel --cache-dir $(PIP_CACHE_FOLDER) From 11afc4ae48623d9ff04872ba573f892dd4d7a831 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:49:21 -0500 Subject: [PATCH 35/46] FIX: Wrong call --- surfer/viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/viz.py b/surfer/viz.py index 935b0f8..8df843c 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2321,7 +2321,7 @@ def _close(self, force_render=True): def __del__(self): # Forcing the GUI updates during GC seems to be problematic - self.close(force_render=False) + self._close(force_render=False) ########################################################################### # SAVING OUTPUT From 283a5800228176c80bec1a83f1b7d80229e8a9b0 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 26 Feb 2020 11:56:55 -0500 Subject: [PATCH 36/46] FIX: Explicit close/del --- surfer/tests/test_viz.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/surfer/tests/test_viz.py b/surfer/tests/test_viz.py index 206d1f2..ac96fe2 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -1,3 +1,4 @@ +import gc import os import os.path as op from os.path import join as pjoin @@ -210,6 +211,17 @@ def test_data(): brain.close() +@requires_fsaverage() +def test_close(): + """Test that close and del actually work.""" + _set_backend() + brain = Brain('fsaverage', 'both', 'inflated') + brain.close() + brain.__del__() + del brain + gc.collect() + + @requires_fsaverage() def test_data_limits(): """Test handling of data limits.""" From ce7ac2dbcd290765fbb1ca80981faded3a48f20c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 27 Feb 2020 11:26:34 -0500 Subject: [PATCH 37/46] FIX: Safer del --- surfer/viz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/viz.py b/surfer/viz.py index 8df843c..b6579c9 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2299,7 +2299,7 @@ def close(self): self._close() def _close(self, force_render=True): - for ri, ff in enumerate(self._figures): + for ri, ff in enumerate(getattr(self, '_figures', [])): for ci, f in enumerate(ff): if f is not None: try: From 1afce4bf3e4e122909dc59a6926adfea50cca076 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 18 May 2020 13:30:31 -0400 Subject: [PATCH 38/46] STY: Dont log info messages by default (#293) * FIX: Verbose * FIX: MNE vers --- .travis.yml | 6 +++++- surfer/io.py | 2 +- surfer/utils.py | 6 +++--- surfer/viz.py | 7 ++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 523612e..9896ed0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,11 @@ before_install: - pip install vtk - pip install mayavi - mkdir -p $SUBJECTS_DIR - - pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master" + - if [ "${PYTHON_VERSION}" == "3.5" ]; then + pip install "mne<0.21"; + else + pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master"; + fi; - python -c "import mne; mne.datasets.fetch_fsaverage(verbose=True)" install: diff --git a/surfer/io.py b/surfer/io.py index efda923..69e7ba4 100644 --- a/surfer/io.py +++ b/surfer/io.py @@ -229,7 +229,7 @@ def project_volume_data(filepath, hemi, reg_file=None, subject_id=None, # Execute the command out_file = mktemp(prefix="pysurfer-v2s", suffix='.mgz') cmd_list.extend(["--o", out_file]) - logger.info(" ".join(cmd_list)) + logger.debug(" ".join(cmd_list)) p = Popen(cmd_list, stdout=PIPE, stderr=PIPE, env=env) stdout, stderr = p.communicate() out = p.returncode diff --git a/surfer/utils.py b/surfer/utils.py index 4a6e4cf..0b1f9ed 100644 --- a/surfer/utils.py +++ b/surfer/utils.py @@ -608,7 +608,7 @@ def _nearest(vertices, adj_mat): def _smooth(vertices, adj_mat, smoothing_steps): from scipy import sparse - logger.info("Updating smoothing matrix, be patient..") + logger.debug("Updating smoothing matrix, be patient..") e = adj_mat.copy() e.data[e.data == 2] = 1 n_vertices = e.shape[0] @@ -626,7 +626,7 @@ def _smooth(vertices, adj_mat, smoothing_steps): smooth_mat = scale_mat * e_use[idx_use, :] * smooth_mat - logger.info("Smoothing matrix creation, step %d" % (k + 1)) + logger.debug("Smoothing matrix creation, step %d" % (k + 1)) if smoothing_steps is None and len(idx_use) >= n_vertices: break @@ -690,7 +690,7 @@ def coord_to_label(subject_id, coord, label, hemi='lh', n_steps=30, idx = np.where(data.ravel() > 0)[0] # Write label label_fname = label + '-' + hemi + '.label' - logger.info("Saving label : %s" % label_fname) + logger.debug("Saving label : %s" % label_fname) f = open(label_fname, 'w') f.write('#label at %s from subject %s\n' % (coord, subject_id)) f.write('%d\n' % len(idx)) diff --git a/surfer/viz.py b/surfer/viz.py index b6579c9..f0af97d 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -2977,12 +2977,13 @@ def _scale_mayavi_lut(lut_table, fmin, fmid, fmax, transparent, trstr = ['(opaque)', '(transparent)'] if divergent: - logger.info( + logger.debug( "colormap divergent: center=%0.2e, [%0.2e, %0.2e, %0.2e] %s" % (center, fmin, fmid, fmax, trstr[transparent])) else: - logger.info("colormap sequential: [%0.2e, %0.2e, %0.2e] %s" - % (fmin, fmid, fmax, trstr[transparent])) + logger.debug( + "colormap sequential: [%0.2e, %0.2e, %0.2e] %s" + % (fmin, fmid, fmax, trstr[transparent])) n_colors = lut_table.shape[0] From 09513f9dd1a18af70b319a15872b66302288ccfd Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 18 May 2020 14:24:55 -0400 Subject: [PATCH 39/46] ENH: Move up to 3.8 (#294) * ENH: Move up to 3.8 * FIX: Newer * FIX: Newer --- .travis.yml | 26 ++++++++++++++++---------- doc/install.rst | 6 +----- setup.py | 4 +++- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9896ed0..ca83d67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: c sudo: false -dist: xenial +dist: bionic services: - xvfb env: - global: PYTHON_VERSION=3.7 - CONDA_DEPENDENCIES="numpy scipy matplotlib pyqt>=5.9 coverage pytest pytest-cov flake8 pygments traits traitsui pyface" + global: PYTHON_VERSION=3.8 + CONDA_DEPENDENCIES="numpy scipy matplotlib pyqt coverage pytest pytest-cov flake8 pygments traits traitsui pyface" PIP_DEPENDENCIES="codecov pytest-sugar pytest-faulthandler nibabel imageio imageio-ffmpeg" DISPLAY=:99.0 SUBJECTS_DIR=~/subjects @@ -18,10 +18,15 @@ matrix: apt: packages: - mencoder + - libosmesa6 + - libglx-mesa0 + - libopengl0 + - libglx0 + - libdbus-1-3 # 3.5, no mencoder - os: linux - env: PYTHON_VERSION=3.5 + env: PYTHON_VERSION=3.6 CONDA_DEPENDENCIES="numpy scipy matplotlib coverage pytest pytest-cov flake8" PIP_DEPENDENCIES="codecov pytest-sugar nibabel imageio imageio-ffmpeg" @@ -37,14 +42,15 @@ before_install: fi; - git clone https://github.com/astropy/ci-helpers.git - source ci-helpers/travis/setup_conda.sh - - pip install vtk - - pip install mayavi - - mkdir -p $SUBJECTS_DIR - - if [ "${PYTHON_VERSION}" == "3.5" ]; then - pip install "mne<0.21"; + - if [ "${PYTHON_VERSION}" == "3.6" ]; then + pip install --only-binary ":all:" "vtk<9"; + pip install mayavi; else - pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master"; + pip install --only-binary ":all:" -f "https://vtk.org/download" "vtk>=9"; + pip install https://github.com/enthought/mayavi/zipball/master; fi; + - mkdir -p $SUBJECTS_DIR + - pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master"; - python -c "import mne; mne.datasets.fetch_fsaverage(verbose=True)" install: diff --git a/doc/install.rst b/doc/install.rst index 91b5691..5fc7d13 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -30,10 +30,7 @@ install from your local source directory:: Dependencies ~~~~~~~~~~~~ -PySurfer works on Python 2.7 and 3.6+. -(Older Python 3 versions will probably work, but are not tested.) - -To use PySurfer, you will need to have the following Python packages: +PySurfer works on Python 3.6+ and requires the following Python packages: * numpy_ * scipy_ @@ -91,4 +88,3 @@ as a static PNG image, or ``'x3d'`` to render it using If you are having trouble getting started using PySurfer, please describe the problem on the `nipy mailing list`_. .. include:: links_names.txt - diff --git a/setup.py b/setup.py index 7df6fbd..3d53a9a 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,8 @@ platforms='any', packages=['surfer', 'surfer.tests'], scripts=['bin/pysurfer'], - install_requires=['numpy', 'scipy', 'matplotlib', 'nibabel >= 1.2', 'mayavi'], + python_requires='>=3.6', + install_requires=[ + 'numpy', 'scipy', 'matplotlib', 'nibabel >= 1.2', 'mayavi'], extras_require={'save_movie': ['imageio >= 1.5']}, ) From b0ca3a15b4c3b816de5b78f02de5c2cf8907ee9d Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 11 Jun 2020 12:02:39 -0400 Subject: [PATCH 40/46] ENH: Add FXAA option (#295) * ENH: Add FXAA option * FIX: Mayavi for VTK9 --- appveyor.yml | 2 +- azure-pipelines.yml | 2 +- surfer/viz.py | 22 ++++++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 39520c5..edf1435 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9" - - "pip install traits traitsui pyface vtk mayavi nibabel" + - "pip install traits traitsui pyface vtk https://github.com/enthought/mayavi/archive/master.zip nibabel" - "powershell make/get_fsaverage.ps1" - "python setup.py develop" - "SET SUBJECTS_DIR=%CD%\\subjects" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7d98a8e..bb560b7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,7 +30,7 @@ jobs: displayName: Cache pip packages - powershell: | pip install numpy scipy matplotlib nose pillow pytest pytest-cov pytest-faulthandler coverage imageio imageio-ffmpeg codecov pyqt5==5.9 --cache-dir $(PIP_CACHE_FOLDER) - pip install traits traitsui pyface vtk mayavi nibabel --cache-dir $(PIP_CACHE_FOLDER) + pip install traits traitsui pyface vtk https://github.com/enthought/mayavi/archive/master.zip nibabel --cache-dir $(PIP_CACHE_FOLDER) displayName: 'Install pip dependencies' - powershell: | powershell make/get_fsaverage.ps1 diff --git a/surfer/viz.py b/surfer/viz.py index f0af97d..014b19a 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -190,7 +190,7 @@ def _force_render(figures): def _make_viewer(figure, n_row, n_col, title, scene_size, offscreen, - interaction='trackball'): + interaction='trackball', antialias=True): """Triage viewer creation If n_row == n_col == 1, then we can use a Mayavi figure, which @@ -229,11 +229,13 @@ def _make_viewer(figure, n_row, n_col, title, scene_size, offscreen, for f in figure: f.scene.interactor.interactor_style = \ tvtk.InteractorStyleTerrain() - for figure in figures: - for f in figure: - # on a non-testing backend, and using modern VTK/Mayavi - if hasattr(getattr(f.scene, 'renderer', None), 'use_fxaa'): - f.scene.renderer.use_fxaa = True + if antialias: + for figure in figures: + for f in figure: + # on a non-testing backend, and using modern VTK/Mayavi + if hasattr(getattr(f.scene, 'renderer', None), + 'use_fxaa'): + f.scene.renderer.use_fxaa = True else: if isinstance(figure, int): # use figure with specified id figure = [mlab.figure(figure, size=scene_size)] @@ -374,6 +376,9 @@ class Brain(object): camera. units : str Can be 'm' or 'mm' (default). + antialias : bool + If True (default), turn on antialiasing. Can be problematic for + some renderers (e.g., software rendering with MESA). Attributes ---------- @@ -397,7 +402,8 @@ def __init__(self, subject_id, hemi, surf, title=None, cortex="classic", alpha=1.0, size=800, background="black", foreground=None, figure=None, subjects_dir=None, views=['lat'], offset=True, show_toolbar=False, - offscreen='auto', interaction='trackball', units='mm'): + offscreen='auto', interaction='trackball', units='mm', + antialias=True): if not isinstance(interaction, string_types) or \ interaction not in ('trackball', 'terrain'): @@ -448,7 +454,7 @@ def __init__(self, subject_id, hemi, surf, title=None, del background, foreground figures, _v = _make_viewer(figure, n_row, n_col, title, self._scene_size, offscreen, - interaction) + interaction, antialias) self._figures = figures self._v = _v self._window_backend = 'Mayavi' if self._v is None else 'TraitsUI' From 9e5fe1f0301c9af77d1e9b86b4c74a38c094e459 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Jun 2020 10:32:34 -0400 Subject: [PATCH 41/46] MRG, MAINT: Simpler vector params (#291) * MAINT: Simpler vector params * FIX: Undo auto scaling * FIX: Dup * FIX: URL * FIX: Better * FIX: More tolerant of type --- .circleci/config.yml | 2 +- .travis.yml | 2 +- surfer/viz.py | 46 ++++++++++++++++++-------------------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0071f48..18f52af 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: command: | python -m pip install --user -q --upgrade pip numpy python -m pip install --user -q --upgrade --progress-bar off scipy matplotlib vtk pyqt5 pyqt5-sip nibabel sphinx numpydoc pillow imageio imageio-ffmpeg sphinx-gallery - python -m pip install --user -q --upgrade mayavi "https://api.github.com/repos/mne-tools/mne-python/zipball/master" + python -m pip install --user -q --upgrade mayavi "https://github.com/mne-tools/mne-python/archive/master.zip" - save_cache: key: pip-cache paths: diff --git a/.travis.yml b/.travis.yml index ca83d67..569e241 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ before_install: pip install https://github.com/enthought/mayavi/zipball/master; fi; - mkdir -p $SUBJECTS_DIR - - pip install "https://api.github.com/repos/mne-tools/mne-python/zipball/master"; + - pip install "https://github.com/mne-tools/mne-python/archive/master.zip" - python -c "import mne; mne.datasets.fetch_fsaverage(verbose=True)" install: diff --git a/surfer/viz.py b/surfer/viz.py index 014b19a..e42c491 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -423,7 +423,7 @@ def __init__(self, subject_id, hemi, surf, title=None, title = subject_id self.subject_id = subject_id - if not isinstance(views, list): + if not isinstance(views, (list, tuple)): views = [views] n_row = len(views) @@ -1095,15 +1095,13 @@ def add_data(self, array, min=None, max=None, thresh=None, smooth_mat = None magnitude = None - magnitude_max = None if array.ndim == 3: if array.shape[1] != 3: raise ValueError('If array has 3 dimensions, array.shape[1] ' 'must equal 3, got %s' % (array.shape[1],)) magnitude = np.linalg.norm(array, axis=1) if scale_factor is None: - distance = np.sum([array[:, dim, :].ptp(axis=0).max() ** 2 - for dim in range(3)]) + distance = 4 * np.linalg.norm(array, axis=1).max() if distance == 0: scale_factor = 1 else: @@ -1111,7 +1109,6 @@ def add_data(self, array, min=None, max=None, thresh=None, (4 * array.shape[0] ** (0.33))) if self._units == 'm': scale_factor = scale_factor / 1000. - magnitude_max = magnitude.max() elif array.ndim not in (1, 2): raise ValueError('array has must have 1, 2, or 3 dimensions, ' 'got (%s)' % (array.ndim,)) @@ -1188,7 +1185,7 @@ def time_label(x): if brain['hemi'] == hemi: s, ct, bar, gl = brain['brain'].add_data( array, min, mid, max, thresh, lut, colormap, alpha, - colorbar, layer_id, smooth_mat, magnitude, magnitude_max, + colorbar, layer_id, smooth_mat, magnitude, scale_factor, vertices, vector_alpha, **kwargs) surfs.append(s) bars.append(bar) @@ -2115,13 +2112,11 @@ def set_data_time_index(self, time_idx, interpolation='quadratic'): if vectors is not None: vectors = vectors[:, :, time_idx] - vector_values = scalar_data.copy() if data['smooth_mat'] is not None: scalar_data = data['smooth_mat'] * scalar_data for brain in self.brains: if brain.hemi == hemi: - brain.set_data(data['layer_id'], scalar_data, - vectors, vector_values) + brain.set_data(data['layer_id'], scalar_data, vectors) del brain data["time_idx"] = time_idx @@ -3225,24 +3220,25 @@ def _remove_scalar_data(self, array_id): self._mesh_clones.pop(array_id).remove() self._mesh_dataset.point_data.remove_array(array_id) - def _add_vector_data(self, vectors, vector_values, fmin, fmid, fmax, - scale_factor_norm, vertices, vector_alpha, lut): + def _add_vector_data(self, vectors, fmin, fmid, fmax, + scale_factor, vertices, vector_alpha, lut): vertices = slice(None) if vertices is None else vertices x, y, z = np.array(self._geo_mesh.data.points.data)[vertices].T vector_alpha = min(vector_alpha, 0.9999999) with warnings.catch_warnings(record=True): # HasTraits quiver = mlab.quiver3d( x, y, z, vectors[:, 0], vectors[:, 1], vectors[:, 2], - scalars=vector_values, colormap='hot', vmin=fmin, + colormap='hot', vmin=fmin, scale_mode='vector', vmax=fmax, figure=self._f, opacity=vector_alpha) # Enable backface culling quiver.actor.property.backface_culling = True quiver.mlab_source.update() - # Compute scaling for the glyphs - quiver.glyph.glyph.scale_factor = (scale_factor_norm * - vector_values.max()) + # Set scaling for the glyphs + quiver.glyph.glyph.scale_factor = scale_factor + quiver.glyph.glyph.clamping = False + quiver.glyph.glyph.range = (0., 1.) # Scale colormap used for the glyphs l_m = quiver.parent.vector_lut_manager @@ -3293,7 +3289,7 @@ def add_overlay(self, old, **kwargs): @verbose def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, - colorbar, layer_id, smooth_mat, magnitude, magnitude_max, + colorbar, layer_id, smooth_mat, magnitude, scale_factor, vertices, vector_alpha, **kwargs): """Add data to the brain""" # Calculate initial data to plot @@ -3308,7 +3304,6 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, array_plot = magnitude[:, 0] else: raise ValueError("data has to be 1D, 2D, or 3D") - vector_values = array_plot if smooth_mat is not None: array_plot = smooth_mat * array_plot @@ -3316,16 +3311,13 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, array_plot = _prepare_data(array_plot) array_id, pipe = self._add_scalar_data(array_plot) - scale_factor_norm = None if array.ndim == 3: - scale_factor_norm = scale_factor / magnitude_max vectors = array[:, :, 0].copy() glyphs = self._add_vector_data( - vectors, vector_values, fmin, fmid, fmax, - scale_factor_norm, vertices, vector_alpha, lut) + vectors, fmin, fmid, fmax, + scale_factor, vertices, vector_alpha, lut) else: glyphs = None - del scale_factor mesh = pipe.parent if thresh is not None: if array_plot.min() >= thresh: @@ -3364,7 +3356,7 @@ def add_data(self, array, fmin, fmid, fmax, thresh, lut, colormap, alpha, self.data[layer_id] = dict( array_id=array_id, mesh=mesh, glyphs=glyphs, - scale_factor_norm=scale_factor_norm) + scale_factor=scale_factor) return surf, orig_ctable, bar, glyphs def add_annotation(self, annot, ids, cmap, **kwargs): @@ -3475,7 +3467,7 @@ def remove_data(self, layer_id): self._remove_scalar_data(data['array_id']) self._remove_vector_data(data['glyphs']) - def set_data(self, layer_id, values, vectors=None, vector_values=None): + def set_data(self, layer_id, values, vectors=None): """Set displayed data values and vectors.""" data = self.data[layer_id] self._mesh_dataset.point_data.get_array( @@ -3492,12 +3484,12 @@ def set_data(self, layer_id, values, vectors=None, vector_values=None): # Update glyphs q.mlab_source.vectors = vectors - q.mlab_source.scalars = vector_values q.mlab_source.update() # Update changed parameters, and glyph scaling - q.glyph.glyph.scale_factor = (data['scale_factor_norm'] * - values.max()) + q.glyph.glyph.scale_factor = data['scale_factor'] + q.glyph.glyph.range = (0., 1.) + q.glyph.glyph.clamping = False l_m.load_lut_from_list(lut / 255.) l_m.data_range = data_range From 85e9e6ca20586957dbbbf53b188be91380211ff1 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Jun 2020 10:44:12 -0400 Subject: [PATCH 42/46] MAINT: Bump version --- CHANGES | 12 ++++++++++-- surfer/__init__.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index 2da1611..a8150e1 100644 --- a/CHANGES +++ b/CHANGES @@ -3,8 +3,16 @@ Changelog .. currentmodule:: surfer -Development version (0.10.dev0) -------------------------------- +Version 0.11.0 +-------------- + +- Minimum Python version increased to 3.6 +- Add support to turn off full-screen antialiasing, which can be problematic on + some drivers (e.g., MESA software rendering on Linux) +- Simplification and refactoring of vector-valued data plotting +- Removal of unnecessary ``info`` log messages about smoothing matrix and + colormap generation (changed to ``debug`` level) +- Clean up of exit functions like ``__del__`` to avoid segfaults Version 0.10.0 diff --git a/surfer/__init__.py b/surfer/__init__.py index 54f3e64..d1297a4 100644 --- a/surfer/__init__.py +++ b/surfer/__init__.py @@ -2,7 +2,7 @@ from .utils import Surface, verbose, set_log_level, set_log_file # noqa from .io import project_volume_data # noqa -__version__ = "0.11.dev0" +__version__ = "0.11.0" set_log_file() # initialize handlers set_log_level() # initialize logging level From f02cca8f1e10f21996ab85d292c8a423faa11af4 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 23 Jun 2020 13:07:18 -0400 Subject: [PATCH 43/46] MAINT: Bump version --- surfer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfer/__init__.py b/surfer/__init__.py index d1297a4..65a815b 100644 --- a/surfer/__init__.py +++ b/surfer/__init__.py @@ -2,7 +2,7 @@ from .utils import Surface, verbose, set_log_level, set_log_file # noqa from .io import project_volume_data # noqa -__version__ = "0.11.0" +__version__ = "0.12.dev0" set_log_file() # initialize handlers set_log_level() # initialize logging level From 2512fc600dcc7428c73e159e375060af5f198195 Mon Sep 17 00:00:00 2001 From: Cristi Donos Date: Wed, 11 Nov 2020 14:41:01 +0200 Subject: [PATCH 44/46] option to apply xfrom from surface files --- surfer/utils.py | 16 ++++++++++++++-- surfer/viz.py | 13 ++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/surfer/utils.py b/surfer/utils.py index 19d6cf3..7fcb048 100644 --- a/surfer/utils.py +++ b/surfer/utils.py @@ -79,10 +79,14 @@ class Surface(object): If None, do not change coordinates (default). units : str Can be 'm' or 'mm' (default). + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. """ def __init__(self, subject_id, hemi, surf, subjects_dir=None, - offset=None, units='mm'): + offset=None, units='mm', apply_surf_xfm=False): """Surface Parameters @@ -97,6 +101,10 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, If 0.0, the surface will be offset such that the medial wall is aligned with the origin. If None, no offset will be applied. If != 0.0, an additional offset will be used. + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. """ if hemi not in ['lh', 'rh']: raise ValueError('hemi must be "lh" or "rh') @@ -108,6 +116,7 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, self.faces = None self.nn = None self.units = _check_units(units) + self.apply_surf_xfm = apply_surf_xfm subjects_dir = _get_subjects_dir(subjects_dir) self.data_path = op.join(subjects_dir, subject_id) @@ -115,7 +124,7 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, def load_geometry(self): surf_path = op.join(self.data_path, "surf", "%s.%s" % (self.hemi, self.surf)) - coords, faces = nib.freesurfer.read_geometry(surf_path) + coords, faces, metadata = nib.freesurfer.read_geometry(surf_path, read_metadata=True) if self.units == 'm': coords /= 1000. if self.offset is not None: @@ -123,6 +132,9 @@ def load_geometry(self): coords[:, 0] -= (np.max(coords[:, 0]) + self.offset) else: coords[:, 0] -= (np.min(coords[:, 0]) + self.offset) + if self.apply_surf_xfm: + sxfm = np.vstack([np.stack([metadata[k] for k in metadata.keys() if 'ras' in k]).T, np.array([0, 0, 0, 1])]) + coords = np.hstack([coords, np.ones((coords.shape[0], 1))]).dot(np.linalg.inv(sxfm))[:,0:3] nn = _compute_normals(coords, faces) if self.coords is None: diff --git a/surfer/viz.py b/surfer/viz.py index 6c2204b..2ad30d0 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -365,6 +365,10 @@ class Brain(object): camera. units : str Can be 'm' or 'mm' (default). + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. Attributes ---------- @@ -387,7 +391,8 @@ def __init__(self, subject_id, hemi, surf, title=None, cortex="classic", alpha=1.0, size=800, background="black", foreground=None, figure=None, subjects_dir=None, views=['lat'], offset=True, show_toolbar=False, - offscreen=False, interaction='trackball', units='mm'): + offscreen=False, interaction='trackball', units='mm', + apply_surf_xfm=False): if not isinstance(interaction, string_types) or \ interaction not in ('trackball', 'terrain'): @@ -406,6 +411,7 @@ def __init__(self, subject_id, hemi, surf, title=None, if title is None: title = subject_id self.subject_id = subject_id + self.apply_surf_xfm = apply_surf_xfm if not isinstance(views, list): views = [views] @@ -426,7 +432,7 @@ def __init__(self, subject_id, hemi, surf, title=None, for h in geo_hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, - units=self._units) + units=self._units, apply_surf_xfm=self.apply_surf_xfm) # Load in the geometry and (maybe) curvature geo.load_geometry() if geo_curv: @@ -1623,7 +1629,8 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, foci_coords = np.atleast_2d(coords) else: foci_surf = Surface(self.subject_id, hemi, map_surface, - subjects_dir=self.subjects_dir) + subjects_dir=self.subjects_dir, + apply_surf_xfm=self.apply_surf_xfm) foci_surf.load_geometry() foci_vtxs = utils.find_closest_vertices(foci_surf.coords, coords) foci_coords = self.geo[hemi].coords[foci_vtxs] From d3749dc5605f7200731ef5c716f6aebe4fd65b31 Mon Sep 17 00:00:00 2001 From: Cristi Donos Date: Wed, 11 Nov 2020 19:21:33 +0200 Subject: [PATCH 45/46] merged changes by CD on top of nipy's f02cca8 version from Jun 23 --- surfer/viz.py | 105 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/surfer/viz.py b/surfer/viz.py index e42c491..ed934bf 100644 --- a/surfer/viz.py +++ b/surfer/viz.py @@ -379,6 +379,10 @@ class Brain(object): antialias : bool If True (default), turn on antialiasing. Can be problematic for some renderers (e.g., software rendering with MESA). + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. Attributes ---------- @@ -403,7 +407,7 @@ def __init__(self, subject_id, hemi, surf, title=None, foreground=None, figure=None, subjects_dir=None, views=['lat'], offset=True, show_toolbar=False, offscreen='auto', interaction='trackball', units='mm', - antialias=True): + antialias=True, apply_surf_xfm=False): if not isinstance(interaction, string_types) or \ interaction not in ('trackball', 'terrain'): @@ -422,6 +426,7 @@ def __init__(self, subject_id, hemi, surf, title=None, if title is None: title = subject_id self.subject_id = subject_id + self.apply_surf_xfm = apply_surf_xfm if not isinstance(views, (list, tuple)): views = [views] @@ -442,7 +447,7 @@ def __init__(self, subject_id, hemi, surf, title=None, for h in geo_hemis: # Initialize a Surface object as the geometry geo = Surface(subject_id, h, surf, subjects_dir, offset, - units=self._units) + units=self._units, apply_surf_xfm=self.apply_surf_xfm) # Load in the geometry and (maybe) curvature geo.load_geometry() if geo_curv: @@ -1643,7 +1648,7 @@ def add_morphometry(self, measure, grayscale=False, hemi=None, def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor=1, color="white", alpha=1, name=None, - hemi=None, **kwargs): + hemi=None, data=None, colormap=None, **kwargs): """Add spherical foci, possibly mapping to displayed surf. The foci spheres can be displayed at the coordinates given, or @@ -1673,6 +1678,14 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, If None, it is assumed to belong to the hemipshere being shown. If two hemispheres are being shown, an error will be thrown. + data : numpy array + None or 1D array the same size as the number of foci (n,). + Spheres sizes will be coded using the absolute values of data. + If data is None, all spheres will have the same size. + colormap : str + Mayavi colormap name. Default is None, which means all foci will + have the same color given by the 'color' argument. If colormap is + not None, foci will be colorcoded acording to data values. **kwargs : additional keyword arguments These are passed to the underlying :func:`mayavi.mlab.points3d` call. @@ -1691,7 +1704,8 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, else: foci_surf = Surface(self.subject_id, hemi, map_surface, subjects_dir=self.subjects_dir, - units=self._units) + units=self._units, + apply_surf_xfm=self.apply_surf_xfm) foci_surf.load_geometry() foci_vtxs = utils.find_closest_vertices(foci_surf.coords, coords) foci_coords = self.geo[hemi].coords[foci_vtxs] @@ -1710,9 +1724,12 @@ def add_foci(self, coords, coords_as_verts=False, map_surface=None, scale_factor = scale_factor / 1000. for brain in self._brain_list: if brain['hemi'] == hemi: + vtxs = utils.find_closest_vertices(self.geo[hemi].coords, + foci_coords) fl.append(brain['brain'].add_foci(foci_coords, scale_factor, - color, alpha, name, - **kwargs)) + color, alpha, name, data, + self.geo[hemi].nn[vtxs, :], + colormap, **kwargs)) self.foci_dict[name] = fl self._toggle_render(True, views) @@ -1821,6 +1838,44 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, if justification is not None: text.property.justification = justification + def add_text3d(self, x, y, z, text, name, color=None, opacity=1.0, + row=-1, col=-1, font_size=None, justification=None): + """ Add a text to the visualization + + Parameters + ---------- + x : Float + x coordinate + y : Float + y coordinate + z : Float + z coordinate + text : str + Text to add + name : str + Name of the text (text label can be updated using update_text()) + color : Tuple + Color of the text. Default is the foreground color set during + initialization (default is black or white depending on the + background color). + opacity : Float + Opacity of the text. Default: 1.0 + row : int + Row index of which brain to use + col : int + Column index of which brain to use + """ + if name in self.texts_dict: + self.texts_dict[name]['text'].remove() + text = self.brain_matrix[row, col].add_text3d(x, y, z, text, + name, color, opacity, **kwargs) + self.texts_dict[name] = dict(row=row, col=col, text=text) + if font_size is not None: + text.property.font_size = font_size + text.actor.text_scale_mode = 'viewport' + if justification is not None: + text.property.justification = justification + def update_text(self, text, name, row=-1, col=-1): """Update text label @@ -3414,16 +3469,30 @@ def add_morphometry(self, morph_data, colormap, measure, return dict(surface=surf, colorbar=bar, measure=measure, brain=self, array_id=array_id) - def add_foci(self, foci_coords, scale_factor, color, alpha, name, - **kwargs): - """Add spherical foci, possibly mapping to displayed surf""" + def add_foci(self, foci_coords, scale_factor, color, alpha, name, data, normals, colormap): + """Add spherical foci with attached data, possibly mapping to displayed surf""" # Create the visualization + if data is None: + u = np.array(normals[:,0]) + v = np.array(normals[:,1]) + w = np.array(normals[:,2]) + else: + u = normals[:,0] * np.abs(data) + v = normals[:,1] * np.abs(data) + w = normals[:,2] * np.abs(data) with warnings.catch_warnings(record=True): # traits - points = mlab.points3d( - foci_coords[:, 0], foci_coords[:, 1], foci_coords[:, 2], - np.ones(foci_coords.shape[0]), name=name, figure=self._f, - scale_factor=(10. * scale_factor), color=color, opacity=alpha, - reset_zoom=False, **kwargs) + if colormap is None: + points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], + u,v,w, scalars = data, mode='sphere', + name=name, figure=self._f, scale_factor=(10. * scale_factor), + color=color, opacity=alpha) + else: + points = mlab.quiver3d(foci_coords[:, 0],foci_coords[:, 1],foci_coords[:, 2], + u,v,w, scalars = data, colormap=colormap, mode='sphere', + name=name, figure=self._f, scale_factor=(10. * scale_factor), + color=color, opacity=alpha) + points.glyph.color_mode = 'color_by_scalar' + points.glyph.glyph_source.glyph_source.center = [0, 0, 0] return points def add_contour_overlay(self, scalar_data, min=None, max=None, @@ -3461,6 +3530,14 @@ def add_text(self, x, y, text, name, color=None, opacity=1.0, **kwargs): opacity=opacity, figure=self._f, **kwargs) return text + def add_text3d(self, x, y, z, text, name, color=None, opacity=1.0): + """ Add a text in 3D to the visualization""" + color = self._fg_color if color is None else color + with warnings.catch_warnings(record=True): + text = mlab.text3d(x, y, z, text, name=name, color=color, + opacity=opacity, figure=self._f) + return text + def remove_data(self, layer_id): """Remove data shown with .add_data()""" data = self.data.pop(layer_id) From 6c9cb10ac77e4a9523250da80482187da0ed011d Mon Sep 17 00:00:00 2001 From: Cristi Donos Date: Wed, 11 Nov 2020 19:22:24 +0200 Subject: [PATCH 46/46] merged changes by CD on top of nipy's f02cca8 version from Jun 23 --- surfer/utils.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/surfer/utils.py b/surfer/utils.py index 0b1f9ed..2bbd1c1 100644 --- a/surfer/utils.py +++ b/surfer/utils.py @@ -79,10 +79,14 @@ class Surface(object): If None, do not change coordinates (default). units : str Can be 'm' or 'mm' (default). + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. """ def __init__(self, subject_id, hemi, surf, subjects_dir=None, - offset=None, units='mm'): + offset=None, units='mm', apply_surf_xfm=False): """Surface Parameters @@ -97,6 +101,10 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, If 0.0, the surface will be offset such that the medial wall is aligned with the origin. If None, no offset will be applied. If != 0.0, an additional offset will be used. + apply_surf_xfm: bool | False + If True, the transform defined by xras, yras,zras and cras + in Freesurfer's surface file will be applied to the surface + node coordinates. """ if hemi not in ['lh', 'rh']: raise ValueError('hemi must be "lh" or "rh') @@ -108,6 +116,7 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, self.faces = None self.nn = None self.units = _check_units(units) + self.apply_surf_xfm = apply_surf_xfm subjects_dir = _get_subjects_dir(subjects_dir) self.data_path = op.join(subjects_dir, subject_id) @@ -115,7 +124,7 @@ def __init__(self, subject_id, hemi, surf, subjects_dir=None, def load_geometry(self): surf_path = op.join(self.data_path, "surf", "%s.%s" % (self.hemi, self.surf)) - coords, faces = nib.freesurfer.read_geometry(surf_path) + coords, faces, metadata = nib.freesurfer.read_geometry(surf_path, read_metadata=True) if self.units == 'm': coords /= 1000. if self.offset is not None: @@ -123,6 +132,9 @@ def load_geometry(self): coords[:, 0] -= (np.max(coords[:, 0]) + self.offset) else: coords[:, 0] -= (np.min(coords[:, 0]) + self.offset) + if self.apply_surf_xfm: + sxfm = np.vstack([np.stack([metadata[k] for k in metadata.keys() if 'ras' in k]).T, np.array([0, 0, 0, 1])]) + coords = np.hstack([coords, np.ones((coords.shape[0], 1))]).dot(np.linalg.inv(sxfm))[:,0:3] nn = _compute_normals(coords, faces) if self.coords is None: