{"action":"created","comment":{"url":"https://api.github.com/repos/nilearn/nilearn/pulls/comments/151397995","pull_request_review_id":77088677,"id":151397995,"diff_hunk":"@@ -0,0 +1,393 @@\n+# Tests for functions in surf_plotting.py\n+\n+import os\n+import tempfile\n+\n+from distutils.version import LooseVersion\n+from nose import SkipTest\n+from numpy.testing import (assert_array_equal, assert_array_almost_equal,\n+                           assert_equal)\n+from nose.tools import assert_true, assert_raises\n+from nilearn._utils.testing import assert_raises_regex\n+\n+import numpy as np\n+import scipy\n+from scipy.spatial import Delaunay\n+\n+import nibabel as nb\n+from nibabel import gifti\n+\n+from nilearn import datasets\n+from nilearn import image\n+from nilearn.image import resampling\n+from nilearn.image.tests.test_resampling import rotation\n+from nilearn import surface\n+from nilearn.surface import load_surf_data, load_surf_mesh\n+\n+currdir = os.path.dirname(os.path.abspath(__file__))\n+datadir = os.path.join(currdir, 'data')\n+\n+\n+def _generate_surf():\n+    rng = np.random.RandomState(42)\n+    coords = rng.rand(20, 3)\n+    faces = rng.randint(coords.shape[0], size=(30, 3))\n+    return [coords, faces]\n+\n+\n+def test_load_surf_data_array():\n+    # test loading and squeezing data from numpy array\n+    data_flat = np.zeros((20, ))\n+    data_squeeze = np.zeros((20, 1, 3))\n+    assert_array_equal(load_surf_data(data_flat), np.zeros((20, )))\n+    assert_array_equal(load_surf_data(data_squeeze), np.zeros((20, 3)))\n+\n+\n+def test_load_surf_data_file_nii_gii():\n+    # test loading of fake data from gifti file\n+    filename_gii = tempfile.mktemp(suffix='.gii')\n+    if LooseVersion(nb.__version__) > LooseVersion('2.0.2'):\n+        darray = gifti.GiftiDataArray(data=np.zeros((20, )))\n+    else:\n+        # Avoid a bug in nibabel 1.2.0 where GiftiDataArray were not\n+        # initialized properly:\n+        darray = gifti.GiftiDataArray.from_array(np.zeros((20, )),\n+                                                 intent='t test')\n+    gii = gifti.GiftiImage(darrays=[darray])\n+    gifti.write(gii, filename_gii)\n+    assert_array_equal(load_surf_data(filename_gii), np.zeros((20, )))\n+    os.remove(filename_gii)\n+\n+    # test loading of data from empty gifti file\n+    filename_gii_empty = tempfile.mktemp(suffix='.gii')\n+    gii_empty = gifti.GiftiImage()\n+    gifti.write(gii_empty, filename_gii_empty)\n+    assert_raises_regex(ValueError,\n+                        'must contain at least one data array',\n+                        load_surf_data, filename_gii_empty)\n+    os.remove(filename_gii_empty)\n+\n+    # test loading of fake data from nifti file\n+    filename_nii = tempfile.mktemp(suffix='.nii')\n+    filename_niigz = tempfile.mktemp(suffix='.nii.gz')\n+    nii = nb.Nifti1Image(np.zeros((20, )), affine=None)\n+    nb.save(nii, filename_nii)\n+    nb.save(nii, filename_niigz)\n+    assert_array_equal(load_surf_data(filename_nii), np.zeros((20, )))\n+    assert_array_equal(load_surf_data(filename_niigz), np.zeros((20, )))\n+    os.remove(filename_nii)\n+    os.remove(filename_niigz)\n+\n+\n+def test_load_surf_data_file_freesurfer():\n+    # test loading of fake data from sulc and thickness files\n+    # using load_surf_data.\n+    # We test load_surf_data by creating fake data with function\n+    # 'write_morph_data' that works only if nibabel\n+    # version is recent with nibabel >= 2.1.0\n+    if LooseVersion(nb.__version__) >= LooseVersion('2.1.0'):\n+        data = np.zeros((20, ))\n+        filename_sulc = tempfile.mktemp(suffix='.sulc')\n+        nb.freesurfer.io.write_morph_data(filename_sulc, data)\n+        assert_array_equal(load_surf_data(filename_sulc), np.zeros((20, )))\n+        os.remove(filename_sulc)\n+\n+        filename_thick = tempfile.mktemp(suffix='.thickness')\n+        nb.freesurfer.io.write_morph_data(filename_thick, data)\n+        assert_array_equal(load_surf_data(filename_thick), np.zeros((20, )))\n+        os.remove(filename_thick)\n+\n+    # test loading of data from real label and annot files\n+    label_start = np.array([5900, 5899, 5901, 5902, 2638])\n+    label_end = np.array([8756, 6241, 8757, 1896, 6243])\n+    label = load_surf_data(os.path.join(datadir, 'test.label'))\n+    assert_array_equal(label[:5], label_start)\n+    assert_array_equal(label[-5:], label_end)\n+    assert_equal(label.shape, (10, ))\n+    del label, label_start, label_end\n+\n+    annot_start = np.array([24, 29, 28, 27, 24, 31, 11, 25, 0, 12])\n+    annot_end = np.array([16, 16, 16, 16, 16, 16, 16, 16, 16, 16])\n+    annot = load_surf_data(os.path.join(datadir, 'test.annot'))\n+    assert_array_equal(annot[:10], annot_start)\n+    assert_array_equal(annot[-10:], annot_end)\n+    assert_equal(annot.shape, (10242, ))\n+    del annot, annot_start, annot_end\n+\n+\n+def test_load_surf_data_file_error():\n+    # test if files with unexpected suffixes raise errors\n+    data = np.zeros((20, ))\n+    wrong_suff = ['.vtk', '.obj', '.mnc', '.txt']\n+    for suff in wrong_suff:\n+        filename_wrong = tempfile.mktemp(suffix=suff)\n+        np.savetxt(filename_wrong, data)\n+        assert_raises_regex(ValueError,\n+                            'input type is not recognized',\n+                            load_surf_data, filename_wrong)\n+        os.remove(filename_wrong)\n+\n+\n+def test_load_surf_mesh_list():\n+    # test if correct list is returned\n+    mesh = _generate_surf()\n+    assert_equal(len(load_surf_mesh(mesh)), 2)\n+    assert_array_equal(load_surf_mesh(mesh)[0], mesh[0])\n+    assert_array_equal(load_surf_mesh(mesh)[1], mesh[1])\n+    # test if incorrect list, array or dict raises error\n+    assert_raises_regex(ValueError, 'it must have two elements',\n+                        load_surf_mesh, [])\n+    assert_raises_regex(ValueError, 'it must have two elements',\n+                        load_surf_mesh, [mesh[0]])\n+    assert_raises_regex(ValueError, 'it must have two elements',\n+                        load_surf_mesh, [mesh[0], mesh[1], mesh[1]])\n+    assert_raises_regex(ValueError, 'input type is not recognized',\n+                        load_surf_mesh, mesh[0])\n+    assert_raises_regex(ValueError, 'input type is not recognized',\n+                        load_surf_mesh, dict())\n+    del mesh\n+\n+\n+def test_load_surf_mesh_file_gii():\n+    # Test the loader `load_surf_mesh`\n+\n+    # If nibabel is of older version we skip tests as nibabel does not\n+    # support intent argument and intent codes are not handled properly with\n+    # older versions\n+\n+    if not LooseVersion(nb.__version__) >= LooseVersion('2.1.0'):\n+        raise SkipTest\n+\n+    mesh = _generate_surf()\n+\n+    # test if correct gii is loaded into correct list\n+    filename_gii_mesh = tempfile.mktemp(suffix='.gii')\n+\n+    coord_array = gifti.GiftiDataArray(data=mesh[0],\n+                                       intent=nb.nifti1.intent_codes[\n+                                           'NIFTI_INTENT_POINTSET'])\n+    face_array = gifti.GiftiDataArray(data=mesh[1],\n+                                      intent=nb.nifti1.intent_codes[\n+                                          'NIFTI_INTENT_TRIANGLE'])\n+\n+    gii = gifti.GiftiImage(darrays=[coord_array, face_array])\n+    gifti.write(gii, filename_gii_mesh)\n+    assert_array_equal(load_surf_mesh(filename_gii_mesh)[0], mesh[0])\n+    assert_array_equal(load_surf_mesh(filename_gii_mesh)[1], mesh[1])\n+    os.remove(filename_gii_mesh)\n+\n+    # test if incorrect gii raises error\n+    filename_gii_mesh_no_point = tempfile.mktemp(suffix='.gii')\n+    gifti.write(gifti.GiftiImage(darrays=[face_array, face_array]),\n+                filename_gii_mesh_no_point)\n+    assert_raises_regex(ValueError, 'NIFTI_INTENT_POINTSET',\n+                        load_surf_mesh, filename_gii_mesh_no_point)\n+    os.remove(filename_gii_mesh_no_point)\n+\n+    filename_gii_mesh_no_face = tempfile.mktemp(suffix='.gii')\n+    gifti.write(gifti.GiftiImage(darrays=[coord_array, coord_array]),\n+                filename_gii_mesh_no_face)\n+    assert_raises_regex(ValueError, 'NIFTI_INTENT_TRIANGLE',\n+                        load_surf_mesh, filename_gii_mesh_no_face)\n+    os.remove(filename_gii_mesh_no_face)\n+\n+\n+def test_load_surf_mesh_file_freesurfer():\n+    # Older nibabel versions does not support 'write_geometry'\n+    if LooseVersion(nb.__version__) <= LooseVersion('1.2.0'):\n+        raise SkipTest\n+\n+    mesh = _generate_surf()\n+    for suff in ['.pial', '.inflated', '.white', '.orig', 'sphere']:\n+        filename_fs_mesh = tempfile.mktemp(suffix=suff)\n+        nb.freesurfer.write_geometry(filename_fs_mesh, mesh[0], mesh[1])\n+        assert_equal(len(load_surf_mesh(filename_fs_mesh)), 2)\n+        assert_array_almost_equal(load_surf_mesh(filename_fs_mesh)[0],\n+                                  mesh[0])\n+        assert_array_almost_equal(load_surf_mesh(filename_fs_mesh)[1],\n+                                  mesh[1])\n+        os.remove(filename_fs_mesh)\n+\n+\n+def test_load_surf_mesh_file_error():\n+    if LooseVersion(nb.__version__) <= LooseVersion('1.2.0'):\n+        raise SkipTest\n+\n+    # test if files with unexpected suffixes raise errors\n+    mesh = _generate_surf()\n+    wrong_suff = ['.vtk', '.obj', '.mnc', '.txt']\n+    for suff in wrong_suff:\n+        filename_wrong = tempfile.mktemp(suffix=suff)\n+        nb.freesurfer.write_geometry(filename_wrong, mesh[0], mesh[1])\n+        assert_raises_regex(ValueError,\n+                            'input type is not recognized',\n+                            load_surf_data, filename_wrong)\n+        os.remove(filename_wrong)\n+\n+\n+def _flat_mesh(x_s, y_s, z=0):\n+    # before 0.14 the vertices of the triangles in the delaunay triangulation\n+    # are not ordered correctly, hence there is no way to know the direction of\n+    # the outer normal.\n+    if LooseVersion(scipy.__version__) < LooseVersion('0.14'):\n+        raise SkipTest()\n+    x, y = np.mgrid[:x_s, :y_s]\n+    x, y = x.ravel(), y.ravel()\n+    z = np.ones(len(x)) * z\n+    vertices = np.asarray([x, y, z]).T\n+    triangulation = Delaunay(vertices[:, :2]).simplices\n+    mesh = [vertices, triangulation]\n+    return mesh\n+\n+\n+def _z_const_img(x_s, y_s, z_s):\n+    hslice = np.arange(x_s * y_s).reshape((x_s, y_s))\n+    return np.ones((x_s, y_s, z_s)) * hslice[:, :, np.newaxis]\n+\n+\n+def test_vertex_outer_normals():\n+    # compute normals for a flat horizontal mesh, they should all be (0, 0, 1)\n+    mesh = _flat_mesh(5, 7)\n+    computed_normals = surface._vertex_outer_normals(mesh)\n+    true_normals = np.zeros((len(mesh[0]), 3))\n+    true_normals[:, 2] = 1\n+    assert_array_almost_equal(computed_normals, true_normals)\n+\n+\n+def test_sample_locations():\n+    # check positions of samples on toy example, with an affine != identity\n+    # flat horizontal mesh\n+    mesh = _flat_mesh(5, 7)\n+    affine = np.diagflat([10, 20, 30, 1])\n+    inv_affine = np.linalg.inv(affine)\n+    # transform vertices to world space\n+    vertices = np.asarray(\n+        resampling.coord_transform(*mesh[0].T, affine=affine)).T\n+    # compute by hand the true offsets in voxel space\n+    # (transformed by affine^-1)\n+    ball_offsets = surface._uniform_ball_cloud(10)\n+    ball_offsets = np.asarray(\n+        resampling.coord_transform(*ball_offsets.T, affine=inv_affine)).T\n+    line_offsets = np.zeros((10, 3))\n+    line_offsets[:, 2] = np.linspace(-1, 1, 10)\n+    line_offsets = np.asarray(\n+        resampling.coord_transform(*line_offsets.T, affine=inv_affine)).T\n+    # check we get the same locations\n+    for kind, offsets in [('line', line_offsets), ('ball', ball_offsets)]:\n+        locations = surface._sample_locations(\n+            [vertices, mesh[1]], affine, 1., kind=kind, n_points=10)\n+        true_locations = [vertex + offsets for vertex in mesh[0]]\n+        assert_array_almost_equal(true_locations, locations)\n+    assert_raises(ValueError, surface._sample_locations,\n+                  mesh, affine, 1., kind='bad_kind')\n+\n+\n+def test_masked_indices():\n+    mask = np.ones((4, 3, 8))\n+    mask[:, :, ::2] = 0\n+    locations = np.mgrid[:5, :3, :8].ravel().reshape((3, -1))\n+    masked = surface._masked_indices(locations.T, mask.shape, mask)\n+    # These elements are masked by the mask\n+    assert_true((masked[::2] == 1).all())\n+    # The last element of locations is one row beyond first image dimension\n+    assert_true((masked[-24:] == 1).all())\n+    # 4 * 3 * 8 / 2 elements should remain unmasked\n+    assert_true((1 - masked).sum() == 48)\n+\n+\n+def test_projection_matrix():\n+    mesh = _flat_mesh(5, 7, 4)\n+    img = _z_const_img(5, 7, 13)\n+    proj = surface.projection_matrix(\n+        mesh, np.eye(4), img.shape, radius=2., n_points=10)\n+    # proj matrix has shape (n_vertices, img_size)\n+    assert_equal(proj.shape, (5 * 7, 5 * 7 * 13))\n+    # proj.dot(img) should give the values of img at the vertices' locations\n+    values = proj.dot(img.ravel()).reshape((5, 7))\n+    assert_array_almost_equal(values, img[:, :, 0])\n+    mesh = _flat_mesh(5, 7)\n+    proj = surface.projection_matrix(\n+        mesh, np.eye(4), (5, 7, 1), radius=.1, n_points=10)\n+    assert_array_almost_equal(proj.toarray(), np.eye(proj.shape[0]))\n+    mask = np.ones(img.shape, dtype=int)\n+    mask[0] = 0\n+    proj = surface.projection_matrix(\n+        mesh, np.eye(4), img.shape, radius=2., n_points=10, mask=mask)\n+    proj = proj.toarray()\n+    # first row of the mesh is masked\n+    assert_array_almost_equal(proj.sum(axis=1)[:7], np.zeros(7))\n+    assert_array_almost_equal(proj.sum(axis=1)[7:], np.ones(proj.shape[0] - 7))\n+    # mask and img should have the same shape\n+    assert_raises(ValueError, surface.projection_matrix,\n+                  mesh, np.eye(4), img.shape, mask=np.ones((3, 3, 2)))\n+\n+\n+def test_sampling_affine():\n+    # check sampled (projected) values on a toy image\n+    img = np.ones((4, 4, 4))\n+    img[1, :, :] = 2\n+    nodes = [[1, 1, 2], [10, 10, 20], [30, 30, 30]]\n+    mesh = [np.asarray(nodes), None]\n+    affine = 10 * np.eye(4)\n+    affine[-1, -1] = 1\n+    texture = surface._nearest_voxel_sampling(\n+        [img], mesh, affine=affine, radius=1, kind='ball')\n+    assert_array_equal(texture[0], [1., 2., 1.])\n+    if LooseVersion(scipy.__version__) < LooseVersion('0.14'):\n+        return\n+    texture = surface._interpolation_sampling(\n+        [img], mesh, affine=affine, radius=0, kind='ball')\n+    assert_array_almost_equal(texture[0], [1.1, 2., 1.])\n+\n+\n+def test_sampling():\n+    mesh = _flat_mesh(5, 7, 4)\n+    img = _z_const_img(5, 7, 13)\n+    mask = np.ones(img.shape, dtype=int)\n+    mask[0] = 0\n+    projectors = [surface._nearest_voxel_sampling]\n+    if LooseVersion(scipy.__version__) >= LooseVersion('0.14'):\n+        projectors.append(surface._interpolation_sampling)\n+    for kind in ('line', 'ball'):\n+        for projector in projectors:\n+            projection = projector([img], mesh, np.eye(4),\n+                                   kind=kind, radius=0.)\n+            assert_array_almost_equal(projection.ravel(), img[:, :, 0].ravel())\n+            projection = projector([img], mesh, np.eye(4),\n+                                   kind=kind, radius=0., mask=mask)\n+            assert_array_almost_equal(projection.ravel()[7:],\n+                                      img[1:, :, 0].ravel())\n+            assert_true(np.isnan(projection.ravel()[:7]).all())\n+\n+\n+def test_vol_to_surf():\n+    # test 3d niimg to cortical surface projection and invariance to a change\n+    # of affine\n+    mni = datasets.load_mni152_template()\n+    mesh = _generate_surf()\n+    _check_vol_to_surf_results(mni, mesh)\n+    if LooseVersion(nb.__version__) > LooseVersion('1.2.0'):\n+        fsaverage = datasets.fetch_surf_fsaverage5().pial_left\n+        _check_vol_to_surf_results(mni, fsaverage)\n+\n+\n+def _check_vol_to_surf_results(img, mesh):\n+    for kind in ['ball', 'line']:\n+        for interpolation in ['linear', 'nearest']:\n+            proj_1 = surface.vol_to_surf(\n+                img, mesh, kind=kind, interpolation=interpolation)\n+            assert_true(proj_1.ndim == 1)\n+            img_rot = image.resample_img(\n+                img, target_affine=rotation(np.pi / 3., np.pi / 4.))\n+            proj_2 = surface.vol_to_surf(\n+                img_rot, mesh, kind=kind, interpolation=interpolation)\n+            # The projection values for the rotated image should be very close\n+            # to the projection for the original image\n+            diff = np.abs(proj_1 - proj_2) / np.abs(proj_1)\n+            assert_true(np.mean(diff[diff < np.inf]) < .03)\n+            img_4d = image.concat_imgs([img, img])\n+            proj_4d = surface.vol_to_surf(","path":"nilearn/tests/test_surface.py","position":389,"original_position":389,"commit_id":"40bbfa473f9e92d4103ecafb53d33587d808830c","original_commit_id":"40bbfa473f9e92d4103ecafb53d33587d808830c","user":{"login":"KamalakerDadi","id":11410385,"avatar_url":"https://avatars2.githubusercontent.com/u/11410385?v=4","gravatar_id":"","url":"https://api.github.com/users/KamalakerDadi","html_url":"https://github.com/KamalakerDadi","followers_url":"https://api.github.com/users/KamalakerDadi/followers","following_url":"https://api.github.com/users/KamalakerDadi/following{/other_user}","gists_url":"https://api.github.com/users/KamalakerDadi/gists{/gist_id}","starred_url":"https://api.github.com/users/KamalakerDadi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KamalakerDadi/subscriptions","organizations_url":"https://api.github.com/users/KamalakerDadi/orgs","repos_url":"https://api.github.com/users/KamalakerDadi/repos","events_url":"https://api.github.com/users/KamalakerDadi/events{/privacy}","received_events_url":"https://api.github.com/users/KamalakerDadi/received_events","type":"User","site_admin":false},"body":"same here, can you please test with mask ?","created_at":"2017-11-16T12:19:46Z","updated_at":"2017-11-16T12:45:53Z","html_url":"https://github.com/nilearn/nilearn/pull/1516#discussion_r151397995","pull_request_url":"https://api.github.com/repos/nilearn/nilearn/pulls/1516","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/comments/151397995"},"html":{"href":"https://github.com/nilearn/nilearn/pull/1516#discussion_r151397995"},"pull_request":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/1516"}}},"pull_request":{"url":"https://api.github.com/repos/nilearn/nilearn/pulls/1516","id":143629738,"html_url":"https://github.com/nilearn/nilearn/pull/1516","diff_url":"https://github.com/nilearn/nilearn/pull/1516.diff","patch_url":"https://github.com/nilearn/nilearn/pull/1516.patch","issue_url":"https://api.github.com/repos/nilearn/nilearn/issues/1516","number":1516,"state":"open","locked":false,"title":"[WIP] Cortex surface projections","user":{"login":"jeromedockes","id":9196501,"avatar_url":"https://avatars3.githubusercontent.com/u/9196501?v=4","gravatar_id":"","url":"https://api.github.com/users/jeromedockes","html_url":"https://github.com/jeromedockes","followers_url":"https://api.github.com/users/jeromedockes/followers","following_url":"https://api.github.com/users/jeromedockes/following{/other_user}","gists_url":"https://api.github.com/users/jeromedockes/gists{/gist_id}","starred_url":"https://api.github.com/users/jeromedockes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeromedockes/subscriptions","organizations_url":"https://api.github.com/users/jeromedockes/orgs","repos_url":"https://api.github.com/users/jeromedockes/repos","events_url":"https://api.github.com/users/jeromedockes/events{/privacy}","received_events_url":"https://api.github.com/users/jeromedockes/received_events","type":"User","site_admin":false},"body":"Hello @GaelVaroquaux , @mrahim , @agramfort, @juhuntenburg  and others,\r\n this is a PR about surface plotting.\r\n\r\nnilearn has some awesome functions to plot surface data in\r\nnilearn.plotting.surf_plotting. However, it doesn't offer a conversion from\r\nvolumetric to surface data.\r\n\r\nIt would be great to add a function to sample or project volumetric data on the\r\nnodes of a cortical mesh; this would allow users to look at surface plots of\r\ntheir 3d images (e.g. statistical maps).\r\n\r\nIn this PR we will try to add this to nilearn.\r\n\r\nMost tools which offer this functionality (e.g. caret, freesurfer, pycortex)\r\nusually propose several projection and sampling strategies, offering different\r\nquality / speed tradeoffs. However, it seems to me that naive strategies are not\r\nso far behind more elaborate ones - see for example [Operto, Grégory, et al.\r\n\"Projection of fMRI data onto the cortical surface using anatomically-informed\r\nconvolution kernels.\" Neuroimage 39.1 (2008): 127-135].\r\nFor plotting and visualisation, the results of a simple strategy are probably\r\naccurate enough for most users.\r\n\r\nI therefore suggest to start by including a very simple and fast projection\r\nscheme, and we can add more elaborate ones later if we want.\r\nI'm just getting started but I think we can already start a discussion.\r\n\r\nThe proposed strategy is simply to draw a sample from a 3mm sphere around each\r\nmesh node, and average the measures.\r\n\r\nThe image below illustrates that strategy: each red circle is a mesh node.\r\nSamples are drawn from the blue crosses that are attached to it, and are inside\r\nthe image, and then averaged to compute the color inside the circle.\r\n(This image is produced by the show_sampling.py example, which is only there to clarify\r\nthe strategy implemented in this PR and will be removed).\r\n\r\n\r\n![illustration_2d](https://user-images.githubusercontent.com/9196501/30964668-aa433a20-a452-11e7-96d6-5a0f8c05c959.png)\r\n\r\n\r\nHere is an example surface plot for a brainpedia image (id 32015 on Neurovault,\r\nhttps://neurovault.org/media/images/1952/task007_face_vs_baseline_pycortex/index.html),\r\nproduced by brainpedia_surface.py:\r\n\r\n![brainpedia_inflated](https://user-images.githubusercontent.com/9196501/30964957-c747b55a-a453-11e7-8a38-777e359d3461.png)\r\n\r\nAnd here is the plot produced by pycortex for the same image, as shown on\r\nNeurovault:\r\n\r\n![brainpedia_pycortex](https://user-images.githubusercontent.com/9196501/30964987-e38cd4de-a453-11e7-9d5f-f1c284f93954.png)\r\n\r\nNote about performance: in order to choose the positions of the samples to draw\r\nfrom a unit ball, for now, we cluster points drawn from a uniform distribution\r\non the ball and keep the centroids (we can think of something better). This\r\ntakes a few seconds and the results are cached with joblib for the time being,\r\nbut since it only needs to be done once, when we have decided how many samples\r\nwe want, the positions will be hardcoded once and for all (no computing, no\r\ncaching). with 100 samples per ball, projecting a stat map of the full brain\r\nwith 2mm voxels on an fsaverage hemisphere mesh takes around 60 ms.\r\n","created_at":"2017-09-28T12:05:23Z","updated_at":"2017-11-16T12:45:53Z","closed_at":null,"merged_at":null,"merge_commit_sha":"72ae77b97f794f4c900341b8ebd811d19104d86a","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/nilearn/nilearn/pulls/1516/commits","review_comments_url":"https://api.github.com/repos/nilearn/nilearn/pulls/1516/comments","review_comment_url":"https://api.github.com/repos/nilearn/nilearn/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nilearn/nilearn/issues/1516/comments","statuses_url":"https://api.github.com/repos/nilearn/nilearn/statuses/40bbfa473f9e92d4103ecafb53d33587d808830c","head":{"label":"jeromedockes:cortex_surface_projections","ref":"cortex_surface_projections","sha":"40bbfa473f9e92d4103ecafb53d33587d808830c","user":{"login":"jeromedockes","id":9196501,"avatar_url":"https://avatars3.githubusercontent.com/u/9196501?v=4","gravatar_id":"","url":"https://api.github.com/users/jeromedockes","html_url":"https://github.com/jeromedockes","followers_url":"https://api.github.com/users/jeromedockes/followers","following_url":"https://api.github.com/users/jeromedockes/following{/other_user}","gists_url":"https://api.github.com/users/jeromedockes/gists{/gist_id}","starred_url":"https://api.github.com/users/jeromedockes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeromedockes/subscriptions","organizations_url":"https://api.github.com/users/jeromedockes/orgs","repos_url":"https://api.github.com/users/jeromedockes/repos","events_url":"https://api.github.com/users/jeromedockes/events{/privacy}","received_events_url":"https://api.github.com/users/jeromedockes/received_events","type":"User","site_admin":false},"repo":{"id":55776026,"name":"nilearn","full_name":"jeromedockes/nilearn","owner":{"login":"jeromedockes","id":9196501,"avatar_url":"https://avatars3.githubusercontent.com/u/9196501?v=4","gravatar_id":"","url":"https://api.github.com/users/jeromedockes","html_url":"https://github.com/jeromedockes","followers_url":"https://api.github.com/users/jeromedockes/followers","following_url":"https://api.github.com/users/jeromedockes/following{/other_user}","gists_url":"https://api.github.com/users/jeromedockes/gists{/gist_id}","starred_url":"https://api.github.com/users/jeromedockes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeromedockes/subscriptions","organizations_url":"https://api.github.com/users/jeromedockes/orgs","repos_url":"https://api.github.com/users/jeromedockes/repos","events_url":"https://api.github.com/users/jeromedockes/events{/privacy}","received_events_url":"https://api.github.com/users/jeromedockes/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/jeromedockes/nilearn","description":"Machine learning for NeuroImaging in Python","fork":true,"url":"https://api.github.com/repos/jeromedockes/nilearn","forks_url":"https://api.github.com/repos/jeromedockes/nilearn/forks","keys_url":"https://api.github.com/repos/jeromedockes/nilearn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jeromedockes/nilearn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jeromedockes/nilearn/teams","hooks_url":"https://api.github.com/repos/jeromedockes/nilearn/hooks","issue_events_url":"https://api.github.com/repos/jeromedockes/nilearn/issues/events{/number}","events_url":"https://api.github.com/repos/jeromedockes/nilearn/events","assignees_url":"https://api.github.com/repos/jeromedockes/nilearn/assignees{/user}","branches_url":"https://api.github.com/repos/jeromedockes/nilearn/branches{/branch}","tags_url":"https://api.github.com/repos/jeromedockes/nilearn/tags","blobs_url":"https://api.github.com/repos/jeromedockes/nilearn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jeromedockes/nilearn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jeromedockes/nilearn/git/refs{/sha}","trees_url":"https://api.github.com/repos/jeromedockes/nilearn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jeromedockes/nilearn/statuses/{sha}","languages_url":"https://api.github.com/repos/jeromedockes/nilearn/languages","stargazers_url":"https://api.github.com/repos/jeromedockes/nilearn/stargazers","contributors_url":"https://api.github.com/repos/jeromedockes/nilearn/contributors","subscribers_url":"https://api.github.com/repos/jeromedockes/nilearn/subscribers","subscription_url":"https://api.github.com/repos/jeromedockes/nilearn/subscription","commits_url":"https://api.github.com/repos/jeromedockes/nilearn/commits{/sha}","git_commits_url":"https://api.github.com/repos/jeromedockes/nilearn/git/commits{/sha}","comments_url":"https://api.github.com/repos/jeromedockes/nilearn/comments{/number}","issue_comment_url":"https://api.github.com/repos/jeromedockes/nilearn/issues/comments{/number}","contents_url":"https://api.github.com/repos/jeromedockes/nilearn/contents/{+path}","compare_url":"https://api.github.com/repos/jeromedockes/nilearn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jeromedockes/nilearn/merges","archive_url":"https://api.github.com/repos/jeromedockes/nilearn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jeromedockes/nilearn/downloads","issues_url":"https://api.github.com/repos/jeromedockes/nilearn/issues{/number}","pulls_url":"https://api.github.com/repos/jeromedockes/nilearn/pulls{/number}","milestones_url":"https://api.github.com/repos/jeromedockes/nilearn/milestones{/number}","notifications_url":"https://api.github.com/repos/jeromedockes/nilearn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jeromedockes/nilearn/labels{/name}","releases_url":"https://api.github.com/repos/jeromedockes/nilearn/releases{/id}","deployments_url":"https://api.github.com/repos/jeromedockes/nilearn/deployments","created_at":"2016-04-08T12:05:30Z","updated_at":"2016-04-08T12:05:32Z","pushed_at":"2017-11-15T21:43:15Z","git_url":"git://github.com/jeromedockes/nilearn.git","ssh_url":"git@github.com:jeromedockes/nilearn.git","clone_url":"https://github.com/jeromedockes/nilearn.git","svn_url":"https://github.com/jeromedockes/nilearn","homepage":"http://nilearn.github.io","size":11536,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"nilearn:master","ref":"master","sha":"e442a34a9eda52b2c2697c5b176ec3d95a99e8d3","user":{"login":"nilearn","id":4646912,"avatar_url":"https://avatars3.githubusercontent.com/u/4646912?v=4","gravatar_id":"","url":"https://api.github.com/users/nilearn","html_url":"https://github.com/nilearn","followers_url":"https://api.github.com/users/nilearn/followers","following_url":"https://api.github.com/users/nilearn/following{/other_user}","gists_url":"https://api.github.com/users/nilearn/gists{/gist_id}","starred_url":"https://api.github.com/users/nilearn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nilearn/subscriptions","organizations_url":"https://api.github.com/users/nilearn/orgs","repos_url":"https://api.github.com/users/nilearn/repos","events_url":"https://api.github.com/users/nilearn/events{/privacy}","received_events_url":"https://api.github.com/users/nilearn/received_events","type":"Organization","site_admin":false},"repo":{"id":1235740,"name":"nilearn","full_name":"nilearn/nilearn","owner":{"login":"nilearn","id":4646912,"avatar_url":"https://avatars3.githubusercontent.com/u/4646912?v=4","gravatar_id":"","url":"https://api.github.com/users/nilearn","html_url":"https://github.com/nilearn","followers_url":"https://api.github.com/users/nilearn/followers","following_url":"https://api.github.com/users/nilearn/following{/other_user}","gists_url":"https://api.github.com/users/nilearn/gists{/gist_id}","starred_url":"https://api.github.com/users/nilearn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nilearn/subscriptions","organizations_url":"https://api.github.com/users/nilearn/orgs","repos_url":"https://api.github.com/users/nilearn/repos","events_url":"https://api.github.com/users/nilearn/events{/privacy}","received_events_url":"https://api.github.com/users/nilearn/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/nilearn/nilearn","description":"Machine learning for NeuroImaging in Python","fork":false,"url":"https://api.github.com/repos/nilearn/nilearn","forks_url":"https://api.github.com/repos/nilearn/nilearn/forks","keys_url":"https://api.github.com/repos/nilearn/nilearn/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nilearn/nilearn/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nilearn/nilearn/teams","hooks_url":"https://api.github.com/repos/nilearn/nilearn/hooks","issue_events_url":"https://api.github.com/repos/nilearn/nilearn/issues/events{/number}","events_url":"https://api.github.com/repos/nilearn/nilearn/events","assignees_url":"https://api.github.com/repos/nilearn/nilearn/assignees{/user}","branches_url":"https://api.github.com/repos/nilearn/nilearn/branches{/branch}","tags_url":"https://api.github.com/repos/nilearn/nilearn/tags","blobs_url":"https://api.github.com/repos/nilearn/nilearn/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nilearn/nilearn/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nilearn/nilearn/git/refs{/sha}","trees_url":"https://api.github.com/repos/nilearn/nilearn/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nilearn/nilearn/statuses/{sha}","languages_url":"https://api.github.com/repos/nilearn/nilearn/languages","stargazers_url":"https://api.github.com/repos/nilearn/nilearn/stargazers","contributors_url":"https://api.github.com/repos/nilearn/nilearn/contributors","subscribers_url":"https://api.github.com/repos/nilearn/nilearn/subscribers","subscription_url":"https://api.github.com/repos/nilearn/nilearn/subscription","commits_url":"https://api.github.com/repos/nilearn/nilearn/commits{/sha}","git_commits_url":"https://api.github.com/repos/nilearn/nilearn/git/commits{/sha}","comments_url":"https://api.github.com/repos/nilearn/nilearn/comments{/number}","issue_comment_url":"https://api.github.com/repos/nilearn/nilearn/issues/comments{/number}","contents_url":"https://api.github.com/repos/nilearn/nilearn/contents/{+path}","compare_url":"https://api.github.com/repos/nilearn/nilearn/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nilearn/nilearn/merges","archive_url":"https://api.github.com/repos/nilearn/nilearn/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nilearn/nilearn/downloads","issues_url":"https://api.github.com/repos/nilearn/nilearn/issues{/number}","pulls_url":"https://api.github.com/repos/nilearn/nilearn/pulls{/number}","milestones_url":"https://api.github.com/repos/nilearn/nilearn/milestones{/number}","notifications_url":"https://api.github.com/repos/nilearn/nilearn/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nilearn/nilearn/labels{/name}","releases_url":"https://api.github.com/repos/nilearn/nilearn/releases{/id}","deployments_url":"https://api.github.com/repos/nilearn/nilearn/deployments","created_at":"2011-01-09T19:02:23Z","updated_at":"2017-11-15T14:26:08Z","pushed_at":"2017-11-15T21:43:17Z","git_url":"git://github.com/nilearn/nilearn.git","ssh_url":"git@github.com:nilearn/nilearn.git","clone_url":"https://github.com/nilearn/nilearn.git","svn_url":"https://github.com/nilearn/nilearn","homepage":"http://nilearn.github.io","size":11275,"stargazers_count":307,"watchers_count":307,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":221,"mirror_url":null,"archived":false,"open_issues_count":242,"forks":221,"open_issues":242,"watchers":307,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/1516"},"html":{"href":"https://github.com/nilearn/nilearn/pull/1516"},"issue":{"href":"https://api.github.com/repos/nilearn/nilearn/issues/1516"},"comments":{"href":"https://api.github.com/repos/nilearn/nilearn/issues/1516/comments"},"review_comments":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/1516/comments"},"review_comment":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nilearn/nilearn/pulls/1516/commits"},"statuses":{"href":"https://api.github.com/repos/nilearn/nilearn/statuses/40bbfa473f9e92d4103ecafb53d33587d808830c"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"KamalakerDadi"}}