diff --git a/fsl/data/image.py b/fsl/data/image.py index 898e5efe6a4b0d2c01e2a34b7399412ccb9081af..0cd061e86098f5485317440fc26042bc8b4e4e79 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -343,7 +343,8 @@ class Image(Nifti1, notifier.Notifier): name=None, header=None, xform=None, - loadData=True): + loadData=True, + calcRange=False): """Create an ``Image`` object with the given image data or file name. :arg image: A string containing the name of an image file to load, @@ -363,13 +364,17 @@ class Image(Nifti1, notifier.Notifier): array, and ``header`` is ``None``. :arg loadData: If ``True`` (the default) the image data is loaded - immediately (although see the note above about large - compressed files). Otherwise, only the image header - information is read - the data may be loaded later on - via the :meth:`loadData` method. In this case, the - reported ``dataRange`` will be ``(None, None)``, and - ``data`` will be ``None``, until the image data is - loaded via a call to :meth:``loadData``. + in to memory. Otherwise, only the image header + information is read, and the image data is kept + from disk. In either case, the image data is + accessed through an :class:`.ImageWrapper` instance. + The data may be loaded into memory later on via the + :meth:`loadData` method. + + :arg calcRange: If ``True``, the image range is calculated immediately. + Otherwise (the default), the image range is + incrementally updated as more data is read from memory + or disk. """ import nibabel as nib @@ -420,12 +425,18 @@ class Image(Nifti1, notifier.Notifier): self.__dataSource = dataSource self.__nibImage = nibImage self.__saveState = dataSource is not None - self.__imageWrapper = None + self.__imageWrapper = imagewrapper.ImageWrapper(self.nibImage, + self.name, + loadData=loadData) - if loadData: - self.loadData() - + if calcRange: + self.calcRange() + + self.__imageWrapper.register( + '{}_{}'.format(id(self), self.name), + self.__dataRangeChanged) + def __hash__(self): """Returns a number which uniquely idenfities this ``Image`` instance (the result of ``id(self)``). @@ -478,49 +489,63 @@ class Image(Nifti1, notifier.Notifier): def dataRange(self): """ """ - if self.__imageWrapper is None: return (None, None) - else: return self.__imageWrapper.dataRange + if self.__imageWrapper is None: drange = (None, None) + else: drange = self.__imageWrapper.dataRange + + + # Fall back to the cal_min/max + # fields in the NIFTI1 header + # if we don't yet know anything + # about the image data range. + if drange[0] is None or drange[1] is None: + drange = (self.header.get('cal_min', None), + self.header.get('cal_max', None)) + + return drange + + + @property + def dtype(self): + """Returns the ``numpy`` data type of the image data. """ + + # Get the data type from the + # first voxel in the image + coords = [0] * len(self.__nibImage.shape) + return self.__nibImage.dataobj[tuple(coords)].dtype def __dataRangeChanged(self, *args, **kwargs): + """Called when the :class:`.ImageWrapper` data range changes. + Notifies any listeners of this ``Image`` (registered through the + :class:`.Notifier` interface) on the ``'dataRange'`` topic. + """ self.notify('dataRange') - - def loadData(self): - """Calculates initial values for :attr:`dataRange`. - """ - if self.__imageWrapper is not None: - raise RuntimeError('loadData can only be called once') + def calcRange(self, sizethres=None): - self.__imageWrapper = imagewrapper.ImageWrapper( - self.nibImage, self.name) - - self.__imageWrapper.register('{}_{}'.format(id(self), self.name), - self.__dataRangeChanged) - - # How big is this image? If it's not too big, - # then we'll calculate the actual data range - # right now. - # - # This hard-coded threshold is equal to twice - # the number of voxels in the MNI152 T1 0.5mm - # standard image. Any bigger than this, and - # we'll calculate the range from a sample: - # # The ImageWrapper automatically calculates # the range of the specified slice, whenever # it gets indexed. All we have to do is # access a portion of the data to trigger the - # range calculation. - # - # Note: Only 3D/4D images supported here -# if np.prod(self.shape) < 115536512: self.__imageWrapper[:] -# elif len(self.shape) >= 4: self.__imageWrapper[:, :, :, 0] -# else: self.__imageWrapper[:, :, 0] + # range calculation. + + # If an image size threshold has not been specified, + # then we'll calculate the full data range right now. + if sizethres is None: + self.__imageWrapper[:] + + # Otherwise if the number of values in the + # image is bigger than the size threshold, + # we'll calculate the range from a sample: + elif np.prod(self.shape) < sizethres: + if len(self.shape) == 3: self.__imageWrapper[:, :, 0] + else: self.__imageWrapper[:, :, :, 0] def __getitem__(self, sliceobj): + """ + """ return self.__imageWrapper.__getitem__(sliceobj)