Skip to content
Snippets Groups Projects
Commit 7f7dd1ed authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Documented fsl.data package.

parent ba84cdf7
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python
#
# __init__.py - fsl.data package.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Data structures and models."""
......@@ -5,6 +5,9 @@
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Classes for representing 3D/4D images, display properties of images, and
collections of images.
"""
import os
import sys
......@@ -28,13 +31,13 @@ log = logging.getLogger(__name__)
def _loadImageFile(filename):
"""
Given the name of an image file, loads it using nibabel. If the file
is large, and is gzipped, it is decompressed to a temporary location,
so that it can be memory-mapped. A tuple is returned, consisting of
the nibabel image object, and the name of the file that it was loaded
from (either the passed-in file name, or the name of the temporary
decompressed file).
"""Given the name of an image file, loads it using nibabel.
If the file is large, and is gzipped, it is decompressed to a temporary
location, so that it can be memory-mapped. A tuple is returned,
consisting of the nibabel image object, and the name of the file that it
was loaded from (either the passed-in file name, or the name of the
temporary decompressed file).
"""
# If we have a GUI, we can display a dialog
......@@ -48,71 +51,113 @@ def _loadImageFile(filename):
pass
realFilename = filename
mbytes = op.getsize(filename) / 1048576.0
if filename.endswith('.nii.gz'):
mbytes = op.getsize(filename) / 1048576.0
# The mbytes limit is arbitrary
if filename.endswith('.nii.gz') and mbytes > 512:
# This limit is arbitrary
if mbytes > 512:
unzipped, filename = tempfile.mkstemp(suffix='.nii')
unzipped, filename = tempfile.mkstemp(suffix='.nii')
unzipped = os.fdopen(unzipped)
unzipped = os.fdopen(unzipped)
msg = '{} is a large file ({} MB) - decompressing ' \
'to {}, to allow memory mapping...'.format(realFilename,
mbytes,
filename)
msg = '{} is a large file ({} MB) - decompressing ' \
'to {}, to allow memory mapping...'.format(realFilename,
mbytes,
filename)
if not haveGui:
log.info(msg)
else:
busyDlg = wx.BusyInfo(msg, wx.GetTopLevelWindows()[0])
if not haveGui:
log.info(msg)
else:
busyDlg = wx.BusyInfo(msg, wx.GetTopLevelWindows()[0])
gzip = ['gzip', '-d', '-c', realFilename]
log.debug('Running {} > {}'.format(' '.join(gzip), filename))
gzip = ['gzip', '-d', '-c', realFilename]
log.debug('Running {} > {}'.format(' '.join(gzip), filename))
# If the gzip call fails, revert to loading from the gzipped file
try:
sp.call(gzip, stdout=unzipped)
unzipped.close()
# If the gzip call fails, revert to loading from the gzipped file
try:
sp.call(gzip, stdout=unzipped)
unzipped.close()
except OSError as e:
log.warn('gzip call failed ({}) - cannot memory '
'map file: {}'.format(e, realFilename),
exc_info=True)
unzipped.close()
os.remove(filename)
filename = realFilename
except OSError as e:
log.warn('gzip call failed ({}) - cannot memory '
'map file: {}'.format(e, realFilename),
exc_info=True)
unzipped.close()
os.remove(filename)
filename = realFilename
if haveGui:
busyDlg.Destroy()
if haveGui:
busyDlg.Destroy()
return nib.load(filename), filename
class Image(props.HasProperties):
"""
Class which represents a 3D/4D image. Internally, the image is
loaded/stored using nibabel.
"""Class which represents a 3D/4D image. Internally, the image is
loaded/stored using :mod:`nibabel`.
Arbitrary data may be associated with an :class:`Image` object, via the
:meth:`getAttribute` and :meth:`setAttribute` methods (which are just
front end wrappers around an internal ``dict`` object).
The following attributes are present on an :class:`Image` object:
:ivar nibImage: The :mod:`nibabel` image object.
:ivar data: A reference to the image data, stored as a
:mod`numpy` array.
:ivar display: A :class:`ImageDisplay` object, defining how this
image should be displayed.
:ivar shape: A list/tuple containing the number of voxels
along each image dimension.
:ivar pixdim: A list/tuple containing the size of one voxel
alon each image dimension.
:ivar voxToWorldMat: A 4*4 array specifying the affine transformation
for transforming voxel coordinates into real world
coordinates.
:ivar worldToVoxMat: A 4*4 array specifying the affine transformation
for transforming real world coordinates into voxel
coordinates.
:ivar imageFile: The name of the file that the image was loaded from.
:ivar tempFile: The name of the temporary file which was created (in
the event that the image was large and was gzipped -
see :func:`_loadImageFile`).
"""
# How the image should be transformd into real world space.
transform = props.Choice(
collections.OrderedDict([
('affine', 'Use qform/sform transformation matrix'),
('pixdim', 'Use pixdims only'),
('id', 'Do not use qform/sform or pixdims')]),
default='affine')
name = props.String()
imageFile = props.FilePath()
tempFile = props.FilePath()
"""This property defines how the image should be transformd into real world
space.
- ``affine``: Use the affine transformation matrix stored in the image
(the ``qform``/``sform`` fields in NIFTI1 headers).
- ``pixdim``: Scale voxel sizes by the ``pixdim`` fields in the image
header.
- ``id``: Perform no scaling or transformation - voxels will be
displayed as :math:`1mm^3` isotropic.
"""
name = props.String()
"""The name of this image."""
def __init__(self, image):
"""
Initialise an Image object with the given image data or file name.
"""Initialise an Image object with the given image data or file name.
:arg image: A string containing the name of an image file to load, or
a :mod:`numpy` array, or a :mod:`nibabel` image object.
"""
# The image parameter may be the name of an image file
......@@ -172,24 +217,27 @@ class Image(props.HasProperties):
def _transformChanged(self, *a):
"""
"""This method is called when the :attr:`transform` property value
changes. It updates the :attr:`voxToWorldMat`, :attr:`worldToVoxMat`,
and :attr:`pixdim` attributes to reflect the new transformation
type.
"""
if self.transform == 'affine':
voxToWorldMat = self.nibImage.get_affine()
pixdims = self.nibImage.get_header().get_zooms()
pixdim = self.nibImage.get_header().get_zooms()
elif self.transform == 'pixdim':
pixdims = self.nibImage.get_header().get_zooms()
voxToWorldMat = np.diag([pixdims[0], pixdims[1], pixdims[2], 1.0])
pixdim = self.nibImage.get_header().get_zooms()
voxToWorldMat = np.diag([pixdim[0], pixdim[1], pixdim[2], 1.0])
elif self.transform == 'id':
voxToWorldMat = np.identity(4)
pixdims = [1.0, 1.0, 1.0]
pixdim = [1.0, 1.0, 1.0]
self.voxToWorldMat = np.array(voxToWorldMat, dtype=np.float32)
self.worldToVoxMat = linalg.inv(self.voxToWorldMat)
self.pixdims = pixdims
self.pixdim = pixdim
self.voxToWorldMat = self.voxToWorldMat.transpose()
self.worldToVoxMat = self.worldToVoxMat.transpose()
......@@ -198,7 +246,7 @@ class Image(props.HasProperties):
# location (0, 0) to map to voxel location (0, 0)
if self.transform in ['pixdim', 'id']:
for i in range(3):
self.voxToWorldMat[3, i] = self.pixdims[i] * 0.5
self.voxToWorldMat[3, i] = self.pixdim[i] * 0.5
self.worldToVoxMat[3, i] = -0.5
log.debug('Image {} transformation matrix changed: {}'.format(
......@@ -207,9 +255,8 @@ class Image(props.HasProperties):
def imageBounds(self, axis):
"""
Return the bounds (min, max) of the image, in real world
coordinates, along the specified axis.
"""Return the bounds (min, max) of the image, in real world
coordinates, along the specified 0-indexed axis.
"""
x, y, z = self.shape[:3]
......@@ -238,24 +285,23 @@ class Image(props.HasProperties):
def worldToVox(self, p, axes=None):
"""
Transforms the given set of points in voxel coordinates to
points in world coordinates, according to the affine
transformation specified in the image file. The returned array
is either a numpy.float64 array, or a single integer value,
depending on the input. There is no guarantee that the returned
array of voxel coordinates is within the bounds of the image
shape. Parameters:
"""Transforms the given set of points in voxel coordinates to points
in world coordinates, according to the current :attr:`transform`.
The returned array is either a :class:`numpy.float64` array, or a
single ``int`` value, depending on the input. There is no guarantee
that the returned array of voxel coordinates is within the bounds of
the image shape. Parameters:
- p: N*A array, where N is the number of points, and A
is the number of axes to consider (default: 3)
:arg p: N*A array, where N is the number of points, and A
is the number of axes to consider (default: 3).
- axes: If None, it is assumed that the input p is a N*3
array, with each point being specified by x,y,z
coordinates. If a single value in the range (0-2),
it is assumed that p is a 1D array. Or, if a
sequence of 2 or 3 values, p must be an array of
N*2 or N*3, respectively.
:arg axes: If ``None``, it is assumed that the input p is a N*3
array, with each point being specified by x,y,z
coordinates. If a single value in the range (0-2),
it is assumed that p is a 1D array. Or, if a
sequence of 2 or 3 values, p must be an array of
N*2 or N*3, respectively.
"""
voxp = self._transform(p, self.worldToVoxMat, axes)
......@@ -273,13 +319,13 @@ class Image(props.HasProperties):
def voxToWorld(self, p, axes=None):
"""
Transforms the given set of points in world coordinates to
points in voxel coordinates, according to the affine
transformation specified in the image file. The returned
array is either a numpy.float64 array, or a single float
value, depending on the input. See the worldToVox
docstring for more details.
"""Transforms the given set of points in world coordinates to
points in voxel coordinates, according to the current
:attr:`transform`.
The returned array is either a :class:`numpy.float64` array,
or a single ``float`` value, depending on the input. See the
:meth:`worldToVox` method for more details.
"""
worldp = self._transform(p, self.voxToWorldMat, axes)
......@@ -289,11 +335,11 @@ class Image(props.HasProperties):
def _transform(self, p, a, axes):
"""
Transforms the given set of points p according to the given
affine transformation a. The transformed points are returned
as a numpy.float64 array. See the worldToVox docstring for
more details.
"""Used by the :meth:`worldToVox` and :meth:`voxToWorld` methods.
Transforms the given set of points ``p`` according to the given affine
transformation ``a``. The transformed points are returned as a
:class:``numpy.float64`` array.
"""
p = self._fillPoints(p, axes)
......@@ -313,9 +359,8 @@ class Image(props.HasProperties):
def _fillPoints(self, p, axes):
"""
Used by the _transform method. Turns the given array p into a N*3
array of x,y,z coordinates. The array p may be a 1D array, or an
"""Used by the :meth:`_transform` method. Turns the given array p into
a N*3 array of x,y,z coordinates. The array p may be a 1D array, or an
N*2 or N*3 array.
"""
......@@ -347,16 +392,12 @@ class Image(props.HasProperties):
def getAttribute(self, name):
"""
Retrieve the attribute with the given name.
"""
"""Retrieve the attribute with the given name."""
return self._attributes[name]
def setAttribute(self, name, value):
"""
Set an attribute with the given name and the given value.
"""
"""Set an attribute with the given name and the given value."""
self._attributes[name] = value
log.debug('Attribute set on {}: {} = {}'.format(
......@@ -364,31 +405,48 @@ class Image(props.HasProperties):
class ImageDisplay(props.HasProperties):
"""
A class which describes how an image should be displayed. There should
be no need to manually instantiate ImageDisplay objects - one is created
for each Image object, and is accessed via the Image.display attribute.
If a single image needs to be displayed in different ways, then create
away, and manage your own ImageDisplay objects.
"""A class which describes how an image should be displayed.
There should be no need to manually instantiate :class:`ImageDisplay`
objects - one is created for each :class:`Image` object, and is accessed
via the :attr:`Image.display` instance attribute. If a single image needs
to be displayed in different ways, then create away, and manage your own
:class:`ImageDisplay` objects.
This class doesn't have any functionality - it is up to things which
actually display an Image to adhere to the properties stored in the
associated ImageDisplay object.
actually display an :class:`Image` to adhere to the properties stored in
the associated :class:`ImageDisplay` object.
"""
enabled = props.Boolean(default=True)
alpha = props.Real(minval=0.0, maxval=1.0, default=1.0)
enabled = props.Boolean(default=True)
"""Should this image be displayed at all?"""
alpha = props.Real(minval=0.0, maxval=1.0, default=1.0)
"""Transparency - 1.0 is fully opaque, and 0.0 is fully transparent."""
displayRange = props.Bounds(ndims=1, editLimits=True,
labels=['Min.', 'Max.'])
"""Image values which map to the minimum and maximum colour map colours."""
samplingRate = props.Int(minval=1, maxval=16, default=1, clamped=True)
rangeClip = props.Boolean(default=False)
cmap = props.ColourMap(default=mplcm.Greys_r)
volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
"""Only display every Nth voxel (a performance tweak)."""
rangeClip = props.Boolean(default=False)
"""If ``True``, don't display voxel values which are beyond the
:attr:`displayRange`.
"""
cmap = props.ColourMap(default=mplcm.Greys_r)
"""The colour map, a :class:`matplotlib.colors.Colourmap` instance."""
volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
"""If a 4D image, the current volume to display."""
def is4DImage(self):
"""Returns ``True`` if this image is 4D, ``False`` otherwise.
"""
return len(self.image.shape) > 3 and self.image.shape[3] > 1
_view = props.VGroup(('enabled',
props.Widget('volume', enabledWhen=is4DImage),
'displayRange',
......@@ -420,9 +478,9 @@ class ImageDisplay(props.HasProperties):
def __init__(self, image):
"""
Create an ImageDisplay for the specified image. The image
parameter should be an Image object (defined above).
"""Create an :class:`ImageDisplay` for the specified image.
:arg image: A :class:`Image` object.
"""
self.image = image
......@@ -453,44 +511,58 @@ class ImageDisplay(props.HasProperties):
class ImageList(props.HasProperties):
"""
Class representing a collection of images to be displayed together.
Contains a List property containing Image objects, and some other
properties on which listeners may register themselves to be notified
when the properties of the image collection changes (e.g. image
bounds).
"""Class representing a collection of images to be displayed together.
Contains a :class:`props.properties_types.List` property containing
:class:`Image` objects, and some other properties on which listeners may
register themselves to be notified when the properties of the image
collection changes (e.g. image bounds).
An :class:`ImageList` object has a few wrapper methods around the
:attr:`images` property, allowing the :class:`ImageList` to be used
as if it were a list itself.
"""
def _validateImage(self, atts, images):
"""Returns ``True`` if all objects in the given ``images`` list are
:class:`Image` objects, ``False`` otherwise.
"""
return all(map(lambda img: isinstance(img, Image), images))
# The images property contains a list of Image objects
images = props.List(validateFunc=_validateImage, allowInvalid=False)
"""A list of :class:`Image` objects. to be displayed"""
# Index of the currently 'selected' image. This property
# is not used by the ImageList, but is provided so that
# other things can control and listen for changes to
# the currently selected image
selectedImage = props.Int(minval=0, clamped=True)
"""Index of the currently 'selected' image. This property is not used by
the :class:`ImageList`, but is provided so that other things can control
and listen for changes to the currently selected image
"""
# The bounds property contains the min/max values of
# a bounding box (in real world coordinates) which
# is big enough to contain all of the images in the
# 'images' list. This property shouid be read-only,
# but I don't have a way to enforce it (yet).
bounds = props.Bounds(ndims=3)
"""This property contains the min/max values of
a bounding box (in real world coordinates) which
is big enough to contain all of the images in the
:attr:`images` list. This property shouid be
read-only, but I don't have a way to enforce it
(yet).
"""
# The location property contains the currently 'selected'
# 3D location in the image list space. This property
# is not used directly by the ImageList object, but it
# is here so that the location selection can be synchronised
# across multiple displays.
location = props.Point(ndims=3, labels=('X', 'Y', 'Z'))
"""The location property contains the currently 'selected'
3D location in the image list space. This property
is not used directly by the ImageList object, but it
is here so that the location selection can be synchronised
across multiple displays.
"""
def __init__(self, images=None):
"""
Create an ImageList object from the given sequence of Image objects.
"""
"""Create an ImageList object from the given sequence of
:class:`Image` objects."""
if images is None: images = []
......@@ -514,10 +586,9 @@ class ImageList(props.HasProperties):
def _imageListChanged(self, *a):
"""
Called whenever an item is added or removed from the list. Registers
listeners with the properties of each image, and updates the image
bounds
"""Called whenever an item is added or removed from the :attr:`images`
list. Registers listeners with the properties of each image, and
calls the :meth:`_updateImageBounds` method.
"""
for img in self.images:
......@@ -535,9 +606,9 @@ class ImageList(props.HasProperties):
def _updateImageBounds(self, *a):
"""
Called whenever an item is added or removed from the list, or an
image property changes. Updates the xyz bounds.
"""Called whenever an item is added or removed from the
:attr:`images` list, or an image property changes. Updates
the :attr:`bounds` property.
"""
if len(self.images) == 0:
......@@ -574,7 +645,7 @@ class ImageList(props.HasProperties):
def __iter__( self): return self.images.__iter__()
def __contains__(self, item): return self.images.__contains__(item)
def __setitem__( self, key, val): return self.images.__setitem__(key, val)
def __delitem( self, key): return self.images.__delitem__(key)
def __delitem__( self, key): return self.images.__delitem__(key)
def index( self, item): return self.images.index(item)
def count( self, item): return self.images.count(item)
def append( self, item): return self.images.append(item)
......
......@@ -5,28 +5,38 @@
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Convenience functions for adding/stripping supported
file extensions to/from image file names.
"""
import os
import os.path as op
# The file extensions which we understand. This list is used
# as the default if if the allowedExts parameter is not passed
# to any of the functions in this module.
_allowedExts = ['.nii', '.img', '.hdr', '.nii.gz', '.img.gz']
"""The file extensions which we understand. This list is used as the default
if if the ``allowedExts`` parameter is not passed to any of the functions in
this module.
"""
_descriptions = ['NIFTI1 images',
'ANALYZE75 images',
'NIFTI1/ANALYZE75 headers',
'Compressed NIFTI1 images',
'Compressed ANALYZE75/NIFTI1 images']
"""Descriptions for each of the extensions in :data:`_allowedExts`. """
# The default file extension (TODO read this from $FSLOUTPUTTYPE)
_defaultExt = '.nii.gz'
"""The default file extension (TODO read this from ``$FSLOUTPUTTYPE``)."""
def wildcard(allowedExts=None):
"""
"""Returns a wildcard string for use in a file dialog, to limit
the acceptable file types.
:arg allowedExts: A list of strings containing the allowed file
extensions.
"""
if allowedExts is None:
......@@ -38,7 +48,7 @@ def wildcard(allowedExts=None):
exts = ['*{}'.format(ext) for ext in allowedExts]
wcParts = ['|'.join((desc,ext)) for (desc,ext) in zip(descs, exts)]
wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)]
print '|'.join(wcParts)
return '|'.join(wcParts)
......@@ -47,8 +57,13 @@ def wildcard(allowedExts=None):
def isSupported(filename, allowedExts=None):
"""
Returns True if the given file has a supported extension, False
Returns ``True`` if the given file has a supported extension, ``False``
otherwise.
:arg filename: The file name to test.
:arg allowedExts: A list of strings containing the allowed file
extensions.
"""
if allowedExts is None: allowedExts = _allowedExts
......@@ -58,8 +73,13 @@ def isSupported(filename, allowedExts=None):
def removeExt(filename, allowedExts=None):
"""
Removes the extension from the given file name. Raises a ValueError
Removes the extension from the given file name. Raises a :exc:`ValueError`
if the file has an unsupported extension.
:arg filename: The file name to strip.
:arg allowedExts: A list of strings containing the allowed file
extensions.
"""
if allowedExts is None: allowedExts = _allowedExts
......@@ -85,22 +105,27 @@ def addExt(
mustExist=False,
allowedExts=None,
defaultExt=None):
"""
Adds a file extension to the given file prefix. If mustExist is False
(the default), and the file does not already have a supported
extension, the default extension is appended and the new file name
returned. If the prefix already has a supported extension, it is
returned unchanged.
"""Adds a file extension to the given file ``prefix``.
If mustExist is True, the function checks to see if any files exist
that have the given prefix, and a supported file extension. A
ValueError is raised if:
If ``mustExist`` is False (the default), and the file does not already
have a supported extension, the default extension is appended and the new
file name returned. If the prefix already has a supported extension,
it is returned unchanged.
If ``mustExist`` is ``True``, the function checks to see if any files
exist that have the given prefix, and a supported file extension. A
:exc:`ValueError` is raised if:
- No files exist with the given prefix and a supported extension.
- More than one file exists with the given prefix, and a supported
extension.
Otherwise the full file name is returned.
:arg prefix: The file name refix to modify.
:arg mustExist: Whether the file must exist or not.
:arg allowedExts: List of allowed file extensions.
:arg defaultExt: Default file extension to use.
"""
if allowedExts is None: allowedExts = _allowedExts
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment