diff --git a/.circleci/config.yml b/.circleci/config.yml index 849d72a..18f52af 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,64 +1,98 @@ 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; + echo "export PATTERN=\"plot_\(?\!fmri_activation_volume\|resting_correlations\)\"" >> $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 + + - 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: + name: Fix libgcc_s.so.1 pthread_cancel bug + command: | + sudo apt-get install qt5-default # Load our data - restore_cache: keys: - - data-cache + - data-cache-0 - pip-cache - # Fix libgcc_s.so.1 pthread_cancel bug: - # 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: Get Python running + 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://github.com/mne-tools/mne-python/archive/master.zip" - 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: | - 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; - - 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 + LIBGL_DEBUG=verbose python -c "from mayavi import mlab; import matplotlib.pyplot as plt; mlab.figure(); plt.figure()" + + - run: + name: Get data + command: | + 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: | + python setup.py develop --user + + - run: + name: Build docs + command: | + cd doc + echo $PATTERN + make html_dev-pattern - store_artifacts: path: doc/_build/html/ destination: html - - save_cache: - key: data-cache - paths: - - "~/subjects" + +workflows: + version: 2 + + default: + jobs: + - build_docs diff --git a/.gitignore b/.gitignore index 35adb4a..5ded8ed 100755 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,9 @@ .#* *.swp *.orig +*.mov build +.idea/ dist/ doc/_build/ diff --git a/.travis.yml b/.travis.yml index f44bb7c..569e241 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ language: c sudo: false - +dist: bionic +services: + - xvfb env: - global: PYTHON_VERSION=3.6 - 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" + 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 matrix: include: @@ -15,12 +18,17 @@ matrix: apt: packages: - mencoder + - libosmesa6 + - libglx-mesa0 + - libopengl0 + - libglx0 + - libdbus-1-3 - # 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.6 + CONDA_DEPENDENCIES="numpy scipy matplotlib coverage pytest pytest-cov flake8" + PIP_DEPENDENCIES="codecov pytest-sugar nibabel imageio imageio-ffmpeg" # OSX - os: osx @@ -32,35 +40,24 @@ 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; + - if [ "${PYTHON_VERSION}" == "3.6" ]; then + pip install --only-binary ":all:" "vtk<9"; pip install mayavi; + else + 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://github.com/mne-tools/mne-python/archive/master.zip" + - 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" - - if [[ $PIP_DEPENDENCIES == *"imageio"* ]] || [ ! -z "$CONDA_ENVIRONMENT" ]; then - python -c "import imageio; imageio.plugins.ffmpeg.download()"; - fi - script: - cd ${SRC_DIR} - pytest surfer --cov=surfer -v diff --git a/CHANGES b/CHANGES index dc0902e..a8150e1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,36 @@ -PySurfer Changes -================ +Changelog +========= + +.. currentmodule:: surfer + +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 +-------------- + +- 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/appveyor.yml b/appveyor.yml index 4a79ae0..edf1435 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 traits traitsui pyface vtk mayavi nibabel" - - "python -c \"import imageio; imageio.plugins.ffmpeg.download()\"" + - "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 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 new file mode 100644 index 0000000..bb560b7 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,57 @@ +trigger: + # start a new build for every push + batch: False + branches: + include: + - master + +jobs: +- job: Windows + variables: + PIP_CACHE_FOLDER: $(Pipeline.Workspace)/.cache/pip + 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 + - 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 https://github.com/enthought/mayavi/archive/master.zip 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=SUBJECTS_DIR]" + $env:SUBJECTS_DIR) + 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() 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/_static/navy.css b/doc/_static/navy.css index 4b1c4e7..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 { @@ -546,4 +548,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/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/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/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/doc/install.rst b/doc/install.rst index bb6c441..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_ @@ -77,7 +74,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/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/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_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/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) diff --git a/setup.cfg b/setup.cfg index 133852e..b869886 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,10 @@ 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: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/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']}, ) diff --git a/surfer/__init__.py b/surfer/__init__.py index 5af068d..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.10.dev0" +__version__ = "0.12.dev0" set_log_file() # initialize handlers set_log_level() # initialize logging level diff --git a/surfer/io.py b/surfer/io.py index 8ddcf69..69e7ba4 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 @@ -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/tests/test_utils.py b/surfer/tests/test_utils.py index 8d71382..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 @@ -32,24 +36,24 @@ 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 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/tests/test_viz.py b/surfer/tests/test_viz.py index 5d534a2..ac96fe2 100644 --- a/surfer/tests/test_viz.py +++ b/surfer/tests/test_viz.py @@ -1,8 +1,8 @@ +import gc import os import os.path as op from os.path import join as pjoin import sys -import warnings import pytest from mayavi import mlab @@ -13,9 +13,8 @@ from unittest import SkipTest from surfer import Brain, io, utils -from surfer.utils import requires_fsaverage, requires_imageio, requires_fs - -warnings.simplefilter('always') +from surfer.utils import (requires_fsaverage, requires_imageio, requires_fs, + _get_extra) subject_id = 'fsaverage' std_args = [subject_id, 'lh', 'inflated'] @@ -83,28 +82,35 @@ 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() - - brain = Brain(*std_args, size=100) 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.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() +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.""" # 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', @@ -117,8 +123,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] @@ -152,7 +157,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') @@ -164,6 +169,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() @@ -200,14 +211,29 @@ 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.""" _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() @@ -219,7 +245,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') @@ -247,30 +277,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 +384,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) @@ -390,7 +421,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")) @@ -432,23 +463,22 @@ 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 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/utils.py b/surfer/utils.py index cd2b9f3..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: @@ -579,10 +591,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..") - + logger.debug("Updating smoothing matrix, be patient..") e = adj_mat.copy() e.data[e.data == 2] = 1 n_vertices = e.shape[0] @@ -600,7 +638,7 @@ def smoothing_matrix(vertices, adj_mat, smoothing_steps=20, verbose=None): 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 @@ -664,7 +702,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)) @@ -745,3 +783,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 cb76a1c..ed934bf 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 @@ -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 @@ -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: @@ -229,6 +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() + 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)] @@ -369,6 +376,13 @@ 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). + 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,11 +401,13 @@ 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, views=['lat'], offset=True, show_toolbar=False, - offscreen='auto', interaction='trackball', units='mm'): + offscreen='auto', interaction='trackball', units='mm', + antialias=True, apply_surf_xfm=False): if not isinstance(interaction, string_types) or \ interaction not in ('trackball', 'terrain'): @@ -410,8 +426,9 @@ 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): + if not isinstance(views, (list, tuple)): views = [views] n_row = len(views) @@ -430,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: @@ -442,7 +459,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' @@ -907,7 +924,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 @@ -926,6 +943,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 @@ -938,7 +959,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 @@ -951,7 +972,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 @@ -999,9 +1021,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 @@ -1029,6 +1054,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 ----- @@ -1072,15 +1100,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: @@ -1088,7 +1114,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,)) @@ -1165,8 +1190,8 @@ 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, - scale_factor, vertices, vector_alpha) + colorbar, layer_id, smooth_mat, magnitude, + scale_factor, vertices, vector_alpha, **kwargs) surfs.append(s) bars.append(bar) glyphs.append(gl) @@ -1182,14 +1207,15 @@ 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, hemi=hemi) if initial_time_index is not None: self.set_data_time_index(initial_time_index) self._toggle_render(True, views) def add_annotation(self, annot, borders=True, alpha=1, hemi=None, - remove_existing=True): + remove_existing=True, color=None, **kwargs): """Add an annotation file. Parameters @@ -1213,6 +1239,12 @@ 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. + **kwargs : additional keyword arguments + These are passed to the underlying + ``mayavi.mlab.pipeline.surface`` call. """ hemis = self._check_hemis(hemi) @@ -1285,14 +1317,21 @@ 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( - 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 @@ -1324,6 +1363,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 ----- @@ -1405,7 +1447,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, @@ -1515,7 +1557,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 @@ -1537,7 +1579,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 = [] @@ -1598,12 +1642,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, data=None, colormap=None, **kwargs): """Add spherical foci, possibly mapping to displayed surf. The foci spheres can be displayed at the coordinates given, or @@ -1633,6 +1678,17 @@ 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. """ from matplotlib.colors import colorConverter hemi = self._check_hemi(hemi) @@ -1648,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] @@ -1667,14 +1724,19 @@ 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)) + color, alpha, name, data, + self.geo[hemi].nn[vtxs, :], + colormap, **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" @@ -1704,7 +1766,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) @@ -1731,11 +1795,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 @@ -1758,11 +1822,53 @@ 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 + text.actor.text_scale_mode = 'viewport' + 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 @@ -1903,7 +2009,8 @@ 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, + hemi=None, verbose=None): """Scale the data colormap. The colormap may be sequential or divergent. When the colormap is @@ -1942,17 +2049,26 @@ 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). + 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 - 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 hemi in hemis: + data = self.data_dict[hemi] + if data is not None: + break + table = data["orig_ctable"].copy() lut = _scale_mayavi_lut(table, fmin, fmid, fmax, transparent, center, alpha) @@ -1962,14 +2078,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]) @@ -1997,7 +2114,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]) @@ -2050,13 +2167,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 @@ -2092,13 +2207,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). """ @@ -2233,22 +2352,32 @@ def hide_colorbar(self, row=-1, col=-1): def close(self): """Close all figures and cleanup data structure.""" - for ri, ff in enumerate(self._figures): + self._close() + + def _close(self, force_render=True): + for ri, ff in enumerate(getattr(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 self._v is not None: - self._v.dispose() + if getattr(self, '_v', None) is not None: + try: + self._v.dispose() + except Exception: + pass self._v = None def __del__(self): - if hasattr(self, '_v') and self._v is not None: - self._v.dispose() - self._v = None + # Forcing the GUI updates during GC seems to be problematic + self._close(force_render=False) ########################################################################### # SAVING OUTPUT @@ -2764,6 +2893,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.""" @@ -2824,8 +2970,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)) @@ -2887,12 +3033,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] @@ -2957,6 +3104,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): @@ -3127,24 +3275,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 = False + 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 @@ -3156,7 +3305,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) @@ -3166,7 +3315,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 @@ -3182,7 +3331,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) @@ -3195,8 +3344,8 @@ 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): + colorbar, layer_id, smooth_mat, magnitude, + scale_factor, vertices, vector_alpha, **kwargs): """Add data to the brain""" # Calculate initial data to plot if array.ndim == 1: @@ -3210,7 +3359,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 @@ -3218,16 +3366,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: @@ -3238,9 +3383,15 @@ 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 + # 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 @@ -3260,33 +3411,33 @@ 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): + 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 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, 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]) @@ -3298,13 +3449,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: @@ -3318,26 +3469,42 @@ 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""" + 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) + 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, 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.) @@ -3355,11 +3522,19 @@ 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, **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 @@ -3369,7 +3544,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( @@ -3386,12 +3561,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