From e4e9271184555c32da4f50244587ee6934f47d9e Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Thu, 9 Jun 2016 14:12:49 +0100
Subject: [PATCH] ImageWrapper coverage is stored as a numpy array

---
 fsl/data/imagewrapper.py | 81 ++++++++++++++++++++++------------------
 1 file changed, 44 insertions(+), 37 deletions(-)

diff --git a/fsl/data/imagewrapper.py b/fsl/data/imagewrapper.py
index 0f42a87ff..c55912d45 100644
--- a/fsl/data/imagewrapper.py
+++ b/fsl/data/imagewrapper.py
@@ -92,28 +92,35 @@ class ImageWrapper(notifier.Notifier):
         # values).
         self.__range = (float(hdr['cal_min']), float(hdr['cal_max']))
 
-        # For each entry in the last (real) dimension of
-        # the image (slice for 3D or volume for 4D), we
-        # record the portions of the image that have
-        # been included in the data range calculation, so
-        # we do not unnecessarily re-calculate ranges on
-        # the same part of the image.
-        self.__coverage = []
-
-        # TODO Use a numpy array of size
-        #      (2, [numRealDims - 1], [shape[numRealDims - 1]])
-        #      instead of a list of lists 
-        
-        # This is a list of lists of (low, high) pairs,
-        # one list for each entry in the last dimension
-        # (e.g. one list per 2D slice or 3D volume), and
-        # one pair for each dimension in the entry (e.g.
-        # row/column for each slice, or row/column/depth
-        # for each volume).
-        for i in range(image.shape[self.__numRealDims - 1]):
+        # The coverage array is used to keep track of
+        # the portions of the image which have been
+        # considered in the data range calculation.
+        # We use this coverage to avoid unnecessarily
+        # re-calculating the data range on the same
+        # part of the image.
+        #
+        # First of all, we're going to store a separate
+        # 'coverage' for each 2D slice in the 3D image
+        # (or 3D volume for 4D images). This effectively
+        # means a seaprate coverage for each index in the
+        # last 'real' image dimension (see above).
+        # 
+        # For each slice/volume, the the coverage is
+        # stored as sequences of (low, high) indices, one
+        # for each dimension in the slice/volume (e.g.
+        # row/column for a slice, or row/column/depth
+        # for a volume).
+        #
+        # All of these indices are stored in a big numpy
+        # array:
+        #   - first dimension:  low/high index
+        #   - second dimension: image dimension
+        #   - third dimension:  slice/volume index
+        self.__coverage = np.zeros(
+            (2, self.__numRealDims - 1, image.shape[self.__numRealDims - 1]),
+            dtype=np.uint32)
 
-            cov = [[None, None] for i in range(self.__numRealDims - 1)]
-            self.__coverage.append(cov)
+        self.__coverage[:] = np.nan
 
         if loadData:
             self.loadData()
@@ -207,7 +214,8 @@ class ImageWrapper(notifier.Notifier):
         self.__range = (newmin, newmax)
 
         for vol, exp in zip(volumes, expansions):
-            self.__coverage[vol] = adjustCoverage(self.__coverage[vol], exp)
+            self.__coverage[..., vol] = adjustCoverage(
+                self.__coverage[..., vol], exp)
 
         # TODO floating point error
         if newmin != oldmin or newmax != oldmax:
@@ -310,25 +318,28 @@ def adjustCoverage(oldCoverage, slices):
     """Adjusts/expands the given ``oldCoverage`` so that it covers the
     given set of ``slices``.
 
-    :arg oldCoverage: A sequence of (low, high) index pairs
+    :arg oldCoverage: A ``numpy`` array of shape ``(2, n)`` containing
+                      the (low, high) index pairs for a single slice/volume
+                      in the image.
+    
     :arg slices:      A sequence of (low, high) index pairs. If ``slices``
                       contains more dimensions than are specified in
                       ``oldCoverage``, the trailing dimensions are ignored.
 
-    :return: A list of (low, high) tuples containing the adjusted coverage.
+    :return: A ``numpy`` array containing the adjusted/expanded coverage.
     """
 
-    newCoverage = []
+    newCoverage = np.zeros(oldCoverage.shape, dtype=np.uint32)
 
-    for dim in range(len(oldCoverage)):
+    for dim in range(oldCoverage.shape[1]):
 
-        low,      high      = slices[     dim]
-        lowCover, highCover = oldCoverage[dim]
+        low,      high      = slices[        dim]
+        lowCover, highCover = oldCoverage[:, dim]
 
         if lowCover  is None or low  < lowCover:  lowCover  = low
         if highCover is None or high > highCover: highCover = high
 
-        newCoverage.append((lowCover, highCover))
+        newCoverage[:, dim] = lowCover, highCover
 
     return newCoverage
 
@@ -348,17 +359,14 @@ def sliceCovered(slices, coverage, shape, realDims):
 
     for vol in range(lowVol, highVol):
 
-        volCoverage = coverage[vol]
-
         for dim, size in enumerate(shape):
 
-            lowCover, highCover = volCoverage[dim]
+            lowCover, highCover = coverage[:, dim, vol]
+            lowSlice, highSlice = slices[     dim] 
 
             if lowCover is None or highCover is None:
                 return False
 
-            lowSlice, highSlice = slices[dim]
-
             if lowSlice  is None: lowSlice  = 0
             if highSlice is None: highSlice = size
 
@@ -383,12 +391,11 @@ def calcSliceExpansion(slices, coverage, realDims, padDims):
 
     for vol in volumes:
 
-        volCoverage = coverage[vol]
-        expansion   = []
+        expansion = []
 
         for dim in range(realDims - 1):
 
-            lowCover, highCover = volCoverage[dim]
+            lowCover, highCover = coverage[:, dim, vol]
             lowSlice, highSlice = slices[     dim]
 
             if lowCover  is None: lowCover  = lowSlice
-- 
GitLab