diff --git a/advanced_topics/08_fslpy.ipynb b/advanced_topics/08_fslpy.ipynb index bd6881587408aacc837eb5993c71d5c686ad80e7..4895b8427fd4d443cab3c951228c6d9b794de17b 100644 --- a/advanced_topics/08_fslpy.ipynb +++ b/advanced_topics/08_fslpy.ipynb @@ -26,13 +26,112 @@ " * [Creating images](#creating-images)\n", " * [Working with image data](#working-with-image-data)\n", " * [Loading other file types](#loading-other-file-types)\n", - "* [FSL atlases](#fsl-atlases)\n", + " * [NIfTI coordinate systems](#nifti-coordinate-systems)\n", + " * [Image processing](#image-processing)\n", "* [The `filetree`](#the-filetree)\n", - "* [Image processing](#image-processing)\n", "* [FSL wrapper functions](#fsl-wrapper-functions)\n", - "* [NIfTI coordinate systems](#nifti-coordinate-systems)\n", + "* [FSL atlases](#fsl-atlases)\n", + " * [Querying atlases](#querying-atlases)\n", + " * [Loading atlas images](#loading-atlas-images)\n", + " * [Working with atlases](#working-with-atlases)\n", + "\n", + "\n", + "Let's start with some standard imports and environment set-up:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "import os.path as op\n", + "import nibabel as nib\n", + "import numpy as np\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And a little function that we can use to generate a simple orthographic plot:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def ortho(data, voxel, fig=None, **kwargs):\n", + " \"\"\"Simple orthographic plot of a 3D array using matplotlib.\n", + "\n", + " :arg data: 3D numpy array\n", + " :arg voxel: XYZ coordinates for each slice\n", + " :arg fig: Existing figure and axes for overlay plotting\n", "\n", + " All other arguments are passed through to the `imshow` function.\n", "\n", + " :returns: The figure and axes (which can be passed back in as the\n", + " `fig` argument to plot overlays).\n", + " \"\"\"\n", + " x, y, z = voxel\n", + " xslice = np.flipud(data[x, :, :].T)\n", + " yslice = np.flipud(data[:, y, :].T)\n", + " zslice = np.flipud(data[:, :, z].T)\n", + "\n", + " if fig is None:\n", + " fig = plt.figure()\n", + " xax = fig.add_subplot(1, 3, 1)\n", + " yax = fig.add_subplot(1, 3, 2)\n", + " zax = fig.add_subplot(1, 3, 3)\n", + " else:\n", + " fig, xax, yax, zax = fig\n", + "\n", + " xax.imshow(xslice, **kwargs)\n", + " yax.imshow(yslice, **kwargs)\n", + " zax.imshow(zslice, **kwargs)\n", + "\n", + " for ax in (xax, yax, zax):\n", + " ax.set_xticks([])\n", + " ax.set_yticks([])\n", + " fig.tight_layout(pad=0)\n", + "\n", + " return (fig, xax, yax, zax)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And another function which uses FSLeyes for more complex plots:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import shlex\n", + "import IPython.display as display\n", + "from fsleyes.render import main\n", + "\n", + "def render(cmdline):\n", + " prefix = '-of screenshot.png -hl -c 2 '\n", + " main(shlex.split(prefix + cmdline))\n", + " return display.Image('screenshot.png')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "<a class=\"anchor\" id=\"the-image-class-and-other-data-types\"></a>\n", "## The `Image` class, and other data types\n", "\n", @@ -67,7 +166,6 @@ "metadata": {}, "outputs": [], "source": [ - "import os.path as op\n", "from fsl.data.image import Image\n", "\n", "stddir = op.expandvars('${FSLDIR}/data/standard/')\n", @@ -92,8 +190,6 @@ "metadata": {}, "outputs": [], "source": [ - "import nibabel as nib\n", - "\n", "# load a nibabel image, and\n", "# convert it into an FSL image\n", "nibimg = nib.load(op.join(stddir, 'MNI152_T1_1mm.nii.gz'))\n", @@ -113,8 +209,6 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "\n", "data = np.zeros((100, 100, 100))\n", "img = Image(data, xform=np.eye(4))" ] @@ -175,7 +269,7 @@ "outputs": [], "source": [ "std2mm = Image(op.join(stddir, 'MNI152_T1_2mm'))\n", - "mask2mm = Image(op.join(stddir, 'MNI152_T1_2mm_mask'))\n", + "mask2mm = Image(op.join(stddir, 'MNI152_T1_2mm_brain_mask'))\n", "\n", "print(std1mm.sameSpace(std2mm))\n", "print(std2mm.sameSpace(mask2mm))\n", @@ -219,7 +313,7 @@ "outputs": [], "source": [ "data = std2mm.data\n", - "print(data.min, data.max())" + "print(data.min(), data.max())" ] }, { @@ -240,7 +334,7 @@ "outputs": [], "source": [ "slc = std2mm[:, :, 45]\n", - "std2mm[0:10, :, :] = 0" + "std2mm[0:10, :, :] *= 2" ] }, { @@ -308,18 +402,73 @@ "> in order to use this class.\n", "\n", "\n", - "<a class=\"anchor\" id=\"fsl-atlases\"></a>\n", - "## FSL atlases\n", + "<a class=\"anchor\" id=\"nifti-coordinate-systems\"></a>\n", + "### NIfTI coordinate systems\n", "\n", "\n", - "The\n", - "[`fsl.data.atlases`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.data.atlases.html)\n", - "module provides access to all of the atlas images that are stored in the\n", - "`$FSLDIR/data/atlases/` directory of a standard FSL installation. It can be\n", - "used to load and query probabilistic and label-based atlases.\n", + "The `Image.getAffine` method gives you access to affine transformations which\n", + "can be used to convert coordinates between the different coordinate systems\n", + "associated with a NIfTI image. Have some MNI coordinates you'd like to convert\n", + "to voxels? Easy!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stddir = op.expandvars('${FSLDIR}/data/standard/')\n", + "std2mm = Image(op.join(stddir, 'MNI152_T1_2mm'))\n", + "\n", + "mnicoords = np.array([[0, 0, 0],\n", + " [0, -18, 18]])\n", "\n", + "world2vox = std2mm.getAffine('world', 'voxel')\n", + "vox2world = std2mm.getAffine('voxel', 'world')\n", "\n", - "The `atlases` module needs to be initialised using the `rescanAtlases` function:" + "# Apply the world->voxel\n", + "# affine to the coordinates\n", + "voxcoords = (np.dot(world2vox[:3, :3], mnicoords.T)).T + world2vox[:3, 3]\n", + "\n", + "# The code above is a bit fiddly, so\n", + "# instead of figuring it out, you can\n", + "# just use the transform() function:\n", + "from fsl.transform.affine import transform\n", + "voxcoords = transform(mnicoords, world2vox)\n", + "\n", + "# just to double check, let's transform\n", + "# those voxel coordinates back into world\n", + "# coordinates\n", + "backtomni = transform(voxcoords, vox2world)\n", + "\n", + "for m, v, b in zip(mnicoords, voxcoords, backtomni):\n", + " print(m, '->', v, '->', b)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> The `Image.getAffine` method can give you transformation matrices\n", + "> between any of these coordinate systems:\n", + ">\n", + "> - `'voxel'`: Image data voxel coordinates\n", + "> - `'world'`: mm coordinates, defined by the sform/qform of an image\n", + "> - `'fsl'`: The FSL coordinate system, used internally by many FSL tools\n", + "> (e.g. FLIRT)\n", + "\n", + "\n", + "Oh, that example was too easy I hear you say? Try this one on for size. Let's\n", + "say we have run FEAT on some task fMRI data, and want to get the MNI\n", + "coordinates of the voxel with peak activation.\n", + "\n", + "\n", + "> This is what people used to use `Featquery` for, back in the un-enlightened\n", + "> days.\n", + "\n", + "\n", + "Let's start by identifying the voxel with the biggest t-statistic:" ] }, { @@ -328,15 +477,27 @@ "metadata": {}, "outputs": [], "source": [ - "import fsl.data.atlases as atlases\n", - "atlases.rescanAtlases()" + "featdir = op.join(op.join('08_fslpy', 'fmri.feat'))\n", + "\n", + "tstat1 = Image(op.join(featdir, 'stats', 'tstat1')).data\n", + "\n", + "# Recall from the numpy practical that\n", + "# argmax gives us a 1D index into a\n", + "# flattened view of the array. We can\n", + "# use the unravel_index function to\n", + "# convert it into a 3D index.\n", + "peakvox = np.abs(tstat1).argmax()\n", + "peakvox = np.unravel_index(peakvox, tstat1.shape)\n", + "print('Peak voxel coordinates for tstat1:', peakvox, tstat1[peakvox])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You can list all of the available atlases using `listAtlases`:" + "Now that we've got the voxel coordinates in functional space, we need to\n", + "transform them into MNI space. FEAT provides a transformation which goes\n", + "directly from functional to standard space, in the `reg` directory:" ] }, { @@ -345,18 +506,19 @@ "metadata": {}, "outputs": [], "source": [ - "for desc in atlases.listAtlases():\n", - " print(desc)" + "func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`listAtlases` returns a list of `AtlasDescription` objects, each of which\n", - "contains descriptive information about one atlas. You can retrieve the\n", - "`AtlasDescription` for a specific atlas via the `getAtlasDescription`\n", - "function:" + "But ... wait a minute ... this is a FLIRT matrix! We can't just plug voxel\n", + "coordinates into a FLIRT matrix and expect to get sensible results, because\n", + "FLIRT works in an internal FSL coordinate system, which is not quite\n", + "`'voxel'`, and not quite `'world'`. So we need to do a little more work.\n", + "Let's start by loading our functional image, and the MNI152 template (the\n", + "source and reference images of our FLIRT matrix):" ] }, { @@ -365,20 +527,21 @@ "metadata": {}, "outputs": [], "source": [ - "desc = atlases.getAtlasDescription('harvardoxford-cortical')\n", - "print(desc.name)\n", - "print(desc.atlasID)\n", - "print(desc.specPath)\n", - "print(desc.atlasType)" + "func = Image(op.join(featdir, 'reg', 'example_func'))\n", + "std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm')))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Each `AtlasDescription` maintains a list of `AtlasLabel` objects, each of\n", - "which represents one region that is defined in the atlas. You can access all\n", - "of the `AtlasLabel` objects via the `labels` attribute:" + "Now we can use them to get affines which convert between all of the different\n", + "coordinate systems - we're going to combine them into a single uber-affine,\n", + "which transforms our functional-space voxels into MNI world coordinates via:\n", + "\n", + " 1. functional voxels -> FLIRT source space\n", + " 2. FLIRT source space -> FLIRT reference space\n", + " 3. FLIRT referece space -> MNI world coordinates" ] }, { @@ -387,15 +550,16 @@ "metadata": {}, "outputs": [], "source": [ - "for lbl in desc.labels[:5]:\n", - " print(lbl)" + "vox2fsl = func.getAffine('voxel', 'fsl')\n", + "fsl2mni = std .getAffine('fsl', 'world')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Or you can retrieve a specific label using the `find` method:" + "Combining two affines into one is just a simple dot-product. There is a\n", + "`concat()` function which does this for us, for any number of affines:" ] }, { @@ -404,18 +568,20 @@ "metadata": {}, "outputs": [], "source": [ - "# search by region name\n", - "print(desc.find(name='Occipital Pole'))\n", + "from fsl.transform.affine import concat\n", "\n", - "# or by label value\n", - "print(desc.find(value=48))" + "# To combine affines together, we\n", + "# have to list them in reverse -\n", + "# linear algebra is *weird*.\n", + "funcvox2mni = concat(fsl2mni, func2std, vox2fsl)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `loadAtlas` function can be used to load the atlas image:" + "So we've now got some voxel coordinates from our functional data, and an\n", + "affine to transform into MNI world coordinates. The rest is easy:" ] }, { @@ -424,34 +590,36 @@ "metadata": {}, "outputs": [], "source": [ - "# For probabilistic atlases, you\n", - "# can ask for the 3D ROI image\n", - "# by setting loadSummary=True.\n", - "# You can also request a\n", - "# resolution - by default the\n", - "# highest resolution version\n", - "# will be loaded.\n", - "lblatlas = atlases.loadAtlas('harvardoxford-cortical',\n", - " loadSummary=True,\n", - " resolution=2)\n", - "\n", - "# By default you will get the 4D\n", - "# probabilistic atlas image (for\n", - "# atlases for which this is\n", - "# available).\n", - "probatlas = atlases.loadAtlas('harvardoxford-cortical',\n", - " resolution=2)\n", - "\n", - "print(lblatlas)\n", - "print(probatlas)" + "mnicoords = transform(peakvox, funcvox2mni)\n", + "mnivoxels = transform(mnicoords, std.getAffine('world', 'voxel'))\n", + "mnivoxels = [int(round(v)) for v in mnivoxels]\n", + "print('Peak activation (MNI coordinates):', mnicoords)\n", + "print('Peak activation (MNI voxels): ', mnivoxels)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`LabelAtlas` objects have a method called `label`, which can be used to\n", - "interrogate the atlas at specific locations:" + "> Note that in the above example we are only applying a linear transformation\n", + "> into MNI space - in reality you would also want to apply your non-linear\n", + "> structural-to-standard transformation too. But this is left as [an exercise\n", + "> for the\n", + "> reader](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.transform.fnirt.html).\n", + "\n", + "\n", + "<a class=\"anchor\" id=\"image-processing\"></a>\n", + "### Image processing\n", + "\n", + "\n", + "Now, it's all well and good to look at t-statistic values and voxel\n", + "coordinates and so on and so forth, but let's spice things up a bit and look\n", + "at some images. Let's display our peak activation location in MNI space. To do\n", + "this, we're going to resample our functional image into MNI space, so we can\n", + "overlay it on the MNI template. This can be done using some handy functions\n", + "from the\n", + "[`fsl.utils.image.resample`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.utils.image.resample.html)\n", + "module:" ] }, { @@ -460,30 +628,38 @@ "metadata": {}, "outputs": [], "source": [ - "# The label method accepts 3D\n", - "# voxel or world coordinates\n", - "val = lblatlas.label((25, 52, 43), voxel=True)\n", - "lbl = lblatlas.find(value=val)\n", - "print('Region at voxel [25, 52, 43]: {} [{}]'.format(val, lbl.name))\n", + "from fsl.transform.flirt import fromFlirt\n", + "from fsl.utils.image.resample import resampleToReference\n", "\n", + "featdir = op.join(op.join('08_fslpy', 'fmri.feat'))\n", + "tstat1 = Image(op.join(featdir, 'stats', 'tstat1'))\n", + "std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm')))\n", "\n", - "# or a 3D weighted or binary mask\n", - "mask = np.zeros(lblatlas.shape)\n", - "mask[30:60, 30:60, 30:60] = 1\n", - "mask = Image(mask, header=lblatlas.header)\n", + "# Load the func2standard FLIRT matrix, and adjust it\n", + "# so that it transforms from functional *world*\n", + "# coordinates into standard *world* coordinates -\n", + "# this is what is expected by the resampleToReference\n", + "# function, used below\n", + "func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat'))\n", + "func2std = fromFlirt(func2std, tstat1, std, 'world', 'world')\n", "\n", - "lbls, props = lblatlas.label(mask)\n", - "print('Labels in mask:')\n", - "for lbl, prop in zip(lbls, props):\n", - " lblname = lblatlas.find(value=lbl).name\n", - " print(' {} [{}]: {:0.2f}%'.format(lbl, lblname, prop))" + "# All of the functions in the resample module\n", + "# return a numpy array containing the resampled\n", + "# data, and an adjusted voxel-to-world affine\n", + "# transformation. But when using the\n", + "# resampleToReference function, the affine will\n", + "# be the same as the MNI152 2mm affine, so we\n", + "# can ignore it.\n", + "std_tstat1 = resampleToReference(tstat1, std, func2std)[0]\n", + "std_tstat1 = Image(std_tstat1, header=std.header)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "`ProbabilisticAtlas` objects have an analogous method called `values`:" + "Now that we have our t-statistic image in MNI152 space, we can plot it in\n", + "standard space using `matplotlib`:" ] }, { @@ -492,48 +668,43 @@ "metadata": {}, "outputs": [], "source": [ - "vals = probatlas.values((25, 52, 43), voxel=True)\n", - "print('Regions at voxel [25, 52, 43]:')\n", - "for idx, val in enumerate(vals):\n", - " if val > 0:\n", - " lbl = probatlas.find(index=idx)\n", - " print(' {} [{}]: {:0.2f}%'.format(lbl.value, lbl.name, val))\n", + "stddir = op.expandvars('${FSLDIR}/data/standard/')\n", + "std2mm = Image(op.join(stddir, 'MNI152_T1_2mm'))\n", "\n", - "print('Average proportions of regions within mask:')\n", - "vals = probatlas.values(mask)\n", - "for idx, val in enumerate(vals):\n", - " if val > 0:\n", - " lbl = probatlas.find(index=idx)\n", - " print(' {} [{}]: {:0.2f}%'.format(lbl.value, lbl.name, val))" + "std_tstat1 = std_tstat1.data\n", + "std_tstat1 = np.ma.masked_where(std_tstat1 < 3, std_tstat1)\n", + "\n", + "fig = ortho(std2mm, mnivoxels, cmap=plt.cm.gray)\n", + "fig = ortho(std_tstat1, mnivoxels, cmap=plt.cm.inferno, fig=fig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "There are a few other useful functions tucked away in the\n", + "[fsl.utils.image](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.utils.image.html)\n", + "package, with more to be added in the future. The [`fsl.transform`]() package\n", + "also contains a wealth of functionality for working with linear (FLIRT) and\n", + "non-linear (FNIRT) transformations.\n", + "\n", + "\n", "<a class=\"anchor\" id=\"the-filetree\"></a>\n", "## The `filetree`\n", "\n", - "<a class=\"anchor\" id=\"nifti-coordinate-systems\"></a>\n", - "## NIfTI coordinate systems\n", - "\n", - "<a class=\"anchor\" id=\"image-processing\"></a>\n", - "## Image processing\n", "\n", "<a class=\"anchor\" id=\"fsl-wrapper-functions\"></a>\n", "## FSL wrapper functions\n", "\n", "\n", - "\n", - "<a class=\"anchor\" id=\"nifti-coordinate-systems\"></a>\n", - "## NIfTI coordinate systems\n", - "\n", + "The\n", + "[fsl.wrappers](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.wrappers.html)\n", + "package is the home of \"wrapper\" functions for a range of FSL tools. You can\n", + "use them to call an FSL tool from Python code, without having to worry about\n", + "constructing a command-line, or saving/loading input/output images.\n", "\n", "\n", - "The `getAffine` method gives you access to affine transformations which can be\n", - "used to convert coordinates between the different coordinate systems\n", - "associated with an image. Have some MNI coordinates you'd like to convert to\n", - "voxels? Easy!" + "You can use the FSL wrapper functions with file names:" ] }, { @@ -542,54 +713,49 @@ "metadata": {}, "outputs": [], "source": [ - "mnicoords = np.array([[0, 0, 0],\n", - " [0, -18, 18]])\n", - "\n", - "world2vox = img.getAffine('world', 'voxel')\n", - "vox2world = img.getAffine('voxel', 'world')\n", - "\n", - "# Apply the world->voxel\n", - "# affine to the coordinates\n", - "voxcoords = (np.dot(world2vox[:3, :3], mnicoords.T)).T + world2vox[:3, 3]\n", - "\n", - "# The code above is a bit fiddly, so\n", - "# instead of figuring it out, you can\n", - "# just use the transform() function:\n", - "from fsl.transform.affine import transform\n", - "voxcoords = transform(mnicoords, world2vox)\n", - "\n", - "# just to double check, let's transform\n", - "# those voxel coordinates back into world\n", - "# coordinates\n", - "backtomni = transform(voxcoords, vox2world)\n", - "\n", - "for m, v, b in zip(mnicoords, voxcoords, backtomni):\n", - " print(m, '->', v, '->', b)" + "from fsl.wrappers import bet, robustfov, LOAD\n", + "os.chdir('08_fslpy')\n", + "robustfov('bighead', 'bighead_cropped')\n", + "render('bighead bighead_cropped -cm blue')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "> The `Image.getAffine` method can give you transformation matrices\n", - "> between any of these coordinate systems:\n", - ">\n", - "> - `'voxel'`: Image data voxel coordinates\n", - "> - `'world'`: mm coordinates, defined by the sform/qform of an image\n", - "> - `'fsl'`: The FSL coordinate system, used internally by many FSL tools\n", - "> (e.g. FLIRT)\n", - "\n", + "Or, if you have images in memory, you can pass the `Image` objects in,\n", + "and have the results automatically loaded in too:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cropped = Image('bighead_cropped')\n", + "betted = bet(cropped, LOAD)['output']\n", "\n", + "fig = ortho(cropped, (80, 112, 85))\n", + "fig = ortho(betted, (80, 112, 85), fig=fig, cmap=plt.cm.inferno)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"fsl-atlases\"></a>\n", + "## FSL atlases\n", "\n", - "Oh, that example was too easy I hear you say? Try this one on for size. Let's\n", - "say we have run FEAT on some task fMRI data, and want to get the MNI\n", - "coordinates of the voxel with peak activation.\n", "\n", - "> This is what people used to use `Featquery` for, back in the un-enlightened\n", - "> days.\n", + "The\n", + "[`fsl.data.atlases`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.data.atlases.html)\n", + "module provides access to all of the atlas images that are stored in the\n", + "`$FSLDIR/data/atlases/` directory of a standard FSL installation. It can be\n", + "used to load and query probabilistic and label-based atlases.\n", "\n", "\n", - "Let's start by identifying the voxel with the biggest t-statistic:" + "The `atlases` module needs to be initialised using the `rescanAtlases` function:" ] }, { @@ -598,30 +764,39 @@ "metadata": {}, "outputs": [], "source": [ - "featdir = op.join(op.join('05_nifti', 'fmri.feat'))\n", + "import fsl.data.atlases as atlases\n", + "atlases.rescanAtlases()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "<a class=\"anchor\" id=\"querying-atlases\"></a>\n", + "### Querying atlases\n", "\n", - "# The Image.data attribute returns a\n", - "# numpy array containing, well, the\n", - "# image data.\n", - "tstat1 = Image(op.join(featdir, 'stats', 'tstat1')).data\n", "\n", - "# Recall from the numpy practical that\n", - "# argmax gives us a 1D index into a\n", - "# flattened view of the array. We can\n", - "# use the unravel_index function to\n", - "# convert it into a 3D index.\n", - "peakvox = np.abs(tstat1).argmax()\n", - "peakvox = np.unravel_index(peakvox, tstat1.shape)\n", - "print('Peak voxel coordinates for tstat1:', peakvox, tstat1[peakvox])" + "You can list all of the available atlases using `listAtlases`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for desc in atlases.listAtlases():\n", + " print(desc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now that we've got the voxel coordinates in functional space, we need to\n", - "transform them into MNI space. FEAT provides a transformation which goes\n", - "directly from functional to standard space, in the `reg` directory:" + "`listAtlases` returns a list of `AtlasDescription` objects, each of which\n", + "contains descriptive information about one atlas. You can retrieve the\n", + "`AtlasDescription` for a specific atlas via the `getAtlasDescription`\n", + "function:" ] }, { @@ -630,19 +805,20 @@ "metadata": {}, "outputs": [], "source": [ - "func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat'))" + "desc = atlases.getAtlasDescription('harvardoxford-cortical')\n", + "print(desc.name)\n", + "print(desc.atlasID)\n", + "print(desc.specPath)\n", + "print(desc.atlasType)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "But ... wait a minute ... this is a FLIRT matrix! We can't just plug voxel\n", - "coordinates into a FLIRT matrix and expect to get sensible results, because\n", - "FLIRT works in an internal FSL coordinate system, which is not quite\n", - "`'voxel'`, and not quite `'world'`. So we need to do a little more work.\n", - "Let's start by loading our functional image, and the MNI152 template (the\n", - "source and reference images of our FLIRT matrix):" + "Each `AtlasDescription` maintains a list of `AtlasLabel` objects, each of\n", + "which represents one region that is defined in the atlas. You can access all\n", + "of the `AtlasLabel` objects via the `labels` attribute:" ] }, { @@ -651,21 +827,15 @@ "metadata": {}, "outputs": [], "source": [ - "func = Image(op.join(featdir, 'reg', 'example_func'))\n", - "std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm')))" + "for lbl in desc.labels[:5]:\n", + " print(lbl)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we can use them to get affines which convert between all of the different\n", - "coordinate systems - we're going to combine them into a single uber-affine,\n", - "which transforms our functional-space voxels into MNI world coordinates via:\n", - "\n", - " 1. functional voxels -> FLIRT source space\n", - " 2. FLIRT source space -> FLIRT reference space\n", - " 3. FLIRT referece space -> MNI world coordinates" + "Or you can retrieve a specific label using the `find` method:" ] }, { @@ -674,16 +844,22 @@ "metadata": {}, "outputs": [], "source": [ - "vox2fsl = func.getAffine('voxel', 'fsl')\n", - "fsl2mni = std .getAffine('fsl', 'world')" + "# search by region name\n", + "print(desc.find(name='Occipital Pole'))\n", + "\n", + "# or by label value\n", + "print(desc.find(value=48))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Combining two affines into one is just a simple dot-product. There is a\n", - "`concat()` function which does this for us, for any number of affines:" + "<a class=\"anchor\" id=\"loading-atlas-images\"></a>\n", + "### Loading atlas images\n", + "\n", + "\n", + "The `loadAtlas` function can be used to load the atlas image:" ] }, { @@ -692,20 +868,38 @@ "metadata": {}, "outputs": [], "source": [ - "from fsl.transform.affine import concat\n", + "# For probabilistic atlases, you\n", + "# can ask for the 3D ROI image\n", + "# by setting loadSummary=True.\n", + "# You can also request a\n", + "# resolution - by default the\n", + "# highest resolution version\n", + "# will be loaded.\n", + "lblatlas = atlases.loadAtlas('harvardoxford-cortical',\n", + " loadSummary=True,\n", + " resolution=2)\n", "\n", - "# To combine affines together, we\n", - "# have to list them in reverse -\n", - "# linear algebra is *weird*.\n", - "funcvox2mni = concat(fsl2mni, func2std, vox2fsl)" + "# By default you will get the 4D\n", + "# probabilistic atlas image (for\n", + "# atlases for which this is\n", + "# available).\n", + "probatlas = atlases.loadAtlas('harvardoxford-cortical',\n", + " resolution=2)\n", + "\n", + "print(lblatlas)\n", + "print(probatlas)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So we've now got some voxel coordinates from our functional data, and an affine\n", - "to transform into MNI world coordinates. The rest is easy:" + "<a class=\"anchor\" id=\"working-with-atlases\"></a>\n", + "### Working with atlases\n", + "\n", + "\n", + "Both `LabelAtlas` and `ProbabilisticAtlas` objects have a method called `get`,\n", + "which can be used to extract ROI images for a specific region:" ] }, { @@ -714,27 +908,52 @@ "metadata": {}, "outputs": [], "source": [ - "mnicoords = transform(peakvox, funcvox2mni)\n", - "print('Peak activation (MNI coordinates):', mnicoords)" + "stddir = op.expandvars('${FSLDIR}/data/standard/')\n", + "std2mm = Image(op.join(stddir, 'MNI152_T1_2mm'))\n", + "\n", + "frontal = lblatlas.get(name='Frontal Pole').data\n", + "frontal = np.ma.masked_where(frontal < 1, frontal)\n", + "\n", + "fig = ortho(std2mm, (45, 54, 45), cmap=plt.cm.gray)\n", + "fig = ortho(frontal, (45, 54, 45), cmap=plt.cm.winter, fig=fig)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "> Note that in the above example we are only applying a linear transformation\n", - "> into MNI space - in reality you would also want to apply your non-linear\n", - "> structural-to-standard transformation too. But this is left as an exercise\n", - "> for the reader ;).\n", + "Calling `get` on a :meth:`ProbabilisticAtlas` will return a probability image:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stddir = op.expandvars('${FSLDIR}/data/standard/')\n", + "std2mm = Image(op.join(stddir, 'MNI152_T1_2mm'))\n", "\n", + "frontal = probatlas.get(name='Frontal Pole')\n", + "frontal = np.ma.masked_where(frontal < 1, frontal)\n", "\n", - "<a class=\"anchor\" id=\"image-processing\"></a>\n", - "## Image processing\n", + "fig = ortho(std2mm, (45, 54, 45), cmap=plt.cm.gray)\n", + "fig = ortho(frontal, (45, 54, 45), cmap=plt.cm.inferno, fig=fig)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `get` method can be used to retrieve an image for a region by:\n", + "- an `AtlasLabel` object\n", + "- The region index\n", + "- The region value\n", + "- The region name\n", "\n", - "Now, it's all well and good to look at t-statistric values and voxel\n", - "coordinates and so on and so forth, but let's spice things up a bit and look\n", - "at some images. Let's display our peak activation location in MNI space. To\n", - "do this, we're going to resample our functional image into" + "\n", + "`LabelAtlas` objects have a method called `label`, which can be used to\n", + "interrogate the atlas at specific locations:" ] }, { @@ -743,18 +962,51 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython.display import Image as Screenshot\n", - "!fsleyes render -of screenshot.png -std" + "# The label method accepts 3D\n", + "# voxel or world coordinates\n", + "val = lblatlas.label((25, 52, 43), voxel=True)\n", + "lbl = lblatlas.find(value=val)\n", + "print('Region at voxel [25, 52, 43]: {} [{}]'.format(val, lbl.name))\n", + "\n", + "\n", + "# or a 3D weighted or binary mask\n", + "mask = np.zeros(lblatlas.shape)\n", + "mask[30:60, 30:60, 30:60] = 1\n", + "mask = Image(mask, header=lblatlas.header)\n", + "\n", + "lbls, props = lblatlas.label(mask)\n", + "print('Labels in mask:')\n", + "for lbl, prop in zip(lbls, props):\n", + " lblname = lblatlas.find(value=lbl).name\n", + " print(' {} [{}]: {:0.2f}%'.format(lbl, lblname, prop))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### (Advanced) Transform coordinates with nonlinear warpfields\n", - "\n", + "`ProbabilisticAtlas` objects have an analogous method called `values`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vals = probatlas.values((25, 52, 43), voxel=True)\n", + "print('Regions at voxel [25, 52, 43]:')\n", + "for idx, val in enumerate(vals):\n", + " if val > 0:\n", + " lbl = probatlas.find(index=idx)\n", + " print(' {} [{}]: {:0.2f}%'.format(lbl.value, lbl.name, val))\n", "\n", - "have to use your own dataset" + "print('Average proportions of regions within mask:')\n", + "vals = probatlas.values(mask)\n", + "for idx, val in enumerate(vals):\n", + " if val > 0:\n", + " lbl = probatlas.find(index=idx)\n", + " print(' {} [{}]: {:0.2f}%'.format(lbl.value, lbl.name, val))" ] } ], diff --git a/advanced_topics/08_fslpy.md b/advanced_topics/08_fslpy.md index ef8b860f4fa964fb8ff9e5f2bbd3391edaef1181..f79cc08addc91bdaf5421e5c4c43eff5db032ec3 100644 --- a/advanced_topics/08_fslpy.md +++ b/advanced_topics/08_fslpy.md @@ -20,11 +20,86 @@ perform analyses and image processing in conjunction with FSL. * [Creating images](#creating-images) * [Working with image data](#working-with-image-data) * [Loading other file types](#loading-other-file-types) -* [FSL atlases](#fsl-atlases) + * [NIfTI coordinate systems](#nifti-coordinate-systems) + * [Image processing](#image-processing) * [The `filetree`](#the-filetree) -* [Image processing](#image-processing) * [FSL wrapper functions](#fsl-wrapper-functions) -* [NIfTI coordinate systems](#nifti-coordinate-systems) +* [FSL atlases](#fsl-atlases) + * [Querying atlases](#querying-atlases) + * [Loading atlas images](#loading-atlas-images) + * [Working with atlases](#working-with-atlases) + + +Let's start with some standard imports and environment set-up: + + +``` +%matplotlib inline +import matplotlib.pyplot as plt +import os +import os.path as op +import nibabel as nib +import numpy as np +import warnings +warnings.filterwarnings("ignore") +``` + + +And a little function that we can use to generate a simple orthographic plot: + + +``` +def ortho(data, voxel, fig=None, **kwargs): + """Simple orthographic plot of a 3D array using matplotlib. + + :arg data: 3D numpy array + :arg voxel: XYZ coordinates for each slice + :arg fig: Existing figure and axes for overlay plotting + + All other arguments are passed through to the `imshow` function. + + :returns: The figure and axes (which can be passed back in as the + `fig` argument to plot overlays). + """ + x, y, z = voxel + xslice = np.flipud(data[x, :, :].T) + yslice = np.flipud(data[:, y, :].T) + zslice = np.flipud(data[:, :, z].T) + + if fig is None: + fig = plt.figure() + xax = fig.add_subplot(1, 3, 1) + yax = fig.add_subplot(1, 3, 2) + zax = fig.add_subplot(1, 3, 3) + else: + fig, xax, yax, zax = fig + + xax.imshow(xslice, **kwargs) + yax.imshow(yslice, **kwargs) + zax.imshow(zslice, **kwargs) + + for ax in (xax, yax, zax): + ax.set_xticks([]) + ax.set_yticks([]) + fig.tight_layout(pad=0) + + return (fig, xax, yax, zax) +``` + + +And another function which uses FSLeyes for more complex plots: + + +``` +import shlex +import IPython.display as display +from fsleyes.render import main + +def render(cmdline): + prefix = '-of screenshot.png -hl -c 2 ' + main(shlex.split(prefix + cmdline)) + return display.Image('screenshot.png') +``` <a class="anchor" id="the-image-class-and-other-data-types"></a> @@ -56,7 +131,6 @@ It's easy to create an `Image` - you can create one from a file name: ``` -import os.path as op from fsl.data.image import Image stddir = op.expandvars('${FSLDIR}/data/standard/') @@ -73,8 +147,6 @@ You can create an `Image` from an existing `nibabel` image: ``` -import nibabel as nib - # load a nibabel image, and # convert it into an FSL image nibimg = nib.load(op.join(stddir, 'MNI152_T1_1mm.nii.gz')) @@ -86,8 +158,6 @@ Or you can create an `Image` from a `numpy` array: ``` -import numpy as np - data = np.zeros((100, 100, 100)) img = Image(data, xform=np.eye(4)) ``` @@ -123,7 +193,7 @@ and a number of useful methods: ``` std2mm = Image(op.join(stddir, 'MNI152_T1_2mm')) -mask2mm = Image(op.join(stddir, 'MNI152_T1_2mm_mask')) +mask2mm = Image(op.join(stddir, 'MNI152_T1_2mm_brain_mask')) print(std1mm.sameSpace(std2mm)) print(std2mm.sameSpace(mask2mm)) @@ -151,7 +221,7 @@ You can get the image data as a `numpy` array via the `data` attribute: ``` data = std2mm.data -print(data.min, data.max()) +print(data.min(), data.max()) ``` > Note that this will give you the data in its underlying type, unlike the @@ -163,7 +233,7 @@ You can also read and write data directly via the `Image` object: ``` slc = std2mm[:, :, 45] -std2mm[0:10, :, :] = 0 +std2mm[0:10, :, :] *= 2 ``` @@ -228,6 +298,257 @@ corresponding `nibabel` types: > in order to use this class. +<a class="anchor" id="nifti-coordinate-systems"></a> +### NIfTI coordinate systems + + +The `Image.getAffine` method gives you access to affine transformations which +can be used to convert coordinates between the different coordinate systems +associated with a NIfTI image. Have some MNI coordinates you'd like to convert +to voxels? Easy! + + +``` +stddir = op.expandvars('${FSLDIR}/data/standard/') +std2mm = Image(op.join(stddir, 'MNI152_T1_2mm')) + +mnicoords = np.array([[0, 0, 0], + [0, -18, 18]]) + +world2vox = std2mm.getAffine('world', 'voxel') +vox2world = std2mm.getAffine('voxel', 'world') + +# Apply the world->voxel +# affine to the coordinates +voxcoords = (np.dot(world2vox[:3, :3], mnicoords.T)).T + world2vox[:3, 3] + +# The code above is a bit fiddly, so +# instead of figuring it out, you can +# just use the transform() function: +from fsl.transform.affine import transform +voxcoords = transform(mnicoords, world2vox) + +# just to double check, let's transform +# those voxel coordinates back into world +# coordinates +backtomni = transform(voxcoords, vox2world) + +for m, v, b in zip(mnicoords, voxcoords, backtomni): + print(m, '->', v, '->', b) +``` + + +> The `Image.getAffine` method can give you transformation matrices +> between any of these coordinate systems: +> +> - `'voxel'`: Image data voxel coordinates +> - `'world'`: mm coordinates, defined by the sform/qform of an image +> - `'fsl'`: The FSL coordinate system, used internally by many FSL tools +> (e.g. FLIRT) + + +Oh, that example was too easy I hear you say? Try this one on for size. Let's +say we have run FEAT on some task fMRI data, and want to get the MNI +coordinates of the voxel with peak activation. + + +> This is what people used to use `Featquery` for, back in the un-enlightened +> days. + + +Let's start by identifying the voxel with the biggest t-statistic: + + +``` +featdir = op.join(op.join('08_fslpy', 'fmri.feat')) + +tstat1 = Image(op.join(featdir, 'stats', 'tstat1')).data + +# Recall from the numpy practical that +# argmax gives us a 1D index into a +# flattened view of the array. We can +# use the unravel_index function to +# convert it into a 3D index. +peakvox = np.abs(tstat1).argmax() +peakvox = np.unravel_index(peakvox, tstat1.shape) +print('Peak voxel coordinates for tstat1:', peakvox, tstat1[peakvox]) +``` + + +Now that we've got the voxel coordinates in functional space, we need to +transform them into MNI space. FEAT provides a transformation which goes +directly from functional to standard space, in the `reg` directory: + + +``` +func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat')) +``` + + +But ... wait a minute ... this is a FLIRT matrix! We can't just plug voxel +coordinates into a FLIRT matrix and expect to get sensible results, because +FLIRT works in an internal FSL coordinate system, which is not quite +`'voxel'`, and not quite `'world'`. So we need to do a little more work. +Let's start by loading our functional image, and the MNI152 template (the +source and reference images of our FLIRT matrix): + + +``` +func = Image(op.join(featdir, 'reg', 'example_func')) +std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm'))) +``` + + +Now we can use them to get affines which convert between all of the different +coordinate systems - we're going to combine them into a single uber-affine, +which transforms our functional-space voxels into MNI world coordinates via: + + 1. functional voxels -> FLIRT source space + 2. FLIRT source space -> FLIRT reference space + 3. FLIRT referece space -> MNI world coordinates + + +``` +vox2fsl = func.getAffine('voxel', 'fsl') +fsl2mni = std .getAffine('fsl', 'world') +``` + + +Combining two affines into one is just a simple dot-product. There is a +`concat()` function which does this for us, for any number of affines: + + +``` +from fsl.transform.affine import concat + +# To combine affines together, we +# have to list them in reverse - +# linear algebra is *weird*. +funcvox2mni = concat(fsl2mni, func2std, vox2fsl) +``` + + +So we've now got some voxel coordinates from our functional data, and an +affine to transform into MNI world coordinates. The rest is easy: + + +``` +mnicoords = transform(peakvox, funcvox2mni) +mnivoxels = transform(mnicoords, std.getAffine('world', 'voxel')) +mnivoxels = [int(round(v)) for v in mnivoxels] +print('Peak activation (MNI coordinates):', mnicoords) +print('Peak activation (MNI voxels): ', mnivoxels) +``` + + +> Note that in the above example we are only applying a linear transformation +> into MNI space - in reality you would also want to apply your non-linear +> structural-to-standard transformation too. But this is left as [an exercise +> for the +> reader](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.transform.fnirt.html). + + +<a class="anchor" id="image-processing"></a> +### Image processing + + +Now, it's all well and good to look at t-statistic values and voxel +coordinates and so on and so forth, but let's spice things up a bit and look +at some images. Let's display our peak activation location in MNI space. To do +this, we're going to resample our functional image into MNI space, so we can +overlay it on the MNI template. This can be done using some handy functions +from the +[`fsl.utils.image.resample`](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.utils.image.resample.html) +module: + + +``` +from fsl.transform.flirt import fromFlirt +from fsl.utils.image.resample import resampleToReference + +featdir = op.join(op.join('08_fslpy', 'fmri.feat')) +tstat1 = Image(op.join(featdir, 'stats', 'tstat1')) +std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm'))) + +# Load the func2standard FLIRT matrix, and adjust it +# so that it transforms from functional *world* +# coordinates into standard *world* coordinates - +# this is what is expected by the resampleToReference +# function, used below +func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat')) +func2std = fromFlirt(func2std, tstat1, std, 'world', 'world') + +# All of the functions in the resample module +# return a numpy array containing the resampled +# data, and an adjusted voxel-to-world affine +# transformation. But when using the +# resampleToReference function, the affine will +# be the same as the MNI152 2mm affine, so we +# can ignore it. +std_tstat1 = resampleToReference(tstat1, std, func2std)[0] +std_tstat1 = Image(std_tstat1, header=std.header) +``` + + +Now that we have our t-statistic image in MNI152 space, we can plot it in +standard space using `matplotlib`: + + +``` +stddir = op.expandvars('${FSLDIR}/data/standard/') +std2mm = Image(op.join(stddir, 'MNI152_T1_2mm')) + +std_tstat1 = std_tstat1.data +std_tstat1 = np.ma.masked_where(std_tstat1 < 3, std_tstat1) + +fig = ortho(std2mm, mnivoxels, cmap=plt.cm.gray) +fig = ortho(std_tstat1, mnivoxels, cmap=plt.cm.inferno, fig=fig) +``` + + +There are a few other useful functions tucked away in the +[fsl.utils.image](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.utils.image.html) +package, with more to be added in the future. The [`fsl.transform`]() package +also contains a wealth of functionality for working with linear (FLIRT) and +non-linear (FNIRT) transformations. + + +<a class="anchor" id="the-filetree"></a> +## The `filetree` + + +<a class="anchor" id="fsl-wrapper-functions"></a> +## FSL wrapper functions + + +The +[fsl.wrappers](https://users.fmrib.ox.ac.uk/~paulmc/fsleyes/fslpy/latest/fsl.wrappers.html) +package is the home of "wrapper" functions for a range of FSL tools. You can +use them to call an FSL tool from Python code, without having to worry about +constructing a command-line, or saving/loading input/output images. + + +You can use the FSL wrapper functions with file names: + +``` +from fsl.wrappers import bet, robustfov, LOAD +os.chdir('08_fslpy') +robustfov('bighead', 'bighead_cropped') +render('bighead bighead_cropped -cm blue') +``` + +Or, if you have images in memory, you can pass the `Image` objects in, +and have the results automatically loaded in too: + +``` +cropped = Image('bighead_cropped') +betted = bet(cropped, LOAD)['output'] + +fig = ortho(cropped, (80, 112, 85)) +fig = ortho(betted, (80, 112, 85), fig=fig, cmap=plt.cm.inferno) +``` + + <a class="anchor" id="fsl-atlases"></a> ## FSL atlases @@ -248,6 +569,10 @@ atlases.rescanAtlases() ``` +<a class="anchor" id="querying-atlases"></a> +### Querying atlases + + You can list all of the available atlases using `listAtlases`: @@ -295,6 +620,10 @@ print(desc.find(value=48)) ``` +<a class="anchor" id="loading-atlas-images"></a> +### Loading atlas images + + The `loadAtlas` function can be used to load the atlas image: @@ -322,6 +651,48 @@ print(probatlas) ``` +<a class="anchor" id="working-with-atlases"></a> +### Working with atlases + + +Both `LabelAtlas` and `ProbabilisticAtlas` objects have a method called `get`, +which can be used to extract ROI images for a specific region: + + +``` +stddir = op.expandvars('${FSLDIR}/data/standard/') +std2mm = Image(op.join(stddir, 'MNI152_T1_2mm')) + +frontal = lblatlas.get(name='Frontal Pole').data +frontal = np.ma.masked_where(frontal < 1, frontal) + +fig = ortho(std2mm, (45, 54, 45), cmap=plt.cm.gray) +fig = ortho(frontal, (45, 54, 45), cmap=plt.cm.winter, fig=fig) +``` + + +Calling `get` on a :meth:`ProbabilisticAtlas` will return a probability image: + + +``` +stddir = op.expandvars('${FSLDIR}/data/standard/') +std2mm = Image(op.join(stddir, 'MNI152_T1_2mm')) + +frontal = probatlas.get(name='Frontal Pole') +frontal = np.ma.masked_where(frontal < 1, frontal) + +fig = ortho(std2mm, (45, 54, 45), cmap=plt.cm.gray) +fig = ortho(frontal, (45, 54, 45), cmap=plt.cm.inferno, fig=fig) +``` + + +The `get` method can be used to retrieve an image for a region by: +- an `AtlasLabel` object +- The region index +- The region value +- The region name + + `LabelAtlas` objects have a method called `label`, which can be used to interrogate the atlas at specific locations: @@ -365,174 +736,3 @@ for idx, val in enumerate(vals): lbl = probatlas.find(index=idx) print(' {} [{}]: {:0.2f}%'.format(lbl.value, lbl.name, val)) ``` - - -<a class="anchor" id="the-filetree"></a> -## The `filetree` - -<a class="anchor" id="nifti-coordinate-systems"></a> -## NIfTI coordinate systems - -<a class="anchor" id="image-processing"></a> -## Image processing - -<a class="anchor" id="fsl-wrapper-functions"></a> -## FSL wrapper functions - - - -<a class="anchor" id="nifti-coordinate-systems"></a> -## NIfTI coordinate systems - - - -The `getAffine` method gives you access to affine transformations which can be -used to convert coordinates between the different coordinate systems -associated with an image. Have some MNI coordinates you'd like to convert to -voxels? Easy! - -``` -mnicoords = np.array([[0, 0, 0], - [0, -18, 18]]) - -world2vox = img.getAffine('world', 'voxel') -vox2world = img.getAffine('voxel', 'world') - -# Apply the world->voxel -# affine to the coordinates -voxcoords = (np.dot(world2vox[:3, :3], mnicoords.T)).T + world2vox[:3, 3] - -# The code above is a bit fiddly, so -# instead of figuring it out, you can -# just use the transform() function: -from fsl.transform.affine import transform -voxcoords = transform(mnicoords, world2vox) - -# just to double check, let's transform -# those voxel coordinates back into world -# coordinates -backtomni = transform(voxcoords, vox2world) - -for m, v, b in zip(mnicoords, voxcoords, backtomni): - print(m, '->', v, '->', b) -``` - -> The `Image.getAffine` method can give you transformation matrices -> between any of these coordinate systems: -> -> - `'voxel'`: Image data voxel coordinates -> - `'world'`: mm coordinates, defined by the sform/qform of an image -> - `'fsl'`: The FSL coordinate system, used internally by many FSL tools -> (e.g. FLIRT) - - - -Oh, that example was too easy I hear you say? Try this one on for size. Let's -say we have run FEAT on some task fMRI data, and want to get the MNI -coordinates of the voxel with peak activation. - -> This is what people used to use `Featquery` for, back in the un-enlightened -> days. - - -Let's start by identifying the voxel with the biggest t-statistic: - -``` -featdir = op.join(op.join('05_nifti', 'fmri.feat')) - -# The Image.data attribute returns a -# numpy array containing, well, the -# image data. -tstat1 = Image(op.join(featdir, 'stats', 'tstat1')).data - -# Recall from the numpy practical that -# argmax gives us a 1D index into a -# flattened view of the array. We can -# use the unravel_index function to -# convert it into a 3D index. -peakvox = np.abs(tstat1).argmax() -peakvox = np.unravel_index(peakvox, tstat1.shape) -print('Peak voxel coordinates for tstat1:', peakvox, tstat1[peakvox]) -``` - - -Now that we've got the voxel coordinates in functional space, we need to -transform them into MNI space. FEAT provides a transformation which goes -directly from functional to standard space, in the `reg` directory: - -``` -func2std = np.loadtxt(op.join(featdir, 'reg', 'example_func2standard.mat')) -``` - -But ... wait a minute ... this is a FLIRT matrix! We can't just plug voxel -coordinates into a FLIRT matrix and expect to get sensible results, because -FLIRT works in an internal FSL coordinate system, which is not quite -`'voxel'`, and not quite `'world'`. So we need to do a little more work. -Let's start by loading our functional image, and the MNI152 template (the -source and reference images of our FLIRT matrix): - -``` -func = Image(op.join(featdir, 'reg', 'example_func')) -std = Image(op.expandvars(op.join('$FSLDIR', 'data', 'standard', 'MNI152_T1_2mm'))) -``` - - -Now we can use them to get affines which convert between all of the different -coordinate systems - we're going to combine them into a single uber-affine, -which transforms our functional-space voxels into MNI world coordinates via: - - 1. functional voxels -> FLIRT source space - 2. FLIRT source space -> FLIRT reference space - 3. FLIRT referece space -> MNI world coordinates - - -``` -vox2fsl = func.getAffine('voxel', 'fsl') -fsl2mni = std .getAffine('fsl', 'world') -``` - -Combining two affines into one is just a simple dot-product. There is a -`concat()` function which does this for us, for any number of affines: - -``` -from fsl.transform.affine import concat - -# To combine affines together, we -# have to list them in reverse - -# linear algebra is *weird*. -funcvox2mni = concat(fsl2mni, func2std, vox2fsl) -``` - -So we've now got some voxel coordinates from our functional data, and an affine -to transform into MNI world coordinates. The rest is easy: - -``` -mnicoords = transform(peakvox, funcvox2mni) -print('Peak activation (MNI coordinates):', mnicoords) -``` - - -> Note that in the above example we are only applying a linear transformation -> into MNI space - in reality you would also want to apply your non-linear -> structural-to-standard transformation too. But this is left as an exercise -> for the reader ;). - - -<a class="anchor" id="image-processing"></a> -## Image processing - -Now, it's all well and good to look at t-statistric values and voxel -coordinates and so on and so forth, but let's spice things up a bit and look -at some images. Let's display our peak activation location in MNI space. To -do this, we're going to resample our functional image into - -``` -from IPython.display import Image as Screenshot -!fsleyes render -of screenshot.png -std -``` - - -### (Advanced) Transform coordinates with nonlinear warpfields - - -have to use your own dataset diff --git a/advanced_topics/08_fslpy/bighead.nii.gz b/advanced_topics/08_fslpy/bighead.nii.gz new file mode 100755 index 0000000000000000000000000000000000000000..6aff1e3c77ded97c81bacb06cd1d84b10220d6ed Binary files /dev/null and b/advanced_topics/08_fslpy/bighead.nii.gz differ diff --git a/advanced_topics/08_fslpy/fmri.feat/reg/example_func.nii.gz b/advanced_topics/08_fslpy/fmri.feat/reg/example_func.nii.gz new file mode 100755 index 0000000000000000000000000000000000000000..68f581786315d538a87884115620bec392ff9d4f Binary files /dev/null and b/advanced_topics/08_fslpy/fmri.feat/reg/example_func.nii.gz differ diff --git a/advanced_topics/08_fslpy/fmri.feat/reg/example_func2standard.mat b/advanced_topics/08_fslpy/fmri.feat/reg/example_func2standard.mat new file mode 100755 index 0000000000000000000000000000000000000000..e0c43af6147ac26c51e16077d5efbedb7196c219 --- /dev/null +++ b/advanced_topics/08_fslpy/fmri.feat/reg/example_func2standard.mat @@ -0,0 +1,4 @@ +1.057622308 0.05073972589 0.008769375553 -31.51452272 +-0.0541050194 0.9680196522 0.1445326796 2.872941273 +-0.01020603009 -0.2324151706 1.127557283 21.35031106 +0 0 0 1 diff --git a/advanced_topics/08_fslpy/fmri.feat/stats/tstat1.nii.gz b/advanced_topics/08_fslpy/fmri.feat/stats/tstat1.nii.gz new file mode 100755 index 0000000000000000000000000000000000000000..3f09fd681deb1e1123ebc4714f31a3665405be47 Binary files /dev/null and b/advanced_topics/08_fslpy/fmri.feat/stats/tstat1.nii.gz differ