diff --git a/fsl/data/image.py b/fsl/data/image.py index fe179c4e714f5a2c6bfbd2cf609bcd573862ae7c..8fcbb7e895b3d0d2f30d4ec7915076f9041919dc 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -23,7 +23,7 @@ file names: .. autosummary:: :nosignatures: - isSupported + looksLikeImage removeExt addExt loadImage @@ -146,8 +146,10 @@ class Nifti1(object): else: self.nibImage = image - self.shape = self.nibImage.get_shape() - self.pixdim = self.nibImage.get_header().get_zooms() + shape, pixdim = self.__determineShape(self.nibImage) + + self.shape = shape + self.pixdim = pixdim self.voxToWorldMat = np.array(self.nibImage.get_affine()) self.worldToVoxMat = transform.invert(self.voxToWorldMat) @@ -158,6 +160,39 @@ class Nifti1(object): if len(self.shape) < 3 or len(self.shape) > 4: raise RuntimeError('Only 3D or 4D images are supported') + + + def __determineShape(self, nibImage): + """This method is called by :meth:`__init__`. It figures out the shape + of the image data, and the zooms/pixdims for each data axis. Any empty + trailing dimensions are squeezed, but the returned shape is guaranteed + to be at least 3 dimensions. + """ + + nibHdr = nibImage.get_header() + shape = list(nibImage.shape) + pixdims = list(nibHdr.get_zooms()) + + # Squeeze out empty dimensions, as + # 3D image can sometimes be listed + # as having 4 or more dimensions + for i in reversed(range(len(shape))): + if shape[i] == 1: shape = shape[:i] + else: break + + # But make sure the shape is 3D + if len(shape) < 3: + shape = shape + [1] * (3 - len(shape)) + + # The same goes for the pixdim - if get_zooms() + # doesn't return at least 3 values, we'll fall + # back to the pixdim field in the header. + if len(pixdims) < 3: + pixdims = nibHdr['pixdim'][1:] + + pixdims = pixdims[:len(shape)] + + return shape, pixdims def loadData(self): @@ -165,30 +200,26 @@ class Nifti1(object): be called if the ``loadData`` parameter passed to :meth:`__init__` was ``False``. """ - - data = self.nibImage.get_data() - # Squeeze out empty dimensions, as - # 3D image can sometimes be listed - # as having 4 or more dimensions - shape = data.shape - - for i in reversed(range(len(shape))): - if shape[i] == 1: data = data.squeeze(axis=i) - else: break + # Get the data, and reshape it according + # to the shape that the __determineShape + # method figured out. + data = self.nibImage.get_data() + origShape = data.shape + data = data.reshape(self.shape) + # Tell numpy to make the + # data array read-only data.flags.writeable = False + + self.data = data log.debug('Loaded image data ({}) - original ' 'shape {}, squeezed shape {}'.format( self.dataSource, - shape, + origShape, data.shape)) - self.data = data - self.shape = self.shape[ :len(data.shape)] - self.pixdim = self.pixdim[:len(data.shape)] - # TODO: Remove this method, and use the shape attribute directly def is4DImage(self): @@ -556,8 +587,8 @@ DEFAULT_EXTENSION = '.nii.gz' """The default file extension (TODO read this from ``$FSLOUTPUTTYPE``).""" -def isSupported(filename, allowedExts=None): - """Returns ``True`` if the given file has a supported extension, ``False`` +def looksLikeImage(filename, allowedExts=None): + """Returns ``True`` if the given file looks like an image, ``False`` otherwise. :arg filename: The file name to test. @@ -568,6 +599,9 @@ def isSupported(filename, allowedExts=None): if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS + # TODO A much more robust approach would be + # to try loading the file using nibabel. + return any(map(lambda ext: filename.endswith(ext), allowedExts)) @@ -787,7 +821,8 @@ def saveImage(image, fromDir=None): path = dlg.GetPath() nibImage = image.nibImage - if not isSupported(path): + # Add a file extension if not specified + if not looksLikeImage(path): path = addExt(path, False) # this is an image which has been diff --git a/fsl/data/tensorimage.py b/fsl/data/tensorimage.py index 838910c8a9438e366a47e83aa1715db900b96b3a..0bc0627ac47e6e46f7779c5ec2b24cedcf812560 100644 --- a/fsl/data/tensorimage.py +++ b/fsl/data/tensorimage.py @@ -34,26 +34,46 @@ def getTensorDataPrefix(path): fas = glob.glob(op.join(path, '*_FA.*')) mds = glob.glob(op.join(path, '*_MD.*')) files = [v1s, v2s, v3s, l1s, l2s, l3s, fas, mds] - - # Make sure there is exactly one - # of each of the above files - def lenone(l): - return len(l) == 1 - - if not all(map(lenone, files)): - return None - files = [f[0] for f in files] - - # Make sure that all of the above - # files have the same prefix + # Gather all of the existing file + # prefixes into a dictionary of + # prefix : [file list] mappings. pattern = '^(.*)_(?:V1|V2|V3|L1|L2|L3|FA|MD).*$' - prefixes = [re.findall(pattern, f)[0] for f in files] - - if any([p != prefixes[0] for p in prefixes]): + prefixes = {} + + for f in [f for flist in files for f in flist]: + prefix = re.findall(pattern, f)[0] + + if prefix not in prefixes: prefixes[prefix] = [f] + else: prefixes[prefix].append(f) + + # Discard any prefixes which are + # not present for every file type. + for prefix, files in list(prefixes.items()): + if len(files) != 8: + prefixes.pop(prefix) + + # Discard any prefixes which + # match any files that do + # not look like image files + for prefix, files in list(prefixes.items()): + if not all([fslimage.looksLikeImage(f) for f in files]): + prefixes.pop(prefix) + + prefixes = list(prefixes.keys()) + + # No more prefixes remaining - + # this is probably not a dtifit + # directory + if len(prefixes) == 0: return None - # And there's our prefix + # If there's more than one remaining + # prefix, I don't know what to do - + # just return the first one. + if len(prefixes) > 1: + log.warning('Multiple dtifit prefixes detected: {}'.format(prefixes)) + return op.basename(prefixes[0]) diff --git a/fsl/fsleyes/displaycontext/vectoropts.py b/fsl/fsleyes/displaycontext/vectoropts.py index dcfb79f46bc5e7af067d1da54195dfa9a73927a0..4326dbe28f9e0a271cddb4f425561adb616b33b0 100644 --- a/fsl/fsleyes/displaycontext/vectoropts.py +++ b/fsl/fsleyes/displaycontext/vectoropts.py @@ -181,7 +181,7 @@ class VectorOpts(volumeopts.Nifti1Opts): """ prop = self.getProp(imageName) - val = self.modulateImage + val = getattr(self, imageName) overlays = self.displayCtx.getOrderedOverlays() options = [None] diff --git a/fsl/fsleyes/gl/gl21/glvector_funcs.py b/fsl/fsleyes/gl/gl21/glvector_funcs.py index 3a95d81e7f7197726d9afaef71cd83634a7bfea5..2e4b59320d9737d574a112f462b3039f158b3334 100644 --- a/fsl/fsleyes/gl/gl21/glvector_funcs.py +++ b/fsl/fsleyes/gl/gl21/glvector_funcs.py @@ -31,6 +31,8 @@ def compileShaders(self, vertShader, indexed=False): opts = self.displayOpts useVolumeFragShader = opts.colourImage is not None + self.useVolumeFragShader = useVolumeFragShader + if useVolumeFragShader: fragShader = 'glvolume' else: fragShader = 'glvector' @@ -48,7 +50,6 @@ def updateFragmentShaderState(self, useSpline=False): changed = False opts = self.displayOpts shader = self.shader - useVolumeFragShader = opts.colourImage is not None invClipValXform = self.clipTexture.invVoxValXform clippingRange = opts.clippingRange @@ -67,7 +68,7 @@ def updateFragmentShaderState(self, useSpline=False): clipLow = -0.1 clipHigh = 1.1 - if useVolumeFragShader: + if self.useVolumeFragShader: voxValXform = self.colourTexture.voxValXform invVoxValXform = self.colourTexture.invVoxValXform diff --git a/fsleyes_doc/editing_images.rst b/fsleyes_doc/editing_images.rst index a6cf5c36b81b62a3a32aac3105c3f423bab3181d..a63669dc89484f4582f6965966d4974b92716c59 100644 --- a/fsleyes_doc/editing_images.rst +++ b/fsleyes_doc/editing_images.rst @@ -1,39 +1,172 @@ +.. |command_key| unicode:: U+2318 +.. |shift_key| unicode:: U+21E7 +.. |control_key| unicode:: U+2303 +.. |alt_key| unicode:: U+2325 +.. |right_arrow| unicode:: U+21D2 + + .. _editing-images: -Editing images -============== +Editing NIFTI images +==================== + + +The ortho view has an *edit mode* which allows you to edit the values of NIFTI +overlays. .. _editing-images-edit-toolbar: +Create a copy! +-------------- + + +If you are worried about destroying your data, you may wish to create a copy +of your image, and edit that copy - you can do this via the *File* +|right_arrow| *Copy overlay* menu option. + + The edit toolbar -^^^^^^^^^^^^^^^^ +---------------- + + +.. TODO:: Image of edit toolbar goes here. + + +Open the edit toolbar (via the *Settings* |right_arrow| *Ortho view* +|right_arrow| *Edit toolbar* menu option), and click on the pencil button to +enter edit mode. + +Modifying the data in an image is a two-stage process: + + 1. Select the voxels you wish to change. + + 2. Change the value of the selected voxels. + + +Selecting voxels +---------------- + + +Voxels can be selected by right-clicking and dragging, or by holding down the +|command_key|/|control_key| and |shift_key| keys and left-clicking and +dragging. + +Voxels can be de-selected by holding down the |command_key|/|control_key| and +|shift_key| keys, and right-clicking and dragging. + +The selection size can be adjusted via the Selection size field in the edit +toolbar, or by holding down the |command_key|/|control_key| and |shift_key| +keys and spinning the mouse wheel. + +By default, the selection block is a 2-dimensional rectangle in the current +slice, but it can be made into a 3-dimensional cube by toggling the 2D/3D +button on the edit toolbar. + + +Select-by-value +--------------- + + +.. image:: images/editing_images_select_by_value_button.png + :align: left + +As an alternate to manually drawing the selection, voxels can be selected by +value. Select-by-value mode is enabled via the select-by-value button on the +edit toolbar. + + +In select-by-value mode, clicking on a voxel (the *seed*) will result in all +voxels that have a value similar to that voxel being selected. The threshold +by which voxels are considered to be similar can be changed via the edit +toolbar, or by spinning the mouse wheel. + + +When in select-by-value mode, the search region can be restricted in the +following ways: + + +.. image:: images/editing_images_2D_button.png + :align: left + +The region can be limited to the current slice, or the entire volume, via the +2D/3D buttons. + + +.. image:: images/editing_images_select_radius_button.png + :align: left + +The region be limited to a radius by pushing the radius button. The radius +can be changed on the edit toolbar, or by holding down the |alt_key| and +|shift_key| keys, and spinning the mouse wheel. + + +.. image:: images/editing_images_local_search_button.png + :align: left + +The search can be restricted to adjacent voxels by pushing the local search +button. When local search is enabled, voxels which are not adjacent to an +already-selected voxel are excluded from the search. + + +Changing voxel values +--------------------- + + +Once you are happy with your selection you can change the value of the +selected voxels in one of the following ways: + +.. image:: images/editing_images_bucket_fill_button.png + :align: left + +The values of all selected voxels can be replaced with the current fill value, +by clicking the bucket-fill button: + + +.. image:: images/editing_images_erase_button.png + :align: left + +The values of all selected voxels can be erased (replaced with 0) by clicking +the erase button: + + +The current fill value can be modified via the Fill value field on the edit +toolbar. + +Creating masks/ROIs +------------------- -.. _editing-images-fill-value: +Once you have made a selection, you can copy that selection into a new overlay, +with the *Create mask* and *Create ROI* buttons. Both buttons will create a new +image which has the same dimensions as the image being edited. -Filling the selection -^^^^^^^^^^^^^^^^^^^^^ -.. _editing-images-erasing: +.. image:: images/editing_images_create_roi_button.png + :align: left -Erasing the selection -^^^^^^^^^^^^^^^^^^^^^ +The *Create ROI* button will create a new image, and will copy the values of +all selected voxels over from the image being edited. All other voxels in the +new image will be set to 0. -.. _editing-images-selectionIs3D: +.. image:: images/editing_images_create_mask_button.png + :align: left -3D selections -^^^^^^^^^^^^^ +The *Create Mask* button will create a new image, and will set the value of +all selected voxels to 1, and the value of all other voxels to 0. + -.. _editing-images-select-by-intensity: +Saving your changes +------------------- -Select by value -^^^^^^^^^^^^^^^ +When you have made changes to an image, or created a mask/ROI image, don't +forget to save them via the *File* |right_arrow| *Save overlay* menu item, or +the floppy disk button on the :ref:`controls-overlay-list`. diff --git a/fsleyes_doc/images/editing_images_2D_button.png b/fsleyes_doc/images/editing_images_2D_button.png new file mode 100644 index 0000000000000000000000000000000000000000..f6ae443adc6b9fd018815825730bc12991a464ed Binary files /dev/null and b/fsleyes_doc/images/editing_images_2D_button.png differ diff --git a/fsleyes_doc/images/editing_images_bucket_fill_button.png b/fsleyes_doc/images/editing_images_bucket_fill_button.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf40da113b17d5ccd46558e788cb560035e05ad Binary files /dev/null and b/fsleyes_doc/images/editing_images_bucket_fill_button.png differ diff --git a/fsleyes_doc/images/editing_images_create_mask_button.png b/fsleyes_doc/images/editing_images_create_mask_button.png new file mode 100644 index 0000000000000000000000000000000000000000..dfacd2d17c3716ddfb0d39dd7070627ae01f43f4 Binary files /dev/null and b/fsleyes_doc/images/editing_images_create_mask_button.png differ diff --git a/fsleyes_doc/images/editing_images_create_roi_button.png b/fsleyes_doc/images/editing_images_create_roi_button.png new file mode 100644 index 0000000000000000000000000000000000000000..03e58dd6538ad0f9a8381271893c803a6f778226 Binary files /dev/null and b/fsleyes_doc/images/editing_images_create_roi_button.png differ diff --git a/fsleyes_doc/images/editing_images_erase_button.png b/fsleyes_doc/images/editing_images_erase_button.png new file mode 100644 index 0000000000000000000000000000000000000000..f8126d7d69a9c790c59d94bde771c2f83cf51b86 Binary files /dev/null and b/fsleyes_doc/images/editing_images_erase_button.png differ diff --git a/fsleyes_doc/images/editing_images_local_search_button.png b/fsleyes_doc/images/editing_images_local_search_button.png new file mode 100644 index 0000000000000000000000000000000000000000..7bf629fca5963da9eed100f12db8b8d946318bc0 Binary files /dev/null and b/fsleyes_doc/images/editing_images_local_search_button.png differ diff --git a/fsleyes_doc/images/editing_images_select_by_value_button.png b/fsleyes_doc/images/editing_images_select_by_value_button.png new file mode 100644 index 0000000000000000000000000000000000000000..eec43d184eb1087c0363bcfbf5f4a7a7fab0dae4 Binary files /dev/null and b/fsleyes_doc/images/editing_images_select_by_value_button.png differ diff --git a/fsleyes_doc/images/editing_images_select_radius_button.png b/fsleyes_doc/images/editing_images_select_radius_button.png new file mode 100644 index 0000000000000000000000000000000000000000..ffaf7f5ab9952829c202177f56b173ebe14361b7 Binary files /dev/null and b/fsleyes_doc/images/editing_images_select_radius_button.png differ diff --git a/fsleyes_doc/quick_start.rst b/fsleyes_doc/quick_start.rst index e43d3d02d235be50ce07ac98ba330c1cf1665cbd..6573b7b7e44d02d904d47d37a7971ec746ed98b7 100644 --- a/fsleyes_doc/quick_start.rst +++ b/fsleyes_doc/quick_start.rst @@ -30,8 +30,9 @@ looks like this: :align: center -This is slightly boring, so let's load an image. Select the *File/Add overlay -from file* menu option, and choose a `.nii.gz` image to load. +This is slightly boring, so let's load an image. Select the *File +|right_arrow| Add overlay from file* menu option, and choose a `.nii.gz` image +to load. Now things are a bit more interesting: @@ -242,54 +243,9 @@ Edit a NIFTI1 image? You can :ref:`edit NIFTI1 image data <editing-images>` from within an ortho view. Open the :ref:`edit toolbar <editing-images-edit-toolbar>` (via the -*Settings* |right_arrow| *<view name>* |right_arrow| *Edit toolbar* menu -option), and click on the pencil button to enter edit mode. - -Modifying the data in an image is a two-stage process: - - 1. Select the voxels you wish to change. - 2. Change the value of the selected voxels. - - -**Selecting voxels** - - - Voxels can be selected by right-clicking and dragging, or by holding down - the |command_key|/|control_key| and |shift_key| keys and left-clicking and - dragging. - - - Voxels can be deselected by holding down the |command_key|/|control_key| - and |shift_key| keys, and right-clicking and dragging. - - - The selection size can be adjusted via the *Selection size* field in the - edit toolbar, or by holding down the |command_key|/|control_key| and - |shift_key| keys and spinning the mouse wheel. - - - By default, the selection block is a 2-dimensional rectangle in the - current slice, but it can be made into a 3-dimensional cube by toggling - the :ref:`2D/3D button <editing-images-selectionIs3D>` on the - edit toolbar. - - - As an alternate to manually drawing the selection, voxels can be - :ref:`selected by value <editing-images-select-by-intensity>`. - - -**Changing voxel values** - - - The values of all selected voxels can be replaced with the current - :ref:`fill value <editing-images-fill-value>`, by clicking the - bucket-fill button on the edit toolbar. - - - The values of all selected voxels can be *erased* (replaced with 0) by - clicking the :ref:`erase button <editing-images-erasing>` on the edit - toolbar. - - - The current fill value can be modified via the *Fill value* field - on the edit toolbar. - - -When you have made changes to an image, don't forget to save them via the -*File* |right_arrow| *Save overlay* menu item, or the floppy disk button on -the :ref:`controls-overlay-list`. +*Settings* |right_arrow| *Ortho view* |right_arrow| *Edit toolbar* menu +option), and click on the pencil button to enter edit mode. See the page on +:ref:`editing images <editing-images>` for more details. Classify ICA components?