diff --git a/fsl/data/image.py b/fsl/data/image.py index b6dafec107a78813f831f0fddfafb90488c1d21c..6d16e0336ceed5574d9be5437466c776f443fccf 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -32,12 +32,15 @@ and file names: """ -import os -import os.path as op -import logging +import os +import os.path as op +import logging -import six -import numpy as np +import six +import numpy as np + +import nibabel as nib +import nibabel.fileslice as fileslice import fsl.utils.transform as transform @@ -175,8 +178,6 @@ class Nifti(notifier.Notifier): image header. """ - import nibabel as nib - # Nifti2Header is a sub-class of Nifti1Header, # and Nifti1Header a sub-class of AnalyzeHeader, # so we only need to test for the latter. @@ -284,8 +285,6 @@ class Nifti(notifier.Notifier): - ``2`` for NIFTI2 """ - import nibabel as nib - # nib.Nifti2 is a subclass of Nifti1, # and Nifti1 a subclass of Analyze, # so we have to check in order @@ -376,7 +375,6 @@ class Nifti(notifier.Notifier): # How convenient - nibabel has a function # that does the dirty work for us. - import nibabel.fileslice as fileslice return fileslice.canonical_slicers(sliceobj, self.__origShape) @@ -444,8 +442,6 @@ class Nifti(notifier.Notifier): transformation matrix. """ - import nibabel as nib - inaxes = [[-1, 1], [-2, 2], [-3, 3]] return nib.orientations.aff2axcodes(xform, inaxes) @@ -536,7 +532,6 @@ class Nifti(notifier.Notifier): if self.getXFormCode() == constants.NIFTI_XFORM_UNKNOWN: return constants.ORIENT_UNKNOWN - import nibabel as nib code = nib.orientations.aff2axcodes( xform, ((constants.ORIENT_R2L, constants.ORIENT_L2R), @@ -657,8 +652,6 @@ class Image(Nifti): to ``False``. Ignored if ``loadData`` is ``True``. """ - import nibabel as nib - nibImage = None dataSource = None fileobj = None @@ -918,8 +911,6 @@ class Image(Nifti): if ``filename`` is ``None``. """ - import nibabel as nib - if self.__dataSource is None and filename is None: raise ValueError('A file name must be specified') @@ -1118,10 +1109,10 @@ def loadIndexedImageFile(filename): Returns a tuple containing the ``nibabel`` NIFTI image, and the open ``IndexedGzipFile`` handle. """ - - import nibabel as nib - import indexed_gzip as igzip + import threading + import indexed_gzip as igzip + log.debug('Loading {} using indexed gzip'.format(filename)) # guessed_image_type returns a @@ -1133,8 +1124,78 @@ def loadIndexedImageFile(filename): spacing=4194304, readbuf_size=1048576) + # See the read_segments + # function below + fobj._arrayproxy_lock = threading.Lock() + fmap = ftype.make_file_map() fmap['image'].fileobj = fobj image = ftype.from_file_map(fmap) return image, fobj + + +def read_segments(fileobj, segments, n_bytes): + """This function is used in place of the + ``nibabel.fileslice.read_segments`` function to ensure thread-safe read + access to image data via the ``nibabel.arrayproxy.ArrayProxy`` (the + ``dataobj`` attribute of a ``nibabel`` image). + + The ``nibabel`` implementation uses multiple calls to ``seek`` and + ``read`` to read segments of data from the file. When accessed by multiple + threads, these seeks and reads can become intertwined, which causes a read + from one thread to read data from the seek location requested by the other + thread. + + This implementation protects the seek/read pairs with a + ``threading.Lock``, which is added to ``IndexedGzipFile`` instances that + are created in the :func:`loadIndexedImageFile` function. + """ + + from mmap import mmap + + try: + # fileobj is a nibabel.openers.ImageOpener - the + # actual file is available via the fobj attribute + lock = getattr(fileobj.fobj, '_arrayproxy_lock') + + except: + return fileslice.orig_read_segments(fileobj, segments, n_bytes) + + if len(segments) == 0: + if n_bytes != 0: + raise ValueError("No segments, but non-zero n_bytes") + return b'' + if len(segments) == 1: + offset, length = segments[0] + + lock.acquire() + try: + fileobj.seek(offset) + bytes = fileobj.read(length) + finally: + lock.release() + + if len(bytes) != n_bytes: + raise ValueError("Whoops, not enough data in file") + return bytes + + # More than one segment + bytes = mmap(-1, n_bytes) + for offset, length in segments: + + lock.acquire() + try: + fileobj.seek(offset) + bytes.write(fileobj.read(length)) + finally: + lock.release() + + if bytes.tell() != n_bytes: + raise ValueError("Oh dear, n_bytes does not look right") + return bytes + + +# Monkey-patch the above implementation into nibabel +fileslice.orig_read_segments = fileslice.read_segments +fileslice.read_segments = read_segments