From 2adb571163559fa8ba3e14e4ef6a58c3867f9625 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 29 Jun 2016 11:55:28 +0100
Subject: [PATCH] Image uses a SafeIndexedGzipFile for indexed images.
 ImageWrapper can optionally be told to calculate data ranges on a separate
 thread.

---
 fsl/data/image.py        | 19 ++++++++++++++----
 fsl/data/imagewrapper.py | 43 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 55 insertions(+), 7 deletions(-)

diff --git a/fsl/data/image.py b/fsl/data/image.py
index abc1160e5..d2d7e967a 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -370,7 +370,8 @@ class Image(Nifti1, notifier.Notifier):
                  xform=None,
                  loadData=True,
                  calcRange=True,
-                 indexed=False):
+                 indexed=False,
+                 threaded=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, 
@@ -406,6 +407,11 @@ class Image(Nifti1, notifier.Notifier):
         :arg indexed:   If ``True``, and the file is gzipped, it is opened 
                         using the :mod:`indexed_gzip` package. Otherwise the
                         file is opened by ``nibabel``.
+
+
+        :arg threaded:  If ``True``, the :class:`.ImageWrapper` will use a
+                        separate thread for data range calculation. Defaults
+                        to ``False``.
         """
 
         import nibabel as nib
@@ -480,7 +486,8 @@ class Image(Nifti1, notifier.Notifier):
         self.__suppressDataRange = False
         self.__imageWrapper      = imagewrapper.ImageWrapper(self.nibImage,
                                                              self.name,
-                                                             loadData=loadData)
+                                                             loadData=loadData,
+                                                             threaded=threaded)
 
         if calcRange:
             self.calcRange()
@@ -510,7 +517,11 @@ class Image(Nifti1, notifier.Notifier):
 
 
     def __del__(self):
-        """Closes any open file handles. """
+        """Closes any open file handles, and clears some references. """
+        
+        self.__nibImage     = None
+        self.__imageWrapper = None
+        
         if self.__fileobj is not None:
             self.__fileobj.close()
         
@@ -805,7 +816,7 @@ def loadIndexedImageFile(filename):
 
     log.debug('Loading {} using indexed gzip'.format(filename))
 
-    fobj = igzip.IndexedGzipFile(
+    fobj = igzip.SafeIndexedGzipFile(
         filename=filename,
         spacing=4194304,
         readbuf_size=131072)
diff --git a/fsl/data/imagewrapper.py b/fsl/data/imagewrapper.py
index 13f0dbe67..dd13a7aaf 100644
--- a/fsl/data/imagewrapper.py
+++ b/fsl/data/imagewrapper.py
@@ -41,6 +41,7 @@ import numpy     as np
 import nibabel   as nib
 
 import fsl.utils.notifier as notifier
+import fsl.utils.async    as async
 
 
 log = logging.getLogger(__name__)
@@ -121,7 +122,12 @@ class ImageWrapper(notifier.Notifier):
     """
 
     
-    def __init__(self, image, name=None, loadData=False, dataRange=None):
+    def __init__(self,
+                 image,
+                 name=None,
+                 loadData=False,
+                 dataRange=None,
+                 threaded=False):
         """Create an ``ImageWrapper``.
 
         :arg image:     A ``nibabel.Nifti1Image``.
@@ -137,6 +143,10 @@ class ImageWrapper(notifier.Notifier):
         :arg dataRange: A tuple containing the initial ``(min, max)``  data
                         range to use. See the :meth:`reset` method for
                         important information about this parameter.
+
+        :arg threaded:  If ``True``, the data range is updated on a
+                        :class:`.TaskThread`. Otherwise (the default), the
+                        data range is updated directly on reads/writes.
         """
 
         self.__image = image
@@ -167,6 +177,22 @@ class ImageWrapper(notifier.Notifier):
         if loadData:
             self.loadData()
 
+        if not threaded:
+            self.__taskThread = None
+        else:
+            self.__taskThread = async.TaskThread()
+            self.__taskThread.start()
+
+
+    def __del__(self):
+        """If this ``ImageWrapper`` was created with ``threaded=True``,
+        the :class:`.TaskThread` is stopped.
+        """
+        self.__image = None
+        if self.__taskThread is not None:
+            self.__taskThread.stop()
+            self.__taskThraed = None
+
 
     def reset(self, dataRange=None):
         """Reset the internal state and known data range of this
@@ -426,7 +452,12 @@ class ImageWrapper(notifier.Notifier):
         #      the provided data to avoid
         #      reading it in again.
 
-        self.__expandCoverage(slices)
+        if self.__taskThread is None:
+            self.__expandCoverage(slices)
+        else:
+            name = '{}_read_{}'.format(id(self), slices)
+            if not self.__taskThread.isQueued(name):
+                self.__taskThread.enqueue(name, self.__expandCoverage, slices)
 
 
     def __updateDataRangeOnWrite(self, slices, data):
@@ -476,7 +507,13 @@ class ImageWrapper(notifier.Notifier):
                 self.__coverage[:, :, vol]    = np.nan
                 self.__volRanges[     vol, :] = np.nan
 
-        self.__expandCoverage(slices)
+
+        if self.__taskThread is None:
+            self.__expandCoverage(slices)
+        else:
+            name = '{}_write_{}'.format(id(self), slices)
+            if not self.__taskThread.isQueued(name):
+                self.__taskThread.enqueue(name, self.__expandCoverage, slices)
 
             
     def __getitem__(self, sliceobj):
-- 
GitLab