Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MRG] Cortex surface projections #1516

Merged
merged 104 commits into from
Nov 19, 2017
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
b966ac9
add simple sampling of volume on mesh
jeromedockes Sep 28, 2017
07881d0
add 4d
jeromedockes Sep 28, 2017
0f29725
import joblib from sklearn.externals
jeromedockes Sep 28, 2017
0771374
remove tuple unpacking for python2
jeromedockes Sep 28, 2017
e15d04b
add tests for cortex projections
jeromedockes Sep 28, 2017
c0c39d3
mention niimg-like compatibility inv niimg_to_surf docstring
jeromedockes Sep 29, 2017
398965d
reorder imports in test_surf_plotting
jeromedockes Sep 29, 2017
4589a95
titles on vol to surf examples
jeromedockes Sep 29, 2017
c7af4b1
add computing normals at mesh vertices
jeromedockes Oct 10, 2017
9f84b24
add sampling along normal
jeromedockes Oct 11, 2017
0c3e1d4
use scipy for interpolation to allow linear interpolation
jeromedockes Oct 11, 2017
c5d83b5
choose kind and interpolation
jeromedockes Oct 17, 2017
d1d9536
make the direction of mesh normals more explicit
jeromedockes Oct 21, 2017
cdcbed1
remove handling of None affine in non-public functions in surf_plotting
jeromedockes Oct 21, 2017
58bd2f8
only ten points on normal projection by default
jeromedockes Oct 21, 2017
33bb932
improve surf plotting tests
jeromedockes Oct 21, 2017
536cc19
improve ball sampling example
jeromedockes Oct 21, 2017
389851d
change cortical projection illustration example
jeromedockes Nov 9, 2017
07d62bd
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 9, 2017
9fb10f0
img.affine rather than utils.compat.get_affine(img)
jeromedockes Nov 9, 2017
d836a6f
improve surface projection docstrings
jeromedockes Nov 9, 2017
5704948
update surface plotting docs, examples and whats new
jeromedockes Nov 9, 2017
eef5060
add possibility to give a mask + speedup nearest neighbour sampling
jeromedockes Nov 11, 2017
85624ab
more tests
jeromedockes Nov 11, 2017
6da5a92
improve docstrings
jeromedockes Nov 11, 2017
c234e60
surf_plotting details (mostly docstrings)
jeromedockes Nov 12, 2017
3cf1f35
add tests for sample locations and masking
jeromedockes Nov 12, 2017
8ad1071
whitespace for sphinx gallery
jeromedockes Nov 13, 2017
2e585d5
resample mask to image
jeromedockes Nov 13, 2017
21a7ff0
test projection_matrix
jeromedockes Nov 13, 2017
bfe4c74
more sampling tests
jeromedockes Nov 13, 2017
11e8c17
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 14, 2017
ecd96c3
3d brain image -> 3d brain volume
jeromedockes Nov 14, 2017
63533ec
rename example plot_3d_map_projection.py -> plot_3d_map_to_surface_pr…
jeromedockes Nov 14, 2017
9d16db7
relative import of nilearn.image.load_img
jeromedockes Nov 14, 2017
c6e34a1
shorter imports in plot_cortex_projection_strategies.py narrative
jeromedockes Nov 14, 2017
c49c5c0
rename plot_cortex_projection_strategies.py -> plot_surface_projectio…
jeromedockes Nov 14, 2017
25ae312
image -> img
jeromedockes Nov 14, 2017
2a20304
mask -> mask_img
jeromedockes Nov 14, 2017
4739d22
niimg -> Niimg
jeromedockes Nov 14, 2017
6ac2386
neighbouring -> neighboring
jeromedockes Nov 14, 2017
1f55f47
===== -> -----
jeromedockes Nov 14, 2017
e8cc677
in doc array -> numpy.ndarray
jeromedockes Nov 14, 2017
55878d6
detail niimg_to_surf_data docstring
jeromedockes Nov 14, 2017
6f309ad
avoid copy when resampling mask
jeromedockes Nov 14, 2017
c50a5c3
use localizer_button_task rather than a brainpedia image for surf pro…
jeromedockes Nov 14, 2017
77dd10f
niimg_to_surf_data -> vol_to_surf
jeromedockes Nov 14, 2017
c5eea1d
fix whats new
jeromedockes Nov 14, 2017
6aa3f12
fall back to nearest neighbours when scipy < 0.14
jeromedockes Nov 14, 2017
fe49498
move surface manipulation functions to nilearn.surface
jeromedockes Nov 14, 2017
244eab2
skip surf proj with interpolation test if scipy < 0.14
jeromedockes Nov 14, 2017
6ba9b53
move technical detail example
jeromedockes Nov 15, 2017
76d4f50
don't load fsaverage in tests if nibabel <= 1.2.0
jeromedockes Nov 15, 2017
1cbd3b3
use scipy for triangulation -> don't import matplotlib out of plotting
jeromedockes Nov 15, 2017
754b834
prettier surface projection example
jeromedockes Nov 15, 2017
7d85051
don't use scipy delaunay in tests if scipy < 0.14
jeromedockes Nov 15, 2017
f1f3836
check fsaverage file
jeromedockes Nov 15, 2017
fa838cf
missing teardown_mock in datasets test_func
jeromedockes Nov 15, 2017
83ca7a4
remove useless print
jeromedockes Nov 15, 2017
40bbfa4
make _uniform_ball_cloud deterministic
jeromedockes Nov 15, 2017
d3d69c2
improve surf projection example description + move imports
jeromedockes Nov 16, 2017
d0982ad
vol_to_surf docstring detail
jeromedockes Nov 16, 2017
2128c77
check_niimg of mask for surf projection
jeromedockes Nov 16, 2017
fcb8954
numpy.ndarray in docstrings
jeromedockes Nov 16, 2017
73c3d25
details about projection method in vol_to_surf docstring
jeromedockes Nov 16, 2017
e0bd9b8
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 16, 2017
756bd31
make projection_matrix private
jeromedockes Nov 16, 2017
6785cda
add readme to 01_plotting/technical_details
jeromedockes Nov 16, 2017
2c74c84
add precomputed ball positions for n_points in 10, 20, 40, 80, 160
jeromedockes Nov 16, 2017
43bae74
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 16, 2017
bf904e3
comment about returning NaN for bad vertices
jeromedockes Nov 16, 2017
5e5f264
check sample_locations shape
jeromedockes Nov 16, 2017
59049a8
comments about scipy version checks
jeromedockes Nov 16, 2017
ea3f39e
no need to check for nibabel > 2.0.2
jeromedockes Nov 16, 2017
94cf01c
test vol_to_surf with mask
jeromedockes Nov 16, 2017
b11d0d1
move example back to 01_plotting/
jeromedockes Nov 16, 2017
fca0e78
define EfficiencyWarning if not in sklearn (new in 0.18)
jeromedockes Nov 16, 2017
1f03b77
fix indentation
jeromedockes Nov 16, 2017
cec5afd
import assert_warns from nilearn
jeromedockes Nov 16, 2017
43e4be2
add ball cloud csvs to package data
jeromedockes Nov 16, 2017
91b2c17
surface in its own package
jeromedockes Nov 17, 2017
ed016cc
fix surface test data location in setup
jeromedockes Nov 17, 2017
a70bb83
skip comparison of ball sample locations with cached values if sklear…
jeromedockes Nov 17, 2017
f313e3d
example description detail
jeromedockes Nov 17, 2017
c0d56d6
link to function in example narrative
jeromedockes Nov 17, 2017
82d7670
minor comment
jeromedockes Nov 17, 2017
a00d642
docstring formatting
jeromedockes Nov 17, 2017
e38ecb9
add note example needs matplotlib 1.3.1 to plot_3d_map_to_surface...
jeromedockes Nov 17, 2017
9f294a8
state n_samples defaults in vol_to_surf doc
jeromedockes Nov 17, 2017
e717059
add versionadded to vol_to_surf
jeromedockes Nov 17, 2017
079ddcd
import load_surf_data from nilearn.surface in plot_surf_stat_map ex
jeromedockes Nov 18, 2017
25a09f6
add load_surf_data and load_surf_mesh to doc/index.rst
jeromedockes Nov 18, 2017
9e52d9f
add README.txt to nilearn/surface/data
jeromedockes Nov 18, 2017
1b2aec1
make interpolation='linear' the default in vol_to_surf
jeromedockes Nov 19, 2017
6c2244f
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 19, 2017
4cca98a
update vol_to_surf docstring to linear is the default
jeromedockes Nov 19, 2017
ed78a66
invert order of interpolation and kind in vol_to_surf signature
jeromedockes Nov 19, 2017
3376c51
improve vol_to_surf docstring
jeromedockes Nov 19, 2017
d1a5779
Merge branch 'master' into cortex_surface_projections
jeromedockes Nov 19, 2017
c8c440d
remove checks for scipy < 0.14 (supported version has been bumped)
jeromedockes Nov 19, 2017
1ad5546
add WARNING experimental to vol_to_surf docstring
jeromedockes Nov 19, 2017
28c26a0
unused import
jeromedockes Nov 19, 2017
256a8b1
use a _kept_indices rather than _masked_indices to avoid ~ operations
jeromedockes Nov 19, 2017
b86bfb9
revert to _masked_indices but manipulate kept internally
jeromedockes Nov 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions examples/01_plotting/brainpedia_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from matplotlib import pyplot as plt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need a docstring with a title, for the gallery. Sphinx-gallery will not accept to build a gallery item from this.


import nilearn.image
import nilearn.datasets
from nilearn.plotting import surf_plotting

brainpedia = nilearn.datasets.fetch_neurovault_ids(image_ids=(32015,))
image = brainpedia.images[0]

fsaverage = nilearn.datasets.fetch_surf_fsaverage5()
pial_left = surf_plotting.load_surf_mesh(fsaverage.pial_left)[0]

texture = surf_plotting.niimg_to_surf_data(image, pial_left)

surf_plotting.plot_surf_stat_map(fsaverage.infl_left, texture, cmap='bwr')

plt.show()
66 changes: 66 additions & 0 deletions examples/01_plotting/show_ball_sampling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import numpy as np
import matplotlib.patches
from matplotlib import pyplot as plt
from nilearn.plotting import surf_plotting


def _piecewise_const_image():
w, h = 7, 10
image = np.random.uniform(0, 1, size=(w, h))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to avoid uncontroled RNGs in the codebase. The right way of doing this is to create a like RNG:

rng = np.random.RandomState(0)
image = rng.uniform(...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I hadn't realized that this was an example, and not a test.

I would simply seed the global RNG here: it's simpler and the side effect is less important.

image -= image.min()
image /= image.max()
big_image = np.empty((10 * w, 10 * h))
for i in range(w):
for j in range(h):
big_image[10 * i:10 * i + 10, 10 * j:10 * j + 10] = image[i, j]
return big_image


def _random_mesh(image_shape, n_nodes=5):
x = np.random.uniform(0, image_shape[0], size=(n_nodes, ))
y = np.random.uniform(0, image_shape[1], size=(n_nodes, ))
return np.asarray([x, y]).T


def show_sampling(ball_radius=10, n_nodes=5, n_points=7, link_points=True):
image = _piecewise_const_image()
images = [image, image > .5, image < .5, image < .8]
mesh = _random_mesh(image.shape, n_nodes=n_nodes)
all_values, sample_points, _ = surf_plotting._ball_sampling(
images, mesh, np.eye(3), n_points=n_points, ball_radius=ball_radius)
fig, axes = plt.subplots(2, 2)
axes = axes.ravel()
for image, values, ax in zip(images, all_values, axes):
ax.imshow(image, cmap='gray', vmin=0, vmax=1)
ax.add_patch(
matplotlib.patches.Rectangle(
(-.5, -.5),
image.shape[1],
image.shape[0],
fill=False,
linestyle='dashed'))
ax.scatter(
mesh[:, 1],
mesh[:, 0],
c=values,
s=300,
cmap='gray',
edgecolors='r',
vmin=0,
vmax=1,
zorder=2)
for sp, mp in zip(sample_points, mesh):
ax.scatter(sp[:, 1], sp[:, 0], marker='x', color='blue', zorder=3)
if link_points:
for s in sp:
ax.plot(
[mp[1], s[1]], [mp[0], s[0]],
color='red',
alpha=.5,
zorder=1)
return fig, axes


if __name__ == '__main__':
ax = show_sampling()
plt.show()
112 changes: 112 additions & 0 deletions nilearn/plotting/surf_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,127 @@
# Import libraries
import nibabel
import numpy as np

# These will be removed (see comment on _points_in_unit_ball)
from sklearn.externals import joblib
from ..datasets.utils import _get_dataset_dir
import sklearn.cluster
# / These will be removed (see comment on _points_in_unit_ball)

import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D
from nibabel import gifti

import nilearn.image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer relative imports.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not from nilearn import image and use functions in image module ?

from .._utils.compat import _basestring
from .. import _utils
from .img_plotting import _get_colorbar_and_data_ranges


# Eventually, when this PR is ready, the sample locations inside the unit ball
# will be hardcoded. For now we just compute them in a simple way (we will find
# something better than the k-means hack) and cache the result.
memory = joblib.Memory(_get_dataset_dir('joblib'), verbose=False)


@memory.cache
def _points_in_unit_ball(n_points=20, dim=3):
mc_cube = np.random.uniform(-1, 1, size=(5000, dim))
mc_ball = mc_cube[(mc_cube**2).sum(axis=1) <= 1.]
centroids, assignments, _ = sklearn.cluster.k_means(
mc_ball, n_clusters=n_points)
return centroids


# Eventually will be replaced by nilearn.image.resampling.coord_transform, but
# it only works for 3d images and 2d is useful for visu and debugging.
def _transform_coord(coords, affine):
return affine.dot(np.vstack([coords.T, np.ones(coords.shape[0])]))[:-1].T
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a function for that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, image.resampling.coord_transform. But it only handles 3d data, so if we
want to keep the example which illustrates the sampling methods in 2d we need
this (see comment). or is there another function for coord transforms in
nilearn?



def _ball_sample_locations(nodes, affine, ball_radius=3, n_points=20):
"""Get n_points regularly spaced inside a ball."""
if affine is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit surprising for a non-public functions

affine = np.eye(nodes.shape[1] + 1)
offsets_world_space = _points_in_unit_ball(
dim=nodes.shape[1], n_points=n_points) * ball_radius
# later:
# offset_voxels = nilearn.image.resampling.coord_transform(
# *offset_voxels.T, np.linalg.inv(affine))
mesh_voxel_space = _transform_coord(nodes, np.linalg.inv(affine))
linear_map = np.eye(affine.shape[0])
linear_map[:-1, :-1] = affine[:-1, :-1]
offsets_voxel_space = _transform_coord(offsets_world_space,
np.linalg.inv(linear_map))
sample_locations_voxel_space = (mesh_voxel_space[:, np.newaxis, :] +
offsets_voxel_space[np.newaxis, :])
return sample_locations_voxel_space, offsets_voxel_space


def _ball_sampling(images, nodes, affine=None, ball_radius=3, n_points=20):
"""In each image, average samples drawn from a ball around each node."""
images = np.asarray(images)
nodes = np.asarray(nodes)
sample_locations_voxel_space, offsets_voxel_space = _ball_sample_locations(
nodes, affine, ball_radius=ball_radius, n_points=n_points)
sample_indices = np.asarray(
np.floor(sample_locations_voxel_space), dtype=int)
pad_size = int(np.ceil(np.abs(offsets_voxel_space).max()))
pad = [(0, 0)] + [(pad_size, pad_size)] * nodes.shape[1]
sample_slices = [slice(None, None, None)] + np.s_[[
ax + pad_size for ax in np.rollaxis(sample_indices, -1)
]]
try:
samples = np.pad(
images, pad, mode='constant',
constant_values=np.nan)[sample_slices]
texture = np.nanmean(samples, axis=2)
if not(np.isfinite(texture).all()):
raise IndexError()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps consider adding a more verbose error message

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. This will not help the user.

except IndexError:
raise ValueError('Some nodes of the mesh are outside the image')
return texture, sample_indices, sample_locations_voxel_space


def niimg_to_surf_data(image, mesh_nodes, ball_radius=3.):
"""Extract surface data from a Nifti image.

Parameters
----------

image : niimg-like object, 3d or 4d.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n capital Niimg


mesh_nodes : array-like,shape n_nodes * 3
The coordinates of the nodes of the mesh.

ball_radius : float, optional (default=3.).
The radius (in mm) of the ball over which image intensities are
averaged around each node.

Returns
-------
texture: array-like, 1d or 2d.
If image was a 3d image (e.g a stat map), a 1d vector is returned,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply "If image was a 3D image" to "If 3D image is provided"

containing one value for each mesh node.
If image was a 4d image, a 2d array is returned, where each row
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: "If image was a 4d image" to "If 4D image is provided"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked doing surface plotting with functions we have using 4D stat map image. Did you checked it works ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked it words, and in test_surf_plotting.test_niimg_to_surf_data, I check we get the same result when we project an image by itself or as part of a 4d image. Maybe it needs more tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't gone through tests yet. I tried to do with ica maps but could not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's easy could you send me the failing example?

corresponds to a mesh node.

"""
image = nilearn.image.load_img(image)
original_dimension = len(image.shape)
image = _utils.check_niimg(image, atleast_4d=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we could simply use this in the first line and remove nilearn.image.load_img

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to load it first to get its original shape. How would you do it?

frames = np.rollaxis(image.get_data(), -1)
texture, indices, locations = _ball_sampling(
frames,
mesh_nodes,
_utils.compat.get_affine(image),
ball_radius=ball_radius)
if original_dimension == 3:
texture = texture[0]
return texture.T


# function to figure out datatype and load data
def load_surf_data(surf_data):
"""Loading data to be represented on a surface mesh.
Expand Down
46 changes: 41 additions & 5 deletions nilearn/plotting/tests/test_surf_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@

from distutils.version import LooseVersion
from nose import SkipTest
from numpy.testing import (assert_array_equal, assert_array_almost_equal,
assert_equal)
from nose.tools import assert_true, assert_raises
from nilearn._utils.testing import assert_raises_regex

import matplotlib
import numpy as np
import nibabel as nb
import matplotlib
import matplotlib.pyplot as plt
import sklearn.preprocessing

import nibabel as nb
from nibabel import gifti
from numpy.testing import (assert_array_equal, assert_array_almost_equal,
assert_equal)

from nilearn._utils.testing import assert_raises_regex
from nilearn import datasets
import nilearn.image
from nilearn.image.tests.test_resampling import rotation

from nilearn.plotting import surf_plotting
from nilearn.plotting.surf_plotting import (load_surf_data, load_surf_mesh,
plot_surf, plot_surf_stat_map,
plot_surf_roi)
Expand Down Expand Up @@ -388,3 +395,32 @@ def test_plot_surf_roi_error():
'Invalid input for roi_map',
plot_surf_roi, mesh,
roi_map={'roi1': roi1, 'roi2': roi2})


def test_ball_sampling():
img = np.eye(3)
img[-1, -1] = 7.5
nodes = [[5, 5], [15, 5], [25, 25]]
affine = 10 * np.eye(3)
affine[-1, -1] = 1
texture = surf_plotting._ball_sampling(
[img], nodes, affine=affine, ball_radius=1)
assert_array_equal(texture[0][0], [1., 0., 7.5])
assert_raises(ValueError, surf_plotting._ball_sampling, [img], nodes)


def test_niimg_to_surf_data():
mni = datasets.load_mni152_template()
fsaverage = datasets.fetch_surf_fsaverage5()
nodes = surf_plotting.load_surf_mesh(fsaverage.pial_left)[0]
proj_1 = surf_plotting.niimg_to_surf_data(mni, nodes)
assert_true(proj_1.ndim == 1)
mni_rot = nilearn.image.resample_img(
mni, target_affine=rotation(np.pi / 3., np.pi / 4.))
proj_2 = surf_plotting.niimg_to_surf_data(mni_rot, nodes)
assert_true((sklearn.preprocessing.normalize([proj_1])[0] *
sklearn.preprocessing.normalize([proj_2])[0]).sum() > .998)
mni_4d = nilearn.image.concat_imgs([mni, mni])
proj_4d = surf_plotting.niimg_to_surf_data(mni_4d, nodes)
assert_array_equal(proj_4d.shape, [10242, 2])
assert_array_almost_equal(proj_4d[:, 0], proj_1, 3)